MarkBoardTrack.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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" style="height: 50px">
  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="{ height: `${topPercent}%` }"
  82. >
  83. <template
  84. v-for="(question, index) in store.currentTask.questionList"
  85. :key="index"
  86. >
  87. <div
  88. class="question tw-rounded tw-cursor-pointer tw-relative tw-mb-2"
  89. :class="isCurrentQuestion(question) && 'current-question'"
  90. @click="chooseQuestion(question)"
  91. @mouseover="
  92. addFocusTrack(
  93. undefined,
  94. question.mainNumber,
  95. question.subNumber,
  96. true
  97. )
  98. "
  99. @mouseleave="removeFocusTrack"
  100. >
  101. <div>
  102. {{ question.title }} {{ question.mainNumber }}-{{
  103. question.subNumber
  104. }}
  105. </div>
  106. <!-- 设置高度 避免动画跳动 -->
  107. <div style="height: 32px">
  108. <transition-group name="score-number-animation" tag="span">
  109. <span
  110. :key="store.currentTask?.markResult.scoreList[index] || 0"
  111. class="tw-font-medium tw-text-2xl score tw-inline-block"
  112. >
  113. <!-- 特殊的空格符号 -->
  114. <!-- eslint-disable-next-line no-irregular-whitespace -->
  115. {{ store.currentTask?.markResult.scoreList[index] ?? " " }}
  116. </span>
  117. </transition-group>
  118. </div>
  119. </div>
  120. </template>
  121. </div>
  122. <div
  123. ref="dragSpliter"
  124. style="
  125. width: 100%;
  126. height: 4px;
  127. border: 2px solid grey;
  128. background-color: grey;
  129. cursor: row-resize;
  130. "
  131. class="split-pane tw-flex tw-justify-evenly"
  132. >
  133. <div
  134. style="
  135. margin-top: -14px;
  136. width: 20px;
  137. height: 16px;
  138. text-align: center;
  139. clip-path: polygon(0 100%, 100% 100%, 50% 0);
  140. background-color: lightskyblue;
  141. cursor: pointer;
  142. "
  143. @click="topPercent = 20"
  144. ></div>
  145. <div
  146. style="
  147. margin-top: -3px;
  148. width: 20px;
  149. height: 16px;
  150. text-align: center;
  151. clip-path: polygon(0 0, 100% 0, 50% 100%);
  152. background-color: lightskyblue;
  153. cursor: pointer;
  154. "
  155. @click="topPercent = 90"
  156. ></div>
  157. </div>
  158. <div
  159. class="tw-flex tw-flex-wrap tw-mt-5 tw-overflow-auto tw-content-start"
  160. style="padding-bottom: 40px; gap: 8px"
  161. :style="{ height: `${100 - topPercent}%` }"
  162. >
  163. <div
  164. v-for="(s, i) in questionScoreSteps"
  165. :key="i"
  166. class="single-score tw-cursor-pointer tw-font-bold"
  167. :class="isCurrentScore(s) && 'current-score'"
  168. @click="chooseScore(s)"
  169. >
  170. {{ s }}
  171. </div>
  172. </div>
  173. </div>
  174. <div
  175. class="tw-flex tw-justify-between tw-mt-4"
  176. style="position: relative; bottom: 0px; right: 0px; width: 230px"
  177. >
  178. <qm-button
  179. type="primary"
  180. shape="round"
  181. size="large"
  182. style="
  183. background-color: var(--app-undo-button-bg-color);
  184. border-color: var(--app-undo-button-bg-color);
  185. "
  186. :clickTimeout="300"
  187. @click="clearLatestMarkOfCurrentQuetion"
  188. >
  189. 回退
  190. </qm-button>
  191. <qm-button
  192. type="primary"
  193. shape="round"
  194. size="large"
  195. :clickTimeout="300"
  196. @click="clearAllMarksOfCurrentQuetion"
  197. >
  198. 清除本题
  199. </qm-button>
  200. </div>
  201. </div>
  202. </template>
  203. <script setup lang="ts">
  204. import type { Question } from "@/types";
  205. import { isNumber } from "lodash-es";
  206. import { onMounted, onUnmounted, watch } from "vue";
  207. import { store } from "@/store/store";
  208. import { autoChooseFirstQuestion } from "./use/autoChooseFirstQuestion";
  209. import { dragSplitPane } from "./use/splitPane";
  210. import { addFocusTrack, removeFocusTrack } from "./use/focusTracks";
  211. const emit = defineEmits(["submit", "allZeroSubmit", "unselectiveSubmit"]);
  212. const { dragSpliter, topPercent } = dragSplitPane();
  213. watch(topPercent, () => {
  214. if (topPercent.value <= 10) {
  215. topPercent.value = 10;
  216. }
  217. if (topPercent.value >= 90) {
  218. topPercent.value = 90;
  219. }
  220. });
  221. const { chooseQuestion } = autoChooseFirstQuestion();
  222. // 切换题目是清空上一题的分数
  223. watch(
  224. () => store.currentQuestion,
  225. () => (store.currentScore = undefined)
  226. );
  227. const questionScore = $computed(
  228. () =>
  229. store.currentTask &&
  230. store.currentQuestion &&
  231. store.currentTask.markResult.scoreList[store.currentQuestion.__index]
  232. );
  233. const questionScoreSteps = $computed(() => {
  234. const question = store.currentQuestion;
  235. if (!question) return [];
  236. const remainScore =
  237. Math.round(question.maxScore * 100 - (questionScore || 0) * 100) / 100;
  238. const steps = [];
  239. for (
  240. let i = 0;
  241. i <= remainScore;
  242. i = Math.round(i * 100 + question.intervalScore * 100) / 100
  243. ) {
  244. steps.push(i);
  245. }
  246. if (
  247. Math.round(remainScore * 100) % Math.round(question.intervalScore * 100) !==
  248. 0
  249. ) {
  250. steps.push(remainScore);
  251. }
  252. return steps;
  253. });
  254. function isCurrentQuestion(question: Question) {
  255. return (
  256. store.currentQuestion?.mainNumber === question.mainNumber &&
  257. store.currentQuestion?.subNumber === question.subNumber
  258. );
  259. }
  260. function isCurrentScore(score: number) {
  261. return store.currentScore === score;
  262. }
  263. function chooseScore(score: number) {
  264. if (store.currentScore === score) {
  265. store.currentScore = undefined;
  266. } else {
  267. store.currentScore = score;
  268. store.currentSpecialTag = undefined;
  269. }
  270. }
  271. let keyPressTimestamp = 0;
  272. let keys: string[] = [];
  273. function numberKeyListener(event: KeyboardEvent) {
  274. // console.log(event);
  275. if (!store.currentQuestion) return;
  276. if (" jiklc".includes(event.key)) return;
  277. function indexOfCurrentQuestion() {
  278. return store.currentTask?.questionList.findIndex(
  279. (q) =>
  280. q.mainNumber === store.currentQuestion?.mainNumber &&
  281. q.subNumber === store.currentQuestion.subNumber
  282. );
  283. }
  284. // tab 循环答题列表
  285. if (event.key === "Tab") {
  286. const idx = indexOfCurrentQuestion() as number;
  287. if (idx >= 0 && store.currentTask) {
  288. const len = store.currentTask.questionList.length;
  289. chooseQuestion(store.currentTask.questionList[(idx + 1) % len]);
  290. event.preventDefault();
  291. }
  292. return;
  293. }
  294. if (event.timeStamp - keyPressTimestamp > 1 * 1000) {
  295. keys = [];
  296. }
  297. keyPressTimestamp = event.timeStamp;
  298. keys.push(event.key);
  299. if (isNaN(parseFloat(keys.join("")))) {
  300. keys = [];
  301. }
  302. if (event.key === "Escape") {
  303. keys = [];
  304. store.currentScore = undefined;
  305. store.currentSpecialTag = undefined;
  306. return;
  307. }
  308. const score = parseFloat(keys.join(""));
  309. if (isNumber(score) && questionScoreSteps.includes(score)) {
  310. chooseScore(score);
  311. }
  312. }
  313. function submitListener(e: KeyboardEvent) {
  314. // if (import.meta.env.DEV && e.ctrlKey && e.key === "Enter") {
  315. if (e.ctrlKey && e.key === "Enter") {
  316. submit();
  317. }
  318. }
  319. onMounted(() => {
  320. document.addEventListener("keydown", numberKeyListener);
  321. document.addEventListener("keydown", submitListener);
  322. });
  323. onUnmounted(() => {
  324. document.removeEventListener("keydown", numberKeyListener);
  325. document.removeEventListener("keydown", submitListener);
  326. });
  327. function clearLatestMarkOfCurrentQuetion() {
  328. if (!store.currentTask?.markResult || !store.currentQuestion) return;
  329. const { __index, mainNumber, subNumber } = store.currentQuestion;
  330. const markResult = store.currentTask.markResult;
  331. const ts = markResult.trackList.filter(
  332. (q) => q.mainNumber === mainNumber && q.subNumber === subNumber
  333. );
  334. if (ts.length === 0) {
  335. return;
  336. }
  337. const lastMark = ts.splice(-1)[0];
  338. store.removeScoreTracks = [lastMark];
  339. markResult.trackList = markResult.trackList.filter((t) => t !== lastMark);
  340. markResult.scoreList[__index] =
  341. ts.length === 0
  342. ? null
  343. : ts
  344. .map((t) => t.score)
  345. .reduce((acc, v) => (acc += Math.round(v * 100)), 0) / 100;
  346. }
  347. function clearAllMarksOfCurrentQuetion() {
  348. if (!store.currentTask?.markResult || !store.currentQuestion) return;
  349. const markResult = store.currentTask.markResult;
  350. store.removeScoreTracks = markResult.trackList.filter(
  351. (q) =>
  352. q.mainNumber === store.currentQuestion?.mainNumber &&
  353. q.subNumber === store.currentQuestion?.subNumber
  354. );
  355. markResult.trackList = markResult.trackList.filter(
  356. (q) => !store.removeScoreTracks.includes(q)
  357. );
  358. const { __index } = store.currentQuestion;
  359. markResult.scoreList[__index] = null;
  360. }
  361. function submit() {
  362. emit("submit");
  363. }
  364. const buttonHeightForSelective = $computed(() =>
  365. store.setting.selective &&
  366. store.setting.enableAllZero &&
  367. !store.setting.forceSpecialTag
  368. ? "36px"
  369. : "76px"
  370. );
  371. </script>
  372. <style scoped>
  373. .mark-board-track-container {
  374. max-width: 290px;
  375. min-width: 290px;
  376. padding: 20px;
  377. max-height: calc(100vh - 56px - 0px);
  378. overflow: auto;
  379. z-index: 1001;
  380. transition: margin-right 0.5s;
  381. color: var(--app-small-header-text-color);
  382. }
  383. .mark-board-track-container.show {
  384. margin-right: 0;
  385. }
  386. .mark-board-track-container.hide {
  387. margin-right: -290px;
  388. }
  389. .top-container {
  390. background-color: var(--app-container-bg-color);
  391. }
  392. .total-score {
  393. color: var(--app-main-text-color);
  394. font-size: 32px;
  395. }
  396. .question {
  397. min-width: 110px;
  398. max-width: 110px;
  399. min-height: 72px;
  400. padding: 10px;
  401. background-color: var(--app-container-bg-color);
  402. }
  403. .current-question {
  404. color: white;
  405. background-color: var(--app-score-color);
  406. }
  407. .single-score {
  408. position: relative;
  409. width: 32px;
  410. height: 32px;
  411. font-size: var(--app-secondary-font-size);
  412. display: grid;
  413. place-content: center;
  414. background-color: var(--app-container-bg-color);
  415. border-radius: 30px;
  416. }
  417. .current-score {
  418. background-color: var(--app-score-color);
  419. color: white;
  420. }
  421. .all-zero-unselective-button {
  422. height: v-bind(buttonHeightForSelective);
  423. border-radius: 10px;
  424. padding: 7px;
  425. background-color: #4db9ff;
  426. border: none;
  427. }
  428. </style>