2
0

MarkBoardKeyBoard.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. <template>
  2. <div
  3. v-if="store.currentTask"
  4. class="mark-board-track-container"
  5. :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
  6. >
  7. <div class="tw-my-2 tw-flex">
  8. <a-dropdown class="tw-self-end">
  9. <template #overlay>
  10. <a-menu>
  11. <a-menu-item key="1" @click="toggleKeyMouse">
  12. 鼠标给分
  13. </a-menu-item>
  14. </a-menu>
  15. </template>
  16. <a-button>
  17. 键盘给分
  18. <DownOutlined style="display: inline-flex" />
  19. </a-button>
  20. </a-dropdown>
  21. </div>
  22. <div
  23. class="tw-flex tw-rounded tw-justify-between tw-p-2 tw-pl-5 top-container tw-mb-4"
  24. >
  25. <div class="tw-flex tw-flex-col">
  26. <div class="tw-flex tw-items-center tw-gap-2">
  27. <img
  28. src="./images/totalscore.png"
  29. style="width: 13px; height: 16px"
  30. />
  31. 总分
  32. </div>
  33. <div class="total-score tw-ml-5 tw-font-bold">
  34. <transition-group name="score-number-animation" tag="span">
  35. <span
  36. :key="
  37. store.currentTaskEnsured.markResult
  38. ? store.currentTaskEnsured.markResult.markerScore + ''
  39. : '0'
  40. "
  41. class="tw-inline-block"
  42. >{{
  43. store.currentTaskEnsured.markResult
  44. ? store.currentTaskEnsured.markResult.markerScore
  45. : ""
  46. }}</span
  47. >
  48. </transition-group>
  49. </div>
  50. </div>
  51. <div class="tw-flex tw-place-content-center tw-items-center tw-gap-2">
  52. <div class="tw-flex tw-flex-col tw-gap-1">
  53. <a-popconfirm
  54. v-if="store.setting.enableAllZero"
  55. title="确定给全零分?"
  56. :overlayStyle="{ width: '200px' }"
  57. @confirm="$emit('allZeroSubmit')"
  58. >
  59. <a-button
  60. type="primary"
  61. size="middle"
  62. class="all-zero-unselective-button"
  63. >
  64. <span>全零分</span>
  65. </a-button>
  66. </a-popconfirm>
  67. <!-- <a-popconfirm
  68. v-if="store.setting.selective"
  69. title="确定是未选做?"
  70. :overlayStyle="{ width: '200px' }"
  71. @confirm="$emit('unselectiveSubmit')"
  72. >
  73. <a-button
  74. type="primary"
  75. size="middle"
  76. class="all-zero-unselective-button"
  77. >
  78. <span>未选做</span>
  79. </a-button>
  80. </a-popconfirm> -->
  81. </div>
  82. <qm-button
  83. type="primary"
  84. shape="round"
  85. size="middle"
  86. style="height: 76px; border-radius: 10px; padding: 12px"
  87. @click="submit"
  88. >
  89. 提交
  90. </qm-button>
  91. </div>
  92. </div>
  93. <div v-if="store.currentTaskEnsured.questionList">
  94. <template
  95. v-for="(question, index) in store.currentTaskEnsured.questionList"
  96. :key="index"
  97. >
  98. <div
  99. :id="'bq-' + question.mainNumber + '-' + question.subNumber"
  100. class="question tw-rounded tw-p-1 tw-mb-1 tw-cursor-pointer tw-relative"
  101. :class="{
  102. 'current-question': isCurrentQuestion(question),
  103. disabled: notInActive(index),
  104. }"
  105. @click="willChooseQuestion(question, index)"
  106. >
  107. <div class="tw-flex tw-justify-between">
  108. <div>
  109. <div v-if="!!question.questionName">
  110. {{ question.questionName }}
  111. </div>
  112. <div v-else>
  113. {{ question.title }} {{ question.mainNumber }}-{{
  114. question.subNumber
  115. }}
  116. </div>
  117. <div
  118. class="tw-text-center tw-text-3xl"
  119. :class="isCurrentQuestion(question) && 'current-score'"
  120. >
  121. {{
  122. isCurrentQuestion(question)
  123. ? scoreStr
  124. : store.currentTask.markResult?.scoreList[index]
  125. }}
  126. </div>
  127. </div>
  128. <div>
  129. <div class="tw-text-center">
  130. 间隔{{ question.intervalScore }}分
  131. </div>
  132. <div class="tw-flex tw-text-3xl" style="width: 80px">
  133. <span class="tw-flex-1">{{ question.minScore }}</span>
  134. <span class="tw-flex-1">~</span>
  135. <span class="tw-flex-1 tw-text-center">{{
  136. question.maxScore
  137. }}</span>
  138. </div>
  139. </div>
  140. </div>
  141. </div>
  142. <div class="tw-mb-2">
  143. <div
  144. v-if="question.selective"
  145. class="tw-cursor-pointer tw-font-bold unselective"
  146. :class="{
  147. 'current-score': question.hasSetUnselective,
  148. }"
  149. @click="setUnselect(question, index)"
  150. >
  151. 未选做
  152. </div>
  153. </div>
  154. </template>
  155. </div>
  156. </div>
  157. </template>
  158. <script setup lang="ts">
  159. import type { Question } from "@/types";
  160. import { isNumber } from "lodash-es";
  161. import { onMounted, onUnmounted, watch, computed } from "vue";
  162. import { store } from "@/store/store";
  163. import { keyMouse } from "./use/keyboardAndMouse";
  164. import { autoChooseFirstQuestion } from "./use/autoChooseFirstQuestion";
  165. import { message } from "ant-design-vue";
  166. import { DownOutlined } from "@ant-design/icons-vue";
  167. import { useRoute } from "vue-router";
  168. const route = useRoute();
  169. const props = defineProps<{ arbitrateIndex?: string }>();
  170. const activeIndex = computed(() => {
  171. return (
  172. props.arbitrateIndex ? props.arbitrateIndex?.split(",") || [] : []
  173. ).map((indexStr: string) => Number(indexStr) - 1);
  174. });
  175. const notInActive = (index: number) => {
  176. return (
  177. activeIndex.value.indexOf(index) == -1 &&
  178. route.path === "/admin/exam/arbitrate/start"
  179. );
  180. };
  181. const emit = defineEmits(["submit", "allZeroSubmit", "unselectiveSubmit"]);
  182. const { toggleKeyMouse } = keyMouse();
  183. const { chooseQuestion } = autoChooseFirstQuestion();
  184. /**
  185. * 当前题的输入串,初次是markResult.scoreList中score,然后接收输入字符,回车时判断是否合法,合法则赋值给markResult.scoreList
  186. * 切换到下一题,则重新开始
  187. * */
  188. let scoreStr = $ref("");
  189. watch(
  190. () => [store.currentQuestion, store.setting.uiSetting["normal.mode"]],
  191. () => {
  192. console.log("store.currentQuestion?.score", store.currentQuestion?.score);
  193. if (isNumber(store.currentQuestion?.score)) {
  194. scoreStr = "" + store.currentQuestion?.score;
  195. } else {
  196. scoreStr = "";
  197. }
  198. },
  199. { immediate: true }
  200. );
  201. const willChooseQuestion = (question, index) => {
  202. if (notInActive(index)) {
  203. return;
  204. }
  205. chooseQuestion(question);
  206. scrollToQuestion(question);
  207. };
  208. const questionScoreSteps = $computed(() => {
  209. const question = store.currentQuestion;
  210. if (!question) return [];
  211. const remainScore = Math.round(question.maxScore * 100) / 100;
  212. const steps = [];
  213. for (
  214. let i = 0;
  215. i <= remainScore;
  216. i = Math.round(i * 100 + question.intervalScore * 100) / 100
  217. ) {
  218. steps.push(i);
  219. }
  220. if (
  221. Math.round(remainScore * 100) % Math.round(question.intervalScore * 100) !==
  222. 0
  223. ) {
  224. steps.push(remainScore);
  225. }
  226. return steps;
  227. });
  228. function isCurrentQuestion(question: Question) {
  229. return (
  230. store.currentQuestion?.mainNumber === question.mainNumber &&
  231. store.currentQuestion?.subNumber === question.subNumber
  232. );
  233. }
  234. const questionScore = $computed(
  235. () =>
  236. store.currentTask &&
  237. store.currentQuestion &&
  238. store.currentTask.markResult.scoreList[store.currentQuestion.__index]
  239. );
  240. function numberKeyListener(event: KeyboardEvent) {
  241. // console.log(event);
  242. if (!store.currentQuestion || !store.currentTask) return;
  243. if (store.globalMask) return;
  244. function indexOfCurrentQuestion() {
  245. return store.currentTaskEnsured.questionList.findIndex(
  246. (q) =>
  247. q.mainNumber === store.currentQuestion?.mainNumber &&
  248. q.subNumber === store.currentQuestion.subNumber
  249. );
  250. }
  251. // 处理Enter跳下一题或submit
  252. if (event.key === "Enter") {
  253. const allScoreMarked = store.currentTask.markResult.scoreList.every((s) =>
  254. isNumber(s)
  255. );
  256. // 如果所有题已赋分,并且当前题赋分和输入串和当前题分数一致,则可以在任意题提交
  257. if (allScoreMarked && scoreStr === "" + questionScore) {
  258. submit();
  259. return;
  260. }
  261. if (scoreStr.length === 0) {
  262. void message.error({ content: "请输入分数", duration: 5 });
  263. console.log("请输入分数");
  264. return;
  265. }
  266. // 0 分
  267. // 整数分 (开始不为0)
  268. // 小数分 (开始和结束不为0,结束必须为1-9
  269. if (
  270. !(
  271. scoreStr === "0" ||
  272. /^0\.[1-9]\d*$/.test(scoreStr) ||
  273. /^[1-9]\d*$/.test(scoreStr) ||
  274. /^[1-9]\d*\.\d*[1-9]$/.test(scoreStr)
  275. )
  276. ) {
  277. void message.error({ content: "分数格式不正确", duration: 5 });
  278. console.log("分数格式不正确");
  279. return;
  280. }
  281. const score = parseFloat(scoreStr);
  282. // console.log(score);
  283. if (!isNumber(score)) {
  284. void message.error({ content: "非数字输入", duration: 5 });
  285. console.log("非数字输入");
  286. return;
  287. }
  288. if (!questionScoreSteps.includes(score)) {
  289. void message.error({ content: "输入的分数不在有效间隔内", duration: 5 });
  290. return;
  291. }
  292. const { __index } = store.currentQuestion;
  293. store.currentTask.markResult.scoreList[__index] = score;
  294. store.currentQuestion.hasSetUnselective = false;
  295. //
  296. // scoreStr = "";
  297. // console.log("give score", score);
  298. const idx = indexOfCurrentQuestion();
  299. if (idx + 1 < store.currentTask.questionList.length) {
  300. chooseQuestion(store.currentTask.questionList[idx + 1]);
  301. }
  302. return;
  303. }
  304. if (event.key === "ArrowLeft") {
  305. const idx = indexOfCurrentQuestion();
  306. if (idx > 0) {
  307. chooseQuestion(store.currentTask.questionList[idx - 1]);
  308. }
  309. event.preventDefault();
  310. return;
  311. }
  312. if (event.key === "ArrowRight") {
  313. const idx = indexOfCurrentQuestion();
  314. if (idx < store.currentTask.questionList.length - 1) {
  315. chooseQuestion(store.currentTask.questionList[idx + 1]);
  316. }
  317. event.preventDefault();
  318. return;
  319. }
  320. // 处理回退删除分数
  321. if (event.key === "Backspace") {
  322. if (scoreStr.length > 0) {
  323. scoreStr = scoreStr.slice(0, -1);
  324. } else {
  325. return;
  326. }
  327. }
  328. if (event.key === "Escape") {
  329. scoreStr = "";
  330. }
  331. // 此时不再接受任何非数字键
  332. if (".0123456789".includes(event.key)) {
  333. scoreStr += event.key;
  334. }
  335. }
  336. onMounted(() => {
  337. document.addEventListener("keydown", numberKeyListener);
  338. });
  339. onUnmounted(() => {
  340. document.removeEventListener("keydown", numberKeyListener);
  341. });
  342. const scrollToQuestion = (question: Question) => {
  343. const node = document.querySelector(
  344. `#q-${question.mainNumber}-${question.subNumber}`
  345. );
  346. node && node.scrollIntoView({ behavior: "smooth" });
  347. };
  348. watch(
  349. () => store.currentQuestion,
  350. () => {
  351. store.currentQuestion && scrollToQuestion(store.currentQuestion);
  352. }
  353. );
  354. function submit() {
  355. emit("submit");
  356. }
  357. const buttonHeightForSelective = $computed(() =>
  358. store.setting.selective && store.setting.enableAllZero ? "36px" : "76px"
  359. );
  360. function setUnselect(question: Question, index: number) {
  361. if (!question.hasSetUnselective) {
  362. const markResult = store.currentTask.markResult;
  363. markResult.scoreList[index] = null;
  364. store.currentScore = undefined;
  365. // scoreStr = "未选做";
  366. scoreStr = "";
  367. } else {
  368. scoreStr = "";
  369. }
  370. question.hasSetUnselective = !question.hasSetUnselective;
  371. }
  372. </script>
  373. <style lang="less" scoped>
  374. .unselective {
  375. width: 72px;
  376. height: 32px;
  377. font-size: var(--app-secondary-font-size);
  378. display: grid;
  379. place-content: center;
  380. background-color: var(--app-container-bg-color);
  381. border-radius: 30px;
  382. }
  383. .mark-board-track-container {
  384. max-width: 290px;
  385. min-width: 290px;
  386. padding: 20px;
  387. max-height: calc(100vh - 56px - 0px);
  388. overflow: auto;
  389. z-index: 1001;
  390. transition: margin-right 0.5s;
  391. color: var(--app-small-header-text-color);
  392. }
  393. .mark-board-track-container.show {
  394. margin-right: 0;
  395. }
  396. .mark-board-track-container.hide {
  397. margin-right: -290px;
  398. }
  399. .top-container {
  400. background-color: var(--app-container-bg-color);
  401. }
  402. .total-score {
  403. color: var(--app-main-text-color);
  404. font-size: 32px;
  405. }
  406. .question {
  407. min-width: 80px;
  408. padding: 10px;
  409. background-color: var(--app-container-bg-color);
  410. &.disabled {
  411. background-color: #f5f5f5 !important;
  412. // pointer-events: none;
  413. cursor: not-allowed !important;
  414. * {
  415. color: rgba(0, 0, 0, 0.25) !important;
  416. }
  417. }
  418. }
  419. .current-question {
  420. color: white;
  421. background-color: var(--app-score-color);
  422. }
  423. .current-score {
  424. background-color: var(--app-score-color);
  425. color: white;
  426. }
  427. .all-zero-unselective-button {
  428. height: v-bind(buttonHeightForSelective);
  429. border-radius: 10px;
  430. padding: 7px;
  431. background-color: #4db9ff;
  432. border: none;
  433. }
  434. </style>