MarkBoardTrack.vue 11 KB

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