فهرست منبع

script setup refactor step 1

Michael Wang 4 سال پیش
والد
کامیت
6023a6df85
4فایلهای تغییر یافته به همراه646 افزوده شده و 719 حذف شده
  1. 6 16
      src/features/mark/AllPaperModal.vue
  2. 8 14
      src/features/mark/AnswerModal.vue
  3. 378 387
      src/features/mark/CommonMarkBody.vue
  4. 254 302
      src/features/mark/Mark.vue

+ 6 - 16
src/features/mark/AllPaperModal.vue

@@ -32,26 +32,16 @@
   </teleport>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent, onUnmounted, reactive, ref } from "vue";
+<script setup lang="ts">
+import { computed, ref } from "vue";
 import { CloseOutlined } from "@ant-design/icons-vue";
 import { store } from "@/features/mark/store";
 
-export default defineComponent({
-  name: "AllPaperModal",
-  components: { CloseOutlined },
-  props: {},
-  emits: ["close"],
-  setup() {
-    const urls = computed(() => {
-      return store.currentTask?.sliceUrls ?? [];
-    });
-
-    const checkedIndex = ref(0);
-
-    return { store, urls, checkedIndex };
-  },
+const urls = computed(() => {
+  return store.currentTask?.sliceUrls ?? [];
 });
+
+const checkedIndex = ref(0);
 </script>
 
 <style scoped>

+ 8 - 14
src/features/mark/AnswerModal.vue

@@ -20,21 +20,15 @@
   </qm-dialog>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent, ref } from "vue";
+<script setup lang="ts">
+import { computed } from "vue";
 import { store } from "./store";
 
-export default defineComponent({
-  name: "AnswerModal",
-  setup() {
-    const answerPDFUrl = computed(() => {
-      return store.setting.subject.answerUrl;
-    });
-
-    const close = () => {
-      store.setting.uiSetting["answer.modal"] = false;
-    };
-    return { store, answerPDFUrl, close };
-  },
+const answerPDFUrl = computed(() => {
+  return store.setting.subject.answerUrl;
 });
+
+const close = () => {
+  store.setting.uiSetting["answer.modal"] = false;
+};
 </script>

+ 378 - 387
src/features/mark/CommonMarkBody.vue

@@ -3,12 +3,6 @@
     class="mark-body-container tw-flex-auto tw-p-2 tw-relative"
     ref="dragContainer"
   >
-    <!-- <a-spin
-      :spinning="rendering"
-      size="large"
-      tip="Loading..."
-      style="margin-top: 50px"
-    > -->
     <div v-if="!store.currentTask" class="tw-text-center">
       {{ store.message }}
     </div>
@@ -47,16 +41,15 @@
         <hr class="image-seperator" />
       </div>
     </div>
-    <!-- </a-spin> -->
   </div>
   <slot name="slot-cursor" />
 </template>
 
-<script lang="ts">
+<script setup lang="ts">
 import {
   computed,
-  defineComponent,
-  PropType,
+  defineEmit,
+  defineProps,
   reactive,
   ref,
   watch,
@@ -64,7 +57,13 @@ import {
 } from "vue";
 import { getMarkStatus } from "./store";
 import MarkDrawTrack from "./MarkDrawTrack.vue";
-import { MarkResult, MarkStore, SliceImage, SpecialTag, Track } from "@/types";
+import type {
+  MarkResult,
+  MarkStore,
+  SliceImage,
+  SpecialTag,
+  Track,
+} from "@/types";
 import { useTimers } from "@/setups/useTimers";
 import {
   getDataUrlForSliceConfig,
@@ -73,401 +72,393 @@ import {
 } from "@/utils/utils";
 import { dragImage } from "./use/draggable";
 
-// should not render twice at the same time
-let __lock = false;
-let __currentLibraryId = -1 as any; // save __currentLibraryId of lock
-export default defineComponent({
-  name: "MarkBody",
-  components: { MarkDrawTrack },
-  props: {
-    useMarkResult: { type: Boolean, default: false },
-    makeTrack: { type: Function },
-    store: { type: Object as PropType<MarkStore>, required: true }, // 实际上不是同一个store!!!
-    uniquePropName: { type: String, default: "libraryId" },
-  },
-  emits: ["error"],
-  setup({ useMarkResult, makeTrack, uniquePropName, store }, { emit }) {
-    const { dragContainer } = dragImage();
-
-    const { addTimeout } = useTimers();
-
-    watch(
-      () => store.minimapScrollTo,
-      () => {
-        const container = document.querySelector(
-          ".mark-body-container"
-        ) as HTMLDivElement;
-        addTimeout(() => {
-          if (container) {
-            const { scrollHeight } = container;
-            container.scrollTo({
-              top: scrollHeight * store.minimapScrollTo,
-            });
-          }
-        }, 10);
+const props =
+  defineProps<{
+    useMarkResult?: boolean;
+    makeTrack: Function;
+    store: MarkStore; // 实际上不是同一个store!!!
+    uniquePropName: string; // TODO: 这个字段不需要了,是以前的rendering字段附带要求的
+  }>();
+
+const emit = defineEmit(["error"]);
+
+const {
+  useMarkResult = false,
+  makeTrack,
+  store,
+  uniquePropName = "libraryId",
+} = props;
+
+// start: 图片拖动。在轨迹模式下,仅当没有选择分数时可用。
+const { dragContainer } = dragImage();
+// end: 图片拖动
+
+const { addTimeout } = useTimers();
+
+// start: 缩略图定位
+watch(
+  () => store.minimapScrollTo,
+  () => {
+    const container = document.querySelector(
+      ".mark-body-container"
+    ) as HTMLDivElement;
+    addTimeout(() => {
+      if (container) {
+        const { scrollHeight } = container;
+        container.scrollTo({
+          top: scrollHeight * store.minimapScrollTo,
+        });
       }
+    }, 10);
+  }
+);
+// end: 缩略图定位
+
+// start: 计算裁切图和裁切图上的分数轨迹和特殊标记轨迹
+let sliceImagesWithTrackList: Array<SliceImage> = reactive([]);
+let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
+let theFinalHeight = 0; // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
+
+async function getImageUsingDataUrl(
+  dataUrl: string
+): Promise<HTMLImageElement> {
+  return new Promise((resolve, reject) => {
+    const image = new Image();
+    image.src = dataUrl;
+    image.onload = function () {
+      resolve(image);
+    };
+    image.onerror = reject;
+  });
+}
+
+async function processSliceConfig() {
+  let markResult = store.currentMarkResult as MarkResult;
+  if (useMarkResult) {
+    // check if have MarkResult for currentTask
+    if (!markResult) return;
+  }
+
+  if (!store.currentTask) return;
+
+  const images = [];
+  // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
+  for (const sliceConfig of store.currentTask.sliceConfig) {
+    const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
+    const image = await loadImage(url);
+    images[sliceConfig.i] = image;
+    if (sliceConfig.w === 0 && sliceConfig.h === 0) {
+      // 选择整图时,w/h 为0
+      sliceConfig.w = image.naturalWidth;
+      sliceConfig.h = image.naturalHeight;
+    }
+  }
+
+  theFinalHeight = store.currentTask.sliceConfig
+    .map((v) => v.h)
+    .reduce((acc, v) => (acc += v));
+  maxSliceWidth = Math.max(...store.currentTask.sliceConfig.map((v) => v.w));
+
+  // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
+  let accumTopHeight = 0;
+  let accumBottomHeight = 0;
+  const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
+  for (const sliceConfig of store.currentTask.sliceConfig) {
+    accumBottomHeight += sliceConfig.h;
+    const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
+    const indexInSliceUrls = sliceConfig.i;
+    const image = images[sliceConfig.i];
+
+    const dataUrl = (await getDataUrlForSliceConfig(
+      image,
+      sliceConfig,
+      maxSliceWidth,
+      url
+    )) as string;
+
+    let trackLists = [] as Array<Track>;
+    if (useMarkResult) {
+      trackLists = markResult.trackList;
+    } else {
+      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
     );
 
-    // let rendering = ref(false);
-    let sliceImagesWithTrackList: Array<SliceImage> = reactive([]);
-    let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
-    let theFinalHeight = 0; // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
-
-    async function getImageUsingDataUrl(
-      dataUrl: string
-    ): Promise<HTMLImageElement> {
-      return new Promise((resolve, reject) => {
-        const image = new Image();
-        image.src = dataUrl;
-        image.onload = function () {
-          resolve(image);
-        };
-        image.onerror = reject;
-      });
+    let tagLists = [] as Array<SpecialTag>;
+    if (useMarkResult) {
+      tagLists = markResult.specialTagList ?? [];
+    } else {
+      tagLists = store.currentTask.specialTagList ?? [];
     }
+    const thisImageTagList = tagLists.filter(
+      (t) => t.offsetIndex === indexInSliceUrls
+    );
 
-    async function processSliceConfig() {
-      let markResult = store.currentMarkResult as MarkResult;
+    const sliceImage = await getImageUsingDataUrl(dataUrl);
+    tempSliceImagesWithTrackList.push({
+      url: dataUrl,
+      indexInSliceUrls: sliceConfig.i,
+      // 通过positionY来定位是第几张slice的还原,并过滤出相应的track
+      trackList: thisImageTrackList.filter(
+        (t) =>
+          t.positionY >= accumTopHeight / theFinalHeight &&
+          t.positionY < accumBottomHeight / theFinalHeight
+      ),
+      tagList: thisImageTagList.filter(
+        (t) =>
+          t.positionY >= accumTopHeight / theFinalHeight &&
+          t.positionY < accumBottomHeight / theFinalHeight
+      ),
+      originalImage: image,
+      sliceImage,
+      dx: sliceConfig.x,
+      dy: sliceConfig.y,
+      accumTopHeight,
+      effectiveWidth: sliceConfig.w,
+    });
+    accumTopHeight = accumBottomHeight;
+  }
+  sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
+}
+
+async function processSplitConfig() {
+  let markResult = store.currentMarkResult as MarkResult;
+  if (useMarkResult) {
+    // check if have MarkResult for currentTask
+    if (!markResult) return;
+  }
+
+  if (!store.currentTask) return;
+
+  const images = [];
+  for (const url of store.currentTask.sliceUrls) {
+    const image = await loadImage(url);
+    images.push(image);
+  }
+
+  const splitConfigPairs = store.setting.splitConfig
+    .map((v, index, ary) => (index % 2 === 0 ? [v, ary[index + 1]] : false))
+    .filter((v) => v) as unknown as Array<[number, number]>;
+
+  const maxSplitConfig = Math.max(...store.setting.splitConfig);
+  maxSliceWidth =
+    Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
+
+  theFinalHeight =
+    splitConfigPairs.length *
+    images.reduce((acc, v) => (acc += v.naturalHeight), 0);
+
+  let accumTopHeight = 0;
+  let accumBottomHeight = 0;
+  const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
+  for (const url of store.currentTask.sliceUrls) {
+    for (const config of splitConfigPairs) {
+      const indexInSliceUrls = store.currentTask.sliceUrls.indexOf(url) + 1;
+      const image = images[indexInSliceUrls - 1];
+
+      accumBottomHeight += image.naturalHeight;
+
+      const dataUrl = (await getDataUrlForSplitConfig(
+        image,
+        config,
+        maxSliceWidth,
+        url
+      )) as string;
+
+      let trackLists = [] as Array<Track>;
       if (useMarkResult) {
-        // check if have MarkResult for currentTask
-        if (!markResult) return;
+        trackLists = markResult.trackList;
+      } else {
+        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
+      );
 
-      if (!store.currentTask) return;
-
-      const images = [];
-      // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
-      for (const sliceConfig of store.currentTask.sliceConfig) {
-        const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
-        const image = await loadImage(url);
-        images[sliceConfig.i] = image;
-        if (sliceConfig.w === 0 && sliceConfig.h === 0) {
-          // 选择整图时,w/h 为0
-          sliceConfig.w = image.naturalWidth;
-          sliceConfig.h = image.naturalHeight;
-        }
+      let tagLists = [] as Array<SpecialTag>;
+      if (useMarkResult) {
+        tagLists = markResult.specialTagList ?? [];
+      } else {
+        tagLists = store.currentTask.specialTagList ?? [];
       }
-
-      theFinalHeight = store.currentTask.sliceConfig
-        .map((v) => v.h)
-        .reduce((acc, v) => (acc += v));
-      maxSliceWidth = Math.max(
-        ...store.currentTask.sliceConfig.map((v) => v.w)
+      const thisImageTagList = tagLists.filter(
+        (t) => t.offsetIndex === indexInSliceUrls
       );
+      const sliceImage = await getImageUsingDataUrl(dataUrl);
+      tempSliceImagesWithTrackList.push({
+        url: dataUrl,
+        indexInSliceUrls: store.currentTask.sliceUrls.indexOf(url) + 1,
+        trackList: thisImageTrackList.filter(
+          (t) =>
+            t.positionY >= accumTopHeight / theFinalHeight &&
+            t.positionY < accumBottomHeight / theFinalHeight
+        ),
+        tagList: thisImageTagList.filter(
+          (t) =>
+            t.positionY >= accumTopHeight / theFinalHeight &&
+            t.positionY < accumBottomHeight / theFinalHeight
+        ),
+        originalImage: image,
+        sliceImage,
+        dx: image.naturalWidth * config[0],
+        dy: 0,
+        accumTopHeight,
+        effectiveWidth: image.naturalWidth * config[1],
+      });
+      accumTopHeight = accumBottomHeight;
+    }
+  }
+  sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
+}
 
-      // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
-      let accumTopHeight = 0;
-      let accumBottomHeight = 0;
-      const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
-      for (const sliceConfig of store.currentTask.sliceConfig) {
-        accumBottomHeight += sliceConfig.h;
-        const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
-        const indexInSliceUrls = sliceConfig.i;
-        const image = images[sliceConfig.i];
-
-        const dataUrl = (await getDataUrlForSliceConfig(
-          image,
-          sliceConfig,
-          maxSliceWidth,
-          url
-        )) as string;
-
-        let trackLists = [] as Array<Track>;
-        if (useMarkResult) {
-          trackLists = markResult.trackList;
-        } else {
-          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
-        );
-
-        let tagLists = [] as Array<SpecialTag>;
-        if (useMarkResult) {
-          tagLists = markResult.specialTagList ?? [];
-        } else {
-          tagLists = store.currentTask.specialTagList ?? [];
-        }
-        const thisImageTagList = tagLists.filter(
-          (t) => t.offsetIndex === indexInSliceUrls
+// should not render twice at the same time
+let renderLock = false;
+const renderPaperAndMark = async () => {
+  if (!store) return;
+  if (renderLock) {
+    console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
+    await new Promise((res) => setTimeout(res, 1000));
+    await renderPaperAndMark();
+    return;
+  }
+  renderLock = true;
+  // for (const s of sliceImagesWithTrackList) {
+  //   // console.log("revoke", s.url);
+  //   URL.revokeObjectURL(s.url);
+  // }
+  sliceImagesWithTrackList.splice(0);
+  // check if have MarkResult for currentTask
+  let markResult = store.currentMarkResult;
+
+  if ((useMarkResult && !markResult) || !store.currentTask) {
+    renderLock = false;
+    return;
+  }
+
+  try {
+    store.globalMask = true;
+
+    const hasSliceConfig = store.currentTask?.sliceConfig?.length;
+
+    if (hasSliceConfig) {
+      await processSliceConfig();
+    } else {
+      await processSplitConfig();
+    }
+  } catch (error) {
+    sliceImagesWithTrackList.splice(0);
+    console.log("render error ", error);
+    // 图片加载出错,自动加载下一个任务
+    emit("error");
+  } finally {
+    renderLock = false;
+    store.globalMask = false;
+  }
+};
+
+watchEffect(renderPaperAndMark);
+// end: 计算裁切图和裁切图上的分数轨迹和特殊标记轨迹
+
+// start: 放大缩小和之后的滚动
+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 + "%";
+});
+// end: 放大缩小和之后的滚动
+
+// start: 显示评分状态和清除轨迹
+const markStatus = ref("");
+if (useMarkResult) {
+  watch(
+    () => store.currentTask,
+    () => {
+      markStatus.value = getMarkStatus();
+    }
+  );
+
+  // 清除分数轨迹
+  watchEffect(() => {
+    for (const track of store.removeScoreTracks) {
+      for (const sliceImage of sliceImagesWithTrackList) {
+        sliceImage.trackList = sliceImage.trackList.filter(
+          (t) =>
+            !(
+              t.mainNumber === track.mainNumber &&
+              t.subNumber === track.subNumber &&
+              t.number === track.number
+            )
         );
-
-        const sliceImage = await getImageUsingDataUrl(dataUrl);
-        tempSliceImagesWithTrackList.push({
-          url: dataUrl,
-          indexInSliceUrls: sliceConfig.i,
-          // 通过positionY来定位是第几张slice的还原,并过滤出相应的track
-          trackList: thisImageTrackList.filter(
-            (t) =>
-              t.positionY >= accumTopHeight / theFinalHeight &&
-              t.positionY < accumBottomHeight / theFinalHeight
-          ),
-          tagList: thisImageTagList.filter(
-            (t) =>
-              t.positionY >= accumTopHeight / theFinalHeight &&
-              t.positionY < accumBottomHeight / theFinalHeight
-          ),
-          originalImage: image,
-          sliceImage,
-          dx: sliceConfig.x,
-          dy: sliceConfig.y,
-          accumTopHeight,
-          effectiveWidth: sliceConfig.w,
-        });
-        accumTopHeight = accumBottomHeight;
       }
-      sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
     }
-
-    async function processSplitConfig() {
-      let markResult = store.currentMarkResult as MarkResult;
-      if (useMarkResult) {
-        // check if have MarkResult for currentTask
-        if (!markResult) return;
-      }
-
-      if (!store.currentTask) return;
-
-      const images = [];
-      for (const url of store.currentTask.sliceUrls) {
-        const image = await loadImage(url);
-        images.push(image);
-      }
-
-      const splitConfigPairs = store.setting.splitConfig
-        .map((v, index, ary) => (index % 2 === 0 ? [v, ary[index + 1]] : false))
-        .filter((v) => v) as unknown as Array<[number, number]>;
-
-      const maxSplitConfig = Math.max(...store.setting.splitConfig);
-      maxSliceWidth =
-        Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
-
-      theFinalHeight =
-        splitConfigPairs.length *
-        images.reduce((acc, v) => (acc += v.naturalHeight), 0);
-
-      let accumTopHeight = 0;
-      let accumBottomHeight = 0;
-      const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
-      for (const url of store.currentTask.sliceUrls) {
-        for (const config of splitConfigPairs) {
-          const indexInSliceUrls = store.currentTask.sliceUrls.indexOf(url) + 1;
-          const image = images[indexInSliceUrls - 1];
-
-          accumBottomHeight += image.naturalHeight;
-
-          const dataUrl = (await getDataUrlForSplitConfig(
-            image,
-            config,
-            maxSliceWidth,
-            url
-          )) as string;
-
-          let trackLists = [] as Array<Track>;
-          if (useMarkResult) {
-            trackLists = markResult.trackList;
-          } else {
-            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
-          );
-
-          let tagLists = [] as Array<SpecialTag>;
-          if (useMarkResult) {
-            tagLists = markResult.specialTagList ?? [];
-          } else {
-            tagLists = store.currentTask.specialTagList ?? [];
-          }
-          const thisImageTagList = tagLists.filter(
-            (t) => t.offsetIndex === indexInSliceUrls
-          );
-          const sliceImage = await getImageUsingDataUrl(dataUrl);
-          tempSliceImagesWithTrackList.push({
-            url: dataUrl,
-            indexInSliceUrls: store.currentTask.sliceUrls.indexOf(url) + 1,
-            trackList: thisImageTrackList.filter(
-              (t) =>
-                t.positionY >= accumTopHeight / theFinalHeight &&
-                t.positionY < accumBottomHeight / theFinalHeight
-            ),
-            tagList: thisImageTagList.filter(
-              (t) =>
-                t.positionY >= accumTopHeight / theFinalHeight &&
-                t.positionY < accumBottomHeight / theFinalHeight
-            ),
-            originalImage: image,
-            sliceImage,
-            dx: image.naturalWidth * config[0],
-            dy: 0,
-            accumTopHeight,
-            effectiveWidth: image.naturalWidth * config[1],
-          });
-          accumTopHeight = accumBottomHeight;
-        }
+    // 清除后,删除,否则会影响下次切换
+    store.removeScoreTracks.splice(0);
+  });
+
+  // 清除特殊标记轨迹
+  watchEffect(() => {
+    for (const track of store.currentMarkResult?.specialTagList || []) {
+      for (const sliceImage of sliceImagesWithTrackList) {
+        sliceImage.tagList = sliceImage.tagList.filter((t) =>
+          store.currentMarkResult?.specialTagList.find(
+            (st) =>
+              st.offsetIndex === t.offsetIndex &&
+              st.offsetX === t.offsetX &&
+              st.offsetY === t.offsetY
+          )
+        );
       }
-      sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
     }
-    const renderPaperAndMark = async () => {
-      if (!store) return;
-      const uid =
-        store.currentTask &&
-        (store.currentTask[
-          uniquePropName as keyof MarkStore["currentTask"]
-        ] as number);
-      if (__lock) {
-        if (uid === __currentLibraryId) {
-          // rendering.value 会触发渲染,所以这里应取消。所以这里更好的做法是watch currentTask?
-          console.log("重复渲染,返回");
-          return;
-        }
-        console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
-        await new Promise((res) => setTimeout(res, 1000));
-        await renderPaperAndMark();
-        return;
-      }
-      __lock = true;
-      __currentLibraryId = uid ?? -1;
-      // for (const s of sliceImagesWithTrackList) {
-      //   // console.log("revoke", s.url);
-      //   URL.revokeObjectURL(s.url);
-      // }
-      sliceImagesWithTrackList.splice(0);
-      // check if have MarkResult for currentTask
-      let markResult = store.currentMarkResult;
-
-      if ((useMarkResult && !markResult) || !store.currentTask) {
-        __lock = false;
-        return;
-      }
-
-      try {
-        // rendering.value = true;
-        store.globalMask = true;
-
-        const hasSliceConfig = store.currentTask?.sliceConfig?.length;
-
-        if (hasSliceConfig) {
-          await processSliceConfig();
-        } else {
-          await processSplitConfig();
-        }
-      } catch (error) {
-        sliceImagesWithTrackList.splice(0);
-        console.log("render error ", error);
-        // 图片加载出错,自动加载下一个任务
-        emit("error");
-      } finally {
-        __lock = false;
-        // rendering.value = false;
-        store.globalMask = 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;
+    if (store.currentMarkResult?.specialTagList.length === 0) {
+      for (const sliceImage of sliceImagesWithTrackList) {
+        sliceImage.tagList = [];
       }
-
-      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 markStatus = ref("");
-    if (useMarkResult) {
-      watch(
-        () => store.currentTask,
-        () => {
-          markStatus.value = getMarkStatus();
-        }
-      );
-
-      // 清除分数轨迹
-      watchEffect(() => {
-        for (const track of store.removeScoreTracks) {
-          for (const sliceImage of sliceImagesWithTrackList) {
-            sliceImage.trackList = sliceImage.trackList.filter(
-              (t) =>
-                !(
-                  t.mainNumber === track.mainNumber &&
-                  t.subNumber === track.subNumber &&
-                  t.number === track.number
-                )
-            );
-          }
-        }
-        // 清除后,删除,否则会影响下次切换
-        store.removeScoreTracks.splice(0);
-      });
-
-      // 清除特殊标记轨迹
-      watchEffect(() => {
-        for (const track of store.currentMarkResult?.specialTagList || []) {
-          for (const sliceImage of sliceImagesWithTrackList) {
-            sliceImage.tagList = sliceImage.tagList.filter((t) =>
-              store.currentMarkResult?.specialTagList.find(
-                (st) =>
-                  st.offsetIndex === t.offsetIndex &&
-                  st.offsetX === t.offsetX &&
-                  st.offsetY === t.offsetY
-              )
-            );
-          }
-        }
-        if (store.currentMarkResult?.specialTagList.length === 0) {
-          for (const sliceImage of sliceImagesWithTrackList) {
-            sliceImage.tagList = [];
-          }
-        }
-      });
     }
+  });
+}
+// end: 显示评分状态和清除轨迹
 
-    const innerMakeTrack = (event: MouseEvent, item: SliceImage) => {
-      makeTrack && makeTrack(event, item, maxSliceWidth, theFinalHeight);
-    };
-    return {
-      dragContainer,
-      store,
-      // rendering,
-      sliceImagesWithTrackList,
-      answerPaperScale,
-      markStatus,
-      innerMakeTrack,
-    };
-  },
-  // renderTriggered({ key, target, type }) {
-  //   console.log({ key, target, type });
-  // },
-});
+// start: 评分
+const innerMakeTrack = (event: MouseEvent, item: SliceImage) => {
+  makeTrack && makeTrack(event, item, maxSliceWidth, theFinalHeight);
+};
+// end: 评分
+
+// onRenderTriggered(({ key, target, type }) => {
+//   console.log({ key, target, type });
+// });
 </script>
 
 <style scoped>

+ 254 - 302
src/features/mark/Mark.vue

@@ -30,8 +30,8 @@
   <SpecialTagModal />
 </template>
 
-<script lang="ts">
-import { computed, defineComponent, onMounted, ref, watch } from "vue";
+<script setup lang="ts">
+import { computed, onMounted, ref, watch } from "vue";
 import {
   clearMarkTask,
   getGroup,
@@ -52,7 +52,8 @@ import MarkBody from "./MarkBody.vue";
 import { useTimers } from "@/setups/useTimers";
 import MarkHistory from "./MarkHistory.vue";
 import MarkBoardTrack from "./MarkBoardTrack.vue";
-import { ModeEnum, Setting, Task } from "@/types";
+import { ModeEnum } from "@/types";
+import type { Setting, Task } from "@/types";
 import MarkBoardKeyBoard from "./MarkBoardKeyBoard.vue";
 import MarkBoardMouse from "./MarkBoardMouse.vue";
 import { isEmpty, isNumber } from "lodash";
@@ -65,338 +66,289 @@ import SheetViewModal from "./SheetViewModal.vue";
 import SpecialTagModal from "./SpecialTagModal.vue";
 import { preDrawImage } from "@/utils/utils";
 
-export default defineComponent({
-  name: "Mark",
-  components: {
-    MarkHeader,
-    MarkBody,
-    MarkHistory,
-    MarkBoardTrack,
-    MarkBoardKeyBoard,
-    MarkBoardMouse,
-    AnswerModal,
-    PaperModal,
-    MinimapModal,
-    AllPaperModal,
-    SheetViewModal,
-    SpecialTagModal,
-  },
-  setup: () => {
-    const { addInterval } = useTimers();
+// name: "Mark",
+const { addInterval } = useTimers();
+
+async function updateMarkTask() {
+  await clearMarkTask();
+}
 
-    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",
+    } as Setting["uiSetting"];
+  }
+  store.setting = settingRes.data;
+  if (store.setting.subject?.answerUrl) {
+    store.setting.subject.answerUrl =
+      store.setting.fileServer + store.setting.subject?.answerUrl;
+  }
+  if (store.setting.subject?.paperUrl) {
+    store.setting.subject.paperUrl =
+      store.setting.fileServer + store.setting.subject?.paperUrl;
+  }
+}
+async function updateStatus() {
+  const res = await getStatus();
+  if (res.data.valid) 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.libraryId) {
+    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;
+    //         for (const sliceUrl of (res.data as Task).sliceUrls) {
+    //   fetch(store.setting.fileServer + sliceUrl);
+    // }
+    store.tasks.push(res.data);
+    if (!store.historyOpen) {
+      // 在正评中,才能替换task
+      // TODO: 疑似替换多次引起重新渲染
+      if (store.currentTask?.studentId !== store.tasks[0].studentId)
+        store.currentTask = store.tasks[0];
     }
 
-    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",
-        } as Setting["uiSetting"];
-      }
-      store.setting = settingRes.data;
-      if (store.setting.subject?.answerUrl) {
-        store.setting.subject.answerUrl =
-          store.setting.fileServer + store.setting.subject?.answerUrl;
-      }
-      if (store.setting.subject?.paperUrl) {
-        store.setting.subject.paperUrl =
-          store.setting.fileServer + store.setting.subject?.paperUrl;
-      }
+    // 如果是评完后,再取到的任务,则此时要更新一下status
+    if (store.status.totalCount - store.status.markedCount === 0) {
+      await updateStatus();
     }
-    async function updateStatus() {
-      const res = await getStatus();
-      if (res.data.valid) store.status = res.data;
+
+    // prefetch sliceUrls image
+    if (store.tasks.length > 1) {
+      // 如果不是当前任务,则先等3秒再去取任务,以免和其他请求争夺网络资源
+      await new Promise((resolve) => setTimeout(resolve, 3000));
     }
-    async function updateGroups() {
-      const res = await getGroup();
-      store.groups = res.data;
+    // for (const sliceUrl of (res.data as Task).sliceUrls) {
+    //   fetch(sliceUrl);
+    // }
+    try {
+      preDrawing = true;
+      await preDrawImage(res.data);
+    } finally {
+      preDrawing = false;
     }
+  } else {
+    store.message = res.data.message;
+  }
+}
 
-    let preDrawing = false;
-    async function updateTask() {
-      const res = await getTask();
-      if (res.data.libraryId) {
-        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;
-        //         for (const sliceUrl of (res.data as Task).sliceUrls) {
-        //   fetch(store.setting.fileServer + sliceUrl);
-        // }
-        store.tasks.push(res.data);
-        if (!store.historyOpen) {
-          // 在正评中,才能替换task
-          // TODO: 疑似替换多次引起重新渲染
-          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));
-        }
-        // for (const sliceUrl of (res.data as Task).sliceUrls) {
-        //   fetch(sliceUrl);
-        // }
-        try {
-          preDrawing = true;
-          await preDrawImage(res.data);
-        } finally {
-          preDrawing = false;
-        }
-      } else {
-        store.message = res.data.message;
-      }
+// 5秒更新一次tasks
+addInterval(() => {
+  // console.log("get task", store.tasks);
+  // 正在预绘制中,则不要再取任务,以免拖慢当前任务的绘制
+  if (!preDrawing) {
+    if (store.tasks.length < (store.setting.prefetchCount ?? 3)) {
+      // 回看打开时,停止取任务
+      if (!store.historyOpen) updateTask();
     }
+  }
+}, 5 * 1000);
 
-    // 5秒更新一次tasks
-    addInterval(() => {
-      // console.log("get task", store.tasks);
-      // 正在预绘制中,则不要再取任务,以免拖慢当前任务的绘制
-      if (!preDrawing) {
-        if (store.tasks.length < (store.setting.prefetchCount ?? 3)) {
-          // 回看打开时,停止取任务
-          if (!store.historyOpen) updateTask();
-        }
-      }
-    }, 5 * 1000);
+// 不需要
+// addInterval(() => {
+//   updateStatus();
+// }, 5 * 60 * 1000);
 
-    // 不需要
-    // addInterval(() => {
-    //   updateStatus();
-    // }, 5 * 60 * 1000);
+onMounted(async () => {
+  await updateMarkTask();
+  await updateSetting();
+  await updateStatus();
+  await updateGroups();
+  await updateTask();
+});
 
-    onMounted(async () => {
-      await updateMarkTask();
-      await updateSetting();
-      await updateStatus();
-      await updateGroups();
-      await updateTask();
-    });
+watch(
+  () => [store.setting.uiSetting, store.setting.mode],
+  () => {
+    updateUISetting(store.setting.mode, store.setting.uiSetting);
+  },
+  { deep: true }
+);
 
-    watch(
-      () => [store.setting.uiSetting, store.setting.mode],
-      () => {
-        updateUISetting(store.setting.mode, store.setting.uiSetting);
-      },
-      { deep: true }
-    );
+// 切换currentTask的同时,切换currentMarkResult
+watch(
+  () => store.currentTask,
+  () => {
+    // 回评切换任务,先删除之前回评任务的markResult
+    removeOldPreviousMarkResult();
+    store.currentMarkResult = findCurrentTaskMarkResult();
 
-    // 切换currentTask的同时,切换currentMarkResult
-    watch(
-      () => store.currentTask,
-      () => {
-        // 回评切换任务,先删除之前回评任务的markResult
-        removeOldPreviousMarkResult();
-        store.currentMarkResult = findCurrentTaskMarkResult();
+    // 重置当前选择的quesiton和score
+    store.currentQuestion = undefined;
+    store.currentScore = undefined;
+  }
+);
 
-        // 重置当前选择的quesiton和score
-        store.currentQuestion = undefined;
-        store.currentScore = undefined;
-      }
-    );
-
-    const showMarkBoardTrack = computed(() => {
-      return store.setting.mode === ModeEnum.TRACK;
-    });
+const showMarkBoardTrack = computed(() => {
+  return store.setting.mode === ModeEnum.TRACK;
+});
 
-    const showMarkBoardKeyBoard = computed(() => {
-      return (
-        store.setting.mode === ModeEnum.COMMON &&
-        store.setting.uiSetting["normal.mode"] === "keyboard"
-      );
-    });
+const showMarkBoardKeyBoard = computed(() => {
+  return (
+    store.setting.mode === ModeEnum.COMMON &&
+    store.setting.uiSetting["normal.mode"] === "keyboard"
+  );
+});
 
-    const showMarkBoardMouse = computed(() => {
-      return (
-        store.setting.mode === ModeEnum.COMMON &&
-        store.setting.uiSetting["normal.mode"] === "mouse"
-      );
-    });
+const showMarkBoardMouse = computed(() => {
+  return (
+    store.setting.mode === ModeEnum.COMMON &&
+    store.setting.uiSetting["normal.mode"] === "mouse"
+  );
+});
 
-    const removeBrokenTask = () => {
-      removeCurrentMarkResult();
-      store.currentTask = undefined;
+const removeBrokenTask = () => {
+  removeCurrentMarkResult();
+  store.currentTask = undefined;
 
-      if (store.historyOpen) {
-        // store.currentTask = store.historyTasks[0];
-        // 避免无限加载
-        store.message = "加载失败,请重新选择。";
-      } else {
-        store.tasks.shift();
-      }
-    };
+  if (store.historyOpen) {
+    // store.currentTask = store.historyTasks[0];
+    // 避免无限加载
+    store.message = "加载失败,请重新选择。";
+  } else {
+    store.tasks.shift();
+  }
+};
 
-    const shouldReloadHistory = ref(0);
+const shouldReloadHistory = ref(0);
 
-    const shouldReloadFunc = () => {
-      shouldReloadHistory.value = Date.now();
-    };
+const shouldReloadFunc = () => {
+  shouldReloadHistory.value = Date.now();
+};
 
-    const allZeroSubmit = async () => {
-      const markResult = store.currentMarkResult;
-      if (!markResult) return;
+const allZeroSubmit = async () => {
+  const markResult = store.currentMarkResult;
+  if (!markResult) return;
 
-      const { markerScore, scoreList, trackList, specialTagList } = markResult;
-      markResult.markerScore = 0;
-      const ss = new Array(store.currentTask?.questionList.length);
-      markResult.scoreList = ss.fill(0);
-      markResult.trackList = [];
+  const { markerScore, scoreList, trackList, specialTagList } = markResult;
+  markResult.markerScore = 0;
+  const ss = new Array(store.currentTask?.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 saveTaskToServer = async () => {
-      const markResult = store.currentMarkResult;
-      if (!markResult) return;
+  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 saveTaskToServer = async () => {
+  const markResult = store.currentMarkResult;
+  if (!markResult) return;
 
-      if (
-        markResult.scoreList.length !==
-          store.currentTask?.questionList.length ||
-        !markResult.scoreList.every((s) => isNumber(s))
-      ) {
-        console.error("markResult格式不正确,缺少分数");
-        return;
-      }
+  if (
+    markResult.scoreList.length !== store.currentTask?.questionList.length ||
+    !markResult.scoreList.every((s) => isNumber(s))
+  ) {
+    console.error("markResult格式不正确,缺少分数");
+    return;
+  }
 
-      if (
-        markResult.scoreList.length !==
-          store.currentTask?.questionList.length &&
-        markResult.scoreList.every((s) => isNumber(s))
-      ) {
-        // 轨迹回评普通打分时,有分数无轨迹,导致markResult的scoreList计算不准确
-        if (store.currentTask?.questionList.every((q) => isNumber(q.score))) {
-          let question;
-          for (const q of store.currentTask?.questionList) {
-            const valid = markResult.trackList.some(
-              (t) =>
-                q.mainNumber === t.mainNumber && q.subNumber === t.subNumber
-            );
-            if (!valid) {
-              question = q;
-              break;
-            }
-          }
-          if (question) {
-            message.error({
-              content: `${question.mainNumber}-${question.subNumber} 没有轨迹。`,
-              duration: 5,
-            });
-          } else {
-            message.error({ content: "少有人见过的错误" });
-          }
-        } else {
-          message.error({ content: "还有题目没有评分。", duration: 5 });
+  if (
+    markResult.scoreList.length !== store.currentTask?.questionList.length &&
+    markResult.scoreList.every((s) => isNumber(s))
+  ) {
+    // 轨迹回评普通打分时,有分数无轨迹,导致markResult的scoreList计算不准确
+    if (store.currentTask?.questionList.every((q) => isNumber(q.score))) {
+      let question;
+      for (const q of store.currentTask?.questionList) {
+        const valid = markResult.trackList.some(
+          (t) => q.mainNumber === t.mainNumber && q.subNumber === t.subNumber
+        );
+        if (!valid) {
+          question = q;
+          break;
         }
-        return;
       }
-      if (store.setting.mode !== ModeEnum.TRACK) {
-        markResult.trackList = [];
+      if (question) {
+        message.error({
+          content: `${question.mainNumber}-${question.subNumber} 没有轨迹。`,
+          duration: 5,
+        });
       } else {
-        const trackScores =
-          markResult.trackList
-            .map((q) => Math.round((q.score || 0) * 100))
-            .reduce((acc, s) => acc + s, 0) / 100;
-        if (trackScores !== markResult.markerScore) {
-          message.error({
-            content: "轨迹分与总分不一致,请检查。",
-            duration: 3,
-          });
-          return;
-        }
-      }
-      if (store.setting.forceSpecialTag) {
-        if (
-          markResult.trackList.length === 0 &&
-          markResult.specialTagList.length === 0
-        ) {
-          message.error({
-            content: "强制标记已开启,请至少使用一个标记。",
-            duration: 5,
-          });
-          return;
-        }
-      }
-      console.log("save task to server");
-      const mkey = "save_task_key";
-      message.loading({ content: "保存评卷任务...", key: mkey });
-      const res = (await saveTask()) as any;
-      updateStatus();
-      if (res.data.success && store.currentTask) {
-        message.success({ content: "保存成功", key: mkey, duration: 2 });
-        if (!store.historyOpen) {
-          removeCurrentMarkResult();
-          store.currentTask = undefined;
-          store.tasks.shift();
-          store.currentTask = store.tasks[0];
-        } 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 });
+        message.error({ content: "少有人见过的错误" });
       }
-    };
-
-    return {
-      store,
-      updateTask,
-      allZeroSubmit,
-      saveTaskToServer,
-      showMarkBoardTrack,
-      showMarkBoardKeyBoard,
-      showMarkBoardMouse,
-      shouldReloadHistory,
-      shouldReloadFunc,
-      removeBrokenTask,
-    };
-  },
-});
+    } else {
+      message.error({ content: "还有题目没有评分。", duration: 5 });
+    }
+    return;
+  }
+  if (store.setting.mode !== ModeEnum.TRACK) {
+    markResult.trackList = [];
+  } else {
+    const trackScores =
+      markResult.trackList
+        .map((q) => Math.round((q.score || 0) * 100))
+        .reduce((acc, s) => acc + s, 0) / 100;
+    if (trackScores !== markResult.markerScore) {
+      message.error({
+        content: "轨迹分与总分不一致,请检查。",
+        duration: 3,
+      });
+      return;
+    }
+  }
+  if (store.setting.forceSpecialTag) {
+    if (
+      markResult.trackList.length === 0 &&
+      markResult.specialTagList.length === 0
+    ) {
+      message.error({
+        content: "强制标记已开启,请至少使用一个标记。",
+        duration: 5,
+      });
+      return;
+    }
+  }
+  console.log("save task to server");
+  const mkey = "save_task_key";
+  message.loading({ content: "保存评卷任务...", key: mkey });
+  const res = (await saveTask()) as any;
+  updateStatus();
+  if (res.data.success && store.currentTask) {
+    message.success({ content: "保存成功", key: mkey, duration: 2 });
+    if (!store.historyOpen) {
+      removeCurrentMarkResult();
+      store.currentTask = undefined;
+      store.tasks.shift();
+      store.currentTask = store.tasks[0];
+    } 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 });
+  }
+};
 </script>
 
 <style scoped>
 .my-container {
   width: 100%;
 }
-a {
-  color: #42b983;
-}
-
-label {
-  margin: 0 0.5em;
-  font-weight: bold;
-}
-
-code {
-  background-color: #eee;
-  padding: 2px 4px;
-  border-radius: 4px;
-  color: #304455;
-}
 </style>