Selaa lähdekoodia

关于双评的需求

刘洋 2 vuotta sitten
vanhempi
commit
e848b5a7e4
6 muutettua tiedostoa jossa 815 lisäystä ja 790 poistoa
  1. 60 60
      package.json
  2. 72 72
      src/devLoginParams.ts
  3. 150 150
      src/features/mark/MarkDrawTrack.vue
  4. 276 251
      src/features/student/studentInspect/MarkBody.vue
  5. 224 225
      src/store/store.ts
  6. 33 32
      tsconfig.json

+ 60 - 60
package.json

@@ -1,61 +1,61 @@
-{
-  "name": "stmms-web",
-  "version": "1.3.9",
-  "private": "true",
-  "scripts": {
-    "start": "vite --host 0.0.0.0",
-    "dev": "vite --force",
-    "prebuild": "node --experimental-json-modules prebuild.mjs",
-    "build": "vue-tsc --noEmit --skipLibCheck && vite build",
-    "build-without-type-check": "vite build",
-    "typecheck": "vue-tsc --noEmit --skipLibCheck",
-    "serve": "vite preview",
-    "lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
-    "format": "prettier .  --write",
-    "test": "cypress open"
-  },
-  "dependencies": {
-    "@ant-design/icons-vue": "^6.1.0",
-    "ant-design-vue": "^3.2.10",
-    "axios": "^0.26.1",
-    "axios-progress-bar": "^1.2.0",
-    "axios-retry": "^3.3.1",
-    "custom-cursor.js": "1.3.6",
-    "lodash-es": "^4.17.21",
-    "mitt": "^3.0.0",
-    "moment": "^2.29.4",
-    "pinia": "^2.0.16",
-    "tailwindcss": "^3.1.6",
-    "ua-parser-js": "^1.0.2",
-    "viewerjs": "^1.10.5",
-    "vue": "^3.2.37",
-    "vue-router": "^4.1.2"
-  },
-  "devDependencies": {
-    "@types/lodash-es": "^4.17.6",
-    "@types/node": "^18.0.6",
-    "@types/ua-parser-js": "^0.7.36",
-    "@typescript-eslint/eslint-plugin": "^5.30.7",
-    "@typescript-eslint/parser": "^5.30.7",
-    "@vitejs/plugin-vue": "^3.0.1",
-    "@vue/runtime-core": "^3.2.37",
-    "autoprefixer": "^10.4.7",
-    "eslint": "^8.20.0",
-    "eslint-config-prettier": "^8.5.0",
-    "eslint-plugin-vue": "^9.2.0",
-    "postcss": "^8.4.14",
-    "prettier": "^2.7.1",
-    "typescript": "^4.7.4",
-    "unplugin-vue-components": "^0.21.1",
-    "vite": "^3.0.2",
-    "vue-eslint-parser": "^9.0.3",
-    "vue-tsc": "^0.38.8"
-  },
-  "vetur": {
-    "tags": "vetur/tags.json",
-    "attributes": "vetur/attributes.json"
-  },
-  "optionalDependencies": {
-    "cypress": "^10.3.1"
-  }
+{
+  "name": "stmms-web",
+  "version": "1.4.0",
+  "private": "true",
+  "scripts": {
+    "start": "vite --host 0.0.0.0",
+    "dev": "vite --force",
+    "prebuild": "node --experimental-json-modules prebuild.mjs",
+    "build": "vue-tsc --noEmit --skipLibCheck && vite build",
+    "build-without-type-check": "vite build",
+    "typecheck": "vue-tsc --noEmit --skipLibCheck",
+    "serve": "vite preview",
+    "lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
+    "format": "prettier .  --write",
+    "test": "cypress open"
+  },
+  "dependencies": {
+    "@ant-design/icons-vue": "^6.1.0",
+    "ant-design-vue": "^3.2.10",
+    "axios": "^0.26.1",
+    "axios-progress-bar": "^1.2.0",
+    "axios-retry": "^3.3.1",
+    "custom-cursor.js": "1.3.6",
+    "lodash-es": "^4.17.21",
+    "mitt": "^3.0.0",
+    "moment": "^2.29.4",
+    "pinia": "^2.0.16",
+    "tailwindcss": "^3.1.6",
+    "ua-parser-js": "^1.0.2",
+    "viewerjs": "^1.10.5",
+    "vue": "^3.2.37",
+    "vue-router": "^4.1.2"
+  },
+  "devDependencies": {
+    "@types/lodash-es": "^4.17.6",
+    "@types/node": "^18.0.6",
+    "@types/ua-parser-js": "^0.7.36",
+    "@typescript-eslint/eslint-plugin": "^5.30.7",
+    "@typescript-eslint/parser": "^5.30.7",
+    "@vitejs/plugin-vue": "^3.0.1",
+    "@vue/runtime-core": "^3.2.37",
+    "autoprefixer": "^10.4.7",
+    "eslint": "^8.20.0",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-plugin-vue": "^9.2.0",
+    "postcss": "^8.4.14",
+    "prettier": "^2.7.1",
+    "typescript": "^4.7.4",
+    "unplugin-vue-components": "^0.21.1",
+    "vite": "^3.0.2",
+    "vue-eslint-parser": "^9.0.3",
+    "vue-tsc": "^0.38.8"
+  },
+  "vetur": {
+    "tags": "vetur/tags.json",
+    "attributes": "vetur/attributes.json"
+  },
+  "optionalDependencies": {
+    "cypress": "^10.3.1"
+  }
 }

+ 72 - 72
src/devLoginParams.ts

@@ -1,72 +1,72 @@
-/** 224 评卷员 */
-// export const LOGIN_CONFIG = {
-// };
-// export const isAdmin= false;
-// export const forceChange=true;
-// export const loginName="1-431-5-1";
-// export const password="123456";
-// export const examId="1";
-// export const markerId="419";
-
-/** 244 评卷员 */
-// export const LOGIN_CONFIG = {
-//   isAdmin: false,
-//   forceChange: true,
-//   loginName: "1-339-5-1",
-//   // loginName: "liuyang",
-//   password: "123456",
-//   examId: "1",
-//   markerId: "147",
-//   // markerId: "482",
-//   // markerId: "483",
-// };
-// export const LOGIN_CONFIG = {
-//   isAdmin: false,
-//   forceChange: false,
-//   loginName: "spj111-01",
-//   // loginName: "spj432-01",
-//   // loginName: "liuyang",
-//   password: "123456",
-//   examId: "232",
-//   markerId: "2688",
-//   // markerId: "2692",
-//   // markerId: "482",
-//   // markerId: "483",
-// };
-/** 224 管理员 */
-export const LOGIN_CONFIG = {
-  isAdmin: true,
-  forceChange: true,
-  loginName: "admin-test",
-  password: "123456",
-  examId: "1",
-  markerId: "339",
-};
-
-/** 255 评卷员 */
-// export const LOGIN_CONFIG = {
-//   isAdmin: false,
-//   forceChange: true,
-//   loginName: "4-1-1",
-//   password: "123456",
-//   examId: "128",
-//   markerId: "2258",
-// };
-
-/** 225 管理员 */
-
-// export const LOGIN_CONFIG = {
-//   isAdmin: true,
-//   forceChange: true,
-//   loginName: "admin3",
-//   password: "123456",
-//   examId: "130",
-//   markerId: "null",
-// };
-
-// export const loginName = "admin-ch";
-// export const password = "123456";
-// export const examId = "1";
-// export const markerId = "null";
-// export const isAdmin = true;
-// export const forceChange = true;
+/** 224 评卷员 */
+// export const LOGIN_CONFIG = {
+// };
+// export const isAdmin= false;
+// export const forceChange=true;
+// export const loginName="1-431-5-1";
+// export const password="123456";
+// export const examId="1";
+// export const markerId="419";
+
+/** 244 评卷员 */
+// export const LOGIN_CONFIG = {
+//   isAdmin: false,
+//   forceChange: true,
+//   loginName: "1-339-5-1",
+//   // loginName: "liuyang",
+//   password: "123456",
+//   examId: "1",
+//   markerId: "147",
+//   // markerId: "482",
+//   // markerId: "483",
+// };
+// export const LOGIN_CONFIG = {
+//   isAdmin: false,
+//   forceChange: false,
+//   loginName: "spj111-01",
+//   // loginName: "spj432-01",
+//   // loginName: "liuyang",
+//   password: "123456",
+//   examId: "232",
+//   markerId: "2688",
+//   // markerId: "2692",
+//   // markerId: "482",
+//   // markerId: "483",
+// };
+/** 224 管理员 */
+export const LOGIN_CONFIG = {
+  isAdmin: true,
+  forceChange: true,
+  loginName: "admin-test",
+  password: "123456",
+  examId: "1",
+  markerId: "339",
+};
+
+/** 255 评卷员 */
+// export const LOGIN_CONFIG = {
+//   isAdmin: false,
+//   forceChange: true,
+//   loginName: "4-1-1",
+//   password: "123456",
+//   examId: "128",
+//   markerId: "2258",
+// };
+
+/** 225 管理员 */
+
+// export const LOGIN_CONFIG = {
+//   isAdmin: true,
+//   forceChange: true,
+//   loginName: "admin3",
+//   password: "123456",
+//   examId: "130",
+//   markerId: "null",
+// };
+
+// export const loginName = "admin-ch";
+// export const password = "123456";
+// export const examId = "1";
+// export const markerId = "null";
+// export const isAdmin = true;
+// export const forceChange = true;

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

@@ -1,150 +1,150 @@
-<template>
-  <transition-group name="track-score" tag="div">
-    <template v-for="track in trackList">
-      <div
-        v-if="store.shouldShowTrack"
-        :key="`key-${track.mainNumber}-${track.subNumber}-${track.offsetY}-${track.offsetX}`"
-        class="score-container"
-        :class="[focusedTrack(track) && 'score-animation']"
-        :style="computeTopAndLeft(track)"
-      >
-        <span
-          :id="`a-${track.mainNumber}-${track.subNumber}-${track.offsetY}-${track.offsetX}`"
-          class="tw-m-auto"
-        >
-          {{ track.unanswered ? "空" : track.score }}
-        </span>
-      </div>
-    </template>
-  </transition-group>
-  <template v-for="(tag, index) in specialTagList" :key="index">
-    <div class="score-container" :style="computeTopAndLeft(tag)">
-      <span class="tw-m-auto">
-        {{ tag.tagName }}
-      </span>
-    </div>
-  </template>
-</template>
-
-<script setup lang="ts">
-import type { SpecialTag, Track } from "@/types";
-import { toRefs, watch, nextTick } from "vue";
-import { store } from "@/store/store";
-import { message } from "ant-design-vue";
-
-const props = defineProps<{
-  trackList: Array<Track | SpecialTag>;
-  specialTagList: Array<Track | SpecialTag>;
-  sliceImageWidth: number;
-  sliceImageHeight: number;
-  dx: number;
-  dy: number;
-}>();
-const { trackList } = toRefs(props);
-
-const computeTopAndLeft = (track: Track | SpecialTag) => {
-  const topInsideSlice = track.offsetY - props.dy;
-  const leftInsideSlice = track.offsetX - props.dx;
-  const topInsideSliceRatio = topInsideSlice / props.sliceImageHeight;
-  const leftInsideSliceRatio = leftInsideSlice / props.sliceImageWidth;
-  if (
-    topInsideSliceRatio < 0 ||
-    topInsideSliceRatio > 1 ||
-    leftInsideSliceRatio < 0 ||
-    leftInsideSliceRatio > 1
-  ) {
-    /** 解决message提示死循环的问题 */
-    void nextTick(() => {
-      void message.error("轨迹坐标有误,可能是图片被修改过,请联系管理员!");
-    });
-  }
-
-  return {
-    color: track.color || 'red',
-    top: topInsideSliceRatio * 100 + "%",
-    left: leftInsideSliceRatio * 100 + "%",
-    "font-size":
-      (store.setting.uiSetting["score.fontSize.scale"] || 1) *
-        store.setting.uiSetting["answer.paper.scale"] *
-        2.2 +
-      "em",
-  };
-};
-
-const focusedTrack = (track: Track) => {
-  return store.focusTracks.includes(track);
-};
-
-watch(
-  () => store.focusTracks,
-  () => {
-    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}-${topTrack.offsetX}`
-        )
-        ?.scrollIntoView({ behavior: "smooth" });
-    }
-  },
-  {
-    deep: true
-  }
-);
-</script>
-
-<style scoped>
-.score-container {
-  position: absolute;
-  display: flex;
-  place-content: center;
-  /* color: red; */
-
-  /* to center score */
-  width: 200px;
-  height: 200px;
-  margin-top: -100px;
-  margin-left: -100px;
-
-  /* to click through div */
-  pointer-events: none;
-}
-.score-animation {
-  animation: 2s ease-in-out 0s infinite alternate change_size;
-}
-
-@keyframes change_size {
-  from {
-    font-size: 2em;
-    margin-top: -100px;
-    margin-left: -100px;
-  }
-  to {
-    font-size: 4em;
-    margin-top: -80px;
-    margin-left: -80px;
-  }
-}
-.track-score-enter-active {
-  transition: opacity 0.3s ease;
-}
-.track-score-leave-active {
-  transition: opacity 0.6s ease;
-}
-
-.track-score-enter-from,
-.track-score-leave-to {
-  opacity: 0;
-}
-</style>
+<template>
+  <transition-group name="track-score" tag="div">
+    <template v-for="track in trackList">
+      <div
+        v-if="store.shouldShowTrack"
+        :key="`key-${track.mainNumber}-${track.subNumber}-${track.offsetY}-${track.offsetX}`"
+        class="score-container"
+        :class="[focusedTrack(track) && 'score-animation']"
+        :style="computeTopAndLeft(track)"
+      >
+        <span
+          :id="`a-${track.mainNumber}-${track.subNumber}-${track.offsetY}-${track.offsetX}`"
+          class="tw-m-auto"
+        >
+          {{ track.unanswered ? "空" : track.score }}
+        </span>
+      </div>
+    </template>
+  </transition-group>
+  <template v-for="(tag, index) in specialTagList" :key="index">
+    <div class="score-container" :style="computeTopAndLeft(tag)">
+      <span class="tw-m-auto">
+        {{ tag.tagName }}
+      </span>
+    </div>
+  </template>
+</template>
+
+<script setup lang="ts">
+import type { SpecialTag, Track } from "@/types";
+import { toRefs, watch, nextTick } from "vue";
+import { store } from "@/store/store";
+import { message } from "ant-design-vue";
+
+const props = defineProps<{
+  trackList: Array<Track>;
+  specialTagList: Array<SpecialTag>;
+  sliceImageWidth: number;
+  sliceImageHeight: number;
+  dx: number;
+  dy: number;
+}>();
+const { trackList } = toRefs(props);
+
+const computeTopAndLeft = (track: Track | SpecialTag) => {
+  const topInsideSlice = track.offsetY - props.dy;
+  const leftInsideSlice = track.offsetX - props.dx;
+  const topInsideSliceRatio = topInsideSlice / props.sliceImageHeight;
+  const leftInsideSliceRatio = leftInsideSlice / props.sliceImageWidth;
+  if (
+    topInsideSliceRatio < 0 ||
+    topInsideSliceRatio > 1 ||
+    leftInsideSliceRatio < 0 ||
+    leftInsideSliceRatio > 1
+  ) {
+    /** 解决message提示死循环的问题 */
+    void nextTick(() => {
+      void message.error("轨迹坐标有误,可能是图片被修改过,请联系管理员!");
+    });
+  }
+
+  return {
+    color: track.color || 'red',
+    top: topInsideSliceRatio * 100 + "%",
+    left: leftInsideSliceRatio * 100 + "%",
+    "font-size":
+      (store.setting.uiSetting["score.fontSize.scale"] || 1) *
+        store.setting.uiSetting["answer.paper.scale"] *
+        2.2 +
+      "em",
+  };
+};
+
+const focusedTrack = (track: Track) => {
+  return store.focusTracks.includes(track);
+};
+
+watch(
+  () => store.focusTracks,
+  () => {
+    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}-${topTrack.offsetX}`
+        )
+        ?.scrollIntoView({ behavior: "smooth" });
+    }
+  },
+  {
+    deep: true
+  }
+);
+</script>
+
+<style scoped>
+.score-container {
+  position: absolute;
+  display: flex;
+  place-content: center;
+  /* color: red; */
+
+  /* to center score */
+  width: 200px;
+  height: 200px;
+  margin-top: -100px;
+  margin-left: -100px;
+
+  /* to click through div */
+  pointer-events: none;
+}
+.score-animation {
+  animation: 2s ease-in-out 0s infinite alternate change_size;
+}
+
+@keyframes change_size {
+  from {
+    font-size: 2em;
+    margin-top: -100px;
+    margin-left: -100px;
+  }
+  to {
+    font-size: 4em;
+    margin-top: -80px;
+    margin-left: -80px;
+  }
+}
+.track-score-enter-active {
+  transition: opacity 0.3s ease;
+}
+.track-score-leave-active {
+  transition: opacity 0.6s ease;
+}
+
+.track-score-enter-from,
+.track-score-leave-to {
+  opacity: 0;
+}
+</style>

+ 276 - 251
src/features/student/studentInspect/MarkBody.vue

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

+ 224 - 225
src/store/store.ts

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

+ 33 - 32
tsconfig.json

@@ -1,32 +1,33 @@
-{
-  "compilerOptions": {
-    "target": "esnext",
-    "useDefineForClassFields": true,
-    "module": "esnext",
-    "moduleResolution": "node",
-    "strict": true,
-    "jsx": "preserve",
-    "sourceMap": true,
-    "isolatedModules": true,
-    "resolveJsonModule": true,
-    "esModuleInterop": true,
-    "lib": ["esnext", "dom"],
-    "types": ["./node_modules/vite/client", "./node_modules/vue/macros-global"],
-    "baseUrl": "./",
-    "paths": {
-      "@/*": ["src/*"]
-    }
-  },
-  "include": [
-    ".eslintrc.js",
-    "src/**/*.ts",
-    "src/**/*.d.ts",
-    "src/**/*.tsx",
-    "src/**/*.vue",
-    "src/*.vue",
-    "*.js",
-    "components.d.ts",
-    // "cypress/**/*.ts",
-    "cypress.config.ts"
-  ]
-}
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "useDefineForClassFields": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "strict": false,
+    "jsx": "preserve",
+    "sourceMap": true,
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "esModuleInterop": true,
+    "noImplicitAny": false,
+    "lib": ["esnext", "dom"],
+    "types": ["./node_modules/vite/client", "./node_modules/vue/macros-global"],
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": [
+    ".eslintrc.js",
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "src/*.vue",
+    "*.js",
+    "components.d.ts",
+    // "cypress/**/*.ts",
+    "cypress.config.ts"
+  ]
+}