Browse Source

feat: 调整第一期

zhangjie 4 months ago
parent
commit
edc2a929f7
82 changed files with 2445 additions and 2359 deletions
  1. 1 1
      src/App.vue
  2. 1 1
      src/api/markPage.ts
  3. 1 1
      src/components/CommonMarkHeader.vue
  4. 6 4
      src/components/QmDialog.vue
  5. 1 1
      src/components/ZoomPaper.vue
  6. 5 5
      src/directives/eleMove.ts
  7. 1 1
      src/features/arbitrate/Arbitrate.vue
  8. 1 1
      src/features/arbitrate/ArbitrateMarkList.vue
  9. 3 3
      src/features/arbitrate/MarkBody.vue
  10. 1 1
      src/features/arbitrate/MarkHeader.vue
  11. 4 4
      src/features/check/CommonMarkBody.vue
  12. 1 1
      src/features/check/MarkBody.vue
  13. 1 1
      src/features/check/ObjectiveAnswer.vue
  14. 4 4
      src/features/check/SubjectiveAnswer.vue
  15. 1 1
      src/features/library/inspect/LibraryInspect.vue
  16. 5 5
      src/features/library/inspect/MarkBoardInspect.vue
  17. 3 3
      src/features/library/inspect/MarkBody.vue
  18. 1 1
      src/features/library/inspect/MarkHeader.vue
  19. 1 1
      src/features/library/libraryTrack/LibraryTrack.vue
  20. 3 3
      src/features/library/libraryTrack/MarkBody.vue
  21. 3 3
      src/features/library/quality/MarkBody.vue
  22. 1 1
      src/features/library/quality/Quality.vue
  23. 43 400
      src/features/mark/Mark.vue
  24. 8 319
      src/features/mark/MarkBody.vue
  25. 4 4
      src/features/mark/MarkBodyBase.vue
  26. 155 0
      src/features/mark/MarkBodyCursor.vue
  27. 1 1
      src/features/mark/MarkDrawTrack.vue
  28. 1 1
      src/features/mark/MarkHistory.vue
  29. 1 1
      src/features/mark/MultiMediaMarkBody.vue
  30. 62 0
      src/features/mark/composables/useAutoChooseFirstQuestion.ts
  31. 17 19
      src/features/mark/composables/useDragSplitPane.ts
  32. 62 0
      src/features/mark/composables/useDraggable.ts
  33. 104 0
      src/features/mark/composables/useFocusTracks.ts
  34. 161 0
      src/features/mark/composables/useMakeTrack.ts
  35. 191 0
      src/features/mark/composables/useMarkSubmit.ts
  36. 93 0
      src/features/mark/composables/useMarkTask.ts
  37. 54 0
      src/features/mark/composables/useSetting.ts
  38. 46 0
      src/features/mark/composables/useStatus.ts
  39. 58 0
      src/features/mark/composables/useTaskRejection.ts
  40. 10 4
      src/features/mark/modals/ModalAllPaper.vue
  41. 6 4
      src/features/mark/modals/ModalAnswer.vue
  42. 7 5
      src/features/mark/modals/ModalMinimap.vue
  43. 6 4
      src/features/mark/modals/ModalPaper.vue
  44. 10 7
      src/features/mark/modals/ModalSheetView.vue
  45. 5 3
      src/features/mark/modals/ModalShortCut.vue
  46. 22 20
      src/features/mark/modals/ModalSpecialTag.vue
  47. 40 36
      src/features/mark/scoring/MarkBoardKeyBoard.vue
  48. 20 18
      src/features/mark/scoring/MarkBoardMouse.vue
  49. 72 74
      src/features/mark/scoring/MarkBoardTrack.vue
  50. 5 3
      src/features/mark/scoring/MarkBoardTrackDialog.vue
  51. 173 0
      src/features/mark/stores/mark.ts
  52. 5 3
      src/features/mark/toolbar/MarkChangeProfile.vue
  53. 67 53
      src/features/mark/toolbar/MarkHeader.vue
  54. 10 8
      src/features/mark/toolbar/MarkProblemDialog.vue
  55. 6 5
      src/features/mark/toolbar/MarkSwitchGroupDialog.vue
  56. 42 42
      src/features/mark/toolbar/MarkTool.vue
  57. 0 60
      src/features/mark/use/autoChooseFirstQuestion.ts
  58. 0 62
      src/features/mark/use/draggable.ts
  59. 0 101
      src/features/mark/use/focusTracks.ts
  60. 0 13
      src/features/mark/use/keyboardAndMouse.ts
  61. 1 1
      src/features/reject/Reject.vue
  62. 1 1
      src/features/reject/RejectBoard.vue
  63. 1 1
      src/features/student/importInspect/ImportInspect.vue
  64. 5 7
      src/features/student/importInspect/MarkBoardInspect.vue
  65. 1 1
      src/features/student/importInspect/MarkHeader.vue
  66. 227 228
      src/features/student/scoreVerify/MarkBoardInspect.vue
  67. 57 57
      src/features/student/scoreVerify/MarkHeader.vue
  68. 173 173
      src/features/student/scoreVerify/ScoreVerify.vue
  69. 309 309
      src/features/student/scoreVerify/markBody.vue
  70. 6 10
      src/features/student/studentInspect/MarkBoardInspect.vue
  71. 3 3
      src/features/student/studentInspect/MarkBody.vue
  72. 1 1
      src/features/student/studentInspect/MarkHeader.vue
  73. 1 1
      src/features/student/studentInspect/StudentInspect.vue
  74. 1 1
      src/features/student/studentTrack/StudentTrack.vue
  75. 1 1
      src/filters/index.ts
  76. 2 9
      src/main.ts
  77. 1 1
      src/plugins/axiosApp.ts
  78. 26 0
      src/store/app.ts
  79. 11 0
      src/store/index.ts
  80. 0 235
      src/store/store.ts
  81. 0 1
      src/types/index.ts
  82. 1 1
      src/utils/utils.ts

+ 1 - 1
src/App.vue

@@ -13,7 +13,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { watch, watchEffect } from "vue";
 import { watch, watchEffect } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import zhCN from "ant-design-vue/es/locale/zh_CN";
 import zhCN from "ant-design-vue/es/locale/zh_CN";
 
 
 let spinning = $ref(false);
 let spinning = $ref(false);

+ 1 - 1
src/api/markPage.ts

@@ -1,4 +1,4 @@
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { httpApp } from "@/plugins/axiosApp";
 import { httpApp } from "@/plugins/axiosApp";
 import {
 import {
   Setting,
   Setting,

+ 1 - 1
src/components/CommonMarkHeader.vue

@@ -170,7 +170,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted } from "vue";
 import { onMounted } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import {
 import {
   UserOutlined,
   UserOutlined,
   PoweroffOutlined,
   PoweroffOutlined,

+ 6 - 4
src/components/QmDialog.vue

@@ -37,7 +37,9 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted, onUpdated, reactive } from "vue";
 import { onMounted, onUpdated, reactive } from "vue";
-import { store } from "@/store/store";
+import { useAppStore } from "@/store";
+
+const appStore = useAppStore();
 
 
 // 因为要更改props取得的值,所以不需要reactivity
 // 因为要更改props取得的值,所以不需要reactivity
 const {
 const {
@@ -172,9 +174,9 @@ onUpdated(() => {
 });
 });
 
 
 const increaseZIndex = () => {
 const increaseZIndex = () => {
-  positionStyle.zIndex = store.maxModalZIndex++;
-  if (store.maxModalZIndex === 5000) {
-    store.maxModalZIndex = 1020;
+  positionStyle.zIndex = appStore.maxModalZIndex++;
+  if (appStore.maxModalZIndex === 5000) {
+    appStore.maxModalZIndex = 1020;
   }
   }
 };
 };
 </script>
 </script>

+ 1 - 1
src/components/ZoomPaper.vue

@@ -43,7 +43,7 @@ import {
   RotateRightOutlined,
   RotateRightOutlined,
 } from "@ant-design/icons-vue";
 } from "@ant-design/icons-vue";
 import { computed, onMounted, onUnmounted } from "vue";
 import { computed, onMounted, onUnmounted } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 
 
 const props = defineProps<{ showRotate?: boolean; fixed?: boolean }>();
 const props = defineProps<{ showRotate?: boolean; fixed?: boolean }>();
 defineEmits(["rotateRight"]);
 defineEmits(["rotateRight"]);

+ 5 - 5
src/features/mark/use/eleMove.ts → src/directives/eleMove.ts

@@ -1,8 +1,8 @@
 export const vEleMoveDirective = {
 export const vEleMoveDirective = {
   mounted(el, { value, modifiers }) {
   mounted(el, { value, modifiers }) {
-    let [_x, _y] = [0, 0];
+    const [_x, _y] = [0, 0];
     // 只允许鼠标左键触发
     // 只允许鼠标左键触发
-    let moveHandle = function (e) {
+    const moveHandle = function (e) {
       if (e.button !== 0) return;
       if (e.button !== 0) return;
       if (modifiers.prevent) {
       if (modifiers.prevent) {
         e.preventDefault();
         e.preventDefault();
@@ -11,13 +11,13 @@ export const vEleMoveDirective = {
         e.stopPropagation();
         e.stopPropagation();
       }
       }
 
 
-      let left = e.pageX - _x;
-      let top = e.pageY - _y;
+      const left = e.pageX - _x;
+      const top = e.pageY - _y;
 
 
       value.moveElement({ left, top });
       value.moveElement({ left, top });
     };
     };
 
 
-    let upHandle = function (e) {
+    const upHandle = function (e) {
       if (e.button !== 0) return;
       if (e.button !== 0) return;
       if (modifiers.prevent) {
       if (modifiers.prevent) {
         e.preventDefault();
         e.preventDefault();

+ 1 - 1
src/features/arbitrate/Arbitrate.vue

@@ -39,7 +39,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted, watch, h } from "vue";
 import { onMounted, watch, h } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkHeader from "./MarkHeader.vue";
 import MarkHeader from "./MarkHeader.vue";
 import MarkTool from "../mark/MarkTool.vue";
 import MarkTool from "../mark/MarkTool.vue";
 import MarkBody from "./MarkBody.vue";
 import MarkBody from "./MarkBody.vue";

+ 1 - 1
src/features/arbitrate/ArbitrateMarkList.vue

@@ -33,7 +33,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { reactive, watch } from "vue";
 import { reactive, watch } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { getArbitrateList } from "@/api/arbitratePage";
 import { getArbitrateList } from "@/api/arbitratePage";
 import { MarkDetail } from "@/types";
 import { MarkDetail } from "@/types";
 
 

+ 3 - 3
src/features/arbitrate/MarkBody.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <CommonMarkBody
+  <MarkBodyBase
     :hasMarkResultToRender="true"
     :hasMarkResultToRender="true"
     :makeTrack="makeTrack"
     :makeTrack="makeTrack"
     @error="$emit('error')"
     @error="$emit('error')"
@@ -15,8 +15,8 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import CommonMarkBody from "@/features/mark/CommonMarkBody.vue";
-import { store } from "@/store/store";
+import MarkBodyBase from "@/features/mark/MarkBodyBase.vue";
+import { store } from "@/store/app";
 import CustomCursor from "custom-cursor.js";
 import CustomCursor from "custom-cursor.js";
 import { onMounted, onUnmounted, watch } from "vue";
 import { onMounted, onUnmounted, watch } from "vue";
 import { SliceImage, SpecialTag, Track } from "@/types";
 import { SliceImage, SpecialTag, Track } from "@/types";

+ 1 - 1
src/features/arbitrate/MarkHeader.vue

@@ -73,7 +73,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { doLogout } from "@/api/markPage";
 import { doLogout } from "@/api/markPage";
 
 
 const { isSingleStudent = false } = defineProps<{
 const { isSingleStudent = false } = defineProps<{

+ 4 - 4
src/features/check/CommonMarkBody.vue

@@ -95,18 +95,18 @@ import {
   watchEffect,
   watchEffect,
   nextTick,
   nextTick,
 } from "vue";
 } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkDrawTrack from "../mark/MarkDrawTrack.vue";
 import MarkDrawTrack from "../mark/MarkDrawTrack.vue";
 import type { SliceImage, SpecialTag, Track } from "@/types";
 import type { SliceImage, SpecialTag, Track } from "@/types";
 import { useTimers } from "@/setups/useTimers";
 import { useTimers } from "@/setups/useTimers";
 import { loadImage, randomCode, addHeaderTrackColorAttr } from "@/utils/utils";
 import { loadImage, randomCode, addHeaderTrackColorAttr } from "@/utils/utils";
-import { dragImage } from "../mark/use/draggable";
+import useDraggable from "../mark/composables/useDraggable";
 import MultiMediaMarkBody from "../mark/MultiMediaMarkBody.vue";
 import MultiMediaMarkBody from "../mark/MultiMediaMarkBody.vue";
 import "viewerjs/dist/viewer.css";
 import "viewerjs/dist/viewer.css";
 import Viewer from "viewerjs";
 import Viewer from "viewerjs";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
 import EventBus from "@/plugins/eventBus";
 import EventBus from "@/plugins/eventBus";
-import { vEleMoveDirective } from "../mark/use/eleMove";
+import { vEleMoveDirective } from "../../directives/eleMove";
 
 
 type MakeTrack = (
 type MakeTrack = (
   event: MouseEvent,
   event: MouseEvent,
@@ -137,7 +137,7 @@ const clickSpecialtag = (event: MouseEvent, item: SliceImage) => {
 };
 };
 
 
 //#region : 图片拖动。在轨迹模式下,仅当没有选择分数时可用。
 //#region : 图片拖动。在轨迹模式下,仅当没有选择分数时可用。
-const { dragContainer } = dragImage();
+const { dragContainer } = useDraggable();
 //#endregion : 图片拖动
 //#endregion : 图片拖动
 
 
 const { addTimeout } = useTimers();
 const { addTimeout } = useTimers();

+ 1 - 1
src/features/check/MarkBody.vue

@@ -34,7 +34,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted, onUnmounted, watch } from "vue";
 import { onMounted, onUnmounted, watch } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { SliceImage, SpecialTag, Track } from "@/types";
 import { SliceImage, SpecialTag, Track } from "@/types";
 import CustomCursor from "custom-cursor.js";
 import CustomCursor from "custom-cursor.js";
 import CommonMarkBody from "./CommonMarkBody.vue";
 import CommonMarkBody from "./CommonMarkBody.vue";

+ 1 - 1
src/features/check/ObjectiveAnswer.vue

@@ -191,7 +191,7 @@ import { message } from "ant-design-vue";
 import { onMounted, watch } from "vue";
 import { onMounted, watch } from "vue";
 import "viewerjs/dist/viewer.css";
 import "viewerjs/dist/viewer.css";
 import Viewer from "viewerjs";
 import Viewer from "viewerjs";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import ZoomPaper from "@/components/ZoomPaper.vue";
 import ZoomPaper from "@/components/ZoomPaper.vue";
 import { useTimers } from "@/setups/useTimers";
 import { useTimers } from "@/setups/useTimers";
 import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons-vue";
 import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons-vue";

+ 4 - 4
src/features/check/SubjectiveAnswer.vue

@@ -121,7 +121,7 @@ import {
   saveStudentSubjectiveCheck,
   saveStudentSubjectiveCheck,
 } from "@/api/checkPage";
 } from "@/api/checkPage";
 import { doLogout, updateUISetting, getSetting } from "@/api/markPage";
 import { doLogout, updateUISetting, getSetting } from "@/api/markPage";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkTool from "../mark/MarkTool.vue";
 import MarkTool from "../mark/MarkTool.vue";
 import MarkBody from "./MarkBody.vue";
 import MarkBody from "./MarkBody.vue";
 import MarkBoardTrack from "../mark/MarkBoardTrack.vue";
 import MarkBoardTrack from "../mark/MarkBoardTrack.vue";
@@ -133,9 +133,9 @@ import { message } from "ant-design-vue";
 import AnswerModal from "../mark/AnswerModal.vue";
 import AnswerModal from "../mark/AnswerModal.vue";
 import MinimapModal from "../mark/MinimapModal.vue";
 import MinimapModal from "../mark/MinimapModal.vue";
 import AllPaperModal from "../mark/AllPaperModal.vue";
 import AllPaperModal from "../mark/AllPaperModal.vue";
-import SheetViewModal from "../mark/SheetViewModal.vue";
-import SpecialTagModal from "../mark/SpecialTagModal.vue";
-import ShortCutModal from "../mark/ShortCutModal.vue";
+import SheetViewModal from "../mark/modals/SheetViewModal.vue";
+import SpecialTagModal from "../mark/modals/SpecialTagModal.vue";
+import ShortCutModal from "../mark/modals/ShortCutModal.vue";
 import MarkBoardTrackDialog from "../mark/MarkBoardTrackDialog.vue";
 import MarkBoardTrackDialog from "../mark/MarkBoardTrackDialog.vue";
 import vls from "@/utils/storage";
 import vls from "@/utils/storage";
 
 

+ 1 - 1
src/features/library/inspect/LibraryInspect.vue

@@ -18,7 +18,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted, watch } from "vue";
 import { onMounted, watch } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkHeader from "./MarkHeader.vue";
 import MarkHeader from "./MarkHeader.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";

+ 5 - 5
src/features/library/inspect/MarkBoardInspect.vue

@@ -119,14 +119,14 @@
 import type { Question } from "@/types";
 import type { Question } from "@/types";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
 import { reactive, watch } from "vue";
 import { reactive, watch } from "vue";
-import { store } from "@/store/store";
-import {
-  addFocusTrack,
-  removeFocusTrack,
-} from "@/features/mark/use/focusTracks";
+import { store } from "@/store/app";
+import useFocusTracks from "@/features/mark/composables/useFocusTracks";
 import ReviewReturnDialog from "./ReviewReturnDialog.vue";
 import ReviewReturnDialog from "./ReviewReturnDialog.vue";
 
 
 const emit = defineEmits(["inspect", "reject"]);
 const emit = defineEmits(["inspect", "reject"]);
+
+const { addFocusTrack, removeFocusTrack } = useFocusTracks();
+
 let checkedQuestions: Question[] = reactive([]);
 let checkedQuestions: Question[] = reactive([]);
 let reviewReturnVisible = $ref(false);
 let reviewReturnVisible = $ref(false);
 watch(
 watch(

+ 3 - 3
src/features/library/inspect/MarkBody.vue

@@ -1,11 +1,11 @@
 <template>
 <template>
-  <CommonMarkBody v-if="store" @error="$emit('error')" />
+  <MarkBodyBase v-if="store" @error="$emit('error')" />
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import CommonMarkBody from "@/features/mark/CommonMarkBody.vue";
+import MarkBodyBase from "@/features/mark/MarkBodyBase.vue";
 
 
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 
 
 defineEmits(["error"]);
 defineEmits(["error"]);
 </script>
 </script>

+ 1 - 1
src/features/library/inspect/MarkHeader.vue

@@ -12,7 +12,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";
 import { clearInspectedTaskOfLibraryInspect } from "@/api/libraryInspectPage";
 import { clearInspectedTaskOfLibraryInspect } from "@/api/libraryInspectPage";
 import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
 import CommonMarkHeader from "@/components/CommonMarkHeader.vue";

+ 1 - 1
src/features/library/libraryTrack/LibraryTrack.vue

@@ -10,7 +10,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted } from "vue";
 import { onMounted } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkHeader from "./MarkHeader.vue";
 import MarkHeader from "./MarkHeader.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";

+ 3 - 3
src/features/library/libraryTrack/MarkBody.vue

@@ -1,10 +1,10 @@
 <template>
 <template>
-  <CommonMarkBody v-if="store" @error="$emit('error')" />
+  <MarkBodyBase v-if="store" @error="$emit('error')" />
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import CommonMarkBody from "@/features/mark/CommonMarkBody.vue";
-import { store } from "@/store/store";
+import MarkBodyBase from "@/features/mark/MarkBodyBase.vue";
+import { store } from "@/store/app";
 
 
 defineEmits(["error"]);
 defineEmits(["error"]);
 </script>
 </script>

+ 3 - 3
src/features/library/quality/MarkBody.vue

@@ -1,11 +1,11 @@
 <template>
 <template>
-  <CommonMarkBody v-if="store" @error="$emit('error')" />
+  <MarkBodyBase v-if="store" @error="$emit('error')" />
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import CommonMarkBody from "@/features/mark/CommonMarkBody.vue";
+import MarkBodyBase from "@/features/mark/MarkBodyBase.vue";
 
 
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 
 
 defineEmits(["error"]);
 defineEmits(["error"]);
 </script>
 </script>

+ 1 - 1
src/features/library/quality/Quality.vue

@@ -19,7 +19,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted } from "vue";
 import { onMounted } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkHeader from "./MarkHeader.vue";
 import MarkHeader from "./MarkHeader.vue";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";
 import MarkBody from "./MarkBody.vue";
 import MarkBody from "./MarkBody.vue";

+ 43 - 400
src/features/mark/Mark.vue

@@ -24,14 +24,15 @@
       />
       />
     </div>
     </div>
   </div>
   </div>
-  <AnswerModal />
-  <PaperModal />
-  <MinimapModal />
-
-  <AllPaperModal />
-  <SheetViewModal />
-  <SpecialTagModal />
-  <ShortCutModal />
+  <!-- modal -->
+  <modal-answer />
+  <modal-paper />
+  <modal-minimap />
+  <modal-all-paper />
+  <modal-sheet-view />
+  <modal-special-tag />
+  <modal-short-cut />
+  <!-- other -->
   <MarkBoardTrackDialog
   <MarkBoardTrackDialog
     v-if="store.isTrackMode"
     v-if="store.isTrackMode"
     @submit="saveTaskToServer"
     @submit="saveTaskToServer"
@@ -55,150 +56,44 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { onMounted, watch, h, nextTick } from "vue";
-import {
-  clearMarkTask,
-  getGroup,
-  getSetting,
-  getStatus,
-  getTask,
-  saveTask,
-  updateUISetting,
-  doUnselectiveType,
-} from "@/api/markPage";
-import { store } from "@/store/store";
+import { onMounted } from "vue";
+import { useTimers } from "@/setups/useTimers";
+import { getHistoryTask } from "@/api/markPage";
+
+// components
 import MarkHeader from "./MarkHeader.vue";
 import MarkHeader from "./MarkHeader.vue";
 import MarkTool from "./MarkTool.vue";
 import MarkTool from "./MarkTool.vue";
 import MarkBody from "./MarkBody.vue";
 import MarkBody from "./MarkBody.vue";
-import { useTimers } from "@/setups/useTimers";
 import MarkHistory from "./MarkHistory.vue";
 import MarkHistory from "./MarkHistory.vue";
-import MarkBoardTrack from "./MarkBoardTrack.vue";
-import type { Question, Task } from "@/types";
-import MarkBoardKeyBoard from "./MarkBoardKeyBoard.vue";
-import MarkBoardMouse from "./MarkBoardMouse.vue";
-import { debounce, isEmpty, isNumber } from "lodash-es";
-import { message, Modal } from "ant-design-vue";
-import AnswerModal from "./AnswerModal.vue";
-import PaperModal from "./PaperModal.vue";
-import MinimapModal from "./MinimapModal.vue";
-import AllPaperModal from "./AllPaperModal.vue";
-import SheetViewModal from "./SheetViewModal.vue";
-import SpecialTagModal from "./SpecialTagModal.vue";
-import ShortCutModal from "./ShortCutModal.vue";
-import { processSliceUrls } from "@/utils/utils";
-import { getPaper } from "@/api/jsonMark";
-import EventBus from "@/plugins/eventBus";
-import { getHistoryTask } from "@/api/markPage";
-import MarkBoardTrackDialog from "./MarkBoardTrackDialog.vue";
-
-const { addInterval, addTimeout } = useTimers();
-
-//#region status spinning
-/** 试评、正评的页面提示 */
-let statusSpinning = $ref(true);
-/** 是否还在加载statusValue */
-let loadingStatusSpinning = $ref(true);
-
-watch(
-  () => store.setting.statusValue,
-  () => {
-    if (store.setting.statusValue) {
-      loadingStatusSpinning = false;
-      addTimeout(() => (statusSpinning = false), 3000);
-    }
-  }
-);
-
-//#endregion
-
-async function updateMarkTask() {
-  await clearMarkTask();
-}
-
-async function updateSetting() {
-  const settingRes = await getSetting();
-  // 初次使用时,重置并初始化uisetting
-  if (isEmpty(settingRes.data.uiSetting)) {
-    settingRes.data.uiSetting = {
-      "answer.paper.scale": 1,
-      "score.board.collapse": false,
-      "normal.mode": "keyboard",
-      "score.fontSize.scale": 1,
-      "paper.modal": false,
-      "answer.modal": false,
-      "minimap.modal": false,
-      "specialTag.modal": false,
-      "shortCut.modal": false,
-    };
-  } else {
-    settingRes.data.uiSetting = JSON.parse(settingRes.data.uiSetting);
-  }
-  settingRes.data.sheetConfig = settingRes.data.sheetConfig
-    ? JSON.parse(settingRes.data.sheetConfig)
-    : [];
-  store.setting = settingRes.data;
-  if (store.setting.subject?.paperUrl && store.isMultiMedia) {
-    await getPaper(store);
-  }
-}
-
-async function updateStatus() {
-  const res = await getStatus();
-  Object.assign(store.status, res.data);
-}
-async function updateGroups() {
-  const res = await getGroup();
-  store.groups = res.data;
-}
-
-let preDrawing = false;
-async function updateTask() {
-  const res = await getTask();
-  if (res.data?.taskId) {
-    const newTask = res.data;
-    newTask.sheetUrls = newTask.sheetUrls || [];
-    // newTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
-    try {
-      preDrawing = true;
-      newTask.sliceUrls = await processSliceUrls(newTask);
-    } finally {
-      preDrawing = false;
-    }
-
-    store.tasks.push(newTask);
-    if (!store.historyOpen) {
-      // 在正评中,才能替换task
-      if (store.currentTask?.studentId !== store.tasks[0].studentId)
-        store.currentTask = store.tasks[0];
-    }
-
-    // 如果是评完后,再取到的任务,则此时要更新一下status
-    if (store.status.totalCount - store.status.markedCount === 0) {
-      await updateStatus();
-    }
-
-    // prefetch sliceUrls image
-    if (store.tasks.length > 1) {
-      // 如果不是当前任务,则先等3秒再去取任务,以免和其他请求争夺网络资源
-      await new Promise((resolve) => setTimeout(resolve, 3000));
-    }
-  } else {
-    store.message = res.message || "数据错误";
-  }
-  if (!res.data && store.message === "成功") store.message = "当前无评卷任务";
-}
-
-function nextTask() {
-  // 正在预绘制中,则不要再取任务,以免拖慢当前任务的绘制
-  if (!preDrawing) {
-    if (store.tasks.length < (store.setting.prefetchCount ?? 3)) {
-      // 回看打开时,停止取任务
-      if (!store.historyOpen)
-        return updateTask().catch((e) => console.log("定时获取任务出错", e));
-    }
-  }
-}
 
 
+// scoring
+import MarkBoardTrack from "./scoring/MarkBoardTrack.vue";
+import MarkBoardKeyBoard from "./scoring/MarkBoardKeyBoard.vue";
+import MarkBoardMouse from "./scoring/MarkBoardMouse.vue";
+import MarkBoardTrackDialog from "./scoring/MarkBoardTrackDialog.vue";
+
+// modals
+import ModalAnswer from "./modals/ModalAnswer.vue";
+import ModalPaper from "./modals/ModalPaper.vue";
+import ModalMinimap from "./modals/ModalMinimap.vue";
+import ModalAllPaper from "./modals/ModalAllPaper.vue";
+import ModalSheetView from "./modals/ModalSheetView.vue";
+import ModalSpecialTag from "./modals/ModalSpecialTag.vue";
+import ModalShortCut from "./modals/ModalShortCut.vue";
+
+// composables
+import useMarkTask from "./composables/useMarkTask";
+import useSetting from "./composables/useSetting";
+import useStatus from "./composables/useStatus";
+import useMarkSubmit from "./composables/useMarkSubmit";
+
+const { updateMarkTask, nextTask, removeBrokenTask } = useMarkTask();
+const { updateSetting } = useSetting();
+const { statusSpinning, loadingStatusSpinning, updateStatus, updateGroups } =
+  useStatus();
+const { saveTaskToServer, allZeroSubmit, unselectiveSubmit } = useMarkSubmit();
+
+const { addInterval } = useTimers();
 onMounted(async () => {
 onMounted(async () => {
   let result = true;
   let result = true;
   try {
   try {
@@ -208,7 +103,7 @@ onMounted(async () => {
     await updateGroups();
     await updateGroups();
     await nextTask();
     await nextTask();
   } catch (error) {
   } catch (error) {
-    loadingStatusSpinning = false;
+    loadingStatusSpinning.value = false;
     result = false;
     result = false;
   }
   }
 
 
@@ -217,258 +112,6 @@ onMounted(async () => {
   // 5秒更新一次tasks
   // 5秒更新一次tasks
   addInterval(nextTask, 5 * 1000);
   addInterval(nextTask, 5 * 1000);
 });
 });
-
-const __debounceUpdate = debounce(() => {
-  updateUISetting(store.setting.mode, store.setting.uiSetting).catch((e) =>
-    console.log("保存设置出错", e)
-  );
-}, 3000);
-watch(
-  () => [store.setting.uiSetting, store.setting.mode],
-  () => {
-    __debounceUpdate();
-  },
-  { deep: true }
-);
-
-const showRejectedReason = (task: Task) => {
-  if (task.rejected && task.rejectReason) {
-    const [reasonType, reasonDesc] = task.rejectReason.split(":");
-    Modal.info({
-      title: null,
-      closable: false,
-      maskClosable: false,
-      centered: true,
-      icon: null,
-      okText: "知道了",
-      wrapClassName: "custom-modal-info",
-      zIndex: 9999,
-      bodyStyle: {
-        padding: "15px",
-      },
-      content: () =>
-        h(
-          "div",
-          {
-            style: {
-              fontSize: "14px",
-              color: "var(--app-main-text-color)",
-            },
-          },
-          [
-            h("div", { style: { marginBottom: "8px" } }, [
-              h(
-                "span",
-                { style: { fontWeight: "bold", marginRight: "0.5em" } },
-                "打回原因: "
-              ),
-              h("span", reasonType),
-            ]),
-            h("div", { style: { fontWeight: "bold" } }, "详情描述: "),
-            h("div", { style: { padding: "4px 2px" } }, reasonDesc),
-            h("div", { style: { marginBottom: "8px" } }, [
-              h(
-                "span",
-                { style: { fontWeight: "bold", marginRight: "0.5em" } },
-                "给分记录: "
-              ),
-              h("span", task.rejectScoreList),
-            ]),
-          ]
-        ),
-    });
-  }
-};
-
-// 切换currentTask
-watch(
-  () => store.currentTask,
-  () => {
-    // 重置当前选择的quesiton和score
-    store.currentQuestion = undefined;
-    store.currentScore = undefined;
-    void nextTick(() => {
-      if (store.currentTask) {
-        showRejectedReason(store.currentTask);
-      }
-    });
-  }
-);
-
-const removeBrokenTask = () => {
-  store.currentTask = undefined;
-
-  if (store.historyOpen) {
-    // store.currentTask = store.historyTasks[0];
-    // 避免无限加载
-    store.message = "加载失败,请重新选择。";
-  } else {
-    store.tasks.shift();
-  }
-};
-
-const allZeroSubmit = async () => {
-  const markResult = store.currentTask?.markResult;
-  if (!markResult) return;
-
-  const { markerScore, scoreList, trackList, specialTagList } = markResult;
-  markResult.markerScore = 0;
-  const ss = new Array(store.currentTaskEnsured.questionList.length);
-  markResult.scoreList = ss.fill(0);
-  markResult.trackList = [];
-
-  try {
-    await saveTaskToServer();
-  } catch (error) {
-    console.log("error restore");
-  } finally {
-    // console.log({ markerScore, scoreList, trackList });
-    markResult.markerScore = markerScore;
-    markResult.scoreList = scoreList;
-    markResult.trackList = trackList;
-    markResult.specialTagList = specialTagList;
-  }
-};
-
-const unselectiveSubmit = async () => {
-  const markResult = store.currentTask?.markResult;
-  if (!markResult) return;
-
-  try {
-    const res = await doUnselectiveType();
-    if (res?.data.success) {
-      void message.success({ content: "未选做处理成功", duration: 3 });
-      if (!store.historyOpen) {
-        store.currentTask = undefined;
-        store.tasks.shift();
-        store.currentTask = store.tasks[0];
-      }
-      if (store.historyOpen) {
-        EventBus.emit("should-reload-history");
-      }
-      await updateStatus();
-    } else {
-      void message.error({ content: res?.data.message || "错误", duration: 5 });
-    }
-  } catch (error) {
-    console.log("未选做处理失败", error);
-    void message.error({ content: "网络异常", duration: 5 });
-    await new Promise((res) => setTimeout(res, 1500));
-    window.location.reload();
-  }
-};
-
-const saveTaskToServer = async () => {
-  if (!store.currentTask) return;
-  const markResult = store.currentTask.markResult;
-  if (!markResult) return;
-
-  const mkey = "save_task_key";
-
-  type SubmitError = {
-    question: Question;
-    index: number;
-    error: string;
-  };
-
-  const errors: SubmitError[] = [];
-  markResult.scoreList.forEach((score, index) => {
-    if (!store.currentTask) return;
-    const question = store.currentTask.questionList[index]!;
-    let error;
-    if (!isNumber(score)) {
-      error = `${question.mainNumber}-${question.subNumber}${
-        question.questionName ? "(" + question.questionName + ")" : ""
-      } 没有给分,不能提交。`;
-    } else if (isNumber(question.maxScore) && score > question.maxScore) {
-      error = `${question.mainNumber}-${question.subNumber}${
-        question.questionName ? "(" + question.questionName + ")" : ""
-      } 给分大于最高分不能提交。`;
-    } else if (isNumber(question.minScore) && score < question.minScore) {
-      error = `${question.mainNumber}-${question.subNumber}${
-        question.questionName ? "(" + question.questionName + ")" : ""
-      } 给分小于最低分不能提交。`;
-    }
-    if (error) {
-      errors.push({ question, index, error });
-    }
-  });
-  if (errors.length !== 0) {
-    console.log(errors);
-    const msg = errors.map((v) => h("div", `${v.error}`));
-    void message.warning({
-      content: h("span", ["校验失败", ...msg]),
-      duration: 10,
-      key: mkey,
-    });
-    return;
-  }
-
-  if (
-    markResult.scoreList.length !== store.currentTask.questionList.length ||
-    !markResult.scoreList.every((s) => isNumber(s))
-  ) {
-    console.error({ content: "markResult格式不正确,缺少分数", key: mkey });
-    return;
-  }
-
-  if (!store.isTrackMode) {
-    markResult.trackList = [];
-  } else {
-    const trackScores =
-      markResult.trackList
-        .map((t) => Math.round((t.score || 0) * 100))
-        .reduce((acc, s) => acc + s, 0) / 100;
-    if (trackScores !== markResult.markerScore) {
-      void message.error({
-        content: "轨迹分与总分不一致,请检查。",
-        duration: 3,
-        key: mkey,
-      });
-      return;
-    }
-  }
-  if (store.setting.forceSpecialTag) {
-    if (
-      markResult.trackList.length === 0 &&
-      markResult.specialTagList.length === 0
-    ) {
-      void message.error({
-        content: "强制标记已开启,请至少使用一个标记。",
-        duration: 5,
-        key: mkey,
-      });
-      return;
-    }
-  }
-  console.log("save task to server");
-  void message.loading({ content: "保存评卷任务...", key: mkey });
-  const res = await saveTask();
-  if (!res) return;
-  // 故意不在此处同步等待,因为不必等待
-  updateStatus().catch((e) => console.log("保存任务后获取status出错", e));
-  if (res.data.success && store.currentTask) {
-    void message.success({ content: "保存成功", key: mkey, duration: 2 });
-    if (!store.historyOpen) {
-      store.currentTask = undefined;
-      store.tasks.shift();
-      store.currentTask = store.tasks[0];
-    } else {
-      EventBus.emit("should-reload-history");
-    }
-  } else if (!res.data.success) {
-    void message.error({
-      content: "提交失败,请刷新页面",
-      key: mkey,
-      duration: 10,
-    });
-    return;
-  } else if (!store.currentTask) {
-    void message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
-  }
-  // 获取下一个任务
-  void nextTask();
-};
 </script>
 </script>
 
 
 <style>
 <style>

+ 8 - 319
src/features/mark/MarkBody.vue

@@ -1,329 +1,18 @@
 <template>
 <template>
-  <CommonMarkBody
-    :hasMarkResultToRender="true"
+  <MarkBodyBase
+    hasMarkResultToRender
     :makeTrack="makeTrack"
     :makeTrack="makeTrack"
     @error="$emit('error')"
     @error="$emit('error')"
   />
   />
-  <div class="cursor">
-    <div class="cursor-border">
-      <span
-        v-if="store.currentSpecialTagType === 'TEXT'"
-        class="text text-edit"
-      ></span>
-      <span
-        v-else-if="
-          store.currentSpecialTagType === 'LINE' ||
-          store.currentSpecialTagType === 'CIRCLE'
-        "
-        class="point"
-      >
-      </span>
-      <span v-else-if="store.currentSpecialTagType === 'RIGHT'" class="text">
-        <CheckOutlined />
-      </span>
-      <span v-else-if="store.currentSpecialTag" class="text">
-        {{ store.currentSpecialTag }}
-      </span>
-      <span v-else class="text">{{
-        Object.is(store.currentScore, -0) ? "空" : store.currentScore
-      }}</span>
-    </div>
-  </div>
-  <!-- <MarkBody /> -->
+  <MarkBodyCursor />
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { onMounted, onUnmounted, watch } from "vue";
-import { store } from "@/store/store";
-import { SliceImage, SpecialTag, Track } from "@/types";
-import CustomCursor from "custom-cursor.js";
-import CommonMarkBody from "./CommonMarkBody.vue";
-import { CheckOutlined } from "@ant-design/icons-vue";
+import MarkBodyBase from "./MarkBodyBase.vue";
+import useMakeTrack from "./composables/useMakeTrack";
+import MarkBodyCursor from "./MarkBodyCursor.vue";
 
 
-// import { message } from "ant-design-vue";
-// 开启本组件,测试后台在整卷的还原效果
-// import MarkBody from "@/features/student/studentInspect/MarkBody.vue";
+defineEmits(["error"]);
 
 
-defineEmits(["error", "allZeroSubmit"]);
-
-const makeScoreTrack = (
-  event: MouseEvent,
-  item: SliceImage,
-  maxSliceWidth: number,
-  theFinalHeight: number
-) => {
-  // console.log(item);
-  if (!store.currentQuestion || typeof store.currentScore === "undefined")
-    return;
-  const target = event.target as HTMLImageElement;
-  const track: Track = {
-    mainNumber: store.currentQuestion?.mainNumber,
-    subNumber: store.currentQuestion?.subNumber,
-    score: store.currentScore,
-    unanswered: Object.is(store.currentScore, -0),
-    offsetIndex: item.indexInSliceUrls,
-    // 新增轨迹时会校验轨迹是否距离边缘过近,使用round产生的误差可以忽略,不会发生超出边界的问题
-    offsetX: Math.round(
-      event.offsetX * (target.naturalWidth / target.width) + item.dx
-    ),
-    offsetY: Math.round(
-      event.offsetY * (target.naturalHeight / target.height) + item.dy
-    ),
-    positionX: -1,
-    positionY: -1,
-    number: -1,
-  };
-  track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
-  track.positionY =
-    (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
-
-  // const isIllegalRange = (testNum: number, min: number, max: number) => {
-  //   return testNum < min || testNum > max;
-  // };
-
-  // // 检测有问题,此处没有给原图的宽高,如果有的话,要稍微修改下数据类型
-  // // 但其实下面也做了一个基本检测
-  // if (
-  //   isIllegalRange(track.offsetX, 0, target.naturalWidth) ||
-  //   isIllegalRange(track.offsetY, 0, target.naturalHeight) ||
-  //   isIllegalRange(track.positionX, 0, 1) ||
-  //   isIllegalRange(track.positionY, 0, 1)
-  // ) {
-  //   console.error(
-  //     "错误的track",
-  //     track,
-  //     target.naturalWidth,
-  //     target.naturalHeight
-  //   );
-  //   void message.error("系统错误,请联系管理员!");
-  // }
-
-  if (track.offsetX > item.effectiveWidth + item.dx) {
-    console.log("不在有效宽度内,轨迹不生效");
-    return;
-  }
-  if (
-    item.trackList.some((t) => {
-      return (
-        Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
-          Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
-        500
-      );
-    })
-  ) {
-    console.log("两个轨迹相距过近");
-    return;
-  }
-  // 是否保留当前的轨迹分
-  const questionScore =
-    store.currentTask &&
-    store.currentQuestion &&
-    store.currentTask.markResult.scoreList[store.currentQuestion.__index];
-  const ifKeepScore =
-    Math.round(
-      store.currentQuestion.maxScore * 1000 -
-        (questionScore || 0) * 1000 -
-        store.currentScore * 2 * 1000
-    ) / 1000;
-  if (ifKeepScore < 0 && store.currentScore > 0) {
-    store.currentScore = undefined;
-  }
-  const markResult = store.currentTaskEnsured.markResult;
-  const maxNumber =
-    markResult.trackList.length === 0
-      ? 0
-      : Math.max(...markResult.trackList.map((t) => t.number));
-  track.number = maxNumber + 1;
-  // console.log(
-  //   maxNumber,
-  //   track.number,
-  //   markResult.trackList.map((t) => t.number),
-  //   Math.max(...markResult.trackList.map((t) => t.number))
-  // );
-  markResult.trackList = [...markResult.trackList, track];
-  const { __index, mainNumber, subNumber } = store.currentQuestion;
-  markResult.scoreList[__index] =
-    markResult.trackList
-      .filter((t) => t.mainNumber === mainNumber && t.subNumber === subNumber)
-      .map((t) => t.score)
-      .reduce((acc, v) => (acc += Math.round(v * 1000)), 0) / 1000;
-  item.trackList.push(track);
-};
-
-const makeSpecialTagTrack = (
-  event: MouseEvent,
-  item: SliceImage,
-  maxSliceWidth: number,
-  theFinalHeight: number
-) => {
-  // console.log(item);
-  if (!store.currentTask || typeof store.currentSpecialTag === "undefined")
-    return;
-
-  const target = event.target as HTMLImageElement;
-  const track: SpecialTag = {
-    tagName: store.currentSpecialTag,
-    tagType: store.currentSpecialTagType,
-    offsetIndex: item.indexInSliceUrls,
-    offsetX: Math.round(
-      event.offsetX * (target.naturalWidth / target.width) + item.dx
-    ),
-    offsetY: Math.round(
-      event.offsetY * (target.naturalHeight / target.height) + item.dy
-    ),
-    positionX: -1,
-    positionY: -1,
-  };
-  track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
-  track.positionY =
-    (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
-
-  if (
-    item.tagList.some((t) => {
-      return (
-        Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
-          Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
-        500
-      );
-    })
-  ) {
-    console.log("两个轨迹相距过近");
-    return;
-  }
-  store.currentTaskEnsured.markResult.specialTagList.push(track);
-  item.tagList.push(track);
-};
-
-const makeTrack = (
-  event: MouseEvent,
-  item: SliceImage,
-  maxSliceWidth: number,
-  theFinalHeight: number
-) => {
-  if (store.currentSpecialTagType) {
-    makeSpecialTagTrack(event, item, maxSliceWidth, theFinalHeight);
-    if (store.currentSpecialTagType === "TEXT") {
-      store.currentSpecialTag = undefined;
-      store.currentSpecialTagType = undefined;
-    }
-  } else {
-    makeScoreTrack(event, item, maxSliceWidth, theFinalHeight);
-  }
-};
-
-watch(
-  () => store.setting.mode,
-  () => {
-    const shouldHide = store.setting.mode === "COMMON";
-    if (shouldHide) {
-      // console.log("hide cursor", theCursor);
-      // eslint-disable-next-line @typescript-eslint/no-unsafe-call
-      theCursor && theCursor.destroy();
-    } else {
-      if (document.querySelector(".cursor")) {
-        // console.log("show cursor", theCursor);
-        // theCursor && theCursor.enable();
-        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
-        theCursor = new CustomCursor(".cursor", {
-          focusElements: [
-            {
-              selector: ".mark-body-container",
-              focusClass: "cursor--focused-view",
-            },
-          ],
-        }).initialize();
-      }
-    }
-  }
-);
-let theCursor = null as any;
-onMounted(() => {
-  if (store.isTrackMode) {
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-call
-    theCursor = new CustomCursor(".cursor", {
-      focusElements: [
-        {
-          selector: ".mark-body-container",
-          focusClass: "cursor--focused-view",
-        },
-      ],
-    }).initialize();
-  }
-});
-
-onUnmounted(() => {
-  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
-  theCursor && theCursor.destroy();
-});
+const { makeTrack } = useMakeTrack();
 </script>
 </script>
-
-<style scoped>
-.cursor {
-  color: #ff5050;
-  display: none;
-  pointer-events: none;
-  user-select: none;
-  top: 0;
-  left: 0;
-  position: fixed;
-  will-change: transform;
-  z-index: 1000;
-}
-
-.cursor-border {
-  position: absolute;
-  box-sizing: border-box;
-  align-items: center;
-  border: 1px solid #ff5050;
-  border-radius: 50%;
-  display: flex;
-  justify-content: center;
-  height: 0px;
-  width: 0px;
-  left: 0;
-  top: 0;
-  transform: translate(-50%, -50%);
-  transition: all 360ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-
-.cursor.cursor--initialized {
-  display: block;
-}
-
-.cursor .text {
-  font-size: 2rem;
-  opacity: 0;
-  transition: opacity 80ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.cursor .point {
-  display: inline-block;
-  vertical-align: middle;
-  width: 4px;
-  height: 4px;
-  border-radius: 50%;
-  background: red;
-}
-
-.cursor .text-edit {
-  display: inline-block;
-  vertical-align: middle;
-  width: 0;
-  height: 20px;
-  border-left: 2px solid #ff5050;
-}
-
-.cursor.cursor--off-screen {
-  opacity: 0;
-}
-
-.cursor.cursor--focused .cursor-border,
-.cursor.cursor--focused-view .cursor-border {
-  width: 90px;
-  height: 90px;
-}
-
-.cursor.cursor--focused-view .text {
-  opacity: 1;
-  transition: opacity 360ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-</style>

+ 4 - 4
src/features/mark/CommonMarkBody.vue → src/features/mark/MarkBodyBase.vue

@@ -101,7 +101,7 @@ import {
   watch,
   watch,
   watchEffect,
   watchEffect,
 } from "vue";
 } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkDrawTrack from "./MarkDrawTrack.vue";
 import MarkDrawTrack from "./MarkDrawTrack.vue";
 import type { SliceImage, SpecialTag } from "@/types";
 import type { SliceImage, SpecialTag } from "@/types";
 import { useTimers } from "@/setups/useTimers";
 import { useTimers } from "@/setups/useTimers";
@@ -111,13 +111,13 @@ import {
   loadImage,
   loadImage,
   randomCode,
   randomCode,
 } from "@/utils/utils";
 } from "@/utils/utils";
-import { dragImage } from "./use/draggable";
+import useDraggable from "./composables/useDraggable";
 import MultiMediaMarkBody from "./MultiMediaMarkBody.vue";
 import MultiMediaMarkBody from "./MultiMediaMarkBody.vue";
 import "viewerjs/dist/viewer.css";
 import "viewerjs/dist/viewer.css";
 import Viewer from "viewerjs";
 import Viewer from "viewerjs";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
 import EventBus from "@/plugins/eventBus";
 import EventBus from "@/plugins/eventBus";
-import { vEleMoveDirective } from "./use/eleMove";
+import { vEleMoveDirective } from "../../directives/eleMove";
 
 
 type MakeTrack = (
 type MakeTrack = (
   event: MouseEvent,
   event: MouseEvent,
@@ -137,7 +137,7 @@ const {
 const emit = defineEmits(["error"]);
 const emit = defineEmits(["error"]);
 
 
 //#region : 图片拖动。在轨迹模式下,仅当没有选择分数时可用。
 //#region : 图片拖动。在轨迹模式下,仅当没有选择分数时可用。
-const { dragContainer } = dragImage();
+const { dragContainer } = useDraggable();
 //#endregion : 图片拖动
 //#endregion : 图片拖动
 
 
 const { addTimeout } = useTimers();
 const { addTimeout } = useTimers();

+ 155 - 0
src/features/mark/MarkBodyCursor.vue

@@ -0,0 +1,155 @@
+<template>
+  <div class="cursor">
+    <div class="cursor-border">
+      <span
+        v-if="markStore.currentSpecialTagType === 'TEXT'"
+        class="text text-edit"
+      ></span>
+      <span
+        v-else-if="
+          markStore.currentSpecialTagType === 'LINE' ||
+          markStore.currentSpecialTagType === 'CIRCLE'
+        "
+        class="point"
+      >
+      </span>
+      <span
+        v-else-if="markStore.currentSpecialTagType === 'RIGHT'"
+        class="text"
+      >
+        <CheckOutlined />
+      </span>
+      <span v-else-if="markStore.currentSpecialTag" class="text">
+        {{ markStore.currentSpecialTag }}
+      </span>
+      <span v-else class="text">{{
+        Object.is(markStore.currentScore, -0) ? "空" : markStore.currentScore
+      }}</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { watch, onMounted, onUnmounted } from "vue";
+import { useMarkStore } from "@/store";
+import { CheckOutlined } from "@ant-design/icons-vue";
+import CustomCursor from "custom-cursor.js";
+
+const markStore = useMarkStore();
+
+let theCursor = null as any;
+
+watch(
+  () => markStore.setting.mode,
+  () => {
+    const shouldHide = markStore.setting.mode === "COMMON";
+    if (shouldHide) {
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+      theCursor && theCursor.destroy();
+      return;
+    }
+
+    if (document.querySelector(".cursor")) {
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+      theCursor = new CustomCursor(".cursor", {
+        focusElements: [
+          {
+            selector: ".mark-body-container",
+            focusClass: "cursor--focused-view",
+          },
+        ],
+      }).initialize();
+    }
+  }
+);
+
+onMounted(() => {
+  if (markStore.isTrackMode) {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+    theCursor = new CustomCursor(".cursor", {
+      focusElements: [
+        {
+          selector: ".mark-body-container",
+          focusClass: "cursor--focused-view",
+        },
+      ],
+    }).initialize();
+  }
+});
+
+onUnmounted(() => {
+  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+  theCursor && theCursor.destroy();
+});
+</script>
+
+<style scoped>
+.cursor {
+  color: #ff5050;
+  display: none;
+  pointer-events: none;
+  user-select: none;
+  top: 0;
+  left: 0;
+  position: fixed;
+  will-change: transform;
+  z-index: 1000;
+}
+
+.cursor-border {
+  position: absolute;
+  box-sizing: border-box;
+  align-items: center;
+  border: 1px solid #ff5050;
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  height: 0px;
+  width: 0px;
+  left: 0;
+  top: 0;
+  transform: translate(-50%, -50%);
+  transition: all 360ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+
+.cursor.cursor--initialized {
+  display: block;
+}
+
+.cursor .text {
+  font-size: 2rem;
+  opacity: 0;
+  transition: opacity 80ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.cursor .point {
+  display: inline-block;
+  vertical-align: middle;
+  width: 4px;
+  height: 4px;
+  border-radius: 50%;
+  background: red;
+}
+
+.cursor .text-edit {
+  display: inline-block;
+  vertical-align: middle;
+  width: 0;
+  height: 20px;
+  border-left: 2px solid #ff5050;
+}
+
+.cursor.cursor--off-screen {
+  opacity: 0;
+}
+
+.cursor.cursor--focused .cursor-border,
+.cursor.cursor--focused-view .cursor-border {
+  width: 90px;
+  height: 90px;
+}
+
+.cursor.cursor--focused-view .text {
+  opacity: 1;
+  transition: opacity 360ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+</style>

+ 1 - 1
src/features/mark/MarkDrawTrack.vue

@@ -62,7 +62,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import type { SpecialTag, Track } from "@/types";
 import type { SpecialTag, Track } from "@/types";
 import { toRefs, watch, nextTick, computed } from "vue";
 import { toRefs, watch, nextTick, computed } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
 import { CheckOutlined } from "@ant-design/icons-vue";
 import { CheckOutlined } from "@ant-design/icons-vue";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";

+ 1 - 1
src/features/mark/MarkHistory.vue

@@ -80,7 +80,7 @@ import type {
   Task,
   Task,
 } from "@/types";
 } from "@/types";
 import { watch } from "vue";
 import { watch } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import EventBus from "@/plugins/eventBus";
 import EventBus from "@/plugins/eventBus";
 import { preDrawImageHistory } from "@/utils/utils";
 import { preDrawImageHistory } from "@/utils/utils";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";

+ 1 - 1
src/features/mark/MultiMediaMarkBody.vue

@@ -69,7 +69,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { getStudentAnswerJSON } from "@/api/jsonMark";
 import { getStudentAnswerJSON } from "@/api/jsonMark";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { onUpdated, watch } from "vue";
 import { onUpdated, watch } from "vue";
 import { renderRichText } from "@/utils/renderJSON";
 import { renderRichText } from "@/utils/renderJSON";
 import type { RichTextJSON, QuestionForRender } from "@/types";
 import type { RichTextJSON, QuestionForRender } from "@/types";

+ 62 - 0
src/features/mark/composables/useAutoChooseFirstQuestion.ts

@@ -0,0 +1,62 @@
+import { Question } from "@/types";
+import useMarkStore from "@/store";
+import { watch } from "vue";
+
+/** chooseQuestion 当currentTask改变是,自动选择第一题 */
+export default function useAutoChooseFirstQuestion() {
+  const markStore = useMarkStore();
+
+  watch(
+    () => markStore.currentTask,
+    () => {
+      // FIXed ME: 此时取到的还是score:null,但是 chooseQuestion之后就变成了score:0
+      const firstQuestion = markStore.currentTask?.questionList[0];
+      if (firstQuestion) {
+        chooseQuestion(firstQuestion);
+      }
+    }
+  );
+
+  const scrollToQuestionOfBoard = async (question: Question) => {
+    const node = document.querySelector(
+      `#bq-${question.mainNumber}-${question.subNumber}`
+    );
+    const questionNode = document.querySelector(
+      `#q-${question.mainNumber}-${question.subNumber}`
+    );
+    if (!questionNode) {
+      // 非多媒体阅卷
+      node && node.scrollIntoView({ block: "center", behavior: "smooth" });
+      return;
+    }
+    // console.log(node);
+    // node && node.scrollIntoView({ behavior: "smooth" });
+    // if (node) node.scrollBy({ top: -50 });
+    // setTimeout(() => {
+    //   if (node) node.parentElement?.scrollTo({ top: 50, left: 0 });
+    //   // node && node.scrollTop = 50//node.scrollIntoView({ behavior: "auto", block: "center" });
+    //   if(node.)
+    // }, 1500);
+    async function checkIfEleMoving(ele: Element) {
+      const { top: oldTop } = ele.getBoundingClientRect();
+      await new Promise((res) => setTimeout(res, 200));
+      // console.log(ele.getBoundingClientRect().top, oldTop);
+      return ele.getBoundingClientRect().top - oldTop !== 0;
+    }
+    if (questionNode) {
+      let isMoving = await checkIfEleMoving(questionNode);
+      while (isMoving) {
+        isMoving = await checkIfEleMoving(questionNode);
+      }
+      node && node.scrollIntoView({ block: "center", behavior: "smooth" });
+    }
+  };
+
+  function chooseQuestion(question: Question) {
+    store.currentQuestion = question;
+    // FIXME: maybe should be an async function, temp fix for eslint
+    void scrollToQuestionOfBoard(question);
+  }
+
+  return { chooseQuestion };
+}

+ 17 - 19
src/features/mark/use/splitPane.ts → src/features/mark/composables/useDragSplitPane.ts

@@ -1,22 +1,20 @@
-import { onMounted, onUnmounted, watchEffect } from "vue";
+import { onMounted, onUnmounted, watchEffect, ref } from "vue";
 
 
-export function dragSplitPane() {
-  let pos = { y: 0 };
-  const dragSpliter = $ref<HTMLDivElement>();
-  let areaHeight = $ref(0);
-  let initAreaHeight = 0;
+export default function useDragSplitPane() {
+  const pos = { y: 0 };
+  const dragSpliter = ref<HTMLDivElement>();
+  const areaHeight = ref(0);
+  const initAreaHeight = ref(0);
   const minAreaHeight = 74;
   const minAreaHeight = 74;
   let maxAreaHeight = null;
   let maxAreaHeight = null;
 
 
   const mouseDownHandler = function (e: MouseEvent) {
   const mouseDownHandler = function (e: MouseEvent) {
     // console.log(e);
     // console.log(e);
-    if (e.target !== dragSpliter) return;
+    if (e.target !== dragSpliter.value) return;
     e.preventDefault();
     e.preventDefault();
-    pos = {
-      y: e.clientY,
-    };
+    pos.y = e.clientY;
 
 
-    const parentDom = dragSpliter.parentElement.parentElement;
+    const parentDom = dragSpliter.value.parentElement.parentElement;
     let sumHeight = 0;
     let sumHeight = 0;
     for (let i = 0; i < parentDom.childNodes.length; i++) {
     for (let i = 0; i < parentDom.childNodes.length; i++) {
       const dom = parentDom.childNodes[i];
       const dom = parentDom.childNodes[i];
@@ -29,9 +27,9 @@ export function dragSplitPane() {
       }
       }
     }
     }
     maxAreaHeight = parentDom.offsetHeight - sumHeight - 16 * 4 - 68;
     maxAreaHeight = parentDom.offsetHeight - sumHeight - 16 * 4 - 68;
-    initAreaHeight =
-      dragSpliter.parentElement.previousElementSibling.offsetHeight;
-    if (dragSpliter) {
+    initAreaHeight.value =
+      dragSpliter.value.parentElement.previousElementSibling.offsetHeight;
+    if (dragSpliter.value) {
       document.addEventListener("mousemove", mouseMoveHandler);
       document.addEventListener("mousemove", mouseMoveHandler);
       document.addEventListener("mouseup", mouseUpHandler);
       document.addEventListener("mouseup", mouseUpHandler);
     }
     }
@@ -41,14 +39,14 @@ export function dragSplitPane() {
   const mouseMoveHandler = function (e: MouseEvent) {
   const mouseMoveHandler = function (e: MouseEvent) {
     e.preventDefault();
     e.preventDefault();
     const dy = e.clientY - pos.y;
     const dy = e.clientY - pos.y;
-    const curHeight = initAreaHeight + dy;
+    const curHeight = initAreaHeight.value + dy;
     areaHeight = Math.min(maxAreaHeight, Math.max(curHeight, minAreaHeight));
     areaHeight = Math.min(maxAreaHeight, Math.max(curHeight, minAreaHeight));
 
 
     return false;
     return false;
   };
   };
 
 
   const mouseUpHandler = function () {
   const mouseUpHandler = function () {
-    if (dragSpliter) {
+    if (dragSpliter.value) {
       document.removeEventListener("mousemove", mouseMoveHandler);
       document.removeEventListener("mousemove", mouseMoveHandler);
       document.removeEventListener("mouseup", mouseUpHandler);
       document.removeEventListener("mouseup", mouseUpHandler);
     }
     }
@@ -56,18 +54,18 @@ export function dragSplitPane() {
 
 
   onMounted(() => {
   onMounted(() => {
     watchEffect(() => {
     watchEffect(() => {
-      if (dragSpliter) {
+      if (dragSpliter.value) {
         document.addEventListener("mousedown", mouseDownHandler);
         document.addEventListener("mousedown", mouseDownHandler);
       }
       }
     });
     });
   });
   });
   onUnmounted(() => {
   onUnmounted(() => {
-    if (dragSpliter) {
+    if (dragSpliter.value) {
       document.removeEventListener("mousedown", mouseDownHandler);
       document.removeEventListener("mousedown", mouseDownHandler);
       document.removeEventListener("mousemove", mouseMoveHandler);
       document.removeEventListener("mousemove", mouseMoveHandler);
       document.removeEventListener("mouseup", mouseUpHandler);
       document.removeEventListener("mouseup", mouseUpHandler);
     }
     }
   });
   });
 
 
-  return $$({ dragSpliter, areaHeight });
+  return { dragSpliter, areaHeight };
 }
 }

+ 62 - 0
src/features/mark/composables/useDraggable.ts

@@ -0,0 +1,62 @@
+import { onMounted, onUnmounted, ref } from "vue";
+
+export default function useDraggable() {
+  // grab moving
+  let pos = { top: 0, left: 0, x: 0, y: 0 };
+  const dragContainer = ref<HTMLDivElement>();
+  // let isGrabbing = $ref(false);
+
+  const mouseDownHandler = function (e: MouseEvent) {
+    // 防止鼠标左键激发
+    if (e.button !== 0) return;
+    pos = {
+      // The current scroll
+      left: dragContainer.value.scrollLeft,
+      top: dragContainer.value.scrollTop,
+      // Get the current mouse position
+      x: e.clientX,
+      y: e.clientY,
+    };
+    // isGrabbing = true;
+    if (dragContainer.value) {
+      dragContainer.value.style.cursor = "grabbing";
+
+      dragContainer.value.addEventListener("mousemove", mouseMoveHandler);
+      dragContainer.value.addEventListener("mouseup", mouseUpHandler);
+    }
+  };
+
+  const mouseMoveHandler = function (e: MouseEvent) {
+    // if (!isGrabbing) return;
+    // How far the mouse has been moved
+    const dx = e.clientX - pos.x;
+    const dy = e.clientY - pos.y;
+
+    // Scroll the element
+    dragContainer.value.scrollTop = pos.top - dy;
+    dragContainer.value.scrollLeft = pos.left - dx;
+  };
+  const mouseUpHandler = function () {
+    // isGrabbing = false;
+    if (dragContainer.value) {
+      dragContainer.value.removeEventListener("mousemove", mouseMoveHandler);
+      dragContainer.value.removeEventListener("mouseup", mouseUpHandler);
+      dragContainer.value.style.cursor = "auto";
+    }
+  };
+
+  onMounted(() => {
+    if (dragContainer.value) {
+      dragContainer.value.addEventListener("mousedown", mouseDownHandler);
+    }
+  });
+  onUnmounted(() => {
+    if (dragContainer.value) {
+      dragContainer.value.removeEventListener("mousedown", mouseDownHandler);
+      dragContainer.value.removeEventListener("mousemove", mouseMoveHandler);
+      dragContainer.value.removeEventListener("mouseup", mouseUpHandler);
+    }
+  });
+
+  return { dragContainer };
+}

+ 104 - 0
src/features/mark/composables/useFocusTracks.ts

@@ -0,0 +1,104 @@
+import { useMarkStore } from "@/store";
+import { unref } from "vue";
+
+export default function useFocusTracks() {
+  const markStore = useMarkStore();
+
+  let hovering = false;
+  let timeoutId = -1;
+  let removeTrackTimer: number | null = null;
+
+  function addFocusTrack(
+    groupNumber: number | undefined,
+    mainNumber: number | undefined,
+    subNumber: string | undefined,
+    isMark?: boolean | undefined, // 是否是评卷,评卷时要考虑标记删除
+    list?: any
+  ) {
+    hovering = true;
+
+    timeoutId = setTimeout(() => {
+      if (hovering) {
+        _addFocusTrack(groupNumber, mainNumber, subNumber, isMark, list);
+      }
+    }, 200);
+  }
+
+  function _addFocusTrack(
+    groupNumber: number | undefined,
+    mainNumber: number | undefined,
+    subNumber: string | undefined,
+    isMark: boolean | undefined, // 是否是评卷,评卷时要考虑标记删除
+    list?: any
+  ) {
+    markStore.focusTracks.splice(0);
+
+    const listArr = unref(list);
+    // console.log("listArr:", listArr);
+    if (listArr) {
+      const trackList: any = list.map((q) => q.trackList).flat();
+
+      trackList
+        .filter((t) => {
+          if (mainNumber) {
+            return t.mainNumber === mainNumber && t.subNumber === subNumber;
+          } else {
+            return false;
+          }
+        })
+        .forEach((t: any) => {
+          // 回评时,如果没被删除
+          const shouldAdd = isMark ? trackList.includes(t) : true;
+          if (shouldAdd) {
+            markStore.focusTracks.push(t);
+          }
+        });
+      return;
+    }
+
+    if (groupNumber) {
+      markStore.currentTask?.questionList
+        ?.filter((q) => q.groupNumber === groupNumber)
+        ?.map((q) => q.trackList)
+        .flat()
+        .forEach((t) => {
+          // 回评时,如果没被删除
+          const shouldAdd = isMark
+            ? markStore.currentTask?.markResult.trackList.includes(t)
+            : true;
+          if (shouldAdd) markStore.focusTracks.push(t);
+        });
+    } else {
+      markStore.currentTask?.questionList
+        ?.map((q) => q.trackList)
+        .flat()
+        .filter((t) => {
+          if (mainNumber) {
+            return t.mainNumber === mainNumber && t.subNumber === subNumber;
+          } else {
+            return false;
+          }
+        })
+        .forEach((t) => {
+          // 回评时,如果没被删除
+          const shouldAdd = isMark
+            ? markStore.currentTask?.markResult.trackList.includes(t)
+            : true;
+          if (shouldAdd) markStore.focusTracks.push(t);
+        });
+    }
+  }
+
+  function removeFocusTrack() {
+    hovering = false;
+    clearTimeout(timeoutId);
+    removeTrackTimer && clearTimeout(removeTrackTimer);
+    removeTrackTimer = setTimeout(() => {
+      if (!hovering) {
+        markStore.focusTracks.splice(0);
+      }
+    }, 200);
+  }
+
+  return { addFocusTrack, removeFocusTrack };
+}

+ 161 - 0
src/features/mark/composables/useMakeTrack.ts

@@ -0,0 +1,161 @@
+import { SliceImage, SpecialTag, Track } from "@/types";
+import { useMarkStore } from "@/store";
+
+export default function useMakeTrack() {
+  const markStore = useMarkStore();
+
+  // 标记分数轨迹
+  function makeScoreTrack(
+    event: MouseEvent,
+    item: SliceImage,
+    maxSliceWidth: number,
+    theFinalHeight: number
+  ) {
+    if (
+      !markStore.currentQuestion ||
+      typeof markStore.currentScore === "undefined"
+    )
+      return;
+
+    const target = event.target as HTMLImageElement;
+    const track: Track = {
+      mainNumber: markStore.currentQuestion?.mainNumber,
+      subNumber: markStore.currentQuestion?.subNumber,
+      score: markStore.currentScore,
+      unanswered: Object.is(markStore.currentScore, -0),
+      offsetIndex: item.indexInSliceUrls,
+      // 新增轨迹时会校验轨迹是否距离边缘过近,使用round产生的误差可以忽略,不会发生超出边界的问题
+      offsetX: Math.round(
+        event.offsetX * (target.naturalWidth / target.width) + item.dx
+      ),
+      offsetY: Math.round(
+        event.offsetY * (target.naturalHeight / target.height) + item.dy
+      ),
+      positionX: -1,
+      positionY: -1,
+      number: -1,
+    };
+    track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
+    track.positionY =
+      (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
+
+    if (track.offsetX > item.effectiveWidth + item.dx) {
+      console.log("不在有效宽度内,轨迹不生效");
+      return;
+    }
+    if (
+      item.trackList.some((t) => {
+        return (
+          Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
+            Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
+          500
+        );
+      })
+    ) {
+      console.log("两个轨迹相距过近");
+      return;
+    }
+    // 是否保留当前的轨迹分
+    const questionScore =
+      markStore.currentTask &&
+      markStore.currentQuestion &&
+      markStore.currentTask.markResult.scoreList[
+        markStore.currentQuestion.__index
+      ];
+    const ifKeepScore =
+      Math.round(
+        markStore.currentQuestion.maxScore * 1000 -
+          (questionScore || 0) * 1000 -
+          markStore.currentScore * 2 * 1000
+      ) / 1000;
+    if (ifKeepScore < 0 && markStore.currentScore > 0) {
+      markStore.currentScore = undefined;
+    }
+    const markResult = markStore.currentTaskEnsured.markResult;
+    const maxNumber =
+      markResult.trackList.length === 0
+        ? 0
+        : Math.max(...markResult.trackList.map((t) => t.number));
+    track.number = maxNumber + 1;
+
+    markResult.trackList = [...markResult.trackList, track];
+    const { __index, mainNumber, subNumber } = markStore.currentQuestion;
+    markResult.scoreList[__index] =
+      markResult.trackList
+        .filter((t) => t.mainNumber === mainNumber && t.subNumber === subNumber)
+        .map((t) => t.score)
+        .reduce((acc, v) => (acc += Math.round(v * 1000)), 0) / 1000;
+    item.trackList.push(track);
+  }
+
+  // 标记特殊轨迹 (特殊轨迹包括:文字、线条、圆圈)
+  function makeSpecialTagTrack(
+    event: MouseEvent,
+    item: SliceImage,
+    maxSliceWidth: number,
+    theFinalHeight: number
+  ) {
+    if (
+      !markStore.currentTask ||
+      typeof markStore.currentSpecialTag === "undefined"
+    )
+      return;
+
+    const target = event.target as HTMLImageElement;
+    const track: SpecialTag = {
+      tagName: markStore.currentSpecialTag,
+      tagType: markStore.currentSpecialTagType,
+      offsetIndex: item.indexInSliceUrls,
+      offsetX: Math.round(
+        event.offsetX * (target.naturalWidth / target.width) + item.dx
+      ),
+      offsetY: Math.round(
+        event.offsetY * (target.naturalHeight / target.height) + item.dy
+      ),
+      positionX: -1,
+      positionY: -1,
+    };
+    track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
+    track.positionY =
+      (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
+
+    if (
+      item.tagList.some((t) => {
+        return (
+          Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
+            Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
+          500
+        );
+      })
+    ) {
+      console.log("两个轨迹相距过近");
+      return;
+    }
+    markStore.currentTaskEnsured.markResult.specialTagList.push(track);
+    item.tagList.push(track);
+  }
+
+  // 标记轨迹
+  function makeTrack(
+    event: MouseEvent,
+    item: SliceImage,
+    maxSliceWidth: number,
+    theFinalHeight: number
+  ) {
+    if (markStore.currentSpecialTagType) {
+      makeSpecialTagTrack(event, item, maxSliceWidth, theFinalHeight);
+      if (markStore.currentSpecialTagType === "TEXT") {
+        markStore.currentSpecialTag = undefined;
+        markStore.currentSpecialTagType = undefined;
+      }
+    } else {
+      makeScoreTrack(event, item, maxSliceWidth, theFinalHeight);
+    }
+  }
+
+  return {
+    makeScoreTrack,
+    makeSpecialTagTrack,
+    makeTrack,
+  };
+}

+ 191 - 0
src/features/mark/composables/useMarkSubmit.ts

@@ -0,0 +1,191 @@
+import { useMarkStore } from "@/store";
+import { saveTask, doUnselectiveType } from "@/api/markPage";
+import { message } from "ant-design-vue";
+import { isNumber } from "lodash-es";
+import { h } from "vue";
+import EventBus from "@/plugins/eventBus";
+import type { Question } from "@/types";
+import useStatus from "./useStatus";
+import { useMarkTask } from "./useMarkTask";
+
+export function useMarkSubmit() {
+  const markStore = useMarkStore();
+  const { updateStatus } = useStatus();
+  const { nextTask } = useMarkTask();
+
+  const validateScore = (markResult: any) => {
+    const errors: Array<{ question: Question; index: number; error: string }> =
+      [];
+    markResult.scoreList.forEach((score: number, index: number) => {
+      if (!markStore.currentTask) return;
+      const question = markStore.currentTask.questionList[index]!;
+      let error;
+      if (!isNumber(score)) {
+        error = `${question.mainNumber}-${question.subNumber}${
+          question.questionName ? "(" + question.questionName + ")" : ""
+        } 没有给分,不能提交。`;
+      } else if (isNumber(question.maxScore) && score > question.maxScore) {
+        error = `${question.mainNumber}-${question.subNumber}${
+          question.questionName ? "(" + question.questionName + ")" : ""
+        } 给分大于最高分不能提交。`;
+      } else if (isNumber(question.minScore) && score < question.minScore) {
+        error = `${question.mainNumber}-${question.subNumber}${
+          question.questionName ? "(" + question.questionName + ")" : ""
+        } 给分小于最低分不能提交。`;
+      }
+      if (error) {
+        errors.push({ question, index, error });
+      }
+    });
+    return errors;
+  };
+
+  const saveTaskToServer = async () => {
+    if (!markStore.currentTask) return;
+    const markResult = markStore.currentTask.markResult;
+    if (!markResult) return;
+
+    const mkey = "save_task_key";
+    const errors = validateScore(markResult);
+
+    if (errors.length !== 0) {
+      console.log(errors);
+      const msg = errors.map((v) => h("div", `${v.error}`));
+      void message.warning({
+        content: h("span", ["校验失败", ...msg]),
+        duration: 10,
+        key: mkey,
+      });
+      return;
+    }
+
+    if (
+      markResult.scoreList.length !==
+        markStore.currentTask.questionList.length ||
+      !markResult.scoreList.every((s) => isNumber(s))
+    ) {
+      console.error({ content: "markResult格式不正确,缺少分数", key: mkey });
+      return;
+    }
+
+    if (!markStore.isTrackMode) {
+      markResult.trackList = [];
+    } else {
+      const trackScores =
+        markResult.trackList
+          .map((t) => Math.round((t.score || 0) * 100))
+          .reduce((acc, s) => acc + s, 0) / 100;
+      if (trackScores !== markResult.markerScore) {
+        void message.error({
+          content: "轨迹分与总分不一致,请检查。",
+          duration: 3,
+          key: mkey,
+        });
+        return;
+      }
+    }
+
+    if (markStore.setting.forceSpecialTag) {
+      if (
+        markResult.trackList.length === 0 &&
+        markResult.specialTagList.length === 0
+      ) {
+        void message.error({
+          content: "强制标记已开启,请至少使用一个标记。",
+          duration: 5,
+          key: mkey,
+        });
+        return;
+      }
+    }
+
+    console.log("save task to server");
+    void message.loading({ content: "保存评卷任务...", key: mkey });
+    const res = await saveTask();
+    if (!res) return;
+
+    // 故意不在此处同步等待,因为不必等待
+    updateStatus().catch((e) => console.log("保存任务后获取status出错", e));
+    if (res.data.success && markStore.currentTask) {
+      void message.success({ content: "保存成功", key: mkey, duration: 2 });
+      if (!markStore.historyOpen) {
+        markStore.currentTask = undefined;
+        markStore.tasks.shift();
+        markStore.currentTask = markStore.tasks[0];
+      } else {
+        EventBus.emit("should-reload-history");
+      }
+    } else if (!res.data.success) {
+      void message.error({
+        content: "提交失败,请刷新页面",
+        key: mkey,
+        duration: 10,
+      });
+      return;
+    } else if (!markStore.currentTask) {
+      void message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
+    }
+
+    // 获取下一个任务
+    void nextTask();
+  };
+
+  const allZeroSubmit = async () => {
+    const markResult = markStore.currentTask?.markResult;
+    if (!markResult) return;
+
+    const { markerScore, scoreList, trackList, specialTagList } = markResult;
+    markResult.markerScore = 0;
+    const ss = new Array(markStore.currentTaskEnsured.questionList.length);
+    markResult.scoreList = ss.fill(0);
+    markResult.trackList = [];
+
+    try {
+      await saveTaskToServer();
+    } catch (error) {
+      console.log("error restore");
+    } finally {
+      markResult.markerScore = markerScore;
+      markResult.scoreList = scoreList;
+      markResult.trackList = trackList;
+      markResult.specialTagList = specialTagList;
+    }
+  };
+
+  const unselectiveSubmit = async () => {
+    const markResult = markStore.currentTask?.markResult;
+    if (!markResult) return;
+
+    try {
+      const res = await doUnselectiveType();
+      if (res?.data.success) {
+        void message.success({ content: "未选做处理成功", duration: 3 });
+        if (!markStore.historyOpen) {
+          markStore.currentTask = undefined;
+          markStore.tasks.shift();
+          markStore.currentTask = markStore.tasks[0];
+        }
+        if (markStore.historyOpen) {
+          EventBus.emit("should-reload-history");
+        }
+        await updateStatus();
+      } else {
+        void message.error({
+          content: res?.data.message || "错误",
+          duration: 5,
+        });
+      }
+    } catch (error) {
+      console.log("未选做处理失败", error);
+      void message.error({ content: "网络异常", duration: 5 });
+      await new Promise((res) => setTimeout(res, 1500));
+      window.location.reload();
+    }
+  };
+
+  return {
+    saveTaskToServer,
+    allZeroSubmit,
+    unselectiveSubmit,
+  };
+}

+ 93 - 0
src/features/mark/composables/useMarkTask.ts

@@ -0,0 +1,93 @@
+import { clearMarkTask, getTask } from "@/api/markPage";
+import { useMarkStore } from "@/store";
+import { processSliceUrls } from "@/utils/utils";
+import { nextTick, watch } from "vue";
+import { useTaskRejection } from "./useTaskRejection";
+
+export default function useMarkTask() {
+  const markStore = useMarkStore();
+  const { showRejectedReason } = useTaskRejection();
+
+  let preDrawing = false;
+
+  async function updateMarkTask() {
+    await clearMarkTask();
+  }
+
+  async function updateTask() {
+    const res = await getTask();
+    if (res.data?.taskId) {
+      const newTask = res.data;
+      newTask.sheetUrls = newTask.sheetUrls || [];
+      try {
+        preDrawing = true;
+        newTask.sliceUrls = await processSliceUrls(newTask);
+      } finally {
+        preDrawing = false;
+      }
+
+      markStore.tasks.push(newTask);
+      if (!markStore.historyOpen) {
+        if (markStore.currentTask?.studentId !== markStore.tasks[0].studentId)
+          markStore.currentTask = markStore.tasks[0];
+      }
+
+      if (markStore.status.totalCount - markStore.status.markedCount === 0) {
+        await updateStatus();
+      }
+
+      if (markStore.tasks.length > 1) {
+        await new Promise((resolve) => setTimeout(resolve, 3000));
+      }
+    } else {
+      markStore.message = res.message || "数据错误";
+    }
+    if (!res.data && markStore.message === "成功")
+      markStore.message = "当前无评卷任务";
+  }
+
+  function nextTask() {
+    if (!preDrawing) {
+      if (markStore.tasks.length < (markStore.setting.prefetchCount ?? 3)) {
+        if (!markStore.historyOpen)
+          return updateTask().catch((e) => console.log("定时获取任务出错", e));
+      }
+    }
+  }
+
+  function removeBrokenTask() {
+    markStore.currentTask = undefined;
+
+    if (markStore.historyOpen) {
+      // 避免无限加载
+      markStore.message = "加载失败,请重新选择。";
+    } else {
+      markStore.tasks.shift();
+    }
+  }
+
+  function watchCurrentTask() {
+    watch(
+      () => markStore.currentTask,
+      () => {
+        // 重置当前选择的quesiton和score
+        markStore.currentQuestion = undefined;
+        markStore.currentScore = undefined;
+        void nextTick(() => {
+          if (markStore.currentTask) {
+            showRejectedReason(markStore.currentTask);
+          }
+        });
+      }
+    );
+  }
+
+  watchCurrentTask();
+
+  return {
+    updateMarkTask,
+    updateTask,
+    nextTask,
+    removeBrokenTask,
+  };
+}

+ 54 - 0
src/features/mark/composables/useSetting.ts

@@ -0,0 +1,54 @@
+import { useMarkStore } from "@/store";
+import { getSetting, updateUISetting } from "@/api/markPage";
+import { debounce, isEmpty } from "lodash-es";
+import { getPaper } from "@/api/jsonMark";
+import { watch } from "vue";
+
+export default function useSetting() {
+  const markStore = useMarkStore();
+
+  async function updateSetting() {
+    const settingRes = await getSetting();
+    if (isEmpty(settingRes.data.uiSetting)) {
+      settingRes.data.uiSetting = {
+        "answer.paper.scale": 1,
+        "score.board.collapse": false,
+        "normal.mode": "keyboard",
+        "score.fontSize.scale": 1,
+        "paper.modal": false,
+        "answer.modal": false,
+        "minimap.modal": false,
+        "specialTag.modal": false,
+        "shortCut.modal": false,
+      };
+    } else {
+      settingRes.data.uiSetting = JSON.parse(settingRes.data.uiSetting);
+    }
+    settingRes.data.sheetConfig = settingRes.data.sheetConfig
+      ? JSON.parse(settingRes.data.sheetConfig)
+      : [];
+    markStore.setting = settingRes.data;
+    if (markStore.setting.subject?.paperUrl && markStore.isMultiMedia) {
+      await getPaper(markStore);
+    }
+  }
+
+  const debouncedUpdateUISetting = debounce(() => {
+    updateUISetting(markStore.setting.mode, markStore.setting.uiSetting).catch(
+      (e) => console.log("保存设置出错", e)
+    );
+  }, 3000);
+
+  watch(
+    () => [markStore.setting.uiSetting, markStore.setting.mode],
+    () => {
+      debouncedUpdateUISetting();
+    },
+    { deep: true }
+  );
+
+  return {
+    updateSetting,
+    debouncedUpdateUISetting,
+  };
+}

+ 46 - 0
src/features/mark/composables/useStatus.ts

@@ -0,0 +1,46 @@
+import { useMarkStore } from "@/store";
+import { getStatus, getGroup } from "@/api/markPage";
+import { useTimers } from "@/setups/useTimers";
+import { ref, watch } from "vue";
+
+export default function useStatus() {
+  const markStore = useMarkStore();
+
+  const { addTimeout } = useTimers();
+  // 试评、正评的页面提示
+  const statusSpinning = ref(true);
+  // 是否还在加载statusValue
+  const loadingStatusSpinning = ref(true);
+
+  async function updateStatus() {
+    const res = await getStatus();
+    markStore.setInfo({ status: res.data });
+  }
+
+  async function updateGroups() {
+    const res = await getGroup();
+    markStore.setInfo({ groups: res.data });
+  }
+
+  function setupStatusWatch() {
+    watch(
+      () => markStore.setting.statusValue,
+      () => {
+        if (markStore.setting.statusValue) {
+          loadingStatusSpinning.value = false;
+          addTimeout(() => (statusSpinning.value = false), 3000);
+        }
+      }
+    );
+  }
+
+  setupStatusWatch();
+
+  return {
+    statusSpinning,
+    loadingStatusSpinning,
+    updateStatus,
+    updateGroups,
+    setupStatusWatch,
+  };
+}

+ 58 - 0
src/features/mark/composables/useTaskRejection.ts

@@ -0,0 +1,58 @@
+import { Modal } from "ant-design-vue";
+import { h } from "vue";
+import type { Task } from "@/types";
+
+export function useTaskRejection() {
+  const showRejectedReason = (task: Task) => {
+    if (task.rejected && task.rejectReason) {
+      const [reasonType, reasonDesc] = task.rejectReason.split(":");
+      Modal.info({
+        title: null,
+        closable: false,
+        maskClosable: false,
+        centered: true,
+        icon: null,
+        okText: "知道了",
+        wrapClassName: "custom-modal-info",
+        zIndex: 9999,
+        bodyStyle: {
+          padding: "15px",
+        },
+        content: () =>
+          h(
+            "div",
+            {
+              style: {
+                fontSize: "14px",
+                color: "var(--app-main-text-color)",
+              },
+            },
+            [
+              h("div", { style: { marginBottom: "8px" } }, [
+                h(
+                  "span",
+                  { style: { fontWeight: "bold", marginRight: "0.5em" } },
+                  "打回原因: "
+                ),
+                h("span", reasonType),
+              ]),
+              h("div", { style: { fontWeight: "bold" } }, "详情描述: "),
+              h("div", { style: { padding: "4px 2px" } }, reasonDesc),
+              h("div", { style: { marginBottom: "8px" } }, [
+                h(
+                  "span",
+                  { style: { fontWeight: "bold", marginRight: "0.5em" } },
+                  "给分记录: "
+                ),
+                h("span", task.rejectScoreList),
+              ]),
+            ]
+          ),
+      });
+    }
+  };
+
+  return {
+    showRejectedReason,
+  };
+}

+ 10 - 4
src/features/mark/AllPaperModal.vue → src/features/mark/modals/ModalAllPaper.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
   <teleport to="body">
   <teleport to="body">
     <div
     <div
-      v-if="store.allPaperModal"
+      v-if="markStore.allPaperModal"
       class="common-dialog is-fullscreen all-paper-modal"
       class="common-dialog is-fullscreen all-paper-modal"
     >
     >
       <header class="common-dialog-header">
       <header class="common-dialog-header">
@@ -19,7 +19,7 @@
           </div>
           </div>
         </div>
         </div>
 
 
-        <a-button @click="store.allPaperModal = false">
+        <a-button @click="handleClose">
           <template #icon> <SwapLeftOutlined /> </template>
           <template #icon> <SwapLeftOutlined /> </template>
           返回
           返回
         </a-button>
         </a-button>
@@ -34,11 +34,17 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { SwapLeftOutlined } from "@ant-design/icons-vue";
 import { SwapLeftOutlined } from "@ant-design/icons-vue";
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
+
+const markStore = useMarkStore();
 
 
 const urls = $computed(() => {
 const urls = $computed(() => {
-  return store.currentTask?.sliceUrls ?? [];
+  return markStore.currentTask?.sliceUrls ?? [];
 });
 });
 
 
 let checkedIndex = $ref(0);
 let checkedIndex = $ref(0);
+
+function handleClose() {
+  markStore.allPaperModal = false;
+}
 </script>
 </script>

+ 6 - 4
src/features/mark/AnswerModal.vue → src/features/mark/modals/ModalAnswer.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <qm-dialog
   <qm-dialog
-    v-if="store.setting.uiSetting['answer.modal']"
+    v-if="markStore.setting.uiSetting['answer.modal']"
     top="10vh"
     top="10vh"
     width="1200px"
     width="1200px"
     height="600px"
     height="600px"
@@ -8,7 +8,7 @@
     @close="close"
     @close="close"
   >
   >
     <object
     <object
-      :data="store.setting.subject.answerUrl"
+      :data="markStore.setting.subject.answerUrl"
       type="application/pdf"
       type="application/pdf"
       frameBorder="0"
       frameBorder="0"
       scrolling="auto"
       scrolling="auto"
@@ -20,9 +20,11 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
+
+const markStore = useMarkStore();
 
 
 const close = () => {
 const close = () => {
-  store.setting.uiSetting["answer.modal"] = false;
+  markStore.setting.uiSetting["answer.modal"] = false;
 };
 };
 </script>
 </script>

+ 7 - 5
src/features/mark/MinimapModal.vue → src/features/mark/modals/ModalMinimap.vue

@@ -19,11 +19,13 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onBeforeUpdate, watch } from "vue";
 import { onBeforeUpdate, watch } from "vue";
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
+
+const markStore = useMarkStore();
 
 
 // 切换任务后,关闭缩略图
 // 切换任务后,关闭缩略图
 watch(
 watch(
-  () => store.currentTask,
+  () => markStore.currentTask,
   () => {
   () => {
     close();
     close();
   }
   }
@@ -64,12 +66,12 @@ const setScrollTo = (e: MouseEvent) => {
     (e.offsetY + parentPos.y - containerPos.y) /
     (e.offsetY + parentPos.y - containerPos.y) /
     parseFloat(getComputedStyle(container).height);
     parseFloat(getComputedStyle(container).height);
   if (typeof scrollToX === "number" && typeof scrollToY === "number") {
   if (typeof scrollToX === "number" && typeof scrollToY === "number") {
-    store.minimapScrollToX = scrollToX;
-    store.minimapScrollToY = scrollToY;
+    markStore.minimapScrollToX = scrollToX;
+    markStore.minimapScrollToY = scrollToY;
   }
   }
 };
 };
 const close = () => {
 const close = () => {
-  store.setting.uiSetting["minimap.modal"] = false;
+  markStore.setting.uiSetting["minimap.modal"] = false;
 };
 };
 </script>
 </script>
 
 

+ 6 - 4
src/features/mark/PaperModal.vue → src/features/mark/modals/ModalPaper.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <qm-dialog
   <qm-dialog
-    v-if="store.setting.uiSetting['paper.modal']"
+    v-if="markStore.setting.uiSetting['paper.modal']"
     top="10%"
     top="10%"
     width="700px"
     width="700px"
     height="400px"
     height="400px"
@@ -8,7 +8,7 @@
     @close="close"
     @close="close"
   >
   >
     <object
     <object
-      :data="store.setting.subject.paperUrl"
+      :data="markStore.setting.subject.paperUrl"
       type="application/pdf"
       type="application/pdf"
       frameBorder="0"
       frameBorder="0"
       scrolling="auto"
       scrolling="auto"
@@ -20,9 +20,11 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
+
+const markStore = useMarkStore();
 
 
 const close = () => {
 const close = () => {
-  store.setting.uiSetting["paper.modal"] = false;
+  markStore.setting.uiSetting["paper.modal"] = false;
 };
 };
 </script>
 </script>

+ 10 - 7
src/features/mark/SheetViewModal.vue → src/features/mark/modals/ModalSheetView.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <teleport to="body">
   <teleport to="body">
-    <div v-if="store.sheetViewModal" class="dialog-container">
+    <div v-if="markStore.sheetViewModal" class="dialog-container">
       <header class="tw-flex tw-place-content-between tw-items-center">
       <header class="tw-flex tw-place-content-between tw-items-center">
         <div class="tw-text-2xl ctw-text-base tw-ml-5 tw-my-2">原图</div>
         <div class="tw-text-2xl ctw-text-base tw-ml-5 tw-my-2">原图</div>
         <div class="tw-flex tw-items-center tw-gap-2 tw-mx-8 tw-flex-grow">
         <div class="tw-flex tw-items-center tw-gap-2 tw-mx-8 tw-flex-grow">
@@ -16,7 +16,7 @@
         <a-button
         <a-button
           shape="circle"
           shape="circle"
           class="tw-mr-2"
           class="tw-mr-2"
-          @click="store.sheetViewModal = false"
+          @click="markStore.sheetViewModal = false"
         >
         >
           <template #icon><CloseOutlined /></template>
           <template #icon><CloseOutlined /></template>
         </a-button>
         </a-button>
@@ -39,23 +39,26 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { reactive, watch } from "vue";
 import { reactive, watch } from "vue";
 import { CloseOutlined } from "@ant-design/icons-vue";
 import { CloseOutlined } from "@ant-design/icons-vue";
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
 import { loadImage } from "@/utils/utils";
 import { loadImage } from "@/utils/utils";
 import type { PictureSlice } from "@/types";
 import type { PictureSlice } from "@/types";
 
 
 const dataUrls: string[] = reactive([]);
 const dataUrls: string[] = reactive([]);
+
+const markStore = useMarkStore();
+
 watch(
 watch(
-  () => store.sheetViewModal,
+  () => markStore.sheetViewModal,
   async () => {
   async () => {
-    if (!store.sheetViewModal) return;
+    if (!markStore.sheetViewModal) return;
     dataUrls.splice(0);
     dataUrls.splice(0);
-    const urls = store.currentTask?.sheetUrls ?? [];
+    const urls = markStore.currentTask?.sheetUrls ?? [];
     const images = [];
     const images = [];
     for (const url of urls) {
     for (const url of urls) {
       images.push(await loadImage(url));
       images.push(await loadImage(url));
     }
     }
 
 
-    const sheetConfig = store.setting.sheetConfig;
+    const sheetConfig = markStore.setting.sheetConfig;
 
 
     for (let i = 0; i < images.length; i++) {
     for (let i = 0; i < images.length; i++) {
       if (sheetConfig.length === 0) {
       if (sheetConfig.length === 0) {

+ 5 - 3
src/features/mark/ShortCutModal.vue → src/features/mark/modals/ModalShortCut.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <qm-dialog
   <qm-dialog
-    v-if="store.setting.uiSetting['shortCut.modal']"
+    v-if="markStore.setting.uiSetting['shortCut.modal']"
     top="10%"
     top="10%"
     width="442px"
     width="442px"
     height="508px"
     height="508px"
@@ -109,9 +109,11 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
+
+const markStore = useMarkStore();
 
 
 const close = () => {
 const close = () => {
-  store.setting.uiSetting["shortCut.modal"] = false;
+  markStore.setting.uiSetting["shortCut.modal"] = false;
 };
 };
 </script>
 </script>

+ 22 - 20
src/features/mark/SpecialTagModal.vue → src/features/mark/modals/ModalSpecialTag.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <qm-dialog
   <qm-dialog
-    v-if="store.setting.uiSetting['specialTag.modal']"
+    v-if="markStore.setting.uiSetting['specialTag.modal']"
     top="10%"
     top="10%"
     width="378px"
     width="378px"
     height="192px"
     height="192px"
@@ -15,7 +15,7 @@
           :class="[
           :class="[
             'board-score',
             'board-score',
             'score-icon',
             'score-icon',
-            { 'is-current': store.currentSpecialTag === '√' },
+            { 'is-current': markStore.currentSpecialTag === '√' },
           ]"
           ]"
           @click="chooseSpecialTag('√', 'RIGHT')"
           @click="chooseSpecialTag('√', 'RIGHT')"
         >
         >
@@ -26,7 +26,7 @@
             'board-score',
             'board-score',
             'score-icon',
             'score-icon',
             {
             {
-              'is-current': store.currentSpecialTag === '乄',
+              'is-current': markStore.currentSpecialTag === '乄',
             },
             },
           ]"
           ]"
           @click="chooseSpecialTag('乄', 'HALF_RIGTH')"
           @click="chooseSpecialTag('乄', 'HALF_RIGTH')"
@@ -37,7 +37,7 @@
           :class="[
           :class="[
             'board-score',
             'board-score',
             'score-icon',
             'score-icon',
-            { 'is-current': store.currentSpecialTag === 'X' },
+            { 'is-current': markStore.currentSpecialTag === 'X' },
           ]"
           ]"
           @click="chooseSpecialTag('X', 'WRONG')"
           @click="chooseSpecialTag('X', 'WRONG')"
         >
         >
@@ -47,7 +47,7 @@
           :class="[
           :class="[
             'board-score',
             'board-score',
             'score-icon',
             'score-icon',
-            { 'is-current': store.currentSpecialTag === '○' },
+            { 'is-current': markStore.currentSpecialTag === '○' },
           ]"
           ]"
           title="标记圆圈"
           title="标记圆圈"
           @click="chooseSpecialTag('○', 'CIRCLE')"
           @click="chooseSpecialTag('○', 'CIRCLE')"
@@ -58,7 +58,7 @@
           :class="[
           :class="[
             'board-score',
             'board-score',
             'score-icon',
             'score-icon',
-            { 'is-current': store.currentSpecialTagType === 'TEXT' },
+            { 'is-current': markStore.currentSpecialTagType === 'TEXT' },
           ]"
           ]"
           title="标记文本"
           title="标记文本"
           @click="chooseSpecialTag('', 'TEXT')"
           @click="chooseSpecialTag('', 'TEXT')"
@@ -100,32 +100,34 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
+
+const markStore = useMarkStore();
 
 
 function clearLatestTagOfCurrentTask() {
 function clearLatestTagOfCurrentTask() {
-  if (!store.currentTask?.markResult) return;
-  store.currentTask.markResult.specialTagList.splice(-1);
+  if (!markStore.currentTask?.markResult) return;
+  markStore.currentTask.markResult.specialTagList.splice(-1);
 }
 }
 
 
 function clearAllTagsOfCurrentTask() {
 function clearAllTagsOfCurrentTask() {
-  if (!store.currentTask?.markResult) return;
-  store.currentTask.markResult.specialTagList = [];
+  if (!markStore.currentTask?.markResult) return;
+  markStore.currentTask.markResult.specialTagList = [];
 }
 }
 
 
 function chooseSpecialTag(tagName: string, tagType: string) {
 function chooseSpecialTag(tagName: string, tagType: string) {
-  if (store.currentSpecialTag === tagName) {
-    store.currentSpecialTag = undefined;
-    store.currentSpecialTagType = undefined;
+  if (markStore.currentSpecialTag === tagName) {
+    markStore.currentSpecialTag = undefined;
+    markStore.currentSpecialTagType = undefined;
   } else {
   } else {
-    store.currentSpecialTag = tagName;
-    store.currentSpecialTagType = tagType;
-    store.currentScore = undefined;
+    markStore.currentSpecialTag = tagName;
+    markStore.currentSpecialTagType = tagType;
+    markStore.currentScore = undefined;
   }
   }
 }
 }
 
 
 const close = () => {
 const close = () => {
-  store.currentSpecialTag = undefined;
-  store.currentSpecialTagType = undefined;
-  store.setting.uiSetting["specialTag.modal"] = false;
+  markStore.currentSpecialTag = undefined;
+  markStore.currentSpecialTagType = undefined;
+  markStore.setting.uiSetting["specialTag.modal"] = false;
 };
 };
 </script>
 </script>

+ 40 - 36
src/features/mark/MarkBoardKeyBoard.vue → src/features/mark/scoring/MarkBoardKeyBoard.vue

@@ -1,14 +1,14 @@
 <template>
 <template>
   <div
   <div
-    v-if="store.currentTask"
+    v-if="markStore.currentTask"
     class="mark-board-track is-key-board"
     class="mark-board-track is-key-board"
-    :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
+    :class="[markStore.isScoreBoardCollapsed ? 'hide' : 'show']"
   >
   >
     <div class="board-mode">
     <div class="board-mode">
       <a-dropdown>
       <a-dropdown>
         <template #overlay>
         <template #overlay>
           <a-menu>
           <a-menu>
-            <a-menu-item key="1" @click="toggleKeyMouse">
+            <a-menu-item key="1" @click="markStore.toggleKeyMouse">
               鼠标给分
               鼠标给分
             </a-menu-item>
             </a-menu-item>
           </a-menu>
           </a-menu>
@@ -116,9 +116,8 @@
 import type { Question } from "@/types";
 import type { Question } from "@/types";
 import { isNumber } from "lodash-es";
 import { isNumber } from "lodash-es";
 import { ref, onMounted, onUnmounted, watch } from "vue";
 import { ref, onMounted, onUnmounted, watch } from "vue";
-import { store } from "@/store/store";
-import { keyMouse } from "./use/keyboardAndMouse";
-import { autoChooseFirstQuestion } from "./use/autoChooseFirstQuestion";
+import { useMarkStore } from "@/store";
+import useAutoChooseFirstQuestion from "../composables/useAutoChooseFirstQuestion";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
 import { DownOutlined } from "@ant-design/icons-vue";
 import { DownOutlined } from "@ant-design/icons-vue";
 
 
@@ -130,23 +129,24 @@ const emit = defineEmits([
 ]);
 ]);
 const props = defineProps<{ isCheckAnswer?: boolean }>();
 const props = defineProps<{ isCheckAnswer?: boolean }>();
 
 
-const { toggleKeyMouse } = keyMouse();
-const { chooseQuestion } = autoChooseFirstQuestion();
+const { chooseQuestion } = useAutoChooseFirstQuestion();
 const hasModifyScore = ref(false);
 const hasModifyScore = ref(false);
 
 
+const markStore = useMarkStore();
+
 /**
 /**
  * 当前题的输入串,初次是markResult.scoreList中score,然后接收输入字符,回车时判断是否合法,合法则赋值给markResult.scoreList
  * 当前题的输入串,初次是markResult.scoreList中score,然后接收输入字符,回车时判断是否合法,合法则赋值给markResult.scoreList
  * 切换到下一题,则重新开始
  * 切换到下一题,则重新开始
  *  */
  *  */
 let scoreStr = $ref("");
 let scoreStr = $ref("");
 watch(
 watch(
-  () => [store.currentQuestion, store.setting.uiSetting["normal.mode"]],
+  () => [markStore.currentQuestion, markStore.setting.uiSetting["normal.mode"]],
   () => {
   () => {
-    if (store.currentQuestion) {
-      const { __index } = store.currentQuestion;
+    if (markStore.currentQuestion) {
+      const { __index } = markStore.currentQuestion;
       scoreStr =
       scoreStr =
-        store.currentTask?.markResult?.scoreList[__index] ||
-        store.currentQuestion.score;
+        markStore.currentTask?.markResult?.scoreList[__index] ||
+        markStore.currentQuestion.score;
       if (isNumber(scoreStr)) {
       if (isNumber(scoreStr)) {
         scoreStr = "" + scoreStr;
         scoreStr = "" + scoreStr;
       } else {
       } else {
@@ -160,7 +160,7 @@ watch(
 );
 );
 
 
 const questionScoreSteps = $computed(() => {
 const questionScoreSteps = $computed(() => {
-  const question = store.currentQuestion;
+  const question = markStore.currentQuestion;
   if (!question) return [];
   if (!question) return [];
 
 
   const remainScore = Math.round(question.maxScore * 100) / 100;
   const remainScore = Math.round(question.maxScore * 100) / 100;
@@ -184,16 +184,18 @@ const questionScoreSteps = $computed(() => {
 
 
 function isCurrentQuestion(question: Question) {
 function isCurrentQuestion(question: Question) {
   return (
   return (
-    store.currentQuestion?.mainNumber === question.mainNumber &&
-    store.currentQuestion?.subNumber === question.subNumber
+    markStore.currentQuestion?.mainNumber === question.mainNumber &&
+    markStore.currentQuestion?.subNumber === question.subNumber
   );
   );
 }
 }
 
 
 const questionScore = $computed(
 const questionScore = $computed(
   () =>
   () =>
-    store.currentTask &&
-    store.currentQuestion &&
-    store.currentTask.markResult.scoreList[store.currentQuestion.__index]
+    markStore.currentTask &&
+    markStore.currentQuestion &&
+    markStore.currentTask.markResult.scoreList[
+      markStore.currentQuestion.__index
+    ]
 );
 );
 
 
 function numberKeyListener(event: KeyboardEvent) {
 function numberKeyListener(event: KeyboardEvent) {
@@ -202,21 +204,21 @@ function numberKeyListener(event: KeyboardEvent) {
 
 
   // console.log(event);
   // console.log(event);
   // if (event.target.tagName !== "BODY") return;
   // if (event.target.tagName !== "BODY") return;
-  if (!store.currentQuestion || !store.currentTask) return;
-  if (store.globalMask) return;
+  if (!markStore.currentQuestion || !markStore.currentTask) return;
+  if (markStore.globalMask) return;
 
 
   function indexOfCurrentQuestion() {
   function indexOfCurrentQuestion() {
-    return store.currentTaskEnsured.questionList.findIndex(
+    return markStore.currentTaskEnsured.questionList.findIndex(
       (q) =>
       (q) =>
-        q.mainNumber === store.currentQuestion?.mainNumber &&
-        q.subNumber === store.currentQuestion.subNumber
+        q.mainNumber === markStore.currentQuestion?.mainNumber &&
+        q.subNumber === markStore.currentQuestion.subNumber
     );
     );
   }
   }
 
 
   // 处理Enter跳下一题或submit
   // 处理Enter跳下一题或submit
   if (event.key === "Enter") {
   if (event.key === "Enter") {
-    const allScoreMarked = store.currentTask.markResult.scoreList.every((s) =>
-      isNumber(s)
+    const allScoreMarked = markStore.currentTask.markResult.scoreList.every(
+      (s) => isNumber(s)
     );
     );
     // 如果所有题已赋分,并且当前题赋分和输入串和当前题分数一致,则可以在任意题提交
     // 如果所有题已赋分,并且当前题赋分和输入串和当前题分数一致,则可以在任意题提交
     if (allScoreMarked && scoreStr === "" + questionScore) {
     if (allScoreMarked && scoreStr === "" + questionScore) {
@@ -256,29 +258,29 @@ function numberKeyListener(event: KeyboardEvent) {
       void message.error({ content: "输入的分数不在有效间隔内", duration: 5 });
       void message.error({ content: "输入的分数不在有效间隔内", duration: 5 });
       return;
       return;
     }
     }
-    const { __index } = store.currentQuestion;
-    store.currentTask.markResult.scoreList[__index] = score;
+    const { __index } = markStore.currentQuestion;
+    markStore.currentTask.markResult.scoreList[__index] = score;
     //
     //
     // scoreStr = "";
     // scoreStr = "";
     // console.log("give score", score);
     // console.log("give score", score);
     const idx = indexOfCurrentQuestion();
     const idx = indexOfCurrentQuestion();
-    if (idx + 1 < store.currentTask.questionList.length) {
-      chooseQuestion(store.currentTask.questionList[idx + 1]);
+    if (idx + 1 < markStore.currentTask.questionList.length) {
+      chooseQuestion(markStore.currentTask.questionList[idx + 1]);
     }
     }
     return;
     return;
   }
   }
   if (event.key === "ArrowUp") {
   if (event.key === "ArrowUp") {
     const idx = indexOfCurrentQuestion();
     const idx = indexOfCurrentQuestion();
     if (idx > 0) {
     if (idx > 0) {
-      chooseQuestion(store.currentTask.questionList[idx - 1]);
+      chooseQuestion(markStore.currentTask.questionList[idx - 1]);
     }
     }
     event.preventDefault();
     event.preventDefault();
     return;
     return;
   }
   }
   if (event.key === "ArrowDown") {
   if (event.key === "ArrowDown") {
     const idx = indexOfCurrentQuestion();
     const idx = indexOfCurrentQuestion();
-    if (idx < store.currentTask.questionList.length - 1) {
-      chooseQuestion(store.currentTask.questionList[idx + 1]);
+    if (idx < markStore.currentTask.questionList.length - 1) {
+      chooseQuestion(markStore.currentTask.questionList[idx + 1]);
     }
     }
     event.preventDefault();
     event.preventDefault();
     return;
     return;
@@ -315,9 +317,9 @@ const scrollToQuestion = (question: Question) => {
 };
 };
 
 
 watch(
 watch(
-  () => store.currentQuestion,
+  () => markStore.currentQuestion,
   () => {
   () => {
-    store.currentQuestion && scrollToQuestion(store.currentQuestion);
+    markStore.currentQuestion && scrollToQuestion(markStore.currentQuestion);
   }
   }
 );
 );
 
 
@@ -330,7 +332,9 @@ function checkSubmit() {
 }
 }
 
 
 const buttonHeightForSelective = $computed(() =>
 const buttonHeightForSelective = $computed(() =>
-  store.setting.selective && store.setting.enableAllZero ? "36px" : "76px"
+  markStore.setting.selective && markStore.setting.enableAllZero
+    ? "36px"
+    : "76px"
 );
 );
 </script>
 </script>
 
 

+ 20 - 18
src/features/mark/MarkBoardMouse.vue → src/features/mark/scoring/MarkBoardMouse.vue

@@ -1,14 +1,14 @@
 <template>
 <template>
   <div
   <div
-    v-if="store.currentTask"
+    v-if="markStore.currentTask"
     class="mark-board-track is-mouse-board"
     class="mark-board-track is-mouse-board"
-    :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
+    :class="[markStore.isScoreBoardCollapsed ? 'hide' : 'show']"
   >
   >
     <div class="board-mode">
     <div class="board-mode">
       <a-dropdown>
       <a-dropdown>
         <template #overlay>
         <template #overlay>
           <a-menu>
           <a-menu>
-            <a-menu-item key="1" @click="toggleKeyMouse">
+            <a-menu-item key="1" @click="markStore.toggleKeyMouse">
               键盘给分
               键盘给分
             </a-menu-item>
             </a-menu-item>
           </a-menu>
           </a-menu>
@@ -26,8 +26,8 @@
         </div>
         </div>
         <div class="board-header-score">
         <div class="board-header-score">
           <transition-group name="score-number-animation" tag="span">
           <transition-group name="score-number-animation" tag="span">
-            <span :key="store.currentTask.markResult?.markerScore || 0">{{
-              store.currentTask.markResult?.markerScore
+            <span :key="markStore.currentTask.markResult?.markerScore || 0">{{
+              markStore.currentTask.markResult?.markerScore
             }}</span>
             }}</span>
           </transition-group>
           </transition-group>
         </div>
         </div>
@@ -53,14 +53,14 @@
     </div>
     </div>
 
 
     <div
     <div
-      v-if="store.currentTask && store.currentTask.questionList"
+      v-if="markStore.currentTask && markStore.currentTask.questionList"
       class="board-questions"
       class="board-questions"
       :style="{
       :style="{
         flexGrow: 2,
         flexGrow: 2,
       }"
       }"
     >
     >
       <template
       <template
-        v-for="(question, index) in store.currentTask.questionList"
+        v-for="(question, index) in markStore.currentTask.questionList"
         :key="index"
         :key="index"
       >
       >
         <div class="board-question-full-box">
         <div class="board-question-full-box">
@@ -98,11 +98,12 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import type { Question } from "@/types";
 import type { Question } from "@/types";
-import { store } from "@/store/store";
-import { keyMouse } from "./use/keyboardAndMouse";
+import { useMarkStore } from "@/store";
 import { DownOutlined } from "@ant-design/icons-vue";
 import { DownOutlined } from "@ant-design/icons-vue";
 import { ref } from "vue";
 import { ref } from "vue";
 
 
+const markStore = useMarkStore();
+
 const emit = defineEmits([
 const emit = defineEmits([
   "submit",
   "submit",
   "allZeroSubmit",
   "allZeroSubmit",
@@ -111,24 +112,23 @@ const emit = defineEmits([
 ]);
 ]);
 const props = defineProps<{ isCheckAnswer?: boolean }>();
 const props = defineProps<{ isCheckAnswer?: boolean }>();
 
 
-const { toggleKeyMouse } = keyMouse();
 const hasModifyScore = ref(false);
 const hasModifyScore = ref(false);
 
 
 function chooseScore(question: Question, score: number) {
 function chooseScore(question: Question, score: number) {
   // 只要修改了分值,就当做已修改
   // 只要修改了分值,就当做已修改
   hasModifyScore.value = true;
   hasModifyScore.value = true;
 
 
-  store.currentQuestion = question;
-  const { __index } = store.currentQuestion;
-  store.currentTask &&
-    (store.currentTask.markResult.scoreList[__index] = score);
+  markStore.currentQuestion = question;
+  const { __index } = markStore.currentQuestion;
+  markStore.currentTask &&
+    (markStore.currentTask.markResult.scoreList[__index] = score);
 }
 }
 function isCurrentScore(question: Question, score: number) {
 function isCurrentScore(question: Question, score: number) {
   const { __index } = question;
   const { __index } = question;
   const questionScore =
   const questionScore =
-    store.currentTask &&
-    store.currentTask.markResult &&
-    store.currentTask.markResult.scoreList[__index];
+    markStore.currentTask &&
+    markStore.currentTask.markResult &&
+    markStore.currentTask.markResult.scoreList[__index];
   return questionScore === score;
   return questionScore === score;
 }
 }
 
 
@@ -163,7 +163,9 @@ function checkSubmit() {
 }
 }
 
 
 const buttonHeightForSelective = $computed(() =>
 const buttonHeightForSelective = $computed(() =>
-  store.setting.selective && store.setting.enableAllZero ? "36px" : "76px"
+  markStore.setting.selective && markStore.setting.enableAllZero
+    ? "36px"
+    : "76px"
 );
 );
 </script>
 </script>
 
 

+ 72 - 74
src/features/mark/MarkBoardTrack.vue → src/features/mark/scoring/MarkBoardTrack.vue

@@ -1,11 +1,11 @@
 <template>
 <template>
   <div
   <div
-    v-if="store.currentTask"
+    v-if="markStore.currentTask"
     class="mark-board-track"
     class="mark-board-track"
     :class="[
     :class="[
       {
       {
-        hide: store.isScoreBoardCollapsed && !props.modal,
-        'in-dialog': store.isScoreBoardCollapsed && props.modal,
+        hide: markStore.isScoreBoardCollapsed && !props.modal,
+        'in-dialog': markStore.isScoreBoardCollapsed && props.modal,
       },
       },
     ]"
     ]"
     :style="{
     :style="{
@@ -19,8 +19,8 @@
           <span>总分</span>
           <span>总分</span>
         </div>
         </div>
         <div class="board-header-score">
         <div class="board-header-score">
-          <span :key="store.currentTask.markResult?.markerScore || 0">{{
-            store.currentTask.markResult?.markerScore
+          <span :key="markStore.currentTask.markResult?.markerScore || 0">{{
+            markStore.currentTask.markResult?.markerScore
           }}</span>
           }}</span>
         </div>
         </div>
       </div>
       </div>
@@ -29,7 +29,7 @@
         class="board-header-submit"
         class="board-header-submit"
         size="medium"
         size="medium"
         type="primary"
         type="primary"
-        :disabled="!store.currentTask"
+        :disabled="!markStore.currentTask"
         @click="checkSubmit"
         @click="checkSubmit"
       >
       >
         复核
         复核
@@ -45,7 +45,7 @@
       </qm-button>
       </qm-button>
     </div>
     </div>
     <div
     <div
-      v-if="store.currentTask && store.currentTask.questionList"
+      v-if="markStore.currentTask && markStore.currentTask.questionList"
       class="board-questions"
       class="board-questions"
       :style="{
       :style="{
         height: areaHeight ? `${areaHeight}px` : 'auto',
         height: areaHeight ? `${areaHeight}px` : 'auto',
@@ -53,13 +53,13 @@
       }"
       }"
     >
     >
       <template
       <template
-        v-for="(question, index) in store.currentTask.questionList"
+        v-for="(question, index) in markStore.currentTask.questionList"
         :key="index"
         :key="index"
       >
       >
         <div class="board-question-box">
         <div class="board-question-box">
           <div
           <div
             :id="
             :id="
-              store.isScoreBoardCollapsed
+              markStore.isScoreBoardCollapsed
                 ? props.modal
                 ? props.modal
                   ? ['bq', question.mainNumber, question.subNumber].join('-')
                   ? ['bq', question.mainNumber, question.subNumber].join('-')
                   : ''
                   : ''
@@ -79,7 +79,7 @@
             <div
             <div
               v-if="
               v-if="
                 activeRightMenuItem?.id ==
                 activeRightMenuItem?.id ==
-                (store.isScoreBoardCollapsed
+                (markStore.isScoreBoardCollapsed
                   ? props.modal
                   ? props.modal
                     ? ['bq', question.mainNumber, question.subNumber].join('-')
                     ? ['bq', question.mainNumber, question.subNumber].join('-')
                     : ''
                     : ''
@@ -102,10 +102,12 @@
             </div>
             </div>
             <!-- 设置高度 避免动画跳动 -->
             <!-- 设置高度 避免动画跳动 -->
             <div class="question-score">
             <div class="question-score">
-              <span :key="store.currentTask?.markResult?.scoreList[index] || 0">
+              <span
+                :key="markStore.currentTask?.markResult?.scoreList[index] || 0"
+              >
                 <!-- 特殊的空格符号 -->
                 <!-- 特殊的空格符号 -->
                 <!-- eslint-disable-next-line no-irregular-whitespace -->
                 <!-- eslint-disable-next-line no-irregular-whitespace -->
-                {{ store.currentTask?.markResult?.scoreList[index] ?? " " }}
+                {{ markStore.currentTask?.markResult?.scoreList[index] ?? " " }}
               </span>
               </span>
             </div>
             </div>
           </div>
           </div>
@@ -134,7 +136,7 @@
       </div>
       </div>
       <div
       <div
         class="board-score"
         class="board-score"
-        :class="Object.is(store.currentScore, 0) && 'is-current'"
+        :class="Object.is(markStore.currentScore, 0) && 'is-current'"
         @click="chooseScore(0)"
         @click="chooseScore(0)"
       >
       >
         0
         0
@@ -175,18 +177,21 @@
 import type { Question } from "@/types";
 import type { Question } from "@/types";
 import { isNumber } from "lodash-es";
 import { isNumber } from "lodash-es";
 import { onMounted, onUnmounted, watch, ref, reactive, nextTick } from "vue";
 import { onMounted, onUnmounted, watch, ref, reactive, nextTick } from "vue";
-import { store } from "@/store/store";
-import { autoChooseFirstQuestion } from "./use/autoChooseFirstQuestion";
-import { dragSplitPane } from "./use/splitPane";
-import { addFocusTrack, removeFocusTrack } from "./use/focusTracks";
+import { useMarkStore } from "@/store";
+import useAutoChooseFirstQuestion from "../composables/useAutoChooseFirstQuestion";
+import useDragSplitPane from "../composables/useDragSplitPane";
+import useFocusTracks from "../composables/useFocusTracks";
 import EventBus from "@/plugins/eventBus";
 import EventBus from "@/plugins/eventBus";
 import { cloneDeep } from "lodash-es";
 import { cloneDeep } from "lodash-es";
 
 
 const props = defineProps<{ modal?: boolean; isCheckAnswer?: boolean }>();
 const props = defineProps<{ modal?: boolean; isCheckAnswer?: boolean }>();
 const emit = defineEmits(["submit", "unselectiveSubmit", "checkSubmit"]);
 const emit = defineEmits(["submit", "unselectiveSubmit", "checkSubmit"]);
-const { dragSpliter, areaHeight } = dragSplitPane();
+
+const { addFocusTrack, removeFocusTrack } = useFocusTracks();
+const { dragSpliter, areaHeight } = useDragSplitPane();
 const activeRightMenuItem = ref<any>(null);
 const activeRightMenuItem = ref<any>(null);
 const hasModifyScore = ref(false);
 const hasModifyScore = ref(false);
+const markStore = useMarkStore();
 
 
 const tmpStyle = reactive<any>({
 const tmpStyle = reactive<any>({
   left: 0,
   left: 0,
@@ -207,8 +212,8 @@ function getParentNode(el: any, c: string): any {
 const onRightClick = (e: any, index?: any) => {
 const onRightClick = (e: any, index?: any) => {
   e.preventDefault();
   e.preventDefault();
   if (
   if (
-    store.currentTask?.markResult.scoreList[index] ||
-    store.currentTask?.markResult.scoreList[index] === 0
+    markStore.currentTask?.markResult.scoreList[index] ||
+    markStore.currentTask?.markResult.scoreList[index] === 0
   ) {
   ) {
     tmpStyle.left = e.clientX + "px";
     tmpStyle.left = e.clientX + "px";
     tmpStyle.top = e.clientY + "px";
     tmpStyle.top = e.clientY + "px";
@@ -224,10 +229,6 @@ const rightBlur = () => {
   removeFocusTrack();
   removeFocusTrack();
 };
 };
 const positioning = (question: Question) => {
 const positioning = (question: Question) => {
-  // let list =
-  //   store.getMarkStatus === "正评" || store.getMarkStatus === "试评"
-  //     ? sliceImagesWithTrackListCopy.value
-  //     : undefined;
   // addFocusTrack(undefined, question.mainNumber, question.subNumber, true, list);
   // addFocusTrack(undefined, question.mainNumber, question.subNumber, true, list);
   console.log(
   console.log(
     "sliceImagesWithTrackListCopy:",
     "sliceImagesWithTrackListCopy:",
@@ -243,7 +244,7 @@ const positioning = (question: Question) => {
   activeRightMenuItem.value = null;
   activeRightMenuItem.value = null;
 };
 };
 
 
-const { chooseQuestion } = autoChooseFirstQuestion();
+const { chooseQuestion } = useAutoChooseFirstQuestion();
 let sliceImagesWithTrackListCopy = ref([]);
 let sliceImagesWithTrackListCopy = ref([]);
 EventBus.on("draw-change", (list: any) => {
 EventBus.on("draw-change", (list: any) => {
   sliceImagesWithTrackListCopy.value = cloneDeep(list);
   sliceImagesWithTrackListCopy.value = cloneDeep(list);
@@ -251,9 +252,9 @@ EventBus.on("draw-change", (list: any) => {
 
 
 // 切换题目是清空上一题的分数
 // 切换题目是清空上一题的分数
 watch(
 watch(
-  () => store.currentQuestion,
+  () => markStore.currentQuestion,
   () => {
   () => {
-    store.currentScore = undefined;
+    markStore.currentScore = undefined;
     // eslint-disable-next-line @typescript-eslint/no-floating-promises
     // eslint-disable-next-line @typescript-eslint/no-floating-promises
     nextTick(() => {
     nextTick(() => {
       if (!props.isCheckAnswer) chooseScore(questionScoreSteps[1]);
       if (!props.isCheckAnswer) chooseScore(questionScoreSteps[1]);
@@ -262,34 +263,36 @@ watch(
 );
 );
 
 
 watch(
 watch(
-  () => store.currentTask,
+  () => markStore.currentTask,
   () => {
   () => {
-    if (!store.currentTask) return;
+    if (!markStore.currentTask) return;
     if (!props.isCheckAnswer) return;
     if (!props.isCheckAnswer) return;
 
 
     let currentTaskModifyQuestion = {};
     let currentTaskModifyQuestion = {};
-    store.currentTask.questionList.forEach((q) => {
+    markStore.currentTask.questionList.forEach((q) => {
       const qno = `${q.mainNumber}_${q.subNumber}`;
       const qno = `${q.mainNumber}_${q.subNumber}`;
       currentTaskModifyQuestion[qno] = false;
       currentTaskModifyQuestion[qno] = false;
     });
     });
-    store.currentTaskModifyQuestion = currentTaskModifyQuestion;
+    markStore.currentTaskModifyQuestion = currentTaskModifyQuestion;
   }
   }
 );
 );
 
 
 const questionScoreDisabled = $computed(() => {
 const questionScoreDisabled = $computed(() => {
-  const qno = `${store.currentQuestion.mainNumber}_${store.currentQuestion.subNumber}`;
-  return props.isCheckAnswer && !store.currentTaskModifyQuestion[qno];
+  const qno = `${markStore.currentQuestion.mainNumber}_${markStore.currentQuestion.subNumber}`;
+  return props.isCheckAnswer && !markStore.currentTaskModifyQuestion[qno];
 });
 });
 
 
 const questionScore = $computed(
 const questionScore = $computed(
   () =>
   () =>
-    store.currentTask &&
-    store.currentQuestion &&
-    store.currentTask.markResult?.scoreList[store.currentQuestion.__index]
+    markStore.currentTask &&
+    markStore.currentQuestion &&
+    markStore.currentTask.markResult?.scoreList[
+      markStore.currentQuestion.__index
+    ]
 );
 );
 
 
 const questionScoreSteps = $computed(() => {
 const questionScoreSteps = $computed(() => {
-  const question = store.currentQuestion;
+  const question = markStore.currentQuestion;
   if (!question) return [];
   if (!question) return [];
 
 
   const remainScore =
   const remainScore =
@@ -319,43 +322,38 @@ const questionScoreSteps = $computed(() => {
 
 
 function isCurrentQuestion(question: Question) {
 function isCurrentQuestion(question: Question) {
   return (
   return (
-    store.currentQuestion?.mainNumber === question.mainNumber &&
-    store.currentQuestion?.subNumber === question.subNumber
+    markStore.currentQuestion?.mainNumber === question.mainNumber &&
+    markStore.currentQuestion?.subNumber === question.subNumber
   );
   );
 }
 }
 
 
 function isCurrentScore(score: number) {
 function isCurrentScore(score: number) {
-  return store.currentScore === score;
+  return markStore.currentScore === score;
 }
 }
 function chooseScore(score: number) {
 function chooseScore(score: number) {
-  if (store.currentScore === score) {
-    store.currentScore = undefined;
+  if (markStore.currentScore === score) {
+    markStore.currentScore = undefined;
   } else {
   } else {
-    store.currentScore = score;
-    store.currentSpecialTag = undefined;
+    markStore.currentScore = score;
+    markStore.currentSpecialTag = undefined;
   }
   }
-  store.currentSpecialTagType = undefined;
+  markStore.currentSpecialTagType = undefined;
 }
 }
 
 
 let keyPressTimestamp = 0;
 let keyPressTimestamp = 0;
 let keys: string[] = [];
 let keys: string[] = [];
 function numberKeyListener(event: KeyboardEvent) {
 function numberKeyListener(event: KeyboardEvent) {
   // if (event.target.tagName !== "BODY") return;
   // if (event.target.tagName !== "BODY") return;
-  if (!store.currentQuestion) return;
+  if (!markStore.currentQuestion) return;
   if (questionScoreDisabled) return;
   if (questionScoreDisabled) return;
   if (" jiklc".includes(event.key)) return;
   if (" jiklc".includes(event.key)) return;
-  // if (event.key === "#") {
-  //   keys = [];
-  //   store.currentScore = -0;
-  //   return;
-  // }
 
 
   function indexOfCurrentQuestion() {
   function indexOfCurrentQuestion() {
     return (
     return (
-      store.currentTask?.questionList.findIndex(
+      markStore.currentTask?.questionList.findIndex(
         (q) =>
         (q) =>
-          q.mainNumber === store.currentQuestion?.mainNumber &&
-          q.subNumber === store.currentQuestion.subNumber
+          q.mainNumber === markStore.currentQuestion?.mainNumber &&
+          q.subNumber === markStore.currentQuestion.subNumber
       ) ?? -1
       ) ?? -1
     );
     );
   }
   }
@@ -363,9 +361,9 @@ function numberKeyListener(event: KeyboardEvent) {
   // tab 循环答题列表
   // tab 循环答题列表
   if (event.key === "Tab") {
   if (event.key === "Tab") {
     const idx = indexOfCurrentQuestion();
     const idx = indexOfCurrentQuestion();
-    if (idx >= 0 && store.currentTask) {
-      const len = store.currentTask.questionList.length;
-      chooseQuestion(store.currentTask.questionList[(idx + 1) % len]);
+    if (idx >= 0 && markStore.currentTask) {
+      const len = markStore.currentTask.questionList.length;
+      chooseQuestion(markStore.currentTask.questionList[(idx + 1) % len]);
       event.preventDefault();
       event.preventDefault();
     }
     }
     return;
     return;
@@ -382,8 +380,8 @@ function numberKeyListener(event: KeyboardEvent) {
   }
   }
   if (event.key === "Escape") {
   if (event.key === "Escape") {
     keys = [];
     keys = [];
-    store.currentScore = undefined;
-    store.currentSpecialTag = undefined;
+    markStore.currentScore = undefined;
+    markStore.currentSpecialTag = undefined;
     return;
     return;
   }
   }
   const score = parseFloat(keys.join(""));
   const score = parseFloat(keys.join(""));
@@ -409,10 +407,10 @@ onUnmounted(() => {
 });
 });
 
 
 watch(
 watch(
-  () => store.isScoreBoardCollapsed,
+  () => markStore.isScoreBoardCollapsed,
   () => {
   () => {
     // 此处的逻辑是 MarkBoardTrackDialog 带来的,不然 numberKeyListener 在两个组件中多次触发有问题
     // 此处的逻辑是 MarkBoardTrackDialog 带来的,不然 numberKeyListener 在两个组件中多次触发有问题
-    if (store.isScoreBoardCollapsed) {
+    if (markStore.isScoreBoardCollapsed) {
       document.removeEventListener("keydown", numberKeyListener);
       document.removeEventListener("keydown", numberKeyListener);
       document.removeEventListener("keydown", submitListener);
       document.removeEventListener("keydown", submitListener);
     } else {
     } else {
@@ -425,10 +423,10 @@ watch(
 );
 );
 
 
 function clearLatestMarkOfCurrentQuetion() {
 function clearLatestMarkOfCurrentQuetion() {
-  if (!store.currentTask?.markResult || !store.currentQuestion) return;
+  if (!markStore.currentTask?.markResult || !markStore.currentQuestion) return;
 
 
-  const { __index, mainNumber, subNumber } = store.currentQuestion;
-  const markResult = store.currentTask.markResult;
+  const { __index, mainNumber, subNumber } = markStore.currentQuestion;
+  const markResult = markStore.currentTask.markResult;
   const ts = markResult.trackList.filter(
   const ts = markResult.trackList.filter(
     (q) => q.mainNumber === mainNumber && q.subNumber === subNumber
     (q) => q.mainNumber === mainNumber && q.subNumber === subNumber
   );
   );
@@ -438,7 +436,7 @@ function clearLatestMarkOfCurrentQuetion() {
     return;
     return;
   }
   }
   const lastMark = ts.splice(-1)[0];
   const lastMark = ts.splice(-1)[0];
-  store.removeScoreTracks = [lastMark];
+  markStore.removeScoreTracks = [lastMark];
   markResult.trackList = markResult.trackList.filter((t) => t !== lastMark);
   markResult.trackList = markResult.trackList.filter((t) => t !== lastMark);
 
 
   markResult.scoreList[__index] =
   markResult.scoreList[__index] =
@@ -453,21 +451,21 @@ function clearAllMarksOfCurrentQuetion() {
   // 只要清除分数,就当做修改了
   // 只要清除分数,就当做修改了
   hasModifyScore.value = true;
   hasModifyScore.value = true;
 
 
-  if (!store.currentTask?.markResult || !store.currentQuestion) return;
+  if (!markStore.currentTask?.markResult || !markStore.currentQuestion) return;
 
 
-  const qno = `${store.currentQuestion.mainNumber}_${store.currentQuestion.subNumber}`;
-  store.currentTaskModifyQuestion[qno] = true;
+  const qno = `${markStore.currentQuestion.mainNumber}_${markStore.currentQuestion.subNumber}`;
+  markStore.currentTaskModifyQuestion[qno] = true;
 
 
-  const markResult = store.currentTask.markResult;
-  store.removeScoreTracks = markResult.trackList.filter(
+  const markResult = markStore.currentTask.markResult;
+  markStore.removeScoreTracks = markResult.trackList.filter(
     (q) =>
     (q) =>
-      q.mainNumber === store.currentQuestion?.mainNumber &&
-      q.subNumber === store.currentQuestion?.subNumber
+      q.mainNumber === markStore.currentQuestion?.mainNumber &&
+      q.subNumber === markStore.currentQuestion?.subNumber
   );
   );
   markResult.trackList = markResult.trackList.filter(
   markResult.trackList = markResult.trackList.filter(
-    (q) => !store.removeScoreTracks.includes(q)
+    (q) => !markStore.removeScoreTracks.includes(q)
   );
   );
-  const { __index } = store.currentQuestion;
+  const { __index } = markStore.currentQuestion;
   markResult.scoreList[__index] = null;
   markResult.scoreList[__index] = null;
 }
 }
 
 

+ 5 - 3
src/features/mark/MarkBoardTrackDialog.vue → src/features/mark/scoring/MarkBoardTrackDialog.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <qm-dialog
   <qm-dialog
-    v-if="store.isScoreBoardCollapsed"
+    v-if="markStore.isScoreBoardCollapsed"
     title="给分板"
     title="给分板"
     top="10%"
     top="10%"
     fixedWidth
     fixedWidth
@@ -21,13 +21,15 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import QmDialog from "@/components/QmDialog.vue";
 import QmDialog from "@/components/QmDialog.vue";
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
 import MarkBoardTrack from "./MarkBoardTrack.vue";
 import MarkBoardTrack from "./MarkBoardTrack.vue";
 
 
+const markStore = useMarkStore();
+
 defineEmits(["submit", "allZeroSubmit", "unselectiveSubmit"]);
 defineEmits(["submit", "allZeroSubmit", "unselectiveSubmit"]);
 defineProps<{ isCheckAnswer?: boolean }>();
 defineProps<{ isCheckAnswer?: boolean }>();
 
 
 const close = () => {
 const close = () => {
-  store.toggleScoreBoard();
+  markStore.toggleScoreBoard();
 };
 };
 </script>
 </script>

+ 173 - 0
src/features/mark/stores/mark.ts

@@ -0,0 +1,173 @@
+import { defineStore } from "pinia";
+import { Setting, MarkStore, AdminPageSetting, Task } from "@/types";
+
+const useMarkStore = defineStore("mark", {
+  state: (): MarkStore => ({
+    setting: {
+      mode: "TRACK",
+      examType: "SCAN_IMAGE",
+      forceMode: false,
+      sheetView: false,
+      autoScroll: false,
+      sheetConfig: [],
+      enableAllZero: false,
+      enableSplit: true,
+      fileServer: "",
+      userName: "",
+      subject: <Setting["subject"]>{},
+      forceSpecialTag: false,
+      uiSetting: {
+        "answer.paper.scale": 1,
+        "score.board.collapse": false,
+        "normal.mode": "keyboard",
+        "paper.modal": false,
+        "answer.modal": false,
+        "minimap.modal": false,
+        "specialTag.modal": false,
+        "shortCut.modal": false,
+        "score.fontSize.scale": 1,
+      },
+      statusValue: null,
+      problemTypes: [],
+      groupNumber: -987654, // 默认不可能的值
+      groupTitle: "",
+      topCount: 0,
+      splitConfig: [],
+      prefetchCount: 3,
+      startTime: 0,
+      endTime: 0,
+      selective: false,
+    },
+    status: <MarkStore["status"]>{},
+    groups: [],
+    tasks: [],
+    message: null,
+    currentTask: undefined,
+    // 主观题检查时,缓存已经修改过的试题
+    currentTaskModifyQuestion: {},
+    currentQuestion: undefined,
+    currentScore: undefined,
+    currentSpecialTag: undefined,
+    currentSpecialTagType: undefined,
+    historyOpen: false,
+    historyTasks: [],
+    removeScoreTracks: [],
+    focusTracks: [],
+    minimapScrollToX: 0,
+    minimapScrollToY: 0,
+    allPaperModal: false,
+    sheetViewModal: false,
+    globalMask: false,
+  }),
+
+  getters: {
+    info(state: MarkStore): MarkStore {
+      return { ...state };
+    },
+    /** 获得statusValue的中文名 */
+    getStatusValueName(state: MarkStore): string {
+      const st = state.setting.statusValue;
+      if (!st) return "";
+      if (st === "FORMAL") return "正评";
+      if (st === "TRIAL") return "试评";
+      return "";
+    },
+    /** 当前任务。确保不为空,需在上文已经检查过 store.currentTask 不为空 */
+    currentTaskEnsured(state: MarkStore): Task {
+      return state.currentTask;
+    },
+    /** 是否是评卷端的轨迹模式 */
+    isTrackMode(state: MarkStore): boolean {
+      return state.setting.mode && state.setting.mode === "TRACK";
+    },
+    /** 评卷端的轨迹模式显示轨迹 && 管理后台都显示轨迹 */
+    shouldShowTrack(state: MarkStore): boolean {
+      // FIXME: 不是最优雅的方式来判断是否是阅卷端
+      const isWebMark = location.pathname === "/mark/mark";
+      return !isWebMark || state.isTrackMode;
+    },
+    /* 是否是扫描阅卷 */
+    isScanImage(state: MarkStore): boolean {
+      return state.setting.examType === "SCAN_IMAGE";
+    },
+    isMultiMedia(state: MarkStore): boolean {
+      return state.setting.examType === "MULTI_MEDIA";
+    },
+    /* 返回正在评卷的状态 '' | 回评 | 打回 */
+    getMarkStatus(state: MarkStore): string {
+      if (!state.currentTask) return "";
+      if (state.currentTask.previous) return "回评";
+      if (state.currentTask.rejected) return "打回";
+
+      return state.getStatusValueName;
+    },
+    shouldShowMarkBoardKeyBoard(state: MarkStore): boolean {
+      return (
+        state.setting.mode === "COMMON" &&
+        state.setting.uiSetting["normal.mode"] === "keyboard"
+      );
+    },
+    shouldShowMarkBoardMouse(state: MarkStore): boolean {
+      return (
+        state.setting.mode === "COMMON" &&
+        state.setting.uiSetting["normal.mode"] === "mouse"
+      );
+    },
+    isScoreBoardCollapsed(state: MarkStore): boolean {
+      return state.setting.uiSetting["score.board.collapse"];
+    },
+    isScoreBoardVisible(state: MarkStore): boolean {
+      return !state.setting.uiSetting["score.board.collapse"];
+    },
+    toggleKeyMouse(state: MarkStore): void {
+      state.setting.uiSetting["normal.mode"] =
+        state.setting.uiSetting["normal.mode"] === "keyboard"
+          ? "mouse"
+          : "keyboard";
+    },
+  },
+
+  actions: {
+    setInfo(partial: Partial<MarkStore>) {
+      this.$patch(partial);
+    },
+    resetInfo() {
+      this.$reset();
+    },
+    initSetting(adminPageSetting: AdminPageSetting): void {
+      this.setting = {
+        ...this.setting,
+        ...adminPageSetting,
+        mode: "COMMON" as Setting["mode"],
+        uiSetting: {
+          "answer.paper.scale": 1,
+          "score.board.collapse": false,
+          "normal.mode": "keyboard",
+          "score.fontSize.scale": 1,
+        } as Setting["uiSetting"],
+      };
+
+      const fileServer = this.setting.fileServer;
+      if (this.setting.subject?.answerUrl) {
+        this.setting.subject.answerUrl =
+          fileServer + this.setting.subject?.answerUrl;
+      }
+      if (this.setting.subject?.paperUrl) {
+        this.setting.subject.paperUrl =
+          fileServer + this.setting.subject?.paperUrl;
+      }
+    },
+    toggleHistory(): void {
+      this.historyOpen = !this.historyOpen;
+    },
+    toggleScoreBoard(): void {
+      this.setting.uiSetting["score.board.collapse"] =
+        !this.setting.uiSetting["score.board.collapse"];
+    },
+  },
+  persist: {
+    storage: sessionStorage,
+  },
+});
+
+export default useMarkStore;

+ 5 - 3
src/features/mark/MarkChangeProfile.vue → src/features/mark/toolbar/MarkChangeProfile.vue

@@ -39,20 +39,22 @@
 import { changeUserInfo, doLogout } from "@/api/markPage";
 import { changeUserInfo, doLogout } from "@/api/markPage";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
 import { reactive, watchEffect } from "vue";
 import { reactive, watchEffect } from "vue";
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
 
 
 interface User {
 interface User {
   name: string;
   name: string;
   password: string;
   password: string;
   confirmPassword: string;
   confirmPassword: string;
 }
 }
+
+const markStore = useMarkStore();
 const user: User = reactive({
 const user: User = reactive({
   name: "",
   name: "",
   password: "",
   password: "",
   confirmPassword: "",
   confirmPassword: "",
 });
 });
 watchEffect(() => {
 watchEffect(() => {
-  user.name = store.setting.userName;
+  user.name = markStore.setting.userName;
 });
 });
 let visible = $ref(false);
 let visible = $ref(false);
 let confirmLoading = $ref(false);
 let confirmLoading = $ref(false);
@@ -86,7 +88,7 @@ const handleOk = () => {
 };
 };
 
 
 const handleCancel = () => {
 const handleCancel = () => {
-  user.name = store.setting.userName;
+  user.name = markStore.setting.userName;
   user.password = "";
   user.password = "";
   user.confirmPassword = "";
   user.confirmPassword = "";
 };
 };

+ 67 - 53
src/features/mark/MarkHeader.vue → src/features/mark/toolbar/MarkHeader.vue

@@ -1,30 +1,33 @@
 <template>
 <template>
-  <div v-if="store.setting && store.setting.subject.name" class="mark-header">
+  <div
+    v-if="markStore.setting && markStore.setting.subject.name"
+    class="mark-header"
+  >
     <div class="mark-header-part">
     <div class="mark-header-part">
       <div
       <div
         :class="[
         :class="[
           'header-menu',
           'header-menu',
           'header-menu-left',
           'header-menu-left',
-          { 'is-toggled': store.historyOpen },
+          { 'is-toggled': markStore.historyOpen },
         ]"
         ]"
-        @click="store.toggleHistory"
+        @click="markStore.toggleHistory"
       >
       >
         <img class="header-icon" src="@/assets/icons/icon-left-menu.svg" />回评
         <img class="header-icon" src="@/assets/icons/icon-left-menu.svg" />回评
       </div>
       </div>
-      <div class="header-subject" :title="store.setting.subject.name">
+      <div class="header-subject" :title="markStore.setting.subject.name">
         <div>
         <div>
           {{
           {{
-            `${store.setting.subject.code ?? ""}-${
-              store.setting.subject.name ?? ""
+            `${markStore.setting.subject.code ?? ""}-${
+              markStore.setting.subject.name ?? ""
             }`
             }`
           }}
           }}
         </div>
         </div>
       </div>
       </div>
       <a-tooltip overlayClassName="mark-tooltip">
       <a-tooltip overlayClassName="mark-tooltip">
         <template #title>
         <template #title>
-          问题卷 {{ store.status.problemCount }}
+          问题卷 {{ markStore.status.problemCount }}
           <br />
           <br />
-          待仲裁 {{ store.status.arbitrateCount }}
+          待仲裁 {{ markStore.status.arbitrateCount }}
         </template>
         </template>
         <div class="header-programs">
         <div class="header-programs">
           <img
           <img
@@ -38,27 +41,27 @@
         <div class="header-noun">
         <div class="header-noun">
           <span>编号:</span>
           <span>编号:</span>
           <span>
           <span>
-            {{ store.currentTask?.secretNumber ?? "-" }}
+            {{ markStore.currentTask?.secretNumber ?? "-" }}
           </span>
           </span>
         </div>
         </div>
         <div
         <div
           v-if="
           v-if="
-            store.currentTask &&
-            store.currentTask.objectiveScore !== null &&
-            !!store.setting?.showObjectScore
+            markStore.currentTask &&
+            markStore.currentTask.objectiveScore !== null &&
+            !!markStore.setting?.showObjectScore
           "
           "
           class="header-noun"
           class="header-noun"
         >
         >
           <span>客观分:</span>
           <span>客观分:</span>
           <span>
           <span>
-            {{ store.currentTask.objectiveScore }}
+            {{ markStore.currentTask.objectiveScore }}
           </span>
           </span>
         </div>
         </div>
         <div
         <div
           v-if="
           v-if="
             props.showTotalScore &&
             props.showTotalScore &&
-            store.currentTask &&
-            store.currentTask.objectiveScore !== null
+            markStore.currentTask &&
+            markStore.currentTask.objectiveScore !== null
           "
           "
           class="header-noun"
           class="header-noun"
         >
         >
@@ -66,16 +69,16 @@
           <span> {{ totalScore }} </span>
           <span> {{ totalScore }} </span>
         </div>
         </div>
       </div>
       </div>
-      <div v-show="store.status.totalCount" class="header-total">
+      <div v-show="markStore.status.totalCount" class="header-total">
         <span class="header-noun">
         <span class="header-noun">
           <span>已评:</span>
           <span>已评:</span>
-          <span :key="store.status.personCount || 0">
-            {{ store.status.personCount }}
+          <span :key="markStore.status.personCount || 0">
+            {{ markStore.status.personCount }}
           </span>
           </span>
         </span>
         </span>
-        <span v-if="store.setting.topCount" class="header-noun">
+        <span v-if="markStore.setting.topCount" class="header-noun">
           <span>分配:</span>
           <span>分配:</span>
-          <span>{{ store.setting.topCount ?? "-" }}</span>
+          <span>{{ markStore.setting.topCount ?? "-" }}</span>
         </span>
         </span>
         <span class="header-noun">
         <span class="header-noun">
           <span>未评:</span>
           <span>未评:</span>
@@ -94,14 +97,14 @@
       <a-tooltip overlayClassName="mark-tooltip">
       <a-tooltip overlayClassName="mark-tooltip">
         <template #title>
         <template #title>
           {{
           {{
-            store.setting.startTime > 0
-              ? $filters.datetimeFilter(store.setting.startTime)
+            markStore.setting.startTime > 0
+              ? $filters.datetimeFilter(markStore.setting.startTime)
               : "-"
               : "-"
           }}
           }}
           <div style="text-align: center">~</div>
           <div style="text-align: center">~</div>
           {{
           {{
-            store.setting.endTime > 0
-              ? $filters.datetimeFilter(store.setting.endTime)
+            markStore.setting.endTime > 0
+              ? $filters.datetimeFilter(markStore.setting.endTime)
               : "-"
               : "-"
           }}
           }}
         </template>
         </template>
@@ -113,7 +116,7 @@
         </div>
         </div>
       </a-tooltip>
       </a-tooltip>
       <a-dropdown>
       <a-dropdown>
-        <template v-if="!store.setting.forceMode" #overlay>
+        <template v-if="!markStore.setting.forceMode" #overlay>
           <a-menu>
           <a-menu>
             <a-menu-item key="1" @click="toggleSettingMode">
             <a-menu-item key="1" @click="toggleSettingMode">
               {{ exchangeModeName }}
               {{ exchangeModeName }}
@@ -124,36 +127,44 @@
         <div class="header-text-btn">
         <div class="header-text-btn">
           <img src="@/assets/icons/icon-track-mode.svg" class="header-icon" />
           <img src="@/assets/icons/icon-track-mode.svg" class="header-icon" />
           {{ modeName }}
           {{ modeName }}
-          <CaretDownOutlined v-if="!store.setting.forceMode" class="a-icon" />
+          <CaretDownOutlined
+            v-if="!markStore.setting.forceMode"
+            class="a-icon"
+          />
         </div>
         </div>
       </a-dropdown>
       </a-dropdown>
       <div
       <div
         class="header-text-btn"
         class="header-text-btn"
-        :title="store.setting.groupTitle + '-' + store.setting.groupNumber"
+        :title="
+          markStore.setting.groupTitle + '-' + markStore.setting.groupNumber
+        "
         @click="openSwitchGroupModal"
         @click="openSwitchGroupModal"
       >
       >
         <img class="header-icon" src="@/assets/icons/icon-group.svg" />{{
         <img class="header-icon" src="@/assets/icons/icon-group.svg" />{{
-          "分组:" + store.setting.groupNumber
+          "分组:" + markStore.setting.groupNumber
         }}
         }}
-        <CaretDownOutlined v-if="store.groups.length > 1" class="a-icon" />
+        <CaretDownOutlined v-if="markStore.groups.length > 1" class="a-icon" />
       </div>
       </div>
       <div class="header-text-btn">
       <div class="header-text-btn">
         <img class="header-icon" src="@/assets/icons/icon-user.svg" />{{
         <img class="header-icon" src="@/assets/icons/icon-user.svg" />{{
-          store.setting.userName
+          markStore.setting.userName
         }}
         }}
       </div>
       </div>
       <div class="header-text-btn header-logout" @click="logout">
       <div class="header-text-btn header-logout" @click="logout">
         <img class="header-icon" src="@/assets/icons/icon-return.svg" />返回
         <img class="header-icon" src="@/assets/icons/icon-return.svg" />返回
       </div>
       </div>
 
 
-      <a-tooltip v-if="store.isTrackMode" placement="bottomRight">
+      <a-tooltip v-if="markStore.isTrackMode" placement="bottomRight">
         <template #title>弹出给分板</template>
         <template #title>弹出给分板</template>
         <div
         <div
           :class="[
           :class="[
             'header-menu',
             'header-menu',
-            { 'is-toggled': store.isScoreBoardVisible && store.currentTask },
+            {
+              'is-toggled':
+                markStore.isScoreBoardVisible && markStore.currentTask,
+            },
           ]"
           ]"
-          @click="store.toggleScoreBoard"
+          @click="markStore.toggleScoreBoard"
         >
         >
           <img src="@/assets/icons/icon-right-menu.svg" class="header-icon" />
           <img src="@/assets/icons/icon-right-menu.svg" class="header-icon" />
         </div>
         </div>
@@ -170,7 +181,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { doLogout, updateUISetting, clearMarkTask } from "@/api/markPage";
 import { doLogout, updateUISetting, clearMarkTask } from "@/api/markPage";
 import { watch, watchEffect } from "vue";
 import { watch, watchEffect } from "vue";
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
 import MarkChangeProfile from "./MarkChangeProfile.vue";
 import MarkChangeProfile from "./MarkChangeProfile.vue";
 import MarkSwitchGroupDialog from "./MarkSwitchGroupDialog.vue";
 import MarkSwitchGroupDialog from "./MarkSwitchGroupDialog.vue";
 import { isNumber } from "lodash-es";
 import { isNumber } from "lodash-es";
@@ -179,21 +190,23 @@ import { CaretDownOutlined } from "@ant-design/icons-vue";
 
 
 const props = defineProps<{ showTotalScore?: boolean }>();
 const props = defineProps<{ showTotalScore?: boolean }>();
 
 
+const markStore = useMarkStore();
+
 const modeName = $computed(() =>
 const modeName = $computed(() =>
-  store.setting.mode === "TRACK" ? "轨迹模式" : "普通模式"
+  markStore.setting.mode === "TRACK" ? "轨迹模式" : "普通模式"
 );
 );
 
 
 const exchangeModeName = $computed(() =>
 const exchangeModeName = $computed(() =>
-  store.setting.mode === "TRACK" ? "普通模式" : "轨迹模式"
+  markStore.setting.mode === "TRACK" ? "普通模式" : "轨迹模式"
 );
 );
 
 
 async function toggleSettingMode() {
 async function toggleSettingMode() {
-  if (store.isTrackMode) {
-    store.setting.mode = "COMMON";
+  if (markStore.isTrackMode) {
+    markStore.setting.mode = "COMMON";
   } else {
   } else {
-    store.setting.mode = "TRACK";
+    markStore.setting.mode = "TRACK";
   }
   }
-  await updateUISetting(store.setting.mode, store.setting.uiSetting);
+  await updateUISetting(markStore.setting.mode, markStore.setting.uiSetting);
 
 
   const body = document.querySelector("body");
   const body = document.querySelector("body");
   if (body) body.innerHTML = "重新加载中...";
   if (body) body.innerHTML = "重新加载中...";
@@ -203,7 +216,7 @@ async function toggleSettingMode() {
 }
 }
 
 
 const progress = $computed(() => {
 const progress = $computed(() => {
-  const { totalCount, markedCount } = store.status;
+  const { totalCount, markedCount } = markStore.status;
   if (typeof totalCount !== "number" || totalCount === 0) return 0;
   if (typeof totalCount !== "number" || totalCount === 0) return 0;
   let p = markedCount / totalCount;
   let p = markedCount / totalCount;
   if (p < 0.01 && markedCount >= 1) p = 0.01;
   if (p < 0.01 && markedCount >= 1) p = 0.01;
@@ -214,8 +227,8 @@ const progress = $computed(() => {
 const totalScore = $computed(() => {
 const totalScore = $computed(() => {
   return parseFloat(
   return parseFloat(
     (
     (
-      ((Math.max(store.currentTask.objectiveScore || 0, 0) * 100 +
-        Math.max(store.currentTask.markResult?.markerScore || 0, 0) * 100) |
+      ((Math.max(markStore.currentTask.objectiveScore || 0, 0) * 100 +
+        Math.max(markStore.currentTask.markResult?.markerScore || 0, 0) * 100) |
         0) /
         0) /
       100
       100
     ).toFixed(2)
     ).toFixed(2)
@@ -244,9 +257,9 @@ const openSwitchGroupModal = () => {
 
 
 watchEffect(() => {
 watchEffect(() => {
   if (
   if (
-    isNumber(store.setting.topCount) &&
-    store.setting.topCount > 0 &&
-    store.setting.topCount === store.status.personCount
+    isNumber(markStore.setting.topCount) &&
+    markStore.setting.topCount > 0 &&
+    markStore.setting.topCount === markStore.status.personCount
   ) {
   ) {
     Modal.confirm({
     Modal.confirm({
       centered: true,
       centered: true,
@@ -262,19 +275,20 @@ watchEffect(() => {
 });
 });
 
 
 const todoCount = $computed(() =>
 const todoCount = $computed(() =>
-  typeof store.status.totalCount === "number"
-    ? store.status.totalCount -
-      store.status.markedCount -
-      store.status.problemCount -
-      store.status.arbitrateCount
+  typeof markStore.status.totalCount === "number"
+    ? markStore.status.totalCount -
+      markStore.status.markedCount -
+      markStore.status.problemCount -
+      markStore.status.arbitrateCount
     : "-"
     : "-"
 );
 );
 
 
 let questionMarkShouldChange = $ref(false);
 let questionMarkShouldChange = $ref(false);
 watch(
 watch(
-  () => [store.status.problemCount, store.status.arbitrateCount],
+  () => [markStore.status.problemCount, markStore.status.arbitrateCount],
   () => {
   () => {
-    if (!store.status.problemCount && !store.status.arbitrateCount) return;
+    if (!markStore.status.problemCount && !markStore.status.arbitrateCount)
+      return;
     questionMarkShouldChange = true;
     questionMarkShouldChange = true;
     setTimeout(() => {
     setTimeout(() => {
       questionMarkShouldChange = false;
       questionMarkShouldChange = false;

+ 10 - 8
src/features/mark/MarkProblemDialog.vue → src/features/mark/toolbar/MarkProblemDialog.vue

@@ -17,7 +17,7 @@
       <a-form-item class="tw-mb-2" :required="true" label="问题类型">
       <a-form-item class="tw-mb-2" :required="true" label="问题类型">
         <a-select v-model:value="formModel.problemType" placeholder="类型">
         <a-select v-model:value="formModel.problemType" placeholder="类型">
           <a-select-option
           <a-select-option
-            v-for="item in store.setting.problemTypes"
+            v-for="item in markStore.setting.problemTypes"
             :key="item.code"
             :key="item.code"
             :value="item.code"
             :value="item.code"
             >{{ item.name }}</a-select-option
             >{{ item.name }}</a-select-option
@@ -42,10 +42,12 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { doProblemType, getStatus } from "@/api/markPage";
 import { doProblemType, getStatus } from "@/api/markPage";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
 import EventBus from "@/plugins/eventBus";
 import EventBus from "@/plugins/eventBus";
 import { reactive } from "vue";
 import { reactive } from "vue";
 
 
+const markStore = useMarkStore();
+
 let visible = $ref(false);
 let visible = $ref(false);
 
 
 const showModal = () => {
 const showModal = () => {
@@ -63,11 +65,11 @@ const formModel: FormModel = reactive({
 
 
 async function updateStatus() {
 async function updateStatus() {
   const res = await getStatus();
   const res = await getStatus();
-  store.status = res.data;
+  markStore.status = res.data;
 }
 }
 
 
 const handleOk = async () => {
 const handleOk = async () => {
-  if (!store.currentTask) {
+  if (!markStore.currentTask) {
     void message.warn({ content: "没有可以标记的任务", duration: 5 });
     void message.warn({ content: "没有可以标记的任务", duration: 5 });
     return;
     return;
   }
   }
@@ -88,12 +90,12 @@ const handleOk = async () => {
     if (res?.data.success) {
     if (res?.data.success) {
       void message.success({ content: "问题卷处理成功", duration: 3 });
       void message.success({ content: "问题卷处理成功", duration: 3 });
       visible = false;
       visible = false;
-      store.currentTask = undefined;
-      if (store.historyOpen) {
+      markStore.currentTask = undefined;
+      if (markStore.historyOpen) {
         EventBus.emit("should-reload-history");
         EventBus.emit("should-reload-history");
       } else {
       } else {
-        store.tasks.shift();
-        store.currentTask = store.tasks[0];
+        markStore.tasks.shift();
+        markStore.currentTask = markStore.tasks[0];
       }
       }
       await updateStatus();
       await updateStatus();
     } else {
     } else {

+ 6 - 5
src/features/mark/MarkSwitchGroupDialog.vue → src/features/mark/toolbar/MarkSwitchGroupDialog.vue

@@ -15,7 +15,7 @@
         <th>操作</th>
         <th>操作</th>
       </tr>
       </tr>
       <tr
       <tr
-        v-for="(group, index) in store.groups"
+        v-for="(group, index) in markStore.groups"
         :key="index"
         :key="index"
         :class="isCurrentGroup(group.groupNumber) && 'is-current'"
         :class="isCurrentGroup(group.groupNumber) && 'is-current'"
       >
       >
@@ -41,18 +41,19 @@
 </template>
 </template>
 <script setup lang="ts">
 <script setup lang="ts">
 import { getGroup } from "@/api/markPage";
 import { getGroup } from "@/api/markPage";
-// import { message } from "ant-design-vue";
 import { onUpdated } from "vue";
 import { onUpdated } from "vue";
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
 import vls from "@/utils/storage";
 import vls from "@/utils/storage";
 import { clearMarkTask } from "@/api/markPage";
 import { clearMarkTask } from "@/api/markPage";
 
 
+const markStore = useMarkStore();
+
 let visible = $ref(false);
 let visible = $ref(false);
 
 
 onUpdated(async () => {
 onUpdated(async () => {
   if (visible) {
   if (visible) {
     const groups = await getGroup();
     const groups = await getGroup();
-    store.groups = groups.data;
+    markStore.groups = groups.data;
   }
   }
 });
 });
 
 
@@ -69,7 +70,7 @@ const progress = (totalCount: number, markedCount: number) => {
 };
 };
 
 
 const isCurrentGroup = (groupNumber: number) => {
 const isCurrentGroup = (groupNumber: number) => {
-  return groupNumber === store.setting.groupNumber;
+  return groupNumber === markStore.setting.groupNumber;
 };
 };
 
 
 const chooseGroup = async (groupNumber: number) => {
 const chooseGroup = async (groupNumber: number) => {

+ 42 - 42
src/features/mark/MarkTool.vue → src/features/mark/toolbar/MarkTool.vue

@@ -13,7 +13,7 @@
         v-if="checkValid('minimap')"
         v-if="checkValid('minimap')"
         :class="[
         :class="[
           'mark-tool-item',
           'mark-tool-item',
-          { 'is-active': store.setting.uiSetting['minimap.modal'] },
+          { 'is-active': markStore.setting.uiSetting['minimap.modal'] },
         ]"
         ]"
         @click="toThumbnail"
         @click="toThumbnail"
       >
       >
@@ -21,10 +21,10 @@
         <p>缩略图</p>
         <p>缩略图</p>
       </div>
       </div>
       <div
       <div
-        v-if="checkValid('answer') && store.setting.subject.answerUrl"
+        v-if="checkValid('answer') && markStore.setting.subject.answerUrl"
         :class="[
         :class="[
           'mark-tool-item',
           'mark-tool-item',
-          { 'is-active': store.setting.uiSetting['answer.modal'] },
+          { 'is-active': markStore.setting.uiSetting['answer.modal'] },
         ]"
         ]"
         @click="toAnswer"
         @click="toAnswer"
       >
       >
@@ -43,7 +43,7 @@
         <div>
         <div>
           <span>Aa</span>
           <span>Aa</span>
           <a-slider
           <a-slider
-            v-model:value="store.setting.uiSetting['score.fontSize.scale']"
+            v-model:value="markStore.setting.uiSetting['score.fontSize.scale']"
             :min="0.5"
             :min="0.5"
             :step="0.1"
             :step="0.1"
             :max="3"
             :max="3"
@@ -55,7 +55,7 @@
         v-if="checkValid('shortCut')"
         v-if="checkValid('shortCut')"
         :class="[
         :class="[
           'mark-tool-item',
           'mark-tool-item',
-          { 'is-active': store.setting.uiSetting['shortCut.modal'] },
+          { 'is-active': markStore.setting.uiSetting['shortCut.modal'] },
         ]"
         ]"
         @click="toShortcut"
         @click="toShortcut"
       >
       >
@@ -66,7 +66,7 @@
         <div
         <div
           :class="[
           :class="[
             'tag-item',
             'tag-item',
-            { 'is-current': store.currentSpecialTag === '√' },
+            { 'is-current': markStore.currentSpecialTag === '√' },
           ]"
           ]"
           @click="chooseSpecialTag('√', 'RIGHT')"
           @click="chooseSpecialTag('√', 'RIGHT')"
         >
         >
@@ -76,7 +76,7 @@
           :class="[
           :class="[
             'tag-item',
             'tag-item',
             {
             {
-              'is-current': store.currentSpecialTag === '乄',
+              'is-current': markStore.currentSpecialTag === '乄',
             },
             },
           ]"
           ]"
           @click="chooseSpecialTag('乄', 'HALF_RIGTH')"
           @click="chooseSpecialTag('乄', 'HALF_RIGTH')"
@@ -86,7 +86,7 @@
         <div
         <div
           :class="[
           :class="[
             'tag-item',
             'tag-item',
-            { 'is-current': store.currentSpecialTag === 'X' },
+            { 'is-current': markStore.currentSpecialTag === 'X' },
           ]"
           ]"
           @click="chooseSpecialTag('X', 'WRONG')"
           @click="chooseSpecialTag('X', 'WRONG')"
         >
         >
@@ -95,7 +95,7 @@
         <div
         <div
           :class="[
           :class="[
             'tag-item',
             'tag-item',
-            { 'is-current': store.currentSpecialTagType === 'CIRCLE' },
+            { 'is-current': markStore.currentSpecialTagType === 'CIRCLE' },
           ]"
           ]"
           title="标记圆圈"
           title="标记圆圈"
           @click="chooseSpecialTag('○', 'CIRCLE')"
           @click="chooseSpecialTag('○', 'CIRCLE')"
@@ -105,7 +105,7 @@
         <div
         <div
           :class="[
           :class="[
             'tag-item',
             'tag-item',
-            { 'is-current': store.currentSpecialTagType === 'LINE' },
+            { 'is-current': markStore.currentSpecialTagType === 'LINE' },
           ]"
           ]"
           title="标记圆圈"
           title="标记圆圈"
           @click="chooseSpecialTag('-', 'LINE')"
           @click="chooseSpecialTag('-', 'LINE')"
@@ -115,7 +115,7 @@
         <div
         <div
           :class="[
           :class="[
             'tag-item',
             'tag-item',
-            { 'is-current': store.currentSpecialTagType === 'TEXT' },
+            { 'is-current': markStore.currentSpecialTagType === 'TEXT' },
           ]"
           ]"
           title="标记文本"
           title="标记文本"
           @click="chooseSpecialTag('', 'TEXT')"
           @click="chooseSpecialTag('', 'TEXT')"
@@ -141,7 +141,9 @@
         <p>护眼模式</p>
         <p>护眼模式</p>
       </div>
       </div>
       <div
       <div
-        v-if="store.setting.enableAllZero && !store.setting.forceSpecialTag"
+        v-if="
+          markStore.setting.enableAllZero && !markStore.setting.forceSpecialTag
+        "
         class="mark-tool-item"
         class="mark-tool-item"
         @click="toAllZero"
         @click="toAllZero"
       >
       >
@@ -178,10 +180,12 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { computed, onMounted, onUnmounted } from "vue";
 import { computed, onMounted, onUnmounted } from "vue";
-import { store } from "@/store/store";
+import { useMarkStore } from "@/store";
 import { Modal } from "ant-design-vue";
 import { Modal } from "ant-design-vue";
 import MarkProblemDialog from "./MarkProblemDialog.vue";
 import MarkProblemDialog from "./MarkProblemDialog.vue";
 
 
+const markStore = useMarkStore();
+
 /**
 /**
  * allPage:全卷
  * allPage:全卷
  * minimap:缩略图
  * minimap:缩略图
@@ -207,28 +211,24 @@ const emit = defineEmits(["allZeroSubmit"]);
 let problemRef = $ref<InstanceType<typeof MarkProblemDialog>>();
 let problemRef = $ref<InstanceType<typeof MarkProblemDialog>>();
 
 
 const toAllPage = () => {
 const toAllPage = () => {
-  store.allPaperModal = !store.allPaperModal;
+  markStore.allPaperModal = !markStore.allPaperModal;
 };
 };
 const toThumbnail = () => {
 const toThumbnail = () => {
-  store.setting.uiSetting["minimap.modal"] =
-    !store.setting.uiSetting["minimap.modal"];
+  markStore.setting.uiSetting["minimap.modal"] =
+    !markStore.setting.uiSetting["minimap.modal"];
 };
 };
 const toAnswer = () => {
 const toAnswer = () => {
-  store.setting.uiSetting["answer.modal"] =
-    !store.setting.uiSetting["answer.modal"];
+  markStore.setting.uiSetting["answer.modal"] =
+    !markStore.setting.uiSetting["answer.modal"];
 };
 };
 const toIssuePaper = () => {
 const toIssuePaper = () => {
   (problemRef.showModal as ShowModalFunc)();
   (problemRef.showModal as ShowModalFunc)();
 };
 };
 
 
 const toShortcut = () => {
 const toShortcut = () => {
-  store.setting.uiSetting["shortCut.modal"] =
-    !store.setting.uiSetting["shortCut.modal"];
+  markStore.setting.uiSetting["shortCut.modal"] =
+    !markStore.setting.uiSetting["shortCut.modal"];
 };
 };
-// const toSpecialTag = () => {
-//   store.setting.uiSetting["specialTag.modal"] =
-//     !store.setting.uiSetting["specialTag.modal"];
-// };
 
 
 const toAllZero = () => {
 const toAllZero = () => {
   Modal.confirm({
   Modal.confirm({
@@ -240,26 +240,26 @@ const toAllZero = () => {
   });
   });
 };
 };
 const toMagnify = () => {
 const toMagnify = () => {
-  const s = store.setting.uiSetting["answer.paper.scale"];
+  const s = markStore.setting.uiSetting["answer.paper.scale"];
   if (s < 3)
   if (s < 3)
-    store.setting.uiSetting["answer.paper.scale"] = +(s + 0.2).toFixed(1);
+    markStore.setting.uiSetting["answer.paper.scale"] = +(s + 0.2).toFixed(1);
 };
 };
 const toMinify = () => {
 const toMinify = () => {
-  const s = store.setting.uiSetting["answer.paper.scale"];
+  const s = markStore.setting.uiSetting["answer.paper.scale"];
   if (s > 0.2)
   if (s > 0.2)
-    store.setting.uiSetting["answer.paper.scale"] = +(s - 0.2).toFixed(1);
+    markStore.setting.uiSetting["answer.paper.scale"] = +(s - 0.2).toFixed(1);
 };
 };
 const toOrigin = () => {
 const toOrigin = () => {
-  store.setting.uiSetting["answer.paper.scale"] = 1;
+  markStore.setting.uiSetting["answer.paper.scale"] = 1;
 };
 };
 const greaterThanOneScale = computed(() => {
 const greaterThanOneScale = computed(() => {
-  return store.setting.uiSetting["answer.paper.scale"] > 1;
+  return markStore.setting.uiSetting["answer.paper.scale"] > 1;
 });
 });
 const lessThanOneScale = computed(() => {
 const lessThanOneScale = computed(() => {
-  return store.setting.uiSetting["answer.paper.scale"] < 1;
+  return markStore.setting.uiSetting["answer.paper.scale"] < 1;
 });
 });
 const equalOneScale = computed(() => {
 const equalOneScale = computed(() => {
-  return store.setting.uiSetting["answer.paper.scale"] === 1;
+  return markStore.setting.uiSetting["answer.paper.scale"] === 1;
 });
 });
 
 
 let eyecareMode = $ref(Number(window.localStorage.getItem("eyecareMode", "0")));
 let eyecareMode = $ref(Number(window.localStorage.getItem("eyecareMode", "0")));
@@ -279,23 +279,23 @@ function toEyecare() {
 }
 }
 
 
 function clearLatestTagOfCurrentTask() {
 function clearLatestTagOfCurrentTask() {
-  if (!store.currentTask?.markResult) return;
-  store.currentTask.markResult.specialTagList.splice(-1);
+  if (!markStore.currentTask?.markResult) return;
+  markStore.currentTask.markResult.specialTagList.splice(-1);
 }
 }
 
 
 function clearAllTagsOfCurrentTask() {
 function clearAllTagsOfCurrentTask() {
-  if (!store.currentTask?.markResult) return;
-  store.currentTask.markResult.specialTagList = [];
+  if (!markStore.currentTask?.markResult) return;
+  markStore.currentTask.markResult.specialTagList = [];
 }
 }
 
 
 function chooseSpecialTag(tagName: string, tagType: string) {
 function chooseSpecialTag(tagName: string, tagType: string) {
-  if (store.currentSpecialTag === tagName) {
-    store.currentSpecialTag = undefined;
-    store.currentSpecialTagType = undefined;
+  if (markStore.currentSpecialTag === tagName) {
+    markStore.currentSpecialTag = undefined;
+    markStore.currentSpecialTagType = undefined;
   } else {
   } else {
-    store.currentSpecialTag = tagName;
-    store.currentSpecialTagType = tagType;
-    store.currentScore = undefined;
+    markStore.currentSpecialTag = tagName;
+    markStore.currentSpecialTagType = tagType;
+    markStore.currentScore = undefined;
   }
   }
 }
 }
 
 

+ 0 - 60
src/features/mark/use/autoChooseFirstQuestion.ts

@@ -1,60 +0,0 @@
-import { Question } from "@/types";
-import { store } from "@/store/store";
-import { watch } from "vue";
-
-const scrollToQuestionOfBoard = async (question: Question) => {
-  const node = document.querySelector(
-    `#bq-${question.mainNumber}-${question.subNumber}`
-  );
-  const questionNode = document.querySelector(
-    `#q-${question.mainNumber}-${question.subNumber}`
-  );
-  if (!questionNode) {
-    // 非多媒体阅卷
-    node && node.scrollIntoView({ block: "center", behavior: "smooth" });
-    return;
-  }
-  // console.log(node);
-  // node && node.scrollIntoView({ behavior: "smooth" });
-  // if (node) node.scrollBy({ top: -50 });
-  // setTimeout(() => {
-  //   if (node) node.parentElement?.scrollTo({ top: 50, left: 0 });
-  //   // node && node.scrollTop = 50//node.scrollIntoView({ behavior: "auto", block: "center" });
-  //   if(node.)
-  // }, 1500);
-  async function checkIfEleMoving(ele: Element) {
-    const { top: oldTop } = ele.getBoundingClientRect();
-    await new Promise((res) => setTimeout(res, 200));
-    // console.log(ele.getBoundingClientRect().top, oldTop);
-    return ele.getBoundingClientRect().top - oldTop !== 0;
-  }
-  if (questionNode) {
-    let isMoving = await checkIfEleMoving(questionNode);
-    while (isMoving) {
-      isMoving = await checkIfEleMoving(questionNode);
-    }
-    node && node.scrollIntoView({ block: "center", behavior: "smooth" });
-  }
-};
-
-export function chooseQuestion(question: Question) {
-  store.currentQuestion = question;
-  // FIXME: maybe should be an async function, temp fix for eslint
-  void scrollToQuestionOfBoard(question);
-}
-
-/** chooseQuestion 当currentTask改变是,自动选择第一题 */
-export function autoChooseFirstQuestion() {
-  watch(
-    () => store.currentTask,
-    () => {
-      // FIXed ME: 此时取到的还是score:null,但是 chooseQuestion之后就变成了score:0
-      const firstQuestion = store.currentTask?.questionList[0];
-      if (firstQuestion) {
-        chooseQuestion(firstQuestion);
-      }
-    }
-  );
-
-  return { chooseQuestion };
-}

+ 0 - 62
src/features/mark/use/draggable.ts

@@ -1,62 +0,0 @@
-import { onMounted, onUnmounted } from "vue";
-
-export function dragImage() {
-  // grab moving
-  let pos = { top: 0, left: 0, x: 0, y: 0 };
-  const dragContainer = $ref<HTMLDivElement>();
-  // let isGrabbing = $ref(false);
-
-  const mouseDownHandler = function (e: MouseEvent) {
-    // 防止鼠标左键激发
-    if (e.button !== 0) return;
-    pos = {
-      // The current scroll
-      left: dragContainer.scrollLeft,
-      top: dragContainer.scrollTop,
-      // Get the current mouse position
-      x: e.clientX,
-      y: e.clientY,
-    };
-    // isGrabbing = true;
-    if (dragContainer) {
-      dragContainer.style.cursor = "grabbing";
-
-      dragContainer.addEventListener("mousemove", mouseMoveHandler);
-      dragContainer.addEventListener("mouseup", mouseUpHandler);
-    }
-  };
-
-  const mouseMoveHandler = function (e: MouseEvent) {
-    // if (!isGrabbing) return;
-    // How far the mouse has been moved
-    const dx = e.clientX - pos.x;
-    const dy = e.clientY - pos.y;
-
-    // Scroll the element
-    dragContainer.scrollTop = pos.top - dy;
-    dragContainer.scrollLeft = pos.left - dx;
-  };
-  const mouseUpHandler = function () {
-    // isGrabbing = false;
-    if (dragContainer) {
-      dragContainer.removeEventListener("mousemove", mouseMoveHandler);
-      dragContainer.removeEventListener("mouseup", mouseUpHandler);
-      dragContainer.style.cursor = "auto";
-    }
-  };
-
-  onMounted(() => {
-    if (dragContainer) {
-      dragContainer.addEventListener("mousedown", mouseDownHandler);
-    }
-  });
-  onUnmounted(() => {
-    if (dragContainer) {
-      dragContainer.removeEventListener("mousedown", mouseDownHandler);
-      dragContainer.removeEventListener("mousemove", mouseMoveHandler);
-      dragContainer.removeEventListener("mouseup", mouseUpHandler);
-    }
-  });
-
-  return $$({ dragContainer });
-}

+ 0 - 101
src/features/mark/use/focusTracks.ts

@@ -1,101 +0,0 @@
-import { store } from "@/store/store";
-import { unref } from "vue";
-
-let hovering = false;
-let timeoutId = -1;
-
-export function addFocusTrack(
-  groupNumber: number | undefined,
-  mainNumber: number | undefined,
-  subNumber: string | undefined,
-  isMark?: boolean | undefined, // 是否是评卷,评卷时要考虑标记删除
-  list?: any
-) {
-  hovering = true;
-
-  timeoutId = setTimeout(() => {
-    if (hovering) {
-      _addFocusTrack(groupNumber, mainNumber, subNumber, isMark, list);
-    }
-  }, 200);
-}
-
-function _addFocusTrack(
-  groupNumber: number | undefined,
-  mainNumber: number | undefined,
-  subNumber: string | undefined,
-  isMark: boolean | undefined, // 是否是评卷,评卷时要考虑标记删除
-  list?: any
-) {
-  store.focusTracks.splice(0);
-  const listArr = unref(list);
-  console.log("listArr:", listArr);
-  if (listArr) {
-    const trackList: any = list.map((q) => q.trackList).flat();
-
-    trackList
-      .filter((t) => {
-        if (mainNumber) {
-          return t.mainNumber === mainNumber && t.subNumber === subNumber;
-        } else {
-          return false;
-        }
-      })
-      .forEach((t: any) => {
-        // 回评时,如果没被删除
-        const shouldAdd = isMark ? trackList.includes(t) : true;
-        if (shouldAdd) {
-          store.focusTracks.push(t);
-        }
-      });
-    return;
-  }
-
-  if (groupNumber) {
-    store.currentTask?.questionList
-      ?.filter((q) => q.groupNumber === groupNumber)
-      ?.map((q) => q.trackList)
-      .flat()
-      .forEach((t) => {
-        // 回评时,如果没被删除
-        const shouldAdd = isMark
-          ? store.currentTask?.markResult.trackList.includes(t)
-          : true;
-        if (shouldAdd) store.focusTracks.push(t);
-      });
-  } else {
-    store.currentTask?.questionList
-      ?.map((q) => q.trackList)
-      .flat()
-      .filter((t) => {
-        if (mainNumber) {
-          return t.mainNumber === mainNumber && t.subNumber === subNumber;
-        } else {
-          return false;
-        }
-      })
-      .forEach((t) => {
-        // 回评时,如果没被删除
-        const shouldAdd = isMark
-          ? store.currentTask?.markResult.trackList.includes(t)
-          : true;
-        if (shouldAdd) store.focusTracks.push(t);
-      });
-  }
-  // console.log(store.focusTracks);
-}
-
-let removeTrackTimer: number | null = null;
-
-export const removeFocusTrack = function removeFocusTrack() {
-  console.log("removeFocusTrack");
-  hovering = false;
-  clearTimeout(timeoutId);
-  removeTrackTimer && clearTimeout(removeTrackTimer);
-  removeTrackTimer = setTimeout(() => {
-    if (!hovering) {
-      store.focusTracks.splice(0);
-    }
-    // }, 3000);
-  }, 200);
-};

+ 0 - 13
src/features/mark/use/keyboardAndMouse.ts

@@ -1,13 +0,0 @@
-import { store } from "@/store/store";
-
-export function keyMouse() {
-  function toggleKeyMouse() {
-    if (store.setting.uiSetting["normal.mode"] === "keyboard") {
-      store.setting.uiSetting["normal.mode"] = "mouse";
-    } else {
-      store.setting.uiSetting["normal.mode"] = "keyboard";
-    }
-  }
-
-  return { toggleKeyMouse };
-}

+ 1 - 1
src/features/reject/Reject.vue

@@ -43,7 +43,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted } from "vue";
 import { onMounted } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkTool from "@/features/mark/MarkTool.vue";
 import MarkTool from "@/features/mark/MarkTool.vue";
 import MarkBody from "../student/studentInspect/MarkBody.vue";
 import MarkBody from "../student/studentInspect/MarkBody.vue";
 import MinimapModal from "../mark/MinimapModal.vue";
 import MinimapModal from "../mark/MinimapModal.vue";

+ 1 - 1
src/features/reject/RejectBoard.vue

@@ -79,7 +79,7 @@ import type { Rule } from "ant-design-vue/es/form";
 import { message, type FormInstance } from "ant-design-vue";
 import { message, type FormInstance } from "ant-design-vue";
 import { onMounted, ref, reactive } from "vue";
 import { onMounted, ref, reactive } from "vue";
 
 
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { doRejectTask } from "@/api/markPage";
 import { doRejectTask } from "@/api/markPage";
 
 
 interface SubQuestion {
 interface SubQuestion {

+ 1 - 1
src/features/student/importInspect/ImportInspect.vue

@@ -22,7 +22,7 @@ import {
   getSingleInspectedTaskOfImportInspect,
   getSingleInspectedTaskOfImportInspect,
   saveInspectedTaskOfImportInspect,
   saveInspectedTaskOfImportInspect,
 } from "@/api/importInspectPage";
 } from "@/api/importInspectPage";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkHeader from "./MarkHeader.vue";
 import MarkHeader from "./MarkHeader.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";

+ 5 - 7
src/features/student/importInspect/MarkBoardInspect.vue

@@ -93,12 +93,9 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import type { Question } from "@/types";
 import type { Question } from "@/types";
 import { reactive, watch } from "vue";
 import { reactive, watch } from "vue";
-import { store } from "@/store/store";
-import {
-  addFocusTrack,
-  removeFocusTrack,
-} from "@/features/mark/use/focusTracks";
-import { MinusCircleOutlined, MinusCircleFilled } from "@ant-design/icons-vue";
+import { store } from "@/store/app";
+import useFocusTracks from "@/features/mark/composables/useFocusTracks";
+import { MinusCircleFilled } from "@ant-design/icons-vue";
 
 
 const emit = defineEmits(["makeTag", "fetchTask"]);
 const emit = defineEmits(["makeTag", "fetchTask"]);
 const props = defineProps<{
 const props = defineProps<{
@@ -108,6 +105,8 @@ const props = defineProps<{
 }>();
 }>();
 let checkedQuestions: Question[] = reactive([]);
 let checkedQuestions: Question[] = reactive([]);
 
 
+const { addFocusTrack, removeFocusTrack } = useFocusTracks();
+
 watch(
 watch(
   () => store.currentTask,
   () => store.currentTask,
   () => {
   () => {
@@ -191,7 +190,6 @@ function makeTag(isTag: boolean) {
   display: block;
   display: block;
 }
 }
 
 
-
 .full-width-btn {
 .full-width-btn {
   width: 100%;
   width: 100%;
   border-radius: 20px;
   border-radius: 20px;

+ 1 - 1
src/features/student/importInspect/MarkHeader.vue

@@ -30,7 +30,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { clearInspectedTask } from "@/api/inspectPage";
 import { clearInspectedTask } from "@/api/inspectPage";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";
 import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
 import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
 
 

+ 227 - 228
src/features/student/scoreVerify/MarkBoardInspect.vue

@@ -1,228 +1,227 @@
-<template>
-  <div
-    v-if="store.currentTask"
-    class="mark-board-track-container"
-    :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
-  >
-    <div class="top-container tw-flex-shrink-0 tw-flex tw-items-center">
-      <div class="tw-flex tw-flex-col tw-flex-1 tw-text-center">
-        <div class="tw-flex tw-justify-center">
-          <img
-            src="../../mark/images/totalscore.png"
-            style="width: 13px; height: 16px"
-          />
-        </div>
-        <div>试卷总分</div>
-      </div>
-      <div class="tw-flex-1" style="font-size: 40px">
-        {{ markerScore > 0 ? markerScore : 0 }}
-      </div>
-      <div
-        class="star"
-        :class="props.tagged ? 'star-yes' : 'star-no'"
-        @click="makeTag(!props.tagged)"
-      ></div>
-    </div>
-
-    <div v-if="groups" class="tw-flex-grow tw-overflow-auto tw-my-5">
-      <template v-for="(groupNumber, index) in groups" :key="index">
-        <div class="tw-mb-4 tw-bg-white tw-p-4 tw-pl-5 tw-pr-3">
-          <div
-            class="tw-flex tw-justify-between tw-place-items-center hover:tw-bg-gray-200"
-            @mouseover="addFocusTrack(groupNumber, undefined, undefined)"
-            @mouseleave="removeFocusTrack"
-          >
-            <span class="secondary-text">分组 {{ groupNumber }}</span>
-          </div>
-          <div v-if="questions">
-            <template v-for="(question, index2) in questions" :key="index2">
-              <div
-                v-if="question.groupNumber === groupNumber"
-                class="question tw-flex tw-place-items-center tw-mb-1 tw-font-bold hover:tw-bg-gray-200"
-                :class="{ uncalculate: question.uncalculate }"
-                @mouseover="
-                  addFocusTrack(
-                    undefined,
-                    question.mainNumber,
-                    question.subNumber
-                  )
-                "
-                @mouseleave="removeFocusTrack"
-              >
-                <a-tooltip placement="left">
-                  <template #title>
-                    <span>未计入总分</span>
-                  </template>
-                  <MinusCircleFilled class="uncalculate-icon" />
-                </a-tooltip>
-                <span class="question-title">
-                  {{ question.title }} {{ question.mainNumber }}-{{
-                    question.subNumber
-                  }}
-                </span>
-                <span class="tw-text-center question-score">
-                  {{ question.score === -1 ? "未选做" : question.score || 0 }}
-                </span>
-              </div>
-            </template>
-          </div>
-        </div>
-      </template>
-    </div>
-
-    <div class="tw-flex tw-flex-shrink-0 tw-justify-center tw-gap-4">
-      <a-button
-        type="primary"
-        class="full-width-btn"
-        :disabled="props.isFirst"
-        @click="fetchTask(false)"
-      >
-        上一个
-      </a-button>
-      <a-button
-        type="primary"
-        class="full-width-btn"
-        :disabled="props.isLast"
-        @click="fetchTask(true)"
-        >下一个</a-button
-      >
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import type { Question } from "@/types";
-import { reactive, watch } from "vue";
-import { store } from "@/store/store";
-import {
-  addFocusTrack,
-  removeFocusTrack,
-} from "@/features/mark/use/focusTracks";
-import { MinusCircleOutlined, MinusCircleFilled } from "@ant-design/icons-vue";
-
-const emit = defineEmits(["makeTag", "fetchTask"]);
-const props = defineProps<{
-  tagged: boolean;
-  isFirst: boolean;
-  isLast: boolean;
-}>();
-let checkedQuestions: Question[] = reactive([]);
-
-watch(
-  () => store.currentTask,
-  () => {
-    checkedQuestions.splice(0);
-  }
-);
-const groups = $computed(() => {
-  const gs = (store.currentTaskEnsured?.questionList || []).map(
-    (q) => q.groupNumber
-  );
-  return [...new Set(gs)].sort((a, b) => a - b);
-});
-
-const questions = $computed(() => {
-  const qs = store.currentTaskEnsured.questionList;
-  return qs;
-});
-
-const markerScore = $computed(() => store.currentTaskEnsured.markerScore || 0);
-
-function fetchTask(next: boolean) {
-  emit("fetchTask", next);
-}
-
-function makeTag(isTag: boolean) {
-  emit("makeTag", isTag);
-}
-</script>
-
-<style scoped>
-.mark-board-track-container {
-  display: flex;
-  flex-direction: column;
-  max-width: 290px;
-  min-width: 290px;
-  max-height: calc(100vh - 56px);
-  padding: 20px;
-  z-index: 1001;
-  transition: margin-right 0.5s;
-  color: var(--app-small-header-text-color);
-}
-.mark-board-track-container.show {
-  margin-right: 0;
-}
-.mark-board-track-container.hide {
-  margin-right: -290px;
-}
-
-.top-container {
-  background-color: var(--app-container-bg-color);
-  height: 86px;
-  border-radius: 5px;
-
-  color: white;
-  background-color: var(--app-primary-button-bg-color);
-}
-.question {
-  min-width: 80px;
-  background-color: var(--app-container-bg-color);
-}
-.question-title {
-  flex: 1;
-}
-.question-score {
-  flex-basis: 56px;
-  padding: 0 3px;
-}
-
-.question.uncalculate {
-  position: relative;
-}
-
-.question .uncalculate-icon {
-  display: none;
-  color: red;
-  position: absolute;
-  font-size: 15px;
-  left: -16px;
-  top: 0.3em;
-}
-
-.question.uncalculate .uncalculate-icon {
-  display: block;
-}
-
-.full-width-btn {
-  width: 100%;
-  border-radius: 20px;
-}
-
-.star {
-  margin-top: -30px;
-  margin-right: 20px;
-  width: 30px;
-  height: 30px;
-  cursor: pointer;
-
-  clip-path: polygon(
-    50% 0%,
-    61% 35%,
-    98% 35%,
-    68% 57%,
-    79% 91%,
-    50% 70%,
-    21% 91%,
-    32% 57%,
-    2% 35%,
-    39% 35%
-  );
-}
-
-.star.star-yes {
-  background-color: yellowgreen;
-}
-.star.star-no {
-  background-color: white;
-}
-</style>
+<template>
+  <div
+    v-if="store.currentTask"
+    class="mark-board-track-container"
+    :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
+  >
+    <div class="top-container tw-flex-shrink-0 tw-flex tw-items-center">
+      <div class="tw-flex tw-flex-col tw-flex-1 tw-text-center">
+        <div class="tw-flex tw-justify-center">
+          <img
+            src="../../mark/images/totalscore.png"
+            style="width: 13px; height: 16px"
+          />
+        </div>
+        <div>试卷总分</div>
+      </div>
+      <div class="tw-flex-1" style="font-size: 40px">
+        {{ markerScore > 0 ? markerScore : 0 }}
+      </div>
+      <div
+        class="star"
+        :class="props.tagged ? 'star-yes' : 'star-no'"
+        @click="makeTag(!props.tagged)"
+      ></div>
+    </div>
+
+    <div v-if="groups" class="tw-flex-grow tw-overflow-auto tw-my-5">
+      <template v-for="(groupNumber, index) in groups" :key="index">
+        <div class="tw-mb-4 tw-bg-white tw-p-4 tw-pl-5 tw-pr-3">
+          <div
+            class="tw-flex tw-justify-between tw-place-items-center hover:tw-bg-gray-200"
+            @mouseover="addFocusTrack(groupNumber, undefined, undefined)"
+            @mouseleave="removeFocusTrack"
+          >
+            <span class="secondary-text">分组 {{ groupNumber }}</span>
+          </div>
+          <div v-if="questions">
+            <template v-for="(question, index2) in questions" :key="index2">
+              <div
+                v-if="question.groupNumber === groupNumber"
+                class="question tw-flex tw-place-items-center tw-mb-1 tw-font-bold hover:tw-bg-gray-200"
+                :class="{ uncalculate: question.uncalculate }"
+                @mouseover="
+                  addFocusTrack(
+                    undefined,
+                    question.mainNumber,
+                    question.subNumber
+                  )
+                "
+                @mouseleave="removeFocusTrack"
+              >
+                <a-tooltip placement="left">
+                  <template #title>
+                    <span>未计入总分</span>
+                  </template>
+                  <MinusCircleFilled class="uncalculate-icon" />
+                </a-tooltip>
+                <span class="question-title">
+                  {{ question.title }} {{ question.mainNumber }}-{{
+                    question.subNumber
+                  }}
+                </span>
+                <span class="tw-text-center question-score">
+                  {{ question.score === -1 ? "未选做" : question.score || 0 }}
+                </span>
+              </div>
+            </template>
+          </div>
+        </div>
+      </template>
+    </div>
+
+    <div class="tw-flex tw-flex-shrink-0 tw-justify-center tw-gap-4">
+      <a-button
+        type="primary"
+        class="full-width-btn"
+        :disabled="props.isFirst"
+        @click="fetchTask(false)"
+      >
+        上一个
+      </a-button>
+      <a-button
+        type="primary"
+        class="full-width-btn"
+        :disabled="props.isLast"
+        @click="fetchTask(true)"
+        >下一个</a-button
+      >
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { Question } from "@/types";
+import { reactive, watch } from "vue";
+import { store } from "@/store/app";
+import useFocusTracks from "@/features/mark/composables/useFocusTracks";
+import { MinusCircleFilled } from "@ant-design/icons-vue";
+
+const emit = defineEmits(["makeTag", "fetchTask"]);
+const props = defineProps<{
+  tagged: boolean;
+  isFirst: boolean;
+  isLast: boolean;
+}>();
+let checkedQuestions: Question[] = reactive([]);
+
+const { addFocusTrack, removeFocusTrack } = useFocusTracks();
+
+watch(
+  () => store.currentTask,
+  () => {
+    checkedQuestions.splice(0);
+  }
+);
+const groups = $computed(() => {
+  const gs = (store.currentTaskEnsured?.questionList || []).map(
+    (q) => q.groupNumber
+  );
+  return [...new Set(gs)].sort((a, b) => a - b);
+});
+
+const questions = $computed(() => {
+  const qs = store.currentTaskEnsured.questionList;
+  return qs;
+});
+
+const markerScore = $computed(() => store.currentTaskEnsured.markerScore || 0);
+
+function fetchTask(next: boolean) {
+  emit("fetchTask", next);
+}
+
+function makeTag(isTag: boolean) {
+  emit("makeTag", isTag);
+}
+</script>
+
+<style scoped>
+.mark-board-track-container {
+  display: flex;
+  flex-direction: column;
+  max-width: 290px;
+  min-width: 290px;
+  max-height: calc(100vh - 56px);
+  padding: 20px;
+  z-index: 1001;
+  transition: margin-right 0.5s;
+  color: var(--app-small-header-text-color);
+}
+.mark-board-track-container.show {
+  margin-right: 0;
+}
+.mark-board-track-container.hide {
+  margin-right: -290px;
+}
+
+.top-container {
+  background-color: var(--app-container-bg-color);
+  height: 86px;
+  border-radius: 5px;
+
+  color: white;
+  background-color: var(--app-primary-button-bg-color);
+}
+.question {
+  min-width: 80px;
+  background-color: var(--app-container-bg-color);
+}
+.question-title {
+  flex: 1;
+}
+.question-score {
+  flex-basis: 56px;
+  padding: 0 3px;
+}
+
+.question.uncalculate {
+  position: relative;
+}
+
+.question .uncalculate-icon {
+  display: none;
+  color: red;
+  position: absolute;
+  font-size: 15px;
+  left: -16px;
+  top: 0.3em;
+}
+
+.question.uncalculate .uncalculate-icon {
+  display: block;
+}
+
+.full-width-btn {
+  width: 100%;
+  border-radius: 20px;
+}
+
+.star {
+  margin-top: -30px;
+  margin-right: 20px;
+  width: 30px;
+  height: 30px;
+  cursor: pointer;
+
+  clip-path: polygon(
+    50% 0%,
+    61% 35%,
+    98% 35%,
+    68% 57%,
+    79% 91%,
+    50% 70%,
+    21% 91%,
+    32% 57%,
+    2% 35%,
+    39% 35%
+  );
+}
+
+.star.star-yes {
+  background-color: yellowgreen;
+}
+.star.star-no {
+  background-color: white;
+}
+</style>

+ 57 - 57
src/features/student/scoreVerify/MarkHeader.vue

@@ -1,57 +1,57 @@
-<template>
-  <CommonMarkHeader
-    :isSingleStudent="isSingleStudent"
-    :clearTasks="clearTasks"
-    showScoreBoard
-    showPaperAndAnswer
-    :notShowHistoryToggle="true"
-  >
-    <slot name="taskInfo">
-      <div>
-        <span class="header-small-text">学号</span>
-        <span class="highlight-text">
-          {{ store.currentTask?.studentCode ?? "-" }}
-        </span>
-      </div>
-      <div>
-        <span class="header-small-text">姓名</span>
-        <span class="highlight-text">
-          {{ store.currentTask?.studentName ?? "-" }}
-        </span>
-      </div>
-    </slot>
-    <span>
-      <span class="header-small-text">待校验</span>
-      <span class="highlight-text">{{
-        store.status.totalCount - store.status.markedCount || "-"
-      }}</span>
-    </span>
-
-    <template v-if="route.query?.studentId" #studentInfo
-      ><div class="highlight-text">
-        考生:
-        {{
-          store.currentTask?.studentCode +
-          " - " +
-          store.currentTask?.studentName
-        }}
-      </div></template
-    >
-  </CommonMarkHeader>
-</template>
-
-<script setup lang="ts">
-import { clearInspectedTask } from "@/api/inspectPage";
-import { store } from "@/store/store";
-import { useRoute } from "vue-router";
-import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
-
-const route = useRoute();
-let isSingleStudent = !!route.query.studentId;
-const { studentId, subjectCode } = route.query as {
-  studentId: string;
-  subjectCode: string;
-};
-
-let clearTasks = clearInspectedTask.bind(null, studentId, subjectCode);
-</script>
+<template>
+  <CommonMarkHeader
+    :isSingleStudent="isSingleStudent"
+    :clearTasks="clearTasks"
+    showScoreBoard
+    showPaperAndAnswer
+    :notShowHistoryToggle="true"
+  >
+    <slot name="taskInfo">
+      <div>
+        <span class="header-small-text">学号</span>
+        <span class="highlight-text">
+          {{ store.currentTask?.studentCode ?? "-" }}
+        </span>
+      </div>
+      <div>
+        <span class="header-small-text">姓名</span>
+        <span class="highlight-text">
+          {{ store.currentTask?.studentName ?? "-" }}
+        </span>
+      </div>
+    </slot>
+    <span>
+      <span class="header-small-text">待校验</span>
+      <span class="highlight-text">{{
+        store.status.totalCount - store.status.markedCount || "-"
+      }}</span>
+    </span>
+
+    <template v-if="route.query?.studentId" #studentInfo
+      ><div class="highlight-text">
+        考生:
+        {{
+          store.currentTask?.studentCode +
+          " - " +
+          store.currentTask?.studentName
+        }}
+      </div></template
+    >
+  </CommonMarkHeader>
+</template>
+
+<script setup lang="ts">
+import { clearInspectedTask } from "@/api/inspectPage";
+import { store } from "@/store/app";
+import { useRoute } from "vue-router";
+import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
+
+const route = useRoute();
+let isSingleStudent = !!route.query.studentId;
+const { studentId, subjectCode } = route.query as {
+  studentId: string;
+  subjectCode: string;
+};
+
+let clearTasks = clearInspectedTask.bind(null, studentId, subjectCode);
+</script>

+ 173 - 173
src/features/student/scoreVerify/ScoreVerify.vue

@@ -1,173 +1,173 @@
-<template>
-  <div class="my-container">
-    <mark-header />
-    <div class="tw-flex tw-gap-1">
-      <mark-body origImageUrls="sheetUrls" @error="renderError" />
-      <MarkBoardInspect
-        :tagged="isCurrentTagged"
-        :isFirst="isFirst"
-        :isLast="isLast"
-        @makeTag="saveTaskToServer"
-        @fetchTask="fetchTask"
-      />
-    </div>
-  </div>
-  <MinimapModal />
-  <PaperModal />
-</template>
-
-<script setup lang="ts">
-import { onMounted, ref } from "vue";
-// import {
-//   getInspectedSettingOfImportInspect,
-//   getSingleInspectedTaskOfImportInspect,
-//   saveInspectedTaskOfImportInspect,
-// } from "@/api/importInspectPage";
-import {
-  getInspectedSettingOfImportInspect,
-  getSingleInspectedTaskOfImportInspect,
-  saveInspectedTaskOfImportInspect,
-} from "@/api/scoreVerify";
-import { store } from "@/store/store";
-import MarkHeader from "./MarkHeader.vue";
-import MinimapModal from "@/features/mark/MinimapModal.vue";
-import PaperModal from "@/features/mark/PaperModal.vue";
-import { useRoute } from "vue-router";
-// import MarkBody from "../studentInspect/MarkBody.vue";
-import MarkBody from "./markBody.vue";
-import MarkBoardInspect from "./MarkBoardInspect.vue";
-import type { AdminPageSetting } from "@/types";
-import { message } from "ant-design-vue";
-import { addFileServerPrefixToTask } from "@/utils/utils";
-
-const route = useRoute();
-const { studentId } = route.query as {
-  studentId: string | number;
-};
-
-let studentIds: (number | string)[] = $ref([]);
-// let tagIds: number[] = $ref([]);
-let currentStudentId = $ref<string | number>(0);
-const fileServer = ref("");
-
-async function updateSetting() {
-  const settingRes = await getInspectedSettingOfImportInspect(
-    studentId as string
-  );
-  const { examType, fileServer, doubleTrack } = settingRes.data;
-  store.initSetting({ examType, fileServer, doubleTrack } as AdminPageSetting);
-  // store.status.totalCount = settingRes.data.inspectCount;
-  // store.status.markedCount = 0;
-
-  // if (!settingRes.data.inspectCount) {
-  //   store.message = settingRes.data.message;
-  // } else {
-  if (studentId) {
-    studentIds = [studentId];
-  } else {
-    studentIds = settingRes.data.studentIds || [];
-  }
-  if (!studentIds.length) {
-    await message.warning("没有数据需要校验");
-  }
-  // tagIds = settingRes.data.tagIds;
-  // }
-  return fileServer;
-}
-// 要通过fetchTask调用
-async function updateTask() {
-  if (!currentStudentId) {
-    return;
-  }
-  const mkey = "fetch_task_key";
-  void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
-  let res = await getSingleInspectedTaskOfImportInspect("" + currentStudentId);
-  void message.success({
-    content: res.data.task?.studentId ? "获取成功" : "无任务",
-    key: mkey,
-  });
-  isCurrentTagged = !!res.data.flagged;
-  store.setting.subject.paperUrl = res.data.paperUrl
-    ? fileServer.value + res.data.paperUrl
-    : "";
-  if (res.data.task?.studentId) {
-    let rawTask = res.data.task;
-    store.currentTask = addFileServerPrefixToTask(rawTask);
-  } else {
-    store.message = res.data.message;
-  }
-}
-let isCurrentTagged = $ref(false);
-
-// const isCurrentTagged = $computed(() => tagIds.includes(currentStudentId));
-const isFirst = $computed(() => studentIds.indexOf(currentStudentId) === 0);
-const isLast = $computed(
-  () => studentIds.indexOf(currentStudentId) === studentIds.length - 1
-);
-
-async function fetchTask(next: boolean, init?: boolean) {
-  if (init) {
-    currentStudentId = studentIds[0];
-  } else if (isLast && next) {
-    return; // currentStudentId是最后一个不调用
-  } else if (isFirst && !next) {
-    return; // currentStudentId是第一个不调用
-  } else {
-    currentStudentId =
-      studentIds[studentIds.indexOf(currentStudentId) + (next ? 1 : -1)];
-  }
-  if (!currentStudentId) return; // 无currentStudentId不调用
-  store.status.totalCount = studentIds.length;
-  // store.status.markedCount = studentIds.indexOf(currentStudentId) + 1;
-  await updateTask();
-  if (!store.status.markedCountStuIds) {
-    store.status.markedCountStuIds = [currentStudentId];
-  } else {
-    store.status.markedCountStuIds = Array.from(
-      new Set([...store.status.markedCountStuIds, currentStudentId])
-    );
-  }
-  store.status.markedCount = store.status.markedCountStuIds.length;
-}
-onMounted(async () => {
-  fileServer.value = await updateSetting();
-  await fetchTask(true, true);
-});
-
-const saveTaskToServer = async () => {
-  const mkey = "save_task_key";
-  void message.loading({ content: "标记评卷任务...", key: mkey });
-  const res = await saveInspectedTaskOfImportInspect(
-    currentStudentId + "",
-    !isCurrentTagged + ""
-  );
-  if (res.data.success) {
-    void message.success({
-      content: isCurrentTagged ? "取消标记成功" : "标记成功",
-      key: mkey,
-      duration: 2,
-    });
-    isCurrentTagged = !isCurrentTagged;
-    // if (isCurrentTagged) {
-    //   tagIds.splice(tagIds.indexOf(currentStudentId), 1);
-    // } else {
-    //   tagIds.push(currentStudentId);
-    // }
-  } else {
-    console.log(res.data.message);
-    void message.error({ content: res.data.message, key: mkey, duration: 10 });
-  }
-};
-
-const renderError = () => {
-  store.currentTask = undefined;
-  store.message = "加载失败,请重新加载。";
-};
-</script>
-
-<style scoped>
-.my-container {
-  width: 100%;
-  overflow: clip;
-}
-</style>
+<template>
+  <div class="my-container">
+    <mark-header />
+    <div class="tw-flex tw-gap-1">
+      <mark-body origImageUrls="sheetUrls" @error="renderError" />
+      <MarkBoardInspect
+        :tagged="isCurrentTagged"
+        :isFirst="isFirst"
+        :isLast="isLast"
+        @makeTag="saveTaskToServer"
+        @fetchTask="fetchTask"
+      />
+    </div>
+  </div>
+  <MinimapModal />
+  <PaperModal />
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from "vue";
+// import {
+//   getInspectedSettingOfImportInspect,
+//   getSingleInspectedTaskOfImportInspect,
+//   saveInspectedTaskOfImportInspect,
+// } from "@/api/importInspectPage";
+import {
+  getInspectedSettingOfImportInspect,
+  getSingleInspectedTaskOfImportInspect,
+  saveInspectedTaskOfImportInspect,
+} from "@/api/scoreVerify";
+import { store } from "@/store/app";
+import MarkHeader from "./MarkHeader.vue";
+import MinimapModal from "@/features/mark/MinimapModal.vue";
+import PaperModal from "@/features/mark/PaperModal.vue";
+import { useRoute } from "vue-router";
+// import MarkBody from "../studentInspect/MarkBody.vue";
+import MarkBody from "./markBody.vue";
+import MarkBoardInspect from "./MarkBoardInspect.vue";
+import type { AdminPageSetting } from "@/types";
+import { message } from "ant-design-vue";
+import { addFileServerPrefixToTask } from "@/utils/utils";
+
+const route = useRoute();
+const { studentId } = route.query as {
+  studentId: string | number;
+};
+
+let studentIds: (number | string)[] = $ref([]);
+// let tagIds: number[] = $ref([]);
+let currentStudentId = $ref<string | number>(0);
+const fileServer = ref("");
+
+async function updateSetting() {
+  const settingRes = await getInspectedSettingOfImportInspect(
+    studentId as string
+  );
+  const { examType, fileServer, doubleTrack } = settingRes.data;
+  store.initSetting({ examType, fileServer, doubleTrack } as AdminPageSetting);
+  // store.status.totalCount = settingRes.data.inspectCount;
+  // store.status.markedCount = 0;
+
+  // if (!settingRes.data.inspectCount) {
+  //   store.message = settingRes.data.message;
+  // } else {
+  if (studentId) {
+    studentIds = [studentId];
+  } else {
+    studentIds = settingRes.data.studentIds || [];
+  }
+  if (!studentIds.length) {
+    await message.warning("没有数据需要校验");
+  }
+  // tagIds = settingRes.data.tagIds;
+  // }
+  return fileServer;
+}
+// 要通过fetchTask调用
+async function updateTask() {
+  if (!currentStudentId) {
+    return;
+  }
+  const mkey = "fetch_task_key";
+  void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
+  let res = await getSingleInspectedTaskOfImportInspect("" + currentStudentId);
+  void message.success({
+    content: res.data.task?.studentId ? "获取成功" : "无任务",
+    key: mkey,
+  });
+  isCurrentTagged = !!res.data.flagged;
+  store.setting.subject.paperUrl = res.data.paperUrl
+    ? fileServer.value + res.data.paperUrl
+    : "";
+  if (res.data.task?.studentId) {
+    let rawTask = res.data.task;
+    store.currentTask = addFileServerPrefixToTask(rawTask);
+  } else {
+    store.message = res.data.message;
+  }
+}
+let isCurrentTagged = $ref(false);
+
+// const isCurrentTagged = $computed(() => tagIds.includes(currentStudentId));
+const isFirst = $computed(() => studentIds.indexOf(currentStudentId) === 0);
+const isLast = $computed(
+  () => studentIds.indexOf(currentStudentId) === studentIds.length - 1
+);
+
+async function fetchTask(next: boolean, init?: boolean) {
+  if (init) {
+    currentStudentId = studentIds[0];
+  } else if (isLast && next) {
+    return; // currentStudentId是最后一个不调用
+  } else if (isFirst && !next) {
+    return; // currentStudentId是第一个不调用
+  } else {
+    currentStudentId =
+      studentIds[studentIds.indexOf(currentStudentId) + (next ? 1 : -1)];
+  }
+  if (!currentStudentId) return; // 无currentStudentId不调用
+  store.status.totalCount = studentIds.length;
+  // store.status.markedCount = studentIds.indexOf(currentStudentId) + 1;
+  await updateTask();
+  if (!store.status.markedCountStuIds) {
+    store.status.markedCountStuIds = [currentStudentId];
+  } else {
+    store.status.markedCountStuIds = Array.from(
+      new Set([...store.status.markedCountStuIds, currentStudentId])
+    );
+  }
+  store.status.markedCount = store.status.markedCountStuIds.length;
+}
+onMounted(async () => {
+  fileServer.value = await updateSetting();
+  await fetchTask(true, true);
+});
+
+const saveTaskToServer = async () => {
+  const mkey = "save_task_key";
+  void message.loading({ content: "标记评卷任务...", key: mkey });
+  const res = await saveInspectedTaskOfImportInspect(
+    currentStudentId + "",
+    !isCurrentTagged + ""
+  );
+  if (res.data.success) {
+    void message.success({
+      content: isCurrentTagged ? "取消标记成功" : "标记成功",
+      key: mkey,
+      duration: 2,
+    });
+    isCurrentTagged = !isCurrentTagged;
+    // if (isCurrentTagged) {
+    //   tagIds.splice(tagIds.indexOf(currentStudentId), 1);
+    // } else {
+    //   tagIds.push(currentStudentId);
+    // }
+  } else {
+    console.log(res.data.message);
+    void message.error({ content: res.data.message, key: mkey, duration: 10 });
+  }
+};
+
+const renderError = () => {
+  store.currentTask = undefined;
+  store.message = "加载失败,请重新加载。";
+};
+</script>
+
+<style scoped>
+.my-container {
+  width: 100%;
+  overflow: clip;
+}
+</style>

+ 309 - 309
src/features/student/scoreVerify/markBody.vue

@@ -1,309 +1,309 @@
-<template>
-  <div
-    ref="dragContainer"
-    class="mark-body-container tw-flex-auto tw-p-2 tw-pt-0"
-    @scroll="viewScroll"
-  >
-    <div v-if="!store.currentTask" class="tw-text-center none-tip">
-      {{ store.message }}
-    </div>
-    <div
-      v-else-if="!sliceImagesWithTrackList.length"
-      class="tw-text-center none-tip"
-      style="color: red"
-    >
-      考生答卷未上传
-    </div>
-    <div v-else :style="{ width: answerPaperScale }" class="tw-pt-2">
-      <div
-        v-for="(item, index) in sliceImagesWithTrackList"
-        :key="index"
-        class="single-image-container"
-        :style="{
-          width: item.width,
-        }"
-      >
-        <img :src="item.url" draggable="false" />
-        <MarkDrawTrack
-          :trackList="item.trackList"
-          :specialTagList="item.tagList"
-          :sliceImageHeight="item.originalImageHeight"
-          :sliceImageWidth="item.originalImageWidth"
-          :dx="0"
-          :dy="0"
-        />
-        <hr class="image-seperator" />
-      </div>
-    </div>
-    <ZoomPaper v-if="store.isScanImage && sliceImagesWithTrackList.length" />
-  </div>
-</template>
-
-<script setup lang="ts">
-import { reactive, watch } from "vue";
-import { store } from "@/store/store";
-import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
-import type { SpecialTag, Track, ColorMap } from "@/types";
-import { useTimers } from "@/setups/useTimers";
-import { loadImage, addHeaderTrackColorAttr } from "@/utils/utils";
-import { dragImage } from "@/features/mark/use/draggable";
-import ZoomPaper from "@/components/ZoomPaper.vue";
-
-interface SliceImage {
-  url: string;
-  trackList: Array<Track>;
-  tagList: Array<SpecialTag>;
-  originalImageWidth: number;
-  originalImageHeight: number;
-  width: string; // 图片在整个图片列表里面的宽度比例
-}
-
-const { origImageUrls = "sliceUrls" } = defineProps<{
-  origImageUrls?: "sheetUrls" | "sliceUrls";
-}>();
-const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
-
-const { dragContainer } = dragImage();
-const viewScroll = () => {
-  if (
-    dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
-    dragContainer.value.scrollHeight
-  ) {
-    emit("getScrollStatus");
-  }
-};
-const { addTimeout } = useTimers();
-
-let sliceImagesWithTrackList: SliceImage[] = reactive([]);
-let maxImageWidth = 0;
-
-function addTrackColorAttr(tList: Track[]): Track[] {
-  let markerIds: (number | undefined)[] = tList
-    .map((v) => v.markerId)
-    .filter((x) => !!x);
-  markerIds = Array.from(new Set(markerIds));
-  // markerIds.sort();
-  let colorMap: ColorMap = {};
-  for (let i = 0; i < markerIds.length; i++) {
-    const mId: any = markerIds[i];
-    if (i == 0) {
-      colorMap[mId + ""] = "red";
-    } else if (i == 1) {
-      colorMap[mId + ""] = "blue";
-    } else if (i > 1) {
-      colorMap[mId + ""] = "gray";
-    }
-  }
-  if (Object.keys(colorMap).length > 1) {
-    emit("getIsMultComments", true);
-  }
-  tList = tList.map((item: Track) => {
-    item.color = colorMap[item.markerId + ""] || "gray";
-    item.isByMultMark = markerIds.length > 1;
-    return item;
-  });
-  return tList;
-}
-
-function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
-  let markerIds: (number | undefined)[] = tList
-    .map((v) => v.markerId)
-    .filter((x) => !!x);
-  markerIds = Array.from(new Set(markerIds));
-  // markerIds.sort();
-  let colorMap: ColorMap = {};
-  for (let i = 0; i < markerIds.length; i++) {
-    const mId: any = markerIds[i];
-    if (i == 0) {
-      colorMap[mId + ""] = "red";
-    } else if (i == 1) {
-      colorMap[mId + ""] = "blue";
-    } else if (i > 1) {
-      colorMap[mId + ""] = "gray";
-    }
-  }
-  tList = tList.map((item: SpecialTag) => {
-    item.color = colorMap[item.markerId + ""] || "gray";
-    item.isByMultMark = markerIds.length > 1;
-    return item;
-  });
-  return tList;
-}
-
-async function processImage() {
-  if (!store.currentTask) return;
-
-  const images = [];
-  const urls = store.currentTask[origImageUrls] || [];
-  for (const url of urls) {
-    const image = await loadImage(url);
-    images.push(image);
-  }
-
-  maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
-
-  for (const url of urls) {
-    const indexInSliceUrls = urls.indexOf(url) + 1;
-    const image = images[indexInSliceUrls - 1];
-
-    const trackLists = (store.currentTask.questionList || [])
-      // .map((q) => q.trackList)
-      .map((q) => {
-        let tList = q.trackList;
-
-        return q.headerTrack?.length
-          ? addHeaderTrackColorAttr(q.headerTrack)
-          : addTrackColorAttr(tList);
-      })
-      .flat();
-    const thisImageTrackList = trackLists.filter(
-      (t) => t.offsetIndex === indexInSliceUrls
-    );
-
-    const thisImageTagList = store.currentTask.headerTagList?.length
-      ? addHeaderTrackColorAttr(
-          (store.currentTask.headerTagList || []).filter(
-            (t) => t.offsetIndex === indexInSliceUrls
-          )
-        )
-      : addTagColorAttr(
-          (store.currentTask.specialTagList || []).filter(
-            (t) => t.offsetIndex === indexInSliceUrls
-          )
-        );
-
-    // const thisImageTagList = addTagColorAttr(
-    //   (store.currentTask.specialTagList || []).filter(
-    //     (t) => t.offsetIndex === indexInSliceUrls
-    //   )
-    // );
-
-    sliceImagesWithTrackList.push({
-      url,
-      trackList: thisImageTrackList,
-      tagList: thisImageTagList,
-      originalImageWidth: image.naturalWidth,
-      originalImageHeight: image.naturalHeight,
-      width: (image.naturalWidth / maxImageWidth) * 100 + "%",
-    });
-  }
-}
-
-// should not render twice at the same time
-let renderLock = false;
-const renderPaperAndMark = async () => {
-  if (renderLock) {
-    console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
-    await new Promise((res) => setTimeout(res, 1000));
-    await renderPaperAndMark();
-    return;
-  }
-  renderLock = true;
-  sliceImagesWithTrackList.splice(0);
-
-  if (!store.currentTask) {
-    renderLock = false;
-    return;
-  }
-
-  try {
-    store.globalMask = true;
-    await processImage();
-  } catch (error) {
-    sliceImagesWithTrackList.splice(0);
-    console.log("render error ", error);
-    // 图片加载出错,自动加载下一个任务
-    emit("error");
-  } finally {
-    await new Promise((res) => setTimeout(res, 500));
-    store.globalMask = false;
-    renderLock = false;
-  }
-};
-
-watch(() => store.currentTask, renderPaperAndMark);
-
-watch(
-  (): (number | undefined)[] => [
-    store.minimapScrollToX,
-    store.minimapScrollToY,
-  ],
-  () => {
-    const container = document.querySelector<HTMLDivElement>(
-      ".mark-body-container"
-    );
-    addTimeout(() => {
-      if (
-        container &&
-        typeof store.minimapScrollToX === "number" &&
-        typeof store.minimapScrollToY === "number"
-      ) {
-        const { scrollWidth, scrollHeight } = container;
-        container.scrollTo({
-          top: scrollHeight * store.minimapScrollToY,
-          left: scrollWidth * store.minimapScrollToX,
-          behavior: "smooth",
-        });
-      }
-    }, 10);
-  }
-);
-
-const answerPaperScale = $computed(() => {
-  // 放大、缩小不影响页面之前的滚动条定位
-  let percentWidth = 0;
-  let percentTop = 0;
-  const container = document.querySelector(".mark-body-container");
-  if (container) {
-    const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
-    percentWidth = scrollLeft / scrollWidth;
-    percentTop = scrollTop / scrollHeight;
-  }
-
-  addTimeout(() => {
-    if (container) {
-      const { scrollWidth, scrollHeight } = container;
-      container.scrollTo({
-        left: scrollWidth * percentWidth,
-        top: scrollHeight * percentTop,
-      });
-    }
-  }, 10);
-  const scale = store.setting.uiSetting["answer.paper.scale"];
-  return scale * 100 + "%";
-});
-</script>
-
-<style scoped>
-.mark-body-container .none-tip {
-  height: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-size: 28px;
-}
-.mark-body-container {
-  height: calc(100vh - 56px);
-  overflow: auto;
-  background-color: var(--app-container-bg-color);
-  background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
-    linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
-    linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
-    linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
-  background-size: 20px 20px;
-  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
-  transform: inherit;
-
-  cursor: grab;
-  user-select: none;
-}
-.mark-body-container img {
-  width: 100%;
-}
-.single-image-container {
-  position: relative;
-}
-.image-seperator {
-  border: 2px solid rgba(120, 120, 120, 0.1);
-}
-</style>
+<template>
+  <div
+    ref="dragContainer"
+    class="mark-body-container tw-flex-auto tw-p-2 tw-pt-0"
+    @scroll="viewScroll"
+  >
+    <div v-if="!store.currentTask" class="tw-text-center none-tip">
+      {{ store.message }}
+    </div>
+    <div
+      v-else-if="!sliceImagesWithTrackList.length"
+      class="tw-text-center none-tip"
+      style="color: red"
+    >
+      考生答卷未上传
+    </div>
+    <div v-else :style="{ width: answerPaperScale }" class="tw-pt-2">
+      <div
+        v-for="(item, index) in sliceImagesWithTrackList"
+        :key="index"
+        class="single-image-container"
+        :style="{
+          width: item.width,
+        }"
+      >
+        <img :src="item.url" draggable="false" />
+        <MarkDrawTrack
+          :trackList="item.trackList"
+          :specialTagList="item.tagList"
+          :sliceImageHeight="item.originalImageHeight"
+          :sliceImageWidth="item.originalImageWidth"
+          :dx="0"
+          :dy="0"
+        />
+        <hr class="image-seperator" />
+      </div>
+    </div>
+    <ZoomPaper v-if="store.isScanImage && sliceImagesWithTrackList.length" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, watch } from "vue";
+import { store } from "@/store/app";
+import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
+import type { SpecialTag, Track, ColorMap } from "@/types";
+import { useTimers } from "@/setups/useTimers";
+import { loadImage, addHeaderTrackColorAttr } from "@/utils/utils";
+import useDraggable from "@/features/mark/composables/useDraggable";
+import ZoomPaper from "@/components/ZoomPaper.vue";
+
+interface SliceImage {
+  url: string;
+  trackList: Array<Track>;
+  tagList: Array<SpecialTag>;
+  originalImageWidth: number;
+  originalImageHeight: number;
+  width: string; // 图片在整个图片列表里面的宽度比例
+}
+
+const { origImageUrls = "sliceUrls" } = defineProps<{
+  origImageUrls?: "sheetUrls" | "sliceUrls";
+}>();
+const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
+
+const { dragContainer } = useDraggable();
+const viewScroll = () => {
+  if (
+    dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
+    dragContainer.value.scrollHeight
+  ) {
+    emit("getScrollStatus");
+  }
+};
+const { addTimeout } = useTimers();
+
+let sliceImagesWithTrackList: SliceImage[] = reactive([]);
+let maxImageWidth = 0;
+
+function addTrackColorAttr(tList: Track[]): Track[] {
+  let markerIds: (number | undefined)[] = tList
+    .map((v) => v.markerId)
+    .filter((x) => !!x);
+  markerIds = Array.from(new Set(markerIds));
+  // markerIds.sort();
+  let colorMap: ColorMap = {};
+  for (let i = 0; i < markerIds.length; i++) {
+    const mId: any = markerIds[i];
+    if (i == 0) {
+      colorMap[mId + ""] = "red";
+    } else if (i == 1) {
+      colorMap[mId + ""] = "blue";
+    } else if (i > 1) {
+      colorMap[mId + ""] = "gray";
+    }
+  }
+  if (Object.keys(colorMap).length > 1) {
+    emit("getIsMultComments", true);
+  }
+  tList = tList.map((item: Track) => {
+    item.color = colorMap[item.markerId + ""] || "gray";
+    item.isByMultMark = markerIds.length > 1;
+    return item;
+  });
+  return tList;
+}
+
+function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
+  let markerIds: (number | undefined)[] = tList
+    .map((v) => v.markerId)
+    .filter((x) => !!x);
+  markerIds = Array.from(new Set(markerIds));
+  // markerIds.sort();
+  let colorMap: ColorMap = {};
+  for (let i = 0; i < markerIds.length; i++) {
+    const mId: any = markerIds[i];
+    if (i == 0) {
+      colorMap[mId + ""] = "red";
+    } else if (i == 1) {
+      colorMap[mId + ""] = "blue";
+    } else if (i > 1) {
+      colorMap[mId + ""] = "gray";
+    }
+  }
+  tList = tList.map((item: SpecialTag) => {
+    item.color = colorMap[item.markerId + ""] || "gray";
+    item.isByMultMark = markerIds.length > 1;
+    return item;
+  });
+  return tList;
+}
+
+async function processImage() {
+  if (!store.currentTask) return;
+
+  const images = [];
+  const urls = store.currentTask[origImageUrls] || [];
+  for (const url of urls) {
+    const image = await loadImage(url);
+    images.push(image);
+  }
+
+  maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
+
+  for (const url of urls) {
+    const indexInSliceUrls = urls.indexOf(url) + 1;
+    const image = images[indexInSliceUrls - 1];
+
+    const trackLists = (store.currentTask.questionList || [])
+      // .map((q) => q.trackList)
+      .map((q) => {
+        let tList = q.trackList;
+
+        return q.headerTrack?.length
+          ? addHeaderTrackColorAttr(q.headerTrack)
+          : addTrackColorAttr(tList);
+      })
+      .flat();
+    const thisImageTrackList = trackLists.filter(
+      (t) => t.offsetIndex === indexInSliceUrls
+    );
+
+    const thisImageTagList = store.currentTask.headerTagList?.length
+      ? addHeaderTrackColorAttr(
+          (store.currentTask.headerTagList || []).filter(
+            (t) => t.offsetIndex === indexInSliceUrls
+          )
+        )
+      : addTagColorAttr(
+          (store.currentTask.specialTagList || []).filter(
+            (t) => t.offsetIndex === indexInSliceUrls
+          )
+        );
+
+    // const thisImageTagList = addTagColorAttr(
+    //   (store.currentTask.specialTagList || []).filter(
+    //     (t) => t.offsetIndex === indexInSliceUrls
+    //   )
+    // );
+
+    sliceImagesWithTrackList.push({
+      url,
+      trackList: thisImageTrackList,
+      tagList: thisImageTagList,
+      originalImageWidth: image.naturalWidth,
+      originalImageHeight: image.naturalHeight,
+      width: (image.naturalWidth / maxImageWidth) * 100 + "%",
+    });
+  }
+}
+
+// should not render twice at the same time
+let renderLock = false;
+const renderPaperAndMark = async () => {
+  if (renderLock) {
+    console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
+    await new Promise((res) => setTimeout(res, 1000));
+    await renderPaperAndMark();
+    return;
+  }
+  renderLock = true;
+  sliceImagesWithTrackList.splice(0);
+
+  if (!store.currentTask) {
+    renderLock = false;
+    return;
+  }
+
+  try {
+    store.globalMask = true;
+    await processImage();
+  } catch (error) {
+    sliceImagesWithTrackList.splice(0);
+    console.log("render error ", error);
+    // 图片加载出错,自动加载下一个任务
+    emit("error");
+  } finally {
+    await new Promise((res) => setTimeout(res, 500));
+    store.globalMask = false;
+    renderLock = false;
+  }
+};
+
+watch(() => store.currentTask, renderPaperAndMark);
+
+watch(
+  (): (number | undefined)[] => [
+    store.minimapScrollToX,
+    store.minimapScrollToY,
+  ],
+  () => {
+    const container = document.querySelector<HTMLDivElement>(
+      ".mark-body-container"
+    );
+    addTimeout(() => {
+      if (
+        container &&
+        typeof store.minimapScrollToX === "number" &&
+        typeof store.minimapScrollToY === "number"
+      ) {
+        const { scrollWidth, scrollHeight } = container;
+        container.scrollTo({
+          top: scrollHeight * store.minimapScrollToY,
+          left: scrollWidth * store.minimapScrollToX,
+          behavior: "smooth",
+        });
+      }
+    }, 10);
+  }
+);
+
+const answerPaperScale = $computed(() => {
+  // 放大、缩小不影响页面之前的滚动条定位
+  let percentWidth = 0;
+  let percentTop = 0;
+  const container = document.querySelector(".mark-body-container");
+  if (container) {
+    const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
+    percentWidth = scrollLeft / scrollWidth;
+    percentTop = scrollTop / scrollHeight;
+  }
+
+  addTimeout(() => {
+    if (container) {
+      const { scrollWidth, scrollHeight } = container;
+      container.scrollTo({
+        left: scrollWidth * percentWidth,
+        top: scrollHeight * percentTop,
+      });
+    }
+  }, 10);
+  const scale = store.setting.uiSetting["answer.paper.scale"];
+  return scale * 100 + "%";
+});
+</script>
+
+<style scoped>
+.mark-body-container .none-tip {
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 28px;
+}
+.mark-body-container {
+  height: calc(100vh - 56px);
+  overflow: auto;
+  background-color: var(--app-container-bg-color);
+  background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
+    linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
+  background-size: 20px 20px;
+  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
+  transform: inherit;
+
+  cursor: grab;
+  user-select: none;
+}
+.mark-body-container img {
+  width: 100%;
+}
+.single-image-container {
+  position: relative;
+}
+.image-seperator {
+  border: 2px solid rgba(120, 120, 120, 0.1);
+}
+</style>

+ 6 - 10
src/features/student/studentInspect/MarkBoardInspect.vue

@@ -140,27 +140,23 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import type { Question } from "@/types";
 import type { Question } from "@/types";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
-import { MinusCircleOutlined, MinusCircleFilled } from "@ant-design/icons-vue";
+import { MinusCircleFilled } from "@ant-design/icons-vue";
 import { reactive, watch } from "vue";
 import { reactive, watch } from "vue";
-import { store } from "@/store/store";
-import {
-  addFocusTrack,
-  removeFocusTrack,
-} from "@/features/mark/use/focusTracks";
+import { store } from "@/store/app";
+import useFocusTracks from "@/features/mark/composables/useFocusTracks";
 import ReviewReturnDialog from "@/features/library/inspect/ReviewReturnDialog.vue";
 import ReviewReturnDialog from "@/features/library/inspect/ReviewReturnDialog.vue";
 
 
+const { addFocusTrack, removeFocusTrack } = useFocusTracks();
+
 const willAddFocusTrack = (
 const willAddFocusTrack = (
   groupNumber: number | undefined,
   groupNumber: number | undefined,
   mainNumber: number | undefined,
   mainNumber: number | undefined,
   subNumber: string | undefined
   subNumber: string | undefined
 ) => {
 ) => {
-  // if (!isMultComments) {
   addFocusTrack(groupNumber, mainNumber, subNumber);
   addFocusTrack(groupNumber, mainNumber, subNumber);
-  // }
 };
 };
 
 
-const { isMultComments, hasScrollToBottom } = defineProps<{
-  isMultComments: boolean;
+const { hasScrollToBottom } = defineProps<{
   hasScrollToBottom: boolean;
   hasScrollToBottom: boolean;
 }>();
 }>();
 const emit = defineEmits(["inspect", "reject"]);
 const emit = defineEmits(["inspect", "reject"]);

+ 3 - 3
src/features/student/studentInspect/MarkBody.vue

@@ -128,7 +128,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { reactive, watch } from "vue";
 import { reactive, watch } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
 import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
 import type {
 import type {
   SpecialTag,
   SpecialTag,
@@ -145,7 +145,7 @@ import {
   maxNum,
   maxNum,
   toPrecision,
   toPrecision,
 } from "@/utils/utils";
 } from "@/utils/utils";
-import { dragImage } from "@/features/mark/use/draggable";
+import useDraggable from "@/features/mark/composables/useDraggable";
 
 
 interface SliceImage {
 interface SliceImage {
   url: string;
   url: string;
@@ -166,7 +166,7 @@ const { origImageUrls = "sliceUrls", onlyTrack = false } = defineProps<{
 }>();
 }>();
 const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
 const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
 
 
-const { dragContainer } = dragImage();
+const { dragContainer } = useDraggable();
 const viewScroll = () => {
 const viewScroll = () => {
   if (
   if (
     dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
     dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=

+ 1 - 1
src/features/student/studentInspect/MarkHeader.vue

@@ -15,7 +15,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { clearInspectedTask } from "@/api/inspectPage";
 import { clearInspectedTask } from "@/api/inspectPage";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";
 import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
 import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
 
 

+ 1 - 1
src/features/student/studentInspect/StudentInspect.vue

@@ -36,7 +36,7 @@ import {
   rejectInspectedTask,
   rejectInspectedTask,
   saveInspectedTask,
   saveInspectedTask,
 } from "@/api/inspectPage";
 } from "@/api/inspectPage";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkHeader from "./MarkHeader.vue";
 import MarkHeader from "./MarkHeader.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";

+ 1 - 1
src/features/student/studentTrack/StudentTrack.vue

@@ -39,7 +39,7 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted } from "vue";
 import { onMounted } from "vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import MarkTool from "@/features/mark/MarkTool.vue";
 import MarkTool from "@/features/mark/MarkTool.vue";
 import MarkBody from "../studentInspect/MarkBody.vue";
 import MarkBody from "../studentInspect/MarkBody.vue";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";

+ 1 - 1
src/filters/index.ts

@@ -1,6 +1,6 @@
 import { YYYYMMDDHHmmss } from "@/constants/constants";
 import { YYYYMMDDHHmmss } from "@/constants/constants";
 import moment from "moment";
 import moment from "moment";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 
 
 export default {
 export default {
   /** 返回YYYY-MM-DD HH:mm:ss */
   /** 返回YYYY-MM-DD HH:mm:ss */

+ 2 - 9
src/main.ts

@@ -8,8 +8,7 @@ if (!validUA) {
 import "./styles/global.css";
 import "./styles/global.css";
 import "./styles/page.less";
 import "./styles/page.less";
 import { createApp } from "vue";
 import { createApp } from "vue";
-import { createPinia } from "pinia";
-import { initMarkStore } from "@/store/store";
+import pinia from "@/store";
 import App from "./App.vue";
 import App from "./App.vue";
 import router from "@/router";
 import router from "@/router";
 import filters from "@/filters";
 import filters from "@/filters";
@@ -25,19 +24,13 @@ import QmDialog from "@/components/QmDialog.vue";
 // console.log(import.meta.env.DEV);
 // console.log(import.meta.env.DEV);
 const app = createApp(App);
 const app = createApp(App);
 app.use(router);
 app.use(router);
-app.use(createPinia());
+app.use(pinia);
 // app.use(Antd);
 // app.use(Antd);
 app.config.globalProperties.$filters = filters;
 app.config.globalProperties.$filters = filters;
 
 
 app.component("QmButton", QmButton);
 app.component("QmButton", QmButton);
 app.component("QmDialog", QmDialog);
 app.component("QmDialog", QmDialog);
 
 
-/**
- * @description pinia限制,初始化Store, 必须在use pinia插件实例之后。所以在此执行初始化, 此方法调用之后, Store初始化完成
- * @notice 在初始化完成之前,store为null , 请勿在初始化完成之前,直接使用store的方法或属性
- */
-initMarkStore();
-
 if (import.meta.env.DEV && window.localStorage.getItem("dev_simple")) {
 if (import.meta.env.DEV && window.localStorage.getItem("dev_simple")) {
   await import("./devLogin")
   await import("./devLogin")
     .then((m) => {
     .then((m) => {

+ 1 - 1
src/plugins/axiosApp.ts

@@ -3,7 +3,7 @@ import { loadProgressBar } from "axios-progress-bar";
 import { notifyInvalidTokenThrottled } from "./axiosNotice";
 import { notifyInvalidTokenThrottled } from "./axiosNotice";
 import axiosRetry from "axios-retry";
 import axiosRetry from "axios-retry";
 import { message } from "ant-design-vue";
 import { message } from "ant-design-vue";
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import vls from "../utils/storage";
 import vls from "../utils/storage";
 import { getAuthorization } from "../utils/crypto";
 import { getAuthorization } from "../utils/crypto";
 import { DEVICE_ID, PLATFORM } from "../constants/app";
 import { DEVICE_ID, PLATFORM } from "../constants/app";

+ 26 - 0
src/store/app.ts

@@ -0,0 +1,26 @@
+import { defineStore } from "pinia";
+
+interface AppStore {
+  maxModalZIndex: number;
+}
+
+const useAppStore = defineStore("app", {
+  state: (): AppStore => ({
+    maxModalZIndex: 1020,
+  }),
+  getters: {
+    info(state: AppStore): AppStore {
+      return { ...state };
+    },
+  },
+  actions: {
+    setInfo(partial: Partial<AppStore>) {
+      this.$patch(partial);
+    },
+    resetInfo() {
+      this.$reset();
+    },
+  },
+});
+
+export default useAppStore;

+ 11 - 0
src/store/index.ts

@@ -0,0 +1,11 @@
+import { createPinia } from "pinia";
+import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
+
+import useMarkStore from "../features/mark/stores/mark";
+import useAppStore from "./app";
+
+const pinia = createPinia();
+pinia.use(piniaPluginPersistedstate);
+
+export { useMarkStore, useAppStore };
+export default pinia;

+ 0 - 235
src/store/store.ts

@@ -1,235 +0,0 @@
-import { Setting, MarkStore, AdminPageSetting, Task } from "@/types";
-import { watch } from "vue";
-import { defineStore } from "pinia";
-
-const initState: MarkStore = {
-  setting: {
-    mode: "TRACK",
-    examType: "SCAN_IMAGE",
-    forceMode: false,
-    sheetView: false,
-    autoScroll: false,
-    sheetConfig: [],
-    enableAllZero: false,
-    enableSplit: true,
-    fileServer: "",
-    userName: "",
-    subject: <Setting["subject"]>{},
-    forceSpecialTag: false,
-    uiSetting: {
-      "answer.paper.scale": 1,
-      "score.board.collapse": false,
-      "normal.mode": "keyboard",
-      "paper.modal": false,
-      "answer.modal": false,
-      "minimap.modal": false,
-      "specialTag.modal": false,
-      "shortCut.modal": false,
-      "score.fontSize.scale": 1,
-    },
-    statusValue: null,
-    problemTypes: [],
-    groupNumber: -987654, // 默认不可能的值
-    groupTitle: "",
-    topCount: 0,
-    splitConfig: [],
-    prefetchCount: 3,
-    startTime: 0,
-    endTime: 0,
-    selective: false,
-  },
-  status: <MarkStore["status"]>{},
-  groups: [],
-  tasks: [],
-  message: null,
-  currentTask: undefined,
-  // 主观题检查时,缓存已经修改过的试题
-  currentTaskModifyQuestion: {},
-  currentQuestion: undefined,
-  currentScore: undefined,
-  currentSpecialTag: undefined,
-  currentSpecialTagType: undefined,
-  historyOpen: false,
-  historyTasks: [],
-  removeScoreTracks: [],
-  focusTracks: [],
-  maxModalZIndex: 1020,
-  minimapScrollToX: 0,
-  minimapScrollToY: 0,
-  allPaperModal: false,
-  sheetViewModal: false,
-  globalMask: false,
-};
-
-const useMarkStore = defineStore("mark", {
-  state: () => {
-    return initState;
-  },
-  getters: {
-    /** 获得statusValue的中文名 */
-    getStatusValueName() {
-      const st = store.setting.statusValue;
-      if (!st) return "";
-      if (st === "FORMAL") return "正评";
-      if (st === "TRIAL") return "试评";
-      return "";
-    },
-    /** 当前任务。确保不为空,需在上文已经检查过 store.currentTask 不为空 */
-    currentTaskEnsured(): Task {
-      return store.currentTask;
-    },
-    /** 是否是评卷端的轨迹模式 */
-    isTrackMode(): boolean {
-      return store.setting.mode && store.setting.mode === "TRACK";
-    },
-    /** 评卷端的轨迹模式显示轨迹 && 管理后台都显示轨迹 */
-    shouldShowTrack(): boolean {
-      // FIXME: 不是最优雅的方式来判断是否是阅卷端
-      const isWebMark = location.pathname === "/mark/mark";
-      return !isWebMark || this.isTrackMode;
-    },
-    /* 是否是扫描阅卷 */
-    isScanImage(): boolean {
-      return this.setting.examType === "SCAN_IMAGE";
-    },
-    isMultiMedia(): boolean {
-      return this.setting.examType === "MULTI_MEDIA";
-    },
-    /* 返回正在评卷的状态 '' | 回评 | 打回 */
-    getMarkStatus(): string {
-      if (!this.currentTask) return "";
-      if (this.currentTask.previous) return "回评";
-      if (this.currentTask.rejected) return "打回";
-
-      return store.getStatusValueName;
-    },
-    shouldShowMarkBoardKeyBoard(): boolean {
-      return (
-        store.setting.mode === "COMMON" &&
-        store.setting.uiSetting["normal.mode"] === "keyboard"
-      );
-    },
-    shouldShowMarkBoardMouse(): boolean {
-      return (
-        store.setting.mode === "COMMON" &&
-        store.setting.uiSetting["normal.mode"] === "mouse"
-      );
-    },
-    isScoreBoardCollapsed(): boolean {
-      return store.setting.uiSetting["score.board.collapse"];
-    },
-    isScoreBoardVisible(): boolean {
-      return !store.setting.uiSetting["score.board.collapse"];
-    },
-  },
-  actions: {
-    initSetting(adminPageSetting: AdminPageSetting): void {
-      Object.assign(this.setting, adminPageSetting, {
-        mode: "COMMON" as Setting["mode"],
-        uiSetting: {
-          "answer.paper.scale": 1,
-          "score.board.collapse": false,
-          "normal.mode": "keyboard",
-          "score.fontSize.scale": 1,
-        } as Setting["uiSetting"],
-      });
-      const fileServer = this.setting.fileServer;
-      if (this.setting.subject?.answerUrl) {
-        this.setting.subject.answerUrl =
-          fileServer + this.setting.subject?.answerUrl;
-      }
-      if (this.setting.subject?.paperUrl) {
-        this.setting.subject.paperUrl =
-          fileServer + this.setting.subject?.paperUrl;
-      }
-    },
-    toggleHistory(): void {
-      this.historyOpen = !this.historyOpen;
-    },
-    toggleScoreBoard(): void {
-      this.setting.uiSetting["score.board.collapse"] =
-        !this.setting.uiSetting["score.board.collapse"];
-    },
-  },
-});
-
-export let store = null as unknown as ReturnType<typeof useMarkStore>;
-
-export const initMarkStore = () => {
-  store = useMarkStore();
-
-  watch(
-    () => store.currentTask,
-    () => {
-      // 初始化 task.markResult ,始终保证 task 下有 markResult
-      // 1. 评卷时,如果没有 markResult ,则初始化一个 markResult 给它
-      // 1. 回评时,先清空它的 markResult ,然后初始化一个 markResult 给它
-      if (!store.currentTask) return;
-
-      const task = store.currentTask;
-      if (task.previous && task.markResult) {
-        task.markResult = undefined;
-      }
-      if (!task.markResult) {
-        // 管理后台可能不设置 questionList, 而且它不用 markResult
-        if (!task.questionList) {
-          task.questionList = [];
-          // return;
-        }
-        // 初始化 __index
-        task.questionList.forEach((q, i, ar) => (ar[i].__index = i));
-
-        task.__markStartTime = Date.now();
-        const statusValue = store.setting.statusValue;
-        const { taskId, studentId } = task;
-        task.markResult = {
-          statusValue: statusValue,
-          taskId: taskId,
-          studentId: studentId,
-          spent: 0,
-
-          trackList: task.questionList
-            .map((q) =>
-              q.headerTrack && q.headerTrack.length
-                ? q.headerTrack
-                : q.trackList
-            )
-            .flat(),
-          specialTagList: [...(task.specialTagList ?? [])],
-          scoreList: task.questionList.map((q) => q.score),
-          markerScore: null, // 后期通过 scoreList 自动更新
-
-          problem: false,
-          problemType: "",
-          problemRemark: "",
-          unselective: false,
-        };
-        task.markResult.trackList.forEach((t) => {
-          if (t.unanswered) {
-            t.score = -0;
-          }
-        });
-      }
-    }
-  );
-
-  // 唯一根据 scoreList 自动更新 markerScore
-  watch(
-    () => store.currentTask?.markResult.scoreList,
-    () => {
-      if (!store.currentTask) return;
-      const scoreList = store.currentTask.markResult.scoreList.filter(
-        (v) => v !== null
-      );
-      const result =
-        scoreList.length === 0
-          ? null
-          : scoreList.reduce((acc, v) => (acc += Math.round(v * 1000)), 0) /
-            1000;
-      store.currentTask.markResult.markerScore = result;
-    },
-    { deep: true }
-  );
-
-  // scoreList 被 trackList 和用户手动更新
-};

+ 0 - 1
src/types/index.ts

@@ -47,7 +47,6 @@ export interface MarkStore {
   /** 聚焦这些tracks */
   /** 聚焦这些tracks */
   focusTracks: Array<Track>;
   focusTracks: Array<Track>;
   message: string | null;
   message: string | null;
-  maxModalZIndex: number;
   /** 缩略图设置滚动到宽度的百分比 */
   /** 缩略图设置滚动到宽度的百分比 */
   minimapScrollToX?: number;
   minimapScrollToX?: number;
   /** 缩略图设置滚动到高度的百分比 */
   /** 缩略图设置滚动到高度的百分比 */

+ 1 - 1
src/utils/utils.ts

@@ -1,4 +1,4 @@
-import { store } from "@/store/store";
+import { store } from "@/store/app";
 import { PictureSlice, Task } from "@/types";
 import { PictureSlice, Task } from "@/types";
 
 
 // 打开cache后,会造成没有 vue devtools 时,canvas缓存错误,暂时不知道原因
 // 打开cache后,会造成没有 vue devtools 时,canvas缓存错误,暂时不知道原因