MarkBoardTrack.vue 11 KB

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