MarkBoardTrack.vue 11 KB

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