MarkBoardTrack.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. <template>
  2. <div
  3. v-if="store.setting.uiSetting['score.board.collapse']"
  4. @mouseover="handleMouseOverWithBoard"
  5. style="border-left: 1px solid grey"
  6. >
  7. <div
  8. class="tw-self-start tw-cursor-pointer tw-pb-10"
  9. @mouseover.stop="() => true"
  10. >
  11. <a-switch
  12. v-model:checked="store.setting.uiSetting['score.board.collapse']"
  13. />
  14. </div>
  15. </div>
  16. <div
  17. v-if="store.currentTask"
  18. class="mark-board-track-container"
  19. :class="store.setting.uiSetting['score.board.collapse'] && 'hide-board'"
  20. @mouseover="handleMouseOverWithBoard"
  21. @mouseout="handleMouseOutWithBoard"
  22. >
  23. <div class="tw-flex">
  24. <div class="tw-self-start tw-cursor-pointer hover:tw-bg-gray-200">
  25. <a-switch
  26. v-model:checked="store.setting.uiSetting['score.board.collapse']"
  27. />
  28. </div>
  29. <h1 class="tw-text-3xl tw-text-center tw-flex-grow">
  30. 总分:{{ store.currentMarkResult?.markerScore }}
  31. </h1>
  32. </div>
  33. <div class="tw-mb-2 tw-flex tw-place-content-center">
  34. <a-popconfirm
  35. v-if="store.setting.enableAllZero && !store.setting.forceSpecialTag"
  36. title="确定给全零分?"
  37. ok-text="确定"
  38. cancel-text="取消"
  39. @confirm="$emit('allZeroSubmit')"
  40. >
  41. <a-button
  42. type="primary"
  43. shape="round"
  44. size="large"
  45. style="margin-right: 20px !important"
  46. >
  47. 全零分
  48. </a-button>
  49. </a-popconfirm>
  50. <qm-button type="primary" shape="round" size="large" @click="submit">
  51. 提交
  52. </qm-button>
  53. </div>
  54. <div
  55. style="
  56. height: calc(100vh - 41px - 160px);
  57. overflow: hidden;
  58. user-select: none;
  59. "
  60. >
  61. <div
  62. v-if="store.currentTask && store.currentTask.questionList"
  63. class="
  64. tw-flex
  65. tw-gap-1
  66. tw-flex-wrap
  67. tw-justify-between
  68. tw-overflow-auto
  69. tw-content-start
  70. "
  71. style="min-height: 20% !important; max-height: 80% !important"
  72. :style="{ height: `${topPercent}%` }"
  73. >
  74. <template
  75. v-for="(question, index) in store.currentTask?.questionList"
  76. :key="index"
  77. >
  78. <div
  79. @click="chooseQuestion(question)"
  80. class="question tw-rounded tw-p-1 tw-cursor-pointer"
  81. :class="isCurrentQuestion(question) && 'current-question'"
  82. >
  83. <div style="border-bottom: 1px solid grey">
  84. {{ question.title }} {{ question.mainNumber }}-{{
  85. question.subNumber
  86. }}
  87. </div>
  88. <div class="tw-text-center tw-font-medium tw-text-3xl">
  89. <!-- 特殊的空格符号 -->
  90. {{ question.score ?? " " }}
  91. </div>
  92. </div>
  93. </template>
  94. </div>
  95. <div
  96. style="
  97. width: 100%;
  98. height: 4px;
  99. border: 2px solid grey;
  100. cursor: row-resize;
  101. "
  102. ref="dragSpliter"
  103. ></div>
  104. <div
  105. class="
  106. tw-flex
  107. tw-gap-1
  108. tw-flex-wrap
  109. tw-mt-5
  110. tw-overflow-auto
  111. tw-content-start
  112. "
  113. style="padding-bottom: 40px"
  114. :style="{ height: `${100 - topPercent}%` }"
  115. >
  116. <div
  117. v-for="(s, i) in questionScoreSteps"
  118. :key="i"
  119. @click="chooseScore(s)"
  120. class="single-score tw-cursor-pointer"
  121. :class="isCurrentScore(s) && 'current-score'"
  122. >
  123. {{ s }}
  124. </div>
  125. </div>
  126. </div>
  127. <div
  128. class="tw-flex tw-justify-between tw-mt-4"
  129. style="position: relative; bottom: 10px; right: 0px; width: 230px"
  130. >
  131. <qm-button
  132. type="primary"
  133. shape="round"
  134. size="large"
  135. :clickTimeout="300"
  136. @click="clearLatestMarkOfCurrentQuetion"
  137. >
  138. 回退
  139. </qm-button>
  140. <qm-button
  141. type="primary"
  142. shape="round"
  143. size="large"
  144. :clickTimeout="300"
  145. @click="clearAllMarksOfCurrentQuetion"
  146. >
  147. 清除本题
  148. </qm-button>
  149. </div>
  150. </div>
  151. </template>
  152. <script setup lang="ts">
  153. import type { Question } from "@/types";
  154. import { isNumber } from "lodash";
  155. import { computed, defineEmit, onMounted, onUnmounted, watch } from "vue";
  156. import { store } from "./store";
  157. import { autoChooseFirstQuestion } from "./use/autoChooseFirstQuestion";
  158. import { message } from "ant-design-vue";
  159. import { dragSplitPane } from "./use/splitPane";
  160. const emit = defineEmit(["submit", "allZeroSubmit"]);
  161. const { dragSpliter, topPercent } = dragSplitPane();
  162. const { chooseQuestion } = autoChooseFirstQuestion();
  163. const questionScoreSteps = computed(() => {
  164. const question = store.currentQuestion;
  165. if (!question) return [];
  166. const remainScore =
  167. Math.round(question.maxScore * 100 - (question.score || 0) * 100) / 100;
  168. const steps = [];
  169. for (
  170. let i = 0;
  171. i <= remainScore;
  172. i = Math.round(i * 100 + question.intervalScore * 100) / 100
  173. ) {
  174. steps.push(i);
  175. }
  176. if (
  177. Math.round(remainScore * 100) % Math.round(question.intervalScore * 100) !==
  178. 0
  179. ) {
  180. steps.push(remainScore);
  181. }
  182. return steps;
  183. });
  184. function isCurrentQuestion(question: Question) {
  185. return (
  186. store.currentQuestion?.mainNumber === question.mainNumber &&
  187. store.currentQuestion?.subNumber === question.subNumber
  188. );
  189. }
  190. watch(
  191. () => store.currentQuestion,
  192. () => {
  193. store.currentScore = undefined;
  194. }
  195. );
  196. function isCurrentScore(score: number) {
  197. return store.currentScore === score;
  198. }
  199. function chooseScore(score: number) {
  200. if (store.currentScore === score) {
  201. store.currentScore = undefined;
  202. } else {
  203. store.currentScore = score;
  204. store.currentSpecialTag = undefined;
  205. }
  206. }
  207. let keyPressTimestamp = 0;
  208. let keys: string[] = [];
  209. function numberKeyListener(event: KeyboardEvent) {
  210. // console.log(event);
  211. if (!store.currentQuestion) return;
  212. function indexOfCurrentQuestion() {
  213. return store.currentTask?.questionList.findIndex(
  214. (q) =>
  215. q.mainNumber === store.currentQuestion?.mainNumber &&
  216. q.subNumber === store.currentQuestion.subNumber
  217. );
  218. }
  219. // tab 循环答题列表
  220. if (event.key === "Tab") {
  221. const idx = indexOfCurrentQuestion() as number;
  222. if (idx >= 0 && store.currentTask) {
  223. const len = store.currentTask.questionList.length;
  224. chooseQuestion(store.currentTask.questionList[(idx + 1) % len]);
  225. event.preventDefault();
  226. }
  227. return;
  228. }
  229. if (event.timeStamp - keyPressTimestamp > 1 * 1000) {
  230. keys = [];
  231. }
  232. keyPressTimestamp = event.timeStamp;
  233. keys.push(event.key);
  234. if (isNaN(parseFloat(keys.join("")))) {
  235. keys = [];
  236. }
  237. if (event.key === "Escape") {
  238. keys = [];
  239. store.currentScore = undefined;
  240. store.currentSpecialTag = undefined;
  241. return;
  242. }
  243. const score = parseFloat(keys.join(""));
  244. if (isNumber(score) && questionScoreSteps.value.includes(score)) {
  245. chooseScore(score);
  246. }
  247. }
  248. function submitListener(e: KeyboardEvent) {
  249. if (import.meta.env.DEV && e.ctrlKey && e.key === "Enter") {
  250. submit();
  251. }
  252. }
  253. onMounted(() => {
  254. document.addEventListener("keydown", numberKeyListener);
  255. document.addEventListener("keydown", submitListener);
  256. });
  257. onUnmounted(() => {
  258. document.removeEventListener("keydown", numberKeyListener);
  259. document.removeEventListener("keydown", submitListener);
  260. });
  261. const handleMouseOverWithBoard = () => {
  262. if (store.setting.uiSetting["score.board.collapse"]) {
  263. const container = document.querySelector(".mark-board-track-container");
  264. if (container) {
  265. container.classList.add("show-board");
  266. }
  267. }
  268. };
  269. const handleMouseOutWithBoard = () => {
  270. if (store.setting.uiSetting["score.board.collapse"]) {
  271. const container = document.querySelector(".mark-board-track-container");
  272. if (container) {
  273. container.classList.remove("show-board");
  274. }
  275. }
  276. };
  277. function clearLatestMarkOfCurrentQuetion() {
  278. if (!store.currentMarkResult || !store.currentQuestion) return;
  279. const ts = store.currentMarkResult?.trackList.filter(
  280. (q) =>
  281. q.mainNumber === store.currentQuestion?.mainNumber &&
  282. q.subNumber === store.currentQuestion?.subNumber
  283. );
  284. if (ts.length === 0) {
  285. return;
  286. }
  287. if (ts.length === 1) {
  288. // 即将清除最后一条记录,置为0,因为watch trackList 算分要考虑不同模式的回评,无法自动
  289. store.currentQuestion.score = null;
  290. }
  291. const maxNumber = Math.max(...ts.map((q) => q.number));
  292. const idx = store.currentMarkResult.trackList.findIndex(
  293. (q) =>
  294. q.mainNumber === store.currentQuestion?.mainNumber &&
  295. q.subNumber === store.currentQuestion?.subNumber &&
  296. q.number === maxNumber
  297. );
  298. if (idx >= 0) {
  299. store.removeScoreTracks = store.currentMarkResult.trackList.splice(idx, 1);
  300. }
  301. }
  302. function clearAllMarksOfCurrentQuetion() {
  303. if (!store.currentMarkResult || !store.currentQuestion) return;
  304. store.removeScoreTracks = store.currentMarkResult?.trackList.filter(
  305. (q) =>
  306. q.mainNumber === store.currentQuestion?.mainNumber &&
  307. q.subNumber === store.currentQuestion?.subNumber
  308. );
  309. store.currentMarkResult.trackList = store.currentMarkResult?.trackList.filter(
  310. (q) =>
  311. !(
  312. q.mainNumber === store.currentQuestion?.mainNumber &&
  313. q.subNumber === store.currentQuestion?.subNumber
  314. )
  315. );
  316. store.currentQuestion.score = null;
  317. }
  318. function submit() {
  319. const errors: any = [];
  320. store.currentTask?.questionList.forEach((question, index) => {
  321. if (!isNumber(question.score)) {
  322. errors.push({
  323. question,
  324. index,
  325. error: `${question.mainNumber}-${question.subNumber} 没有赋分不能提交。`,
  326. });
  327. }
  328. });
  329. if (errors.length === 0) {
  330. emit("submit");
  331. } else {
  332. console.log(errors);
  333. message.error({
  334. content: errors.map((e: any) => `${e.error}`).join("\n"),
  335. duration: 10,
  336. });
  337. }
  338. }
  339. </script>
  340. <style scoped>
  341. .mark-board-track-container {
  342. max-width: 250px;
  343. min-width: 250px;
  344. border-left: 1px solid grey;
  345. padding-left: 6px;
  346. padding-right: 6px;
  347. max-height: calc(100vh - 41px);
  348. overflow: auto;
  349. z-index: 1001;
  350. }
  351. .hide-board {
  352. display: none;
  353. }
  354. .show-board {
  355. position: absolute;
  356. background-color: white;
  357. right: 0px;
  358. display: block;
  359. }
  360. .question {
  361. min-width: 100px;
  362. max-width: 100px;
  363. min-height: 70px;
  364. border: 1px solid grey;
  365. }
  366. .current-question {
  367. border: 1px solid yellowgreen;
  368. background-color: lightblue;
  369. }
  370. .single-score {
  371. width: 30px;
  372. height: 30px;
  373. display: grid;
  374. place-content: center;
  375. border: 1px solid black;
  376. border-radius: 5px;
  377. }
  378. .current-score {
  379. border: 1px solid yellowgreen;
  380. background-color: lightblue;
  381. }
  382. </style>