|
@@ -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="libraryId"
|
|
|
+ @error="$emit('error')"
|
|
|
+ />
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts">
|
|
|
-import { computed, defineComponent, reactive, ref, watchEffect } 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";
|
|
|
+import CommonMarkBody from "@/features/mark/CommonMarkBody.vue";
|
|
|
+import { defineComponent, watch } from "vue";
|
|
|
+import { store } from "@/features/mark/store";
|
|
|
|
|
|
-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>
|