刘洋 před 1 rokem
rodič
revize
dbe90b0c23

+ 1 - 0
src/devLoginParams.ts

@@ -68,6 +68,7 @@ export const LOGIN_CONFIG = {
   isAdmin: true,
   forceChange: true,
   loginName: "fh161301",
+  // loginName: "admin-1",
   password: "654321",
   examId: "295",
   markerId: null,

+ 2 - 1
src/features/student/scoreVerify/ScoreVerify.vue

@@ -31,7 +31,8 @@ import { store } from "@/store/store";
 import MarkHeader from "./MarkHeader.vue";
 import MinimapModal from "@/features/mark/MinimapModal.vue";
 import { useRoute } from "vue-router";
-import MarkBody from "../studentInspect/MarkBody.vue";
+// import MarkBody from "../studentInspect/MarkBody.vue";
+import MarkBody from "./markBody.vue";
 import MarkBoardInspect from "./MarkBoardInspect.vue";
 import type { AdminPageSetting } from "@/types";
 import { message } from "ant-design-vue";

+ 293 - 0
src/features/student/scoreVerify/markBody.vue

@@ -0,0 +1,293 @@
+<template>
+  <div
+    ref="dragContainer"
+    class="mark-body-container tw-flex-auto tw-p-2 tw-pt-0"
+    @scroll="viewScroll"
+  >
+    <div v-if="!store.currentTask" class="tw-text-center none-tip">
+      {{ store.message }}
+    </div>
+    <div
+      v-else-if="!sliceImagesWithTrackList.length"
+      class="tw-text-center none-tip"
+    >
+      考生答卷未上传
+    </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", "getScrollStatus"]);
+
+const { dragContainer } = dragImage();
+const viewScroll = () => {
+  if (
+    dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
+    dragContainer.value.scrollHeight
+  ) {
+    emit("getScrollStatus");
+  }
+};
+const { addTimeout } = useTimers();
+
+let sliceImagesWithTrackList: SliceImage[] = reactive([]);
+let maxImageWidth = 0;
+
+function addTrackColorAttr(tList: Track[]): Track[] {
+  let markerIds: (number | undefined)[] = tList
+    .map((v) => v.markerId)
+    .filter((x) => !!x);
+  markerIds = Array.from(new Set(markerIds));
+  markerIds.sort();
+  let colorMap: ColorMap = {};
+  for (let i = 0; i < markerIds.length; i++) {
+    const mId: any = markerIds[i];
+    if (i == 0) {
+      colorMap[mId + ""] = "red";
+    } else if (i == 1) {
+      colorMap[mId + ""] = "blue";
+    } else if (i > 1) {
+      colorMap[mId + ""] = "green";
+    }
+  }
+  if (Object.keys(colorMap).length > 1) {
+    emit("getIsMultComments", true);
+  }
+  tList = tList.map((item: Track) => {
+    item.color = colorMap[item.markerId + ""] || "red";
+    item.isByMultMark = markerIds.length > 1;
+    return item;
+  });
+  return tList;
+}
+
+function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
+  let markerIds: (number | undefined)[] = tList
+    .map((v) => v.markerId)
+    .filter((x) => !!x);
+  markerIds = Array.from(new Set(markerIds));
+  markerIds.sort();
+  let colorMap: ColorMap = {};
+  for (let i = 0; i < markerIds.length; i++) {
+    const mId: any = markerIds[i];
+    if (i == 0) {
+      colorMap[mId + ""] = "red";
+    } else if (i == 1) {
+      colorMap[mId + ""] = "blue";
+    } else if (i > 1) {
+      colorMap[mId + ""] = "green";
+    }
+  }
+  tList = tList.map((item: SpecialTag) => {
+    item.color = colorMap[item.markerId + ""] || "red";
+    item.isByMultMark = markerIds.length > 1;
+    return item;
+  });
+  return tList;
+}
+
+async function processImage() {
+  if (!store.currentTask) return;
+
+  const images = [];
+  const urls = store.currentTask[origImageUrls] || [];
+  for (const url of urls) {
+    const image = await loadImage(url);
+    images.push(image);
+  }
+
+  maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
+
+  for (const url of urls) {
+    const indexInSliceUrls = urls.indexOf(url) + 1;
+    const image = images[indexInSliceUrls - 1];
+
+    const trackLists = (store.currentTask.questionList || [])
+      // .map((q) => q.trackList)
+      .map((q) => {
+        let tList = q.trackList;
+
+        return 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");
+  if (container) {
+    const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
+    percentWidth = scrollLeft / scrollWidth;
+    percentTop = scrollTop / scrollHeight;
+  }
+
+  addTimeout(() => {
+    if (container) {
+      const { scrollWidth, scrollHeight } = container;
+      container.scrollTo({
+        left: scrollWidth * percentWidth,
+        top: scrollHeight * percentTop,
+      });
+    }
+  }, 10);
+  const scale = store.setting.uiSetting["answer.paper.scale"];
+  return scale * 100 + "%";
+});
+</script>
+
+<style scoped>
+.mark-body-container .none-tip {
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 28px;
+}
+.mark-body-container {
+  height: calc(100vh - 56px);
+  overflow: auto;
+  background-color: var(--app-container-bg-color);
+  background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
+    linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
+  background-size: 20px 20px;
+  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
+  transform: inherit;
+
+  cursor: grab;
+  user-select: none;
+}
+.mark-body-container img {
+  width: 100%;
+}
+.single-image-container {
+  position: relative;
+}
+.image-seperator {
+  border: 2px solid rgba(120, 120, 120, 0.1);
+}
+</style>