Browse Source

script setup refactor step: student inspect

Michael Wang 4 năm trước cách đây
mục cha
commit
146befaf64

+ 155 - 190
src/features/student/inspect/Inspect.vue

@@ -12,8 +12,8 @@
   </div>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent, onMounted, ref } from "vue";
+<script setup lang="ts">
+import { computed, onMounted, ref } from "vue";
 import {
   clearInspectedTask,
   getInspectedSetting,
@@ -29,194 +29,171 @@ import { useRoute } from "vue-router";
 import MarkBody from "./MarkBody.vue";
 import MarkHistory from "./MarkHistory.vue";
 import MarkBoardInspect from "./MarkBoardInspect.vue";
-import { Question, Task } from "@/types";
+import type { Question, Task } from "@/types";
 import { message } from "ant-design-vue";
 
-export default defineComponent({
-  name: "Inspect",
-  components: {
-    MarkHeader,
-    MarkBody,
-    MarkHistory,
-    MarkBoardInspect,
-  },
-  setup: () => {
-    const route = useRoute();
-    let isSingleStudent = !!route.query.studentId;
-    const {
-      studentId,
-      subjectCode,
-      startScore,
-      endScore,
-      mainNumber,
-      mainStartScore,
-      mainEndScore,
-      questionScore,
-    } = route.query as {
-      studentId: string;
-      subjectCode: string;
-      startScore: string;
-      endScore: string;
-      mainNumber: string;
-      mainStartScore: string;
-      mainEndScore: string;
-      questionScore: string;
-    };
-
-    async function updateClearTask() {
-      await clearInspectedTask(studentId, subjectCode);
-    }
+const route = useRoute();
+let isSingleStudent = !!route.query.studentId;
+const {
+  studentId,
+  subjectCode,
+  startScore,
+  endScore,
+  mainNumber,
+  mainStartScore,
+  mainEndScore,
+  questionScore,
+} = route.query as {
+  studentId: string;
+  subjectCode: string;
+  startScore: string;
+  endScore: string;
+  mainNumber: string;
+  mainStartScore: string;
+  mainEndScore: string;
+  questionScore: string;
+};
+
+async function updateClearTask() {
+  await clearInspectedTask(studentId, subjectCode);
+}
 
-    async function updateSetting() {
-      const settingRes = await getInspectedSetting();
-      store.setting.fileServer = settingRes.data.fileServer;
-      store.setting.userName = settingRes.data.userName;
-      store.setting.uiSetting = {
-        "answer.paper.scale": 1,
-        "score.board.collapse": false,
-      };
-      store.setting.splitConfig = settingRes.data.splitConfig;
-    }
-    async function updateStatus() {
-      const res = await getInspectedTaskStatus({
-        subjectCode,
-        mainNumber,
-        startScore,
-        endScore,
-        mainStartScore,
-        mainEndScore,
-        questionScore,
-      });
-      if (res.data.valid) store.status = res.data;
-    }
-    async function updateTask() {
-      // const mkey = "fetch_task_key";
-      message.info({ content: "获取任务中...", duration: 2 });
-      let res;
-      if (isSingleStudent) {
-        res = await getSingleStuTask();
-      } else {
-        res = await getOneOfStuTask();
-      }
-      // message.success({ content: "获取成功", key: mkey });
+async function updateSetting() {
+  const settingRes = await getInspectedSetting();
+  store.setting.fileServer = settingRes.data.fileServer;
+  store.setting.userName = settingRes.data.userName;
+  store.setting.uiSetting = {
+    "answer.paper.scale": 1,
+    "score.board.collapse": false,
+  };
+  store.setting.splitConfig = settingRes.data.splitConfig;
+}
+async function updateStatus() {
+  const res = await getInspectedTaskStatus({
+    subjectCode,
+    mainNumber,
+    startScore,
+    endScore,
+    mainStartScore,
+    mainEndScore,
+    questionScore,
+  });
+  if (res.data.valid) store.status = res.data;
+}
+async function updateTask() {
+  // const mkey = "fetch_task_key";
+  message.info({ content: "获取任务中...", duration: 2 });
+  let res;
+  if (isSingleStudent) {
+    res = await getSingleStuTask();
+  } else {
+    res = await getOneOfStuTask();
+  }
+  // message.success({ content: "获取成功", key: mkey });
+
+  if (res.data.studentId) {
+    let rawTask = res.data as Task;
+    rawTask.sliceUrls = rawTask.sliceUrls.map(
+      (s) => store.setting.fileServer + s
+    );
+    rawTask.sheetUrls = rawTask.sheetUrls?.map(
+      (s) => store.setting.fileServer + s
+    );
+    rawTask.jsonUrl = store.setting.fileServer + rawTask.jsonUrl;
+    store.currentTask = res.data;
+    if (store.currentTask) store.setting.subject = store.currentTask.subject;
+  } else {
+    store.message = res.data.message;
+  }
+}
 
-      if (res.data.studentId) {
-        let rawTask = res.data as Task;
-        rawTask.sliceUrls = rawTask.sliceUrls.map(
-          (s) => store.setting.fileServer + s
-        );
-        rawTask.sheetUrls = rawTask.sheetUrls?.map(
-          (s) => store.setting.fileServer + s
-        );
-        rawTask.jsonUrl = store.setting.fileServer + rawTask.jsonUrl;
-        store.currentTask = res.data;
-        if (store.currentTask)
-          store.setting.subject = store.currentTask.subject;
-      } else {
-        store.message = res.data.message;
-      }
-    }
+const shouldReloadHistory = ref(0);
 
-    const shouldReloadHistory = ref(0);
+async function reloadAndfetchTask() {
+  await updateClearTask();
+  await updateSetting();
+  await fetchTask();
+}
 
-    async function reloadAndfetchTask() {
-      await updateClearTask();
-      await updateSetting();
-      await fetchTask();
-    }
+async function fetchTask() {
+  !isSingleStudent && (await updateStatus());
+  await updateTask();
+}
 
-    async function fetchTask() {
-      !isSingleStudent && (await updateStatus());
-      await updateTask();
-    }
+onMounted(async () => {
+  // await updateClearTask();
+  // await updateSetting();
+  // await fetchTask(); // mark-header 会调用 (watchEffect)
+});
 
-    onMounted(async () => {
-      // await updateClearTask();
-      // await updateSetting();
-      // await fetchTask(); // mark-header 会调用 (watchEffect)
-    });
+async function getSingleStuTask() {
+  return getSingleInspectedTask(studentId);
+}
 
-    async function getSingleStuTask() {
-      return getSingleInspectedTask(studentId);
-    }
+async function getOneOfStuTask() {
+  return getOneOfInspectedTask({
+    subjectCode,
+    mainNumber,
+    startScore,
+    endScore,
+    mainStartScore,
+    mainEndScore,
+    questionScore,
+  });
+}
 
-    async function getOneOfStuTask() {
-      return getOneOfInspectedTask({
-        subjectCode,
-        mainNumber,
-        startScore,
-        endScore,
-        mainStartScore,
-        mainEndScore,
-        questionScore,
-      });
+const realStudentId = computed(
+  () => (isSingleStudent ? studentId : store.currentTask?.studentId) as string
+);
+const saveTaskToServer = async () => {
+  console.log("save inspect task to server");
+  const mkey = "save_task_key";
+  message.loading({ content: "保存评卷任务...", key: mkey });
+  const res = (await saveInspectedTask(realStudentId.value)) as any;
+  if (res.data.success && store.currentTask) {
+    message.success({ content: "复核成功", key: mkey, duration: 2 });
+    if (!store.historyOpen) {
+      store.currentTask = undefined;
+      if (!isSingleStudent) fetchTask();
+    } else {
+      shouldReloadHistory.value = Date.now();
     }
-
-    const realStudentId = computed(
-      () =>
-        (isSingleStudent ? studentId : store.currentTask?.studentId) as string
-    );
-    const saveTaskToServer = async () => {
-      console.log("save inspect task to server");
-      const mkey = "save_task_key";
-      message.loading({ content: "保存评卷任务...", key: mkey });
-      const res = (await saveInspectedTask(realStudentId.value)) as any;
-      if (res.data.success && store.currentTask) {
-        message.success({ content: "复核成功", key: mkey, duration: 2 });
-        if (!store.historyOpen) {
-          store.currentTask = undefined;
-          if (!isSingleStudent) fetchTask();
-        } else {
-          shouldReloadHistory.value = Date.now();
-        }
-      } else if (res.data.message) {
-        console.log(res.data.message);
-        message.error({ content: res.data.message, key: mkey, duration: 10 });
-      } else if (!store.currentTask) {
-        message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
-      }
-    };
-
-    const rejectQuestions = async (questions: Array<Question>) => {
-      const mkey = "reject_task_key";
-      message.loading({ content: "打回评卷任务...", key: mkey });
-      const res = (await rejectInspectedTask(
-        realStudentId.value,
-        questions
-      )) as any;
-      if (res.data.success && store.currentTask) {
-        store.currentTask = undefined;
-        message.success({ content: "打回成功", key: mkey, duration: 2 });
-        if (!store.historyOpen) {
-          store.currentTask = undefined;
-          if (!isSingleStudent) fetchTask();
-        } else {
-          shouldReloadHistory.value = Date.now();
-        }
-      } else if (res.data.message) {
-        console.log(res.data.message);
-        message.error({ content: res.data.message, key: mkey, duration: 10 });
-      } else if (!store.currentTask) {
-        message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
-      }
-    };
-
-    const renderError = () => {
+  } else if (res.data.message) {
+    console.log(res.data.message);
+    message.error({ content: res.data.message, key: mkey, duration: 10 });
+  } else if (!store.currentTask) {
+    message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
+  }
+};
+
+const rejectQuestions = async (questions: Array<Question>) => {
+  const mkey = "reject_task_key";
+  message.loading({ content: "打回评卷任务...", key: mkey });
+  const res = (await rejectInspectedTask(
+    realStudentId.value,
+    questions
+  )) as any;
+  if (res.data.success && store.currentTask) {
+    store.currentTask = undefined;
+    message.success({ content: "打回成功", key: mkey, duration: 2 });
+    if (!store.historyOpen) {
       store.currentTask = undefined;
-      store.message = "加载失败,请重新加载。";
-    };
-
-    return {
-      store,
-      fetchTask,
-      reloadAndfetchTask,
-      saveTaskToServer,
-      rejectQuestions,
-      shouldReloadHistory,
-      renderError,
-    };
-  },
-});
+      if (!isSingleStudent) fetchTask();
+    } else {
+      shouldReloadHistory.value = Date.now();
+    }
+  } else if (res.data.message) {
+    console.log(res.data.message);
+    message.error({ content: res.data.message, key: mkey, duration: 10 });
+  } else if (!store.currentTask) {
+    message.warn({ content: "暂无新任务", key: mkey, duration: 10 });
+  }
+};
+
+const renderError = () => {
+  store.currentTask = undefined;
+  store.message = "加载失败,请重新加载。";
+};
 </script>
 
 <style scoped>
@@ -226,16 +203,4 @@ export default defineComponent({
 a {
   color: #42b983;
 }
-
-label {
-  margin: 0 0.5em;
-  font-weight: bold;
-}
-
-code {
-  background-color: #eee;
-  padding: 2px 4px;
-  border-radius: 4px;
-  color: #304455;
-}
 </style>

+ 111 - 150
src/features/student/inspect/MarkBoardInspect.vue

@@ -90,151 +90,130 @@
   </div>
 </template>
 
-<script lang="ts">
-import { Question } from "@/types";
+<script setup lang="ts">
+import type { Question } from "@/types";
 import { message } from "ant-design-vue";
-import { computed, defineComponent, reactive, watch } from "vue";
+import { computed, defineEmit, reactive, watch } from "vue";
 import { store } from "./store";
 
-export default defineComponent({
-  name: "MarkBoardInspect",
-  emits: ["inspect", "reject"],
-  setup(props, { emit }) {
-    let checkedQuestions = reactive([] as Array<Question>);
-
-    watch(
-      () => store.currentTask,
-      () => {
-        checkedQuestions.splice(0);
-      }
-    );
-    const groups = computed(() => {
-      const gs = store.currentTask?.questionList.map((q) => q.groupNumber);
-      return [...new Set(gs)].sort((a, b) => a - b);
-    });
-
-    const questions = computed(() => {
-      const qs = store.currentTask?.questionList;
-      return qs;
-    });
-
-    const markerScore = computed(
-      () =>
-        (questions.value
-          ?.map((q) => Math.round((q.score || 0) * 100))
-          .reduce((acc, s) => acc + s) || 0) / 100
-    );
+const emit = defineEmit(["inspect", "reject"]);
+let checkedQuestions = reactive([] as Array<Question>);
+
+watch(
+  () => store.currentTask,
+  () => {
+    checkedQuestions.splice(0);
+  }
+);
+const groups = computed(() => {
+  const gs = store.currentTask?.questionList.map((q) => q.groupNumber);
+  return [...new Set(gs)].sort((a, b) => a - b);
+});
 
-    function addToCheckedQuestion(question: Question) {
-      checkedQuestions.push(question);
-    }
-    function removeCheckedQuestion(question: Question) {
-      const idx = checkedQuestions.indexOf(question);
-      checkedQuestions.splice(idx, 1);
-    }
-    function groupChecked(groupNumber: number) {
-      return (
-        checkedQuestions.filter((q) => q.groupNumber === groupNumber).length ===
-        questions.value?.filter((q) => q.groupNumber === groupNumber).length
-      );
-    }
+const questions = computed(() => {
+  const qs = store.currentTask?.questionList;
+  return qs;
+});
 
-    function questionChecked(question: Question) {
-      return checkedQuestions.includes(question);
-    }
+const markerScore = computed(
+  () =>
+    (questions.value
+      ?.map((q) => Math.round((q.score || 0) * 100))
+      .reduce((acc, s) => acc + s) || 0) / 100
+);
 
-    function questionCheckChanged(question: Question) {
-      const checked = questionChecked(question);
-      if (checked) {
-        removeCheckedQuestion(question);
-      } else {
-        addToCheckedQuestion(question);
-      }
-    }
+function addToCheckedQuestion(question: Question) {
+  checkedQuestions.push(question);
+}
+function removeCheckedQuestion(question: Question) {
+  const idx = checkedQuestions.indexOf(question);
+  checkedQuestions.splice(idx, 1);
+}
+function groupChecked(groupNumber: number) {
+  return (
+    checkedQuestions.filter((q) => q.groupNumber === groupNumber).length ===
+    questions.value?.filter((q) => q.groupNumber === groupNumber).length
+  );
+}
 
-    function groupClicked(groupNumber: number) {
-      if (groupChecked(groupNumber)) {
-        checkedQuestions
-          .filter((q) => q.groupNumber === groupNumber)
-          .forEach((q) => {
-            const idx = checkedQuestions.indexOf(q);
-            checkedQuestions.splice(idx, 1);
-          });
-      } else {
-        questions.value
-          ?.filter((q) => q.groupNumber === groupNumber)
-          .forEach((q) => {
-            if (!questionChecked(q)) checkedQuestions.push(q);
-          });
-      }
-    }
+function questionChecked(question: Question) {
+  return checkedQuestions.includes(question);
+}
 
-    function addFocusTrack(
-      groupNumber: number | undefined,
-      mainNumber: number | undefined,
-      subNumber: string | undefined
-    ) {
-      store.focusTracks.splice(0);
+function questionCheckChanged(question: Question) {
+  const checked = questionChecked(question);
+  if (checked) {
+    removeCheckedQuestion(question);
+  } else {
+    addToCheckedQuestion(question);
+  }
+}
 
-      if (groupNumber) {
-        questions.value
-          ?.filter((q) => q.groupNumber === groupNumber)
-          ?.map((q) => q.trackList)
-          .reduce((acc, ts) => acc.concat(ts))
-          .forEach((t) => {
-            store.focusTracks.push(t);
-          });
-      } else {
-        questions.value
-          ?.map((q) => q.trackList)
-          .reduce((acc, ts) => acc.concat(ts))
-          .filter((t) => {
-            if (mainNumber) {
-              return t.mainNumber === mainNumber && t.subNumber === subNumber;
-            } else {
-              return false;
-            }
-          })
-          .forEach((t) => {
-            store.focusTracks.push(t);
-          });
-      }
-      // console.log(store.focusTracks);
-    }
+function groupClicked(groupNumber: number) {
+  if (groupChecked(groupNumber)) {
+    checkedQuestions
+      .filter((q) => q.groupNumber === groupNumber)
+      .forEach((q) => {
+        const idx = checkedQuestions.indexOf(q);
+        checkedQuestions.splice(idx, 1);
+      });
+  } else {
+    questions.value
+      ?.filter((q) => q.groupNumber === groupNumber)
+      .forEach((q) => {
+        if (!questionChecked(q)) checkedQuestions.push(q);
+      });
+  }
+}
 
-    function removeFocusTrack() {
-      store.focusTracks.splice(0);
-    }
+function addFocusTrack(
+  groupNumber: number | undefined,
+  mainNumber: number | undefined,
+  subNumber: string | undefined
+) {
+  store.focusTracks.splice(0);
+
+  if (groupNumber) {
+    questions.value
+      ?.filter((q) => q.groupNumber === groupNumber)
+      ?.map((q) => q.trackList)
+      .reduce((acc, ts) => acc.concat(ts))
+      .forEach((t) => {
+        store.focusTracks.push(t);
+      });
+  } else {
+    questions.value
+      ?.map((q) => q.trackList)
+      .reduce((acc, ts) => acc.concat(ts))
+      .filter((t) => {
+        if (mainNumber) {
+          return t.mainNumber === mainNumber && t.subNumber === subNumber;
+        } else {
+          return false;
+        }
+      })
+      .forEach((t) => {
+        store.focusTracks.push(t);
+      });
+  }
+  // console.log(store.focusTracks);
+}
 
-    function reject() {
-      if (checkedQuestions.length === 0) {
-        message.warn({ content: "请先选择试题。" });
-        return;
-      }
-      emit("reject", checkedQuestions);
-    }
+function removeFocusTrack() {
+  store.focusTracks.splice(0);
+}
 
-    function inspect() {
-      emit("inspect");
-    }
+function reject() {
+  if (checkedQuestions.length === 0) {
+    message.warn({ content: "请先选择试题。" });
+    return;
+  }
+  emit("reject", checkedQuestions);
+}
 
-    return {
-      store,
-      markerScore,
-      groups,
-      checkedQuestions,
-      questions,
-      groupChecked,
-      questionChecked,
-      questionCheckChanged,
-      groupClicked,
-      addFocusTrack,
-      removeFocusTrack,
-      reject,
-      inspect,
-    };
-  },
-});
+function inspect() {
+  emit("inspect");
+}
 </script>
 
 <style scoped>
@@ -251,22 +230,4 @@ export default defineComponent({
   min-width: 100px;
   border-bottom: 1px dotted grey;
 }
-
-.current-question {
-  border: 1px solid yellowgreen;
-  background-color: lightblue;
-}
-.single-score {
-  width: 30px;
-  height: 30px;
-  display: grid;
-  place-content: center;
-
-  border: 1px solid black;
-  border-radius: 5px;
-}
-.current-score {
-  border: 1px solid yellowgreen;
-  background-color: lightblue;
-}
 </style>

+ 106 - 123
src/features/student/inspect/MarkBody.vue

@@ -28,11 +28,11 @@
   </div>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent, reactive, ref, watchEffect } from "vue";
+<script setup lang="ts">
+import { computed, defineEmit, reactive, ref, watch } from "vue";
 import { store } from "./store";
 import MarkDrawTrack from "./MarkDrawTrack.vue";
-import { SpecialTag, Track } from "@/types";
+import type { SpecialTag, Track } from "@/types";
 import { useTimers } from "@/setups/useTimers";
 import { loadImage } from "@/utils/utils";
 import { dragImage } from "@/features/mark/use/draggable";
@@ -44,128 +44,111 @@ interface SliceImage {
   tagList: Array<SpecialTag>;
   originalImage: HTMLImageElement;
 }
-// should not render twice at the same time
-let __lock = false;
-let __currentStudentId = -1; // save __currentStudentIdof lock
-export default defineComponent({
-  name: "MarkBody",
-  components: { MarkDrawTrack },
-  emits: ["error"],
-  setup(props, { emit }) {
-    const { dragContainer } = dragImage();
-
-    const { addTimeout } = useTimers();
-
-    let rendering = ref(false);
-    let sliceImagesWithTrackList: Array<SliceImage> = reactive([]);
-
-    async function processImage() {
-      if (!store.currentTask) return;
-
-      const images = [];
-      for (const url of store.currentTask.sliceUrls) {
-        const image = await loadImage(url);
-        images.push(image);
-      }
-
-      for (const url of store.currentTask.sliceUrls) {
-        const completeUrl = url;
-
-        const indexInSliceUrls = store.currentTask.sliceUrls.indexOf(url) + 1;
-        const image = images[indexInSliceUrls - 1];
-
-        const trackLists = store.currentTask.questionList
-          .map((q) => q.trackList)
-          .reduce((acc, t) => {
-            acc = acc.concat(t);
-            return acc;
-          }, [] as Array<Track>);
-        const thisImageTrackList = trackLists.filter(
-          (t) => t.offsetIndex === indexInSliceUrls
-        );
-        const thisImageTagList = store.currentTask.specialTagList.filter(
-          (t) => t.offsetIndex === indexInSliceUrls
-        );
-
-        sliceImagesWithTrackList.push({
-          url: completeUrl,
-          indexInSliceUrls,
-          trackList: thisImageTrackList,
-          tagList: thisImageTagList,
-          originalImage: image,
-        });
-      }
-    }
-    const renderPaperAndMark = async () => {
-      if (__lock) {
-        if (store.currentTask?.studentId === __currentStudentId) {
-          console.log("重复渲染,返回");
-          return;
-        }
-        console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
-        await new Promise((res) => setTimeout(res, 1000));
-        await renderPaperAndMark();
-        return;
-      }
-      __lock = true;
-      __currentStudentId = store.currentTask?.studentId ?? -1;
-      sliceImagesWithTrackList.splice(0);
-
-      if (!store.currentTask) {
-        __lock = false;
-        return;
-      }
-
-      try {
-        rendering.value = true;
-        await processImage();
-      } catch (error) {
-        sliceImagesWithTrackList.splice(0);
-        console.log("render error ", error);
-        // 图片加载出错,自动加载下一个任务
-        emit("error");
-      } finally {
-        __lock = false;
-        rendering.value = false;
-      }
-    };
-
-    watchEffect(renderPaperAndMark);
-
-    const answerPaperScale = computed(() => {
-      // 放大、缩小不影响页面之前的滚动条定位
-      let percentWidth = 0;
-      let percentTop = 0;
-      const container = document.querySelector(
-        ".mark-body-container"
-      ) as HTMLDivElement;
-      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 + "%";
+
+const emit = defineEmit(["error"]);
+
+const { dragContainer } = dragImage();
+
+const { addTimeout } = useTimers();
+
+let rendering = ref(false);
+let sliceImagesWithTrackList: Array<SliceImage> = reactive([]);
+
+async function processImage() {
+  if (!store.currentTask) return;
+
+  const images = [];
+  for (const url of store.currentTask.sliceUrls) {
+    const image = await loadImage(url);
+    images.push(image);
+  }
+
+  for (const url of store.currentTask.sliceUrls) {
+    const completeUrl = url;
+
+    const indexInSliceUrls = store.currentTask.sliceUrls.indexOf(url) + 1;
+    const image = images[indexInSliceUrls - 1];
+
+    const trackLists = store.currentTask.questionList
+      .map((q) => q.trackList)
+      .reduce((acc, t) => {
+        acc = acc.concat(t);
+        return acc;
+      }, [] as Array<Track>);
+    const thisImageTrackList = trackLists.filter(
+      (t) => t.offsetIndex === indexInSliceUrls
+    );
+    const thisImageTagList = store.currentTask.specialTagList.filter(
+      (t) => t.offsetIndex === indexInSliceUrls
+    );
+
+    sliceImagesWithTrackList.push({
+      url: completeUrl,
+      indexInSliceUrls,
+      trackList: thisImageTrackList,
+      tagList: thisImageTagList,
+      originalImage: image,
     });
+  }
+}
 
-    return {
-      dragContainer,
-      store,
-      rendering,
-      sliceImagesWithTrackList,
-      answerPaperScale,
-    };
-  },
+// 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 {
+    rendering.value = true;
+    await processImage();
+  } catch (error) {
+    sliceImagesWithTrackList.splice(0);
+    console.log("render error ", error);
+    // 图片加载出错,自动加载下一个任务
+    emit("error");
+  } finally {
+    renderLock = false;
+    rendering.value = false;
+  }
+};
+
+watch(() => store.currentTask, renderPaperAndMark);
+
+const answerPaperScale = computed(() => {
+  // 放大、缩小不影响页面之前的滚动条定位
+  let percentWidth = 0;
+  let percentTop = 0;
+  const container = document.querySelector(
+    ".mark-body-container"
+  ) as HTMLDivElement;
+  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>
 

+ 46 - 58
src/features/student/inspect/MarkDrawTrack.vue

@@ -22,68 +22,56 @@
   </template>
 </template>
 
-<script lang="ts">
-import { SpecialTag, Track } from "@/types";
-import { defineComponent, PropType, watch } from "vue";
+<script setup lang="ts">
+import type { SpecialTag, Track } from "@/types";
+import { defineProps, watch } from "vue";
 import { store } from "./store";
 
-export default defineComponent({
-  name: "MarkDrawTrack",
-  props: {
-    trackList: {
-      type: Array as PropType<Array<Track>>,
-    },
-    specialTagList: {
-      type: Array as PropType<Array<SpecialTag>>,
-    },
-    originalImage: {
-      type: Object as PropType<HTMLImageElement>,
-      required: true,
-    },
-  },
-  setup({ trackList, originalImage }) {
-    const focusedTrack = (track: Track) => {
-      return store.focusTracks.includes(track);
-    };
-    const computeTopAndLeft = (track: Track | SpecialTag) => {
-      const topInsideSlice = track.offsetY;
-      const leftInsideSlice = track.offsetX;
-      return {
-        top: (topInsideSlice / originalImage.naturalHeight) * 100 + "%",
-        left: (leftInsideSlice / originalImage.naturalWidth) * 100 + "%",
-        "font-size": store.setting.uiSetting["answer.paper.scale"] * 2.2 + "em",
-      };
-    };
+const props =
+  defineProps<{
+    trackList: Array<Track>;
+    specialTagList: Array<SpecialTag>;
+    originalImage: HTMLImageElement;
+  }>();
+const { trackList, originalImage } = props;
 
-    watch(
-      () => store.focusTracks.length,
-      () => {
-        if (store.focusTracks.length === 0) return;
-        const minImageIndex = Math.min(
-          ...store.focusTracks.map((t) => t.offsetIndex)
-        );
-        const minImageOffsetY = Math.min(
-          ...store.focusTracks
-            .filter((t) => t.offsetIndex === minImageIndex)
-            .map((t) => t.offsetY)
-        );
-        const topTrack = store.focusTracks.find(
-          (t) =>
-            t.offsetIndex === minImageIndex && t.offsetY === minImageOffsetY
-        );
-        if (topTrack) {
-          document
-            .querySelector(
-              `#a${topTrack.mainNumber + topTrack.subNumber + topTrack.offsetY}`
-            )
-            ?.scrollIntoView({ behavior: "smooth" });
-        }
-      }
-    );
+const focusedTrack = (track: Track) => {
+  return store.focusTracks.includes(track);
+};
+const computeTopAndLeft = (track: Track | SpecialTag) => {
+  const topInsideSlice = track.offsetY;
+  const leftInsideSlice = track.offsetX;
+  return {
+    top: (topInsideSlice / originalImage.naturalHeight) * 100 + "%",
+    left: (leftInsideSlice / originalImage.naturalWidth) * 100 + "%",
+    "font-size": store.setting.uiSetting["answer.paper.scale"] * 2.2 + "em",
+  };
+};
 
-    return { store, focusedTrack, computeTopAndLeft };
-  },
-});
+watch(
+  () => store.focusTracks.length,
+  () => {
+    if (store.focusTracks.length === 0) return;
+    const minImageIndex = Math.min(
+      ...store.focusTracks.map((t) => t.offsetIndex)
+    );
+    const minImageOffsetY = Math.min(
+      ...store.focusTracks
+        .filter((t) => t.offsetIndex === minImageIndex)
+        .map((t) => t.offsetY)
+    );
+    const topTrack = store.focusTracks.find(
+      (t) => t.offsetIndex === minImageIndex && t.offsetY === minImageOffsetY
+    );
+    if (topTrack) {
+      document
+        .querySelector(
+          `#a${topTrack.mainNumber + topTrack.subNumber + topTrack.offsetY}`
+        )
+        ?.scrollIntoView({ behavior: "smooth" });
+    }
+  }
+);
 </script>
 
 <style scoped>

+ 59 - 102
src/features/student/inspect/MarkHeader.vue

@@ -71,9 +71,9 @@
   </div>
 </template>
 
-<script lang="ts">
+<script setup lang="ts">
 import { clearInspectedTask, getInspectedHistory } from "@/api/inspectPage";
-import { computed, defineComponent, onMounted, ref } from "vue";
+import { computed, onMounted, ref } from "vue";
 import { store } from "./store";
 import {
   ZoomInOutlined,
@@ -87,111 +87,68 @@ import {
 } from "@ant-design/icons-vue";
 import { useRoute } from "vue-router";
 
-export default defineComponent({
-  name: "MarkHeader",
-  components: {
-    ZoomInOutlined,
-    ZoomOutOutlined,
-    FullscreenOutlined,
-    SnippetsOutlined,
-    UserOutlined,
-    PoweroffOutlined,
-    ClockCircleOutlined,
-    QuestionCircleOutlined,
-  },
-  setup() {
-    const route = useRoute();
-    let isSingleStudent = ref(false);
-    isSingleStudent.value = !!route.query.studentId;
-    const {
-      studentId,
-      subjectCode,
-      startScore,
-      endScore,
-      mainNumber,
-      mainStartScore,
-      mainEndScore,
-      questionScore,
-    } = route.query as {
-      studentId: string;
-      subjectCode: string;
-      startScore: string;
-      endScore: string;
-      mainNumber: string;
-      mainStartScore: string;
-      mainEndScore: string;
-      questionScore: string;
-    };
+const route = useRoute();
+let isSingleStudent = ref(false);
+isSingleStudent.value = !!route.query.studentId;
+const { studentId, subjectCode } = route.query as {
+  studentId: string;
+  subjectCode: string;
+};
 
-    const upScale = () => {
-      const s = store.setting.uiSetting["answer.paper.scale"];
-      if (s < 3)
-        store.setting.uiSetting["answer.paper.scale"] = +(s + 0.2).toFixed(1);
-    };
-    const downScale = () => {
-      const s = store.setting.uiSetting["answer.paper.scale"];
-      if (s > 0.2)
-        store.setting.uiSetting["answer.paper.scale"] = +(s - 0.2).toFixed(1);
-    };
-    const normalScale = () => {
-      store.setting.uiSetting["answer.paper.scale"] = 1;
-    };
-    const toggleHistory = () => {
-      store.historyOpen = !store.historyOpen;
-    };
-    const greaterThanOneScale = computed(() => {
-      return store.setting.uiSetting["answer.paper.scale"] > 1;
-    });
-    const lessThanOneScale = computed(() => {
-      return store.setting.uiSetting["answer.paper.scale"] < 1;
-    });
-
-    async function updateHistoryTask({
-      pageNumber = 1,
-      pageSize = 10,
-    }: {
-      pageNumber: number; // 从1开始
-      pageSize: number;
-    }) {
-      const res = await getInspectedHistory({
-        pageNumber,
-        pageSize,
-        subjectCode,
-      });
-      if (res.data) {
-        store.historyTasks.push(res.data);
-      }
-    }
+const upScale = () => {
+  const s = store.setting.uiSetting["answer.paper.scale"];
+  if (s < 3)
+    store.setting.uiSetting["answer.paper.scale"] = +(s + 0.2).toFixed(1);
+};
+const downScale = () => {
+  const s = store.setting.uiSetting["answer.paper.scale"];
+  if (s > 0.2)
+    store.setting.uiSetting["answer.paper.scale"] = +(s - 0.2).toFixed(1);
+};
+const normalScale = () => {
+  store.setting.uiSetting["answer.paper.scale"] = 1;
+};
+const toggleHistory = () => {
+  store.historyOpen = !store.historyOpen;
+};
+const greaterThanOneScale = computed(() => {
+  return store.setting.uiSetting["answer.paper.scale"] > 1;
+});
+const lessThanOneScale = computed(() => {
+  return store.setting.uiSetting["answer.paper.scale"] < 1;
+});
 
-    async function updateClearTask() {
-      await clearInspectedTask(studentId, subjectCode);
-    }
+async function updateHistoryTask({
+  pageNumber = 1,
+  pageSize = 10,
+}: {
+  pageNumber: number; // 从1开始
+  pageSize: number;
+}) {
+  const res = await getInspectedHistory({
+    pageNumber,
+    pageSize,
+    subjectCode,
+  });
+  if (res.data) {
+    store.historyTasks.push(res.data);
+  }
+}
 
-    const closeWindow = async () => {
-      await updateClearTask();
-      window.close();
-    };
+async function updateClearTask() {
+  await clearInspectedTask(studentId, subjectCode);
+}
 
-    onMounted(() => {
-      // 不确定是否一定能在关闭页面时调用
-      window.addEventListener("beforeunload", () => {
-        updateClearTask();
-      });
-    });
+const closeWindow = async () => {
+  await updateClearTask();
+  window.close();
+};
 
-    return {
-      store,
-      isSingleStudent,
-      upScale,
-      downScale,
-      normalScale,
-      greaterThanOneScale,
-      lessThanOneScale,
-      updateHistoryTask,
-      toggleHistory,
-      closeWindow,
-    };
-  },
+onMounted(() => {
+  // 不确定是否一定能在关闭页面时调用
+  window.addEventListener("beforeunload", () => {
+    updateClearTask();
+  });
 });
 </script>
 

+ 69 - 88
src/features/student/inspect/MarkHistory.vue

@@ -51,108 +51,89 @@
   </div>
 </template>
 
-<script lang="ts">
+<script setup lang="ts">
 import { getInspectedHistory } from "@/api/inspectPage";
-import { Task } from "@/types";
-import { defineComponent, ref, watch, watchEffect } from "vue";
+import type { Task } from "@/types";
+import { defineEmit, defineProps, ref, watch, watchEffect } from "vue";
 import { useRoute } from "vue-router";
 import { store } from "./store";
 import { CloseOutlined } from "@ant-design/icons-vue";
 import { cloneDeep } from "lodash";
 
-export default defineComponent({
-  name: "MarkHistory",
-  components: { CloseOutlined },
-  props: {
-    shouldReload: { type: Number, required: true },
-  },
-  emits: ["reload"],
-  setup(props, { emit }) {
-    const route = useRoute();
-    const { subjectCode } = route.query as {
-      subjectCode: string;
-    };
+const props = defineProps<{ shouldReload: number }>();
 
-    watchEffect(async () => {
-      if (store.historyOpen) {
-        replaceCurrentTask(undefined);
-        await updateHistoryTask({});
-        replaceCurrentTask(store.historyTasks[0]);
-      } else {
-        emit("reload");
-      }
-    });
+const emit = defineEmit(["reload"]);
+const route = useRoute();
+const { subjectCode } = route.query as {
+  subjectCode: string;
+};
 
-    watch(
-      () => props.shouldReload,
-      async () => {
-        await updateHistoryTask({ pageNumber: currentPage.value });
-        // 提交后,渲染第一条
-        replaceCurrentTask(store.historyTasks[0]);
-      }
-    );
+watchEffect(async () => {
+  if (store.historyOpen) {
+    replaceCurrentTask(undefined);
+    await updateHistoryTask({});
+    replaceCurrentTask(store.historyTasks[0]);
+  } else {
+    emit("reload");
+  }
+});
 
-    const secretNumberInput = ref(null);
-    const loading = ref(false);
-    const currentPage = ref(1);
+watch(
+  () => props.shouldReload,
+  async () => {
+    await updateHistoryTask({ pageNumber: currentPage.value });
+    // 提交后,渲染第一条
+    replaceCurrentTask(store.historyTasks[0]);
+  }
+);
 
-    async function updateHistoryTask({
-      pageNumber = 1,
-      pageSize = 10,
-    }: {
-      pageNumber?: number; // 从1开始
-      pageSize?: number;
-    }) {
-      loading.value = true;
-      const res = await getInspectedHistory({
-        pageNumber,
-        pageSize,
-        subjectCode,
-      });
-      loading.value = false;
-      if (res.data) {
-        let data = cloneDeep(res.data) as Array<Task>;
-        data = data.map((t) => {
-          t.sliceUrls = t.sliceUrls.map((s) => store.setting.fileServer + s);
-          t.sheetUrls = t.sheetUrls?.map((s) => store.setting.fileServer + s);
-          t.jsonUrl = store.setting.fileServer + t.jsonUrl;
+const loading = ref(false);
+const currentPage = ref(1);
 
-          return t;
-        });
-        store.historyTasks = data;
-        replaceCurrentTask(store.historyTasks[0]);
-      }
-    }
+async function updateHistoryTask({
+  pageNumber = 1,
+  pageSize = 10,
+}: {
+  pageNumber?: number; // 从1开始
+  pageSize?: number;
+}) {
+  loading.value = true;
+  const res = await getInspectedHistory({
+    pageNumber,
+    pageSize,
+    subjectCode,
+  });
+  loading.value = false;
+  if (res.data) {
+    let data = cloneDeep(res.data) as Array<Task>;
+    data = data.map((t) => {
+      t.sliceUrls = t.sliceUrls.map((s) => store.setting.fileServer + s);
+      t.sheetUrls = t.sheetUrls?.map((s) => store.setting.fileServer + s);
+      t.jsonUrl = store.setting.fileServer + t.jsonUrl;
 
-    function replaceCurrentTask(task: Task | undefined) {
-      store.currentTask = task;
-    }
+      return t;
+    });
+    store.historyTasks = data;
+    replaceCurrentTask(store.historyTasks[0]);
+  }
+}
 
-    function previousPage() {
-      if (currentPage.value > 1) {
-        currentPage.value -= 1;
-        updateHistoryTask({ pageNumber: currentPage.value });
-      }
-    }
-    function nextPage() {
-      if (store.historyTasks.length >= 10) {
-        currentPage.value += 1;
-        updateHistoryTask({ pageNumber: currentPage.value });
-      }
-    }
+function replaceCurrentTask(task: Task | undefined) {
+  store.currentTask = task;
+}
 
-    return {
-      store,
-      loading,
-      secretNumberInput,
-      updateHistoryTask,
-      replaceCurrentTask,
-      currentPage,
-      previousPage,
-      nextPage,
-    };
-  },
-});
+function previousPage() {
+  if (currentPage.value > 1) {
+    currentPage.value -= 1;
+    updateHistoryTask({ pageNumber: currentPage.value });
+  }
+}
+function nextPage() {
+  if (store.historyTasks.length >= 10) {
+    currentPage.value += 1;
+    updateHistoryTask({ pageNumber: currentPage.value });
+  }
+}
 </script>
 
 <style scoped>