Selaa lähdekoodia

重构:裁切图渲染使用系统的组件

Michael Wang 4 vuotta sitten
vanhempi
commit
9331ea8d60

+ 13 - 334
src/features/library/inspect/MarkBody.vue

@@ -1,347 +1,26 @@
 <template>
-  <div class="mark-body-container tw-flex-auto tw-p-2" 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>
-      <div v-else :style="{ width: answerPaperScale }">
-        <div
-          v-for="(item, index) in sliceImagesWithTrackList"
-          :key="index"
-          class="single-image-container"
-        >
-          <img :src="item.url" draggable="false" />
-          <MarkDrawTrack
-            :track-list="item.trackList"
-            :special-tag-list="item.tagList"
-            :original-image="item.originalImage"
-            :slice-image="item.sliceImage"
-            :dx="item.dx"
-            :dy="item.dy"
-          />
-          <hr class="image-seperator" />
-        </div>
-      </div>
-    </a-spin>
-  </div>
+  <CommonMarkBody
+    v-if="store"
+    :useMarkResult="false"
+    :store="store"
+    uniquePropName="studentId"
+    @error="$emit('error')"
+  />
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, reactive, ref, watchEffect } from "vue";
+import CommonMarkBody from "@/features/mark/CommonMarkBody.vue";
+import { defineComponent, watch } from "vue";
 import { store } from "./store";
-import MarkDrawTrack from "./MarkDrawTrack.vue";
-import { SpecialTag, Track } from "@/types";
-import { useTimers } from "@/setups/useTimers";
-import {
-  getDataUrlForSliceConfig,
-  getDataUrlForSplitConfig,
-  loadImage,
-} from "@/utils/utils";
-import { dragImage } from "@/features/mark/use/draggable";
 
-interface SliceImage {
-  url: string;
-  indexInSliceUrls: number;
-  trackList: Array<Track>;
-  tagList: Array<SpecialTag>;
-  originalImage: HTMLImageElement;
-  sliceImage: HTMLImageElement;
-  dx: number;
-  dy: number;
-  accumTopHeight: number;
-  effectiveWidth: number;
-}
-// should not render twice at the same time
-let __lock = false;
-let __currentStudentId = -1; // save __currentStudentIdof lock
 export default defineComponent({
   name: "MarkBody",
-  components: { MarkDrawTrack },
+  components: { CommonMarkBody },
   emits: ["error"],
-  setup(props, { emit }) {
-    const { dragContainer } = dragImage();
-
-    const { addTimeout } = useTimers();
-
-    function hasSliceConfig() {
-      return store.currentTask?.sliceConfig?.length;
-    }
-
-    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) => {
-        const image = new Image();
-        image.src = dataUrl;
-        image.onload = function () {
-          resolve(image);
-        };
-      });
-    }
-
-    async function processSliceConfig() {
-      if (!store.currentTask) return;
-
-      const images = [];
-      const urls = [];
-      // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
-      for (const sliceConfig of store.currentTask.sliceConfig) {
-        const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
-        const image = await loadImage(url);
-        images.push(image);
-        urls.push(url);
-        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[indexInSliceUrls - 1];
-
-        const dataUrl = await getDataUrlForSliceConfig(
-          image,
-          sliceConfig,
-          maxSliceWidth,
-          url
-        );
-        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);
-
-        const sliceImage = await getImageUsingDataUrl(dataUrl);
-        tempSliceImagesWithTrackList.push({
-          url: dataUrl,
-          indexInSliceUrls,
-          // 通过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() {
-      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
-          );
-
-          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);
-          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);
-    }
-    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;
-      for (const s of sliceImagesWithTrackList) {
-        // console.log("revoke", s.url);
-        URL.revokeObjectURL(s.url);
-      }
-      sliceImagesWithTrackList.splice(0);
-
-      if (!store.currentTask) {
-        __lock = false;
-        return;
-      }
-
-      try {
-        rendering.value = true;
-        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;
-      }
-    };
-
-    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 + "%";
-    });
-
-    return {
-      dragContainer,
-      store,
-      rendering,
-      sliceImagesWithTrackList,
-      answerPaperScale,
-    };
+  setup() {
+    return { store };
   },
 });
 </script>
 
-<style scoped>
-.mark-body-container {
-  height: calc(100vh - 41px);
-  overflow: scroll;
-  background-size: 8px 8px;
-  background-image: linear-gradient(to right, #e7e7e7 4px, transparent 4px),
-    linear-gradient(to bottom, transparent 4px, #e7e7e7 4px);
-}
-.mark-body-container img {
-  width: 100%;
-}
-.single-image-container {
-  position: relative;
-}
-.image-seperator {
-  border: 2px solid rgba(120, 120, 120, 0.1);
-}
-</style>
+<style scoped></style>

+ 470 - 0
src/features/mark/CommonMarkBody.vue

@@ -0,0 +1,470 @@
+<template>
+  <div class="mark-body-container tw-flex-auto tw-p-2" 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>
+      <div v-else :style="{ width: answerPaperScale }">
+        <div
+          style="
+            top: -10px;
+            right: 0;
+            position: absolute;
+            color: red;
+            pointer-events: none;
+            font-size: 30px;
+            z-index: 1000;
+          "
+        >
+          {{ markStatus }}
+        </div>
+        <div
+          v-for="(item, index) in sliceImagesWithTrackList"
+          :key="index"
+          class="single-image-container"
+        >
+          <img
+            :src="item.url"
+            @click="(event) => innerMakeTrack(event, item)"
+            draggable="false"
+          />
+          <MarkDrawTrack
+            :track-list="item.trackList"
+            :special-tag-list="item.tagList"
+            :original-image="item.originalImage"
+            :slice-image="item.sliceImage"
+            :dx="item.dx"
+            :dy="item.dy"
+          />
+          <hr class="image-seperator" />
+        </div>
+      </div>
+    </a-spin>
+  </div>
+  <slot name="slot-cursor" />
+</template>
+
+<script lang="ts">
+import {
+  computed,
+  defineComponent,
+  PropType,
+  reactive,
+  ref,
+  watch,
+  watchEffect,
+} from "vue";
+import { getMarkStatus } from "./store";
+import MarkDrawTrack from "./MarkDrawTrack.vue";
+import { MarkResult, MarkStore, SliceImage, SpecialTag, Track } from "@/types";
+import { useTimers } from "@/setups/useTimers";
+import {
+  getDataUrlForSliceConfig,
+  getDataUrlForSplitConfig,
+  loadImage,
+} 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();
+
+    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;
+      });
+    }
+
+    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 = [];
+      const urls = [];
+      // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
+      for (const sliceConfig of store.currentTask.sliceConfig) {
+        const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
+        const image = await loadImage(url);
+        images.push(image);
+        urls.push(url);
+        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[indexInSliceUrls - 1];
+
+        const dataUrl = await getDataUrlForSliceConfig(
+          image,
+          sliceConfig,
+          maxSliceWidth,
+          url
+        );
+
+        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: 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[store.currentTask.sliceUrls.indexOf(url)];
+
+          accumBottomHeight += image.naturalHeight;
+
+          const dataUrl = await getDataUrlForSplitConfig(
+            image,
+            config,
+            maxSliceWidth,
+            url
+          );
+
+          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;
+        }
+      }
+      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;
+
+        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;
+      }
+    };
+
+    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 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 = [];
+          }
+        }
+      });
+    }
+
+    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 });
+  // },
+});
+</script>
+
+<style scoped>
+.mark-body-container {
+  height: calc(100vh - 41px);
+  overflow: scroll;
+  background-size: 8px 8px;
+  background-image: linear-gradient(to right, #e7e7e7 4px, transparent 4px),
+    linear-gradient(to bottom, transparent 4px, #e7e7e7 4px);
+}
+.mark-body-container img {
+  width: 100%;
+}
+.single-image-container {
+  position: relative;
+}
+.image-seperator {
+  border: 2px solid rgba(120, 120, 120, 0.1);
+}
+</style>

+ 31 - 401
src/features/mark/MarkBody.vue

@@ -1,51 +1,11 @@
 <template>
-  <div class="mark-body-container tw-flex-auto tw-p-2" 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>
-      <div v-else :style="{ width: answerPaperScale }">
-        <div
-          style="
-            top: -10px;
-            right: 0;
-            position: absolute;
-            color: red;
-            pointer-events: none;
-            font-size: 30px;
-            z-index: 1000;
-          "
-        >
-          {{ markStatus }}
-        </div>
-        <div
-          v-for="(item, index) in sliceImagesWithTrackList"
-          :key="index"
-          class="single-image-container"
-        >
-          <img
-            :src="item.url"
-            @click="(event) => makeTrack(event, item)"
-            draggable="false"
-          />
-          <MarkDrawTrack
-            :track-list="item.trackList"
-            :special-tag-list="item.tagList"
-            :original-image="item.originalImage"
-            :slice-image="item.sliceImage"
-            :dx="item.dx"
-            :dy="item.dy"
-          />
-          <hr class="image-seperator" />
-        </div>
-      </div>
-    </a-spin>
-  </div>
+  <CommonMarkBody
+    :useMarkResult="true"
+    uniquePropName="libraryId"
+    :store="store"
+    :makeTrack="makeTrack"
+    @error="$emit('error')"
+  />
   <div class="cursor">
     <div class="cursor-border">
       <span class="text">{{
@@ -57,277 +17,28 @@
 
 <script lang="ts">
 import {
-  computed,
   defineComponent,
   onMounted,
   onUnmounted,
-  reactive,
-  ref,
   watch,
   watchEffect,
 } from "vue";
-import { getMarkStatus, store } from "./store";
+import { store } from "./store";
 import MarkDrawTrack from "./MarkDrawTrack.vue";
-import { ModeEnum, SpecialTag, Track } from "@/types";
+import { ModeEnum, SliceImage, SpecialTag, Track } from "@/types";
 import { useTimers } from "@/setups/useTimers";
-import {
-  getDataUrlForSliceConfig,
-  getDataUrlForSplitConfig,
-  loadImage,
-} from "@/utils/utils";
 import { isNumber } from "lodash";
 // @ts-ignore
 import CustomCursor from "custom-cursor.js";
-import { dragImage } from "./use/draggable";
+import CommonMarkBody from "./CommonMarkBody.vue";
 
-interface SliceImage {
-  url: string;
-  indexInSliceUrls: number;
-  trackList: Array<Track>;
-  tagList: Array<SpecialTag>;
-  originalImage: HTMLImageElement;
-  sliceImage: HTMLImageElement;
-  dx: number;
-  dy: number;
-  accumTopHeight: number;
-  effectiveWidth: number;
-}
-// should not render twice at the same time
-let __lock = false;
-let __currentLibraryId = -1; // save __currentLibraryId of lock
 export default defineComponent({
   name: "MarkBody",
-  components: { MarkDrawTrack },
+  components: { MarkDrawTrack, CommonMarkBody },
   emits: ["error"],
   setup(props, { emit }) {
-    const { dragContainer } = dragImage();
-
     const { addTimeout } = useTimers();
 
-    function hasSliceConfig() {
-      return store.currentTask?.sliceConfig?.length;
-    }
-
-    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) => {
-        const image = new Image();
-        image.src = dataUrl;
-        image.onload = function () {
-          resolve(image);
-        };
-      });
-    }
-
-    async function processSliceConfig() {
-      // check if have MarkResult for currentTask
-      let markResult = store.currentMarkResult;
-
-      if (!markResult || !store.currentTask) return;
-
-      const images = [];
-      const urls = [];
-      // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
-      for (const sliceConfig of store.currentTask.sliceConfig) {
-        const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
-        const image = await loadImage(url);
-        images.push(image);
-        urls.push(url);
-        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 image = images[urls.indexOf(url)];
-
-        const dataUrl = await getDataUrlForSliceConfig(
-          image,
-          sliceConfig,
-          maxSliceWidth,
-          url
-        );
-
-        const thisImageTrackList = markResult.trackList.filter(
-          (v) => v.offsetIndex === sliceConfig.i
-        );
-        const thisImageTagList = markResult.specialTagList.filter(
-          (v) => v.offsetIndex === sliceConfig.i
-        );
-
-        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() {
-      // check if have MarkResult for currentTask
-      let markResult = store.currentMarkResult;
-
-      if (!markResult || !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 image = images[store.currentTask.sliceUrls.indexOf(url)];
-
-          accumBottomHeight += image.naturalHeight;
-
-          const dataUrl = await getDataUrlForSplitConfig(
-            image,
-            config,
-            maxSliceWidth,
-            url
-          );
-
-          const thisImageTrackList = markResult.trackList.filter(
-            (t) =>
-              t.offsetIndex ===
-              (store.currentTask &&
-                store.currentTask.sliceUrls.indexOf(url) + 1)
-          );
-          const thisImageTagList = markResult.specialTagList.filter(
-            (t) =>
-              t.offsetIndex ===
-              (store.currentTask &&
-                store.currentTask.sliceUrls.indexOf(url) + 1)
-          );
-          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);
-    }
-    const renderPaperAndMark = async () => {
-      if (__lock) {
-        if (store.currentTask?.libraryId === __currentLibraryId) {
-          // rendering.value 会触发渲染,所以这里应取消。所以这里更好的做法是watch currentTask?
-          console.log("重复渲染,返回");
-          return;
-        }
-        console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
-        await new Promise((res) => setTimeout(res, 1000));
-        await renderPaperAndMark();
-        return;
-      }
-      __lock = true;
-      __currentLibraryId = store.currentTask?.libraryId ?? -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 (!markResult || !store.currentTask) {
-        __lock = false;
-        return;
-      }
-
-      try {
-        rendering.value = true;
-        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;
-      }
-    };
-
-    watchEffect(renderPaperAndMark);
-
     watch(
       () => store.minimapScrollTo,
       () => {
@@ -345,33 +56,12 @@ export default defineComponent({
       }
     );
 
-    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 makeScoreTrack = (event: MouseEvent, item: SliceImage) => {
+    const makeScoreTrack = (
+      event: MouseEvent,
+      item: SliceImage,
+      maxSliceWidth: number,
+      theFinalHeight: number
+    ) => {
       // console.log(item);
       if (!store.currentQuestion || typeof store.currentScore === "undefined")
         return;
@@ -379,7 +69,6 @@ export default defineComponent({
       const track = {} as Track;
       track.mainNumber = store.currentQuestion?.mainNumber;
       track.subNumber = store.currentQuestion?.subNumber;
-      // track.number = (Date.now() - new Date(2021, 0, 0).valueOf()) / 10e7;
       track.score = store.currentScore;
       track.offsetIndex = item.indexInSliceUrls;
       track.offsetX = Math.round(
@@ -440,7 +129,12 @@ export default defineComponent({
       item.trackList.push(track);
     };
 
-    const makeSpecialTagTrack = (event: MouseEvent, item: SliceImage) => {
+    const makeSpecialTagTrack = (
+      event: MouseEvent,
+      item: SliceImage,
+      maxSliceWidth: number,
+      theFinalHeight: number
+    ) => {
       // console.log(item);
       if (!store.currentTask || typeof store.currentSpecialTag === "undefined")
         return;
@@ -480,56 +174,22 @@ export default defineComponent({
       item.tagList.push(track);
     };
 
-    const makeTrack = (event: MouseEvent, item: SliceImage) => {
+    const makeTrack = (
+      event: MouseEvent,
+      item: SliceImage,
+      maxSliceWidth: number,
+      theFinalHeight: number
+    ) => {
       if (
         store.setting.uiSetting["specialTag.modal"] &&
         store.currentSpecialTag
       ) {
-        makeSpecialTagTrack(event, item);
+        makeSpecialTagTrack(event, item, maxSliceWidth, theFinalHeight);
       } else {
-        makeScoreTrack(event, item);
+        makeScoreTrack(event, item, maxSliceWidth, theFinalHeight);
       }
     };
 
-    // 清除分数轨迹
-    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 = [];
-        }
-      }
-    });
-
     // 轨迹模式下,添加轨迹,更新分数
     watch(
       () => store.currentMarkResult?.trackList,
@@ -620,22 +280,9 @@ export default defineComponent({
       theCursor && theCursor.destroy();
     });
 
-    const markStatus = ref("");
-    watch(
-      () => store.currentTask,
-      () => {
-        markStatus.value = getMarkStatus();
-      }
-    );
-
     return {
-      dragContainer,
       store,
-      rendering,
-      sliceImagesWithTrackList,
-      answerPaperScale,
       makeTrack,
-      markStatus,
     };
   },
   // renderTriggered({ key, target, type }) {
@@ -645,23 +292,6 @@ export default defineComponent({
 </script>
 
 <style scoped>
-.mark-body-container {
-  height: calc(100vh - 41px);
-  overflow: scroll;
-  background-size: 8px 8px;
-  background-image: linear-gradient(to right, #e7e7e7 4px, transparent 4px),
-    linear-gradient(to bottom, transparent 4px, #e7e7e7 4px);
-}
-.mark-body-container img {
-  width: 100%;
-}
-.single-image-container {
-  position: relative;
-}
-.image-seperator {
-  border: 2px solid rgba(120, 120, 120, 0.1);
-}
-
 .hide-cursor {
   display: none !important;
 }

+ 15 - 0
src/types/index.ts

@@ -19,6 +19,7 @@ export interface MarkStore {
   MarkBoardTrackCollapse: boolean; // 是否收缩评分版
   historyTasks: Array<Task>;
   removeScoreTracks: Array<Track>;
+  focusTracks: Array<Track>; // 暂时无用
   message: string | null;
   maxModalZIndex: number;
   minimapScrollTo: number; // 高度的百分比
@@ -198,3 +199,17 @@ export interface InspectStore {
   focusTracks: Array<Track>;
   message: string | null;
 }
+
+// 前端自用,用来渲染裁切图
+export interface SliceImage {
+  url: string; // 当前是 ObjectURL , 因为 DataURL 性能太差
+  indexInSliceUrls: number;
+  trackList: Array<Track>;
+  tagList: Array<SpecialTag>;
+  originalImage: HTMLImageElement;
+  sliceImage: HTMLImageElement;
+  dx: number;
+  dy: number;
+  accumTopHeight: number;
+  effectiveWidth: number;
+}