|
@@ -1,5 +1,5 @@
|
|
|
<template>
|
|
|
- <div class="mark-body-container tw-flex-auto" ref="container">
|
|
|
+ <div class="mark-body-container tw-flex-auto">
|
|
|
<div v-if="!store.currentTask" class="tw-text-center">暂无评卷任务</div>
|
|
|
<div v-else :style="{ width: answerPaperScale }">
|
|
|
<div
|
|
@@ -9,7 +9,7 @@
|
|
|
>
|
|
|
<img
|
|
|
:src="item.url"
|
|
|
- @click="(event) => makeMark(event, item)"
|
|
|
+ @click="(event) => makeScoreTrack(event, item)"
|
|
|
draggable="false"
|
|
|
/>
|
|
|
<MarkDrawTrack
|
|
@@ -30,8 +30,9 @@ import { computed, defineComponent, reactive, ref, watchEffect } from "vue";
|
|
|
import { findCurrentTaskMarkResult, store } from "./store";
|
|
|
import filters from "@/filters";
|
|
|
import MarkDrawTrack from "./MarkDrawTrack.vue";
|
|
|
-import { MarkResult, Track } from "@/types";
|
|
|
+import { Track } from "@/types";
|
|
|
import { useTimers } from "@/setups/useTimers";
|
|
|
+import { loadImage } from "@/utils/utils";
|
|
|
|
|
|
interface SliceImage {
|
|
|
url: string;
|
|
@@ -50,114 +51,175 @@ export default defineComponent({
|
|
|
setup() {
|
|
|
const { addTimeout } = useTimers();
|
|
|
|
|
|
- const container = ref(null);
|
|
|
+ function hasSliceConfig() {
|
|
|
+ return store.currentTask?.sliceConfig?.length;
|
|
|
+ }
|
|
|
+
|
|
|
let sliceImagesWithTrackList: Array<SliceImage> = reactive([]);
|
|
|
let _studentId = -1; // 判断是否改变了任务
|
|
|
- let maxSliceWidth = 0;
|
|
|
- let theFinalHeight = 0;
|
|
|
+ let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
|
|
|
+ let theFinalHeight = 0; // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
|
|
|
|
|
|
- const renderPaperAndMark = async () => {
|
|
|
- async function loadImage(url: string): Promise<HTMLImageElement> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const image = new Image();
|
|
|
- image.setAttribute("crossorigin", "anonymous");
|
|
|
- image.src = url;
|
|
|
- image.onload = () => resolve(image);
|
|
|
- image.onerror = reject;
|
|
|
- });
|
|
|
+ async function processSliceConfig() {
|
|
|
+ // check if have MarkResult for currentTask
|
|
|
+ let markResult = findCurrentTaskMarkResult();
|
|
|
+
|
|
|
+ if (!markResult || !store.currentTask) return;
|
|
|
+
|
|
|
+ // TODO: 图片加载出错,自动加载下一个任务
|
|
|
+ for (const url of store.currentTask.sliceUrls) {
|
|
|
+ await loadImage(filters.toCompleteUrl(url));
|
|
|
}
|
|
|
- if (!store.currentTask?.libraryId) {
|
|
|
- return;
|
|
|
+ // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
|
|
|
+ for (const sliceConfig of store.currentTask.sliceConfig) {
|
|
|
+ const url = filters.toCompleteUrl(
|
|
|
+ store.currentTask.sliceUrls[sliceConfig.i - 1]
|
|
|
+ );
|
|
|
+ const image = await loadImage(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;
|
|
|
+ for (const sliceConfig of store.currentTask.sliceConfig) {
|
|
|
+ accumBottomHeight += sliceConfig.h;
|
|
|
+ const url = filters.toCompleteUrl(
|
|
|
+ store.currentTask.sliceUrls[sliceConfig.i - 1]
|
|
|
+ );
|
|
|
+ const image = await loadImage(url);
|
|
|
|
|
|
+ const canvas = document.createElement("canvas");
|
|
|
+ // canvas.width = sliceConfig.w;
|
|
|
+ canvas.width = Math.max(sliceConfig.w, maxSliceWidth);
|
|
|
+ canvas.height = sliceConfig.h;
|
|
|
+ const ctx = canvas.getContext("2d");
|
|
|
+ if (!ctx) {
|
|
|
+ console.log('canvas.getContext("2d") error');
|
|
|
+ }
|
|
|
+ // drawImage 画图软件透明色
|
|
|
+ ctx?.drawImage(
|
|
|
+ image,
|
|
|
+ sliceConfig.x,
|
|
|
+ sliceConfig.y,
|
|
|
+ sliceConfig.w,
|
|
|
+ sliceConfig.h,
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ sliceConfig.w,
|
|
|
+ sliceConfig.h
|
|
|
+ );
|
|
|
+ // console.log(image, canvas.height, sliceConfig, ctx);
|
|
|
+ // console.log(canvas.toDataURL());
|
|
|
+ const thisImageTrackList = markResult.trackList.filter(
|
|
|
+ (v) => v.offsetIndex === sliceConfig.i
|
|
|
+ );
|
|
|
+
|
|
|
+ const dataUrl = canvas.toDataURL();
|
|
|
+ const sliceImage = new Image();
|
|
|
+ sliceImage.src = dataUrl;
|
|
|
+ // sliceConfig.x + sliceConfig.w
|
|
|
+ sliceImagesWithTrackList.push({
|
|
|
+ url: dataUrl,
|
|
|
+ indexInSliceUrls: sliceConfig.i,
|
|
|
+ // 通过positionY来定位是第几张slice的还原,并过滤出相应的track
|
|
|
+ trackList: thisImageTrackList.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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function processSplitConfig() {
|
|
|
// check if have MarkResult for currentTask
|
|
|
let markResult = findCurrentTaskMarkResult();
|
|
|
- // console.log("watcheffect markResult 1", markResult, store.markResults);
|
|
|
|
|
|
if (!markResult || !store.currentTask) return;
|
|
|
- // store.markResults.splice(store.markResults.indexOf(markResult), 1);
|
|
|
- // store.markResults.push(markResult);
|
|
|
- // console.log("watcheffect markResult 3", markResult, store.markResults);
|
|
|
- // const allTrackList = findCurrentTaskMarkResult().trackList;
|
|
|
- // console.log(allTrackList);
|
|
|
- // console.log(store.markResults);
|
|
|
|
|
|
- // reset sliceImagesWithTrackList
|
|
|
- if (_studentId !== store.currentTask.studentId) {
|
|
|
- // 还原轨迹用得上
|
|
|
- sliceImagesWithTrackList.splice(0);
|
|
|
- _studentId = store.currentTask.studentId;
|
|
|
+ const images = [];
|
|
|
+ for (const url of store.currentTask.sliceUrls) {
|
|
|
+ const image = await loadImage(filters.toCompleteUrl(url));
|
|
|
+ images.push(image);
|
|
|
}
|
|
|
|
|
|
- if (store.currentTask.sliceConfig?.length) {
|
|
|
- for (const url of store.currentTask.sliceUrls) {
|
|
|
- await loadImage(filters.toCompleteUrl(url));
|
|
|
- }
|
|
|
- // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
|
|
|
- for (const sliceConfig of store.currentTask.sliceConfig) {
|
|
|
- const url = filters.toCompleteUrl(
|
|
|
- store.currentTask.sliceUrls[sliceConfig.i - 1]
|
|
|
- );
|
|
|
- const image = await loadImage(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));
|
|
|
- let accumTopHeight = 0;
|
|
|
- let accumBottomHeight = 0;
|
|
|
- for (const sliceConfig of store.currentTask.sliceConfig) {
|
|
|
- accumBottomHeight += sliceConfig.h;
|
|
|
- const url = filters.toCompleteUrl(
|
|
|
- store.currentTask.sliceUrls[sliceConfig.i - 1]
|
|
|
- );
|
|
|
- const image = await loadImage(url);
|
|
|
- if (sliceConfig.w === 0 && sliceConfig.h === 0) {
|
|
|
- // 选择整图时,w/h 为0
|
|
|
- sliceConfig.w = image.naturalWidth;
|
|
|
- sliceConfig.h = image.naturalHeight;
|
|
|
- }
|
|
|
+ // TODO: add loading
|
|
|
+ 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 div = (container.value as unknown) as HTMLDivElement;
|
|
|
- maxSliceWidth = Math.max(
|
|
|
- ...store.currentTask.sliceConfig.map((v) => v.w)
|
|
|
- );
|
|
|
+ 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;
|
|
|
+ for (const url of store.currentTask.sliceUrls) {
|
|
|
+ const completeUrl = filters.toCompleteUrl(url);
|
|
|
+
|
|
|
+ for (const config of splitConfigPairs) {
|
|
|
+ const image = await loadImage(completeUrl);
|
|
|
+
|
|
|
+ accumBottomHeight += image.naturalHeight;
|
|
|
+
|
|
|
+ const width = image.naturalWidth * (config[1] - config[0]);
|
|
|
const canvas = document.createElement("canvas");
|
|
|
- // canvas.width = sliceConfig.w;
|
|
|
- canvas.width = Math.max(sliceConfig.w, maxSliceWidth);
|
|
|
- canvas.height = sliceConfig.h;
|
|
|
+ canvas.width = Math.max(width, maxSliceWidth);
|
|
|
+ canvas.height = image.naturalHeight;
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
+ if (!ctx) {
|
|
|
+ console.log('canvas.getContext("2d") error');
|
|
|
+ }
|
|
|
// drawImage 画图软件透明色
|
|
|
ctx?.drawImage(
|
|
|
image,
|
|
|
- sliceConfig.x,
|
|
|
- sliceConfig.y,
|
|
|
- sliceConfig.w,
|
|
|
- sliceConfig.h,
|
|
|
+ image.naturalWidth * config[0],
|
|
|
0,
|
|
|
+ image.naturalWidth * config[1],
|
|
|
+ image.naturalHeight,
|
|
|
0,
|
|
|
- sliceConfig.w,
|
|
|
- sliceConfig.h
|
|
|
+ 0,
|
|
|
+ image.naturalWidth * config[1],
|
|
|
+ image.naturalHeight
|
|
|
);
|
|
|
// console.log(image, canvas.height, sliceConfig, ctx);
|
|
|
// console.log(canvas.toDataURL());
|
|
|
+
|
|
|
const thisImageTrackList = markResult.trackList.filter(
|
|
|
- (v) => v.offsetIndex === sliceConfig.i
|
|
|
+ (t) =>
|
|
|
+ t.offsetIndex ===
|
|
|
+ (store.currentTask &&
|
|
|
+ store.currentTask.sliceUrls.indexOf(url) + 1)
|
|
|
);
|
|
|
|
|
|
const dataUrl = canvas.toDataURL();
|
|
|
const sliceImage = new Image();
|
|
|
sliceImage.src = dataUrl;
|
|
|
- // sliceConfig.x + sliceConfig.w
|
|
|
sliceImagesWithTrackList.push({
|
|
|
- url: dataUrl,
|
|
|
- indexInSliceUrls: sliceConfig.i,
|
|
|
- // 通过positionY来定位是第几张slice的还原,并过滤出相应的track
|
|
|
+ url: canvas.toDataURL(),
|
|
|
+ indexInSliceUrls: store.currentTask.sliceUrls.indexOf(url) + 1,
|
|
|
trackList: thisImageTrackList.filter(
|
|
|
(t) =>
|
|
|
t.positionY >= accumTopHeight / theFinalHeight &&
|
|
@@ -165,94 +227,32 @@ export default defineComponent({
|
|
|
),
|
|
|
originalImage: image,
|
|
|
sliceImage,
|
|
|
- dx: sliceConfig.x,
|
|
|
- dy: sliceConfig.y,
|
|
|
+ dx: image.naturalWidth * config[0],
|
|
|
+ dy: 0,
|
|
|
accumTopHeight,
|
|
|
- effectiveWidth: sliceConfig.w,
|
|
|
+ effectiveWidth: image.naturalWidth * config[1],
|
|
|
});
|
|
|
accumTopHeight = accumBottomHeight;
|
|
|
}
|
|
|
- } else {
|
|
|
- const images = [];
|
|
|
- for (const url of store.currentTask.sliceUrls) {
|
|
|
- const image = await loadImage(filters.toCompleteUrl(url));
|
|
|
- images.push(image);
|
|
|
- }
|
|
|
-
|
|
|
- // TODO: add loading
|
|
|
- const newConfig = (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 =
|
|
|
- newConfig.length *
|
|
|
- images.reduce((acc, v) => (acc += v.naturalHeight), 0);
|
|
|
-
|
|
|
- let accumTopHeight = 0;
|
|
|
- let accumBottomHeight = 0;
|
|
|
- for (const url of store.currentTask.sliceUrls) {
|
|
|
- const completeUrl = filters.toCompleteUrl(url);
|
|
|
-
|
|
|
- for (const config of newConfig) {
|
|
|
- const image = await loadImage(completeUrl);
|
|
|
-
|
|
|
- accumBottomHeight += image.naturalHeight;
|
|
|
- const div = (container.value as unknown) as HTMLDivElement;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const renderPaperAndMark = async () => {
|
|
|
+ // check if have MarkResult for currentTask
|
|
|
+ let markResult = findCurrentTaskMarkResult();
|
|
|
|
|
|
- const width = image.naturalWidth * (config[1] - config[0]);
|
|
|
- const canvas = document.createElement("canvas");
|
|
|
- canvas.width = Math.max(width, maxSliceWidth);
|
|
|
- canvas.height = image.naturalHeight;
|
|
|
- const ctx = canvas.getContext("2d");
|
|
|
- // drawImage 画图软件透明色
|
|
|
- ctx?.drawImage(
|
|
|
- image,
|
|
|
- image.naturalWidth * config[0],
|
|
|
- 0,
|
|
|
- image.naturalWidth * config[1],
|
|
|
- image.naturalHeight,
|
|
|
- 0,
|
|
|
- 0,
|
|
|
- image.naturalWidth * config[1],
|
|
|
- image.naturalHeight
|
|
|
- );
|
|
|
- // console.log(image, canvas.height, sliceConfig, ctx);
|
|
|
- // console.log(canvas.toDataURL());
|
|
|
+ if (!markResult || !store.currentTask) return;
|
|
|
|
|
|
- const thisImageTrackList = markResult.trackList.filter(
|
|
|
- (t) =>
|
|
|
- t.offsetIndex ===
|
|
|
- (store.currentTask &&
|
|
|
- store.currentTask.sliceUrls.indexOf(url) + 1)
|
|
|
- );
|
|
|
+ // reset sliceImagesWithTrackList ,当切换任务时,要重新绘制图片和轨迹
|
|
|
+ if (_studentId !== store.currentTask.studentId) {
|
|
|
+ // 还原轨迹用得上
|
|
|
+ sliceImagesWithTrackList.splice(0);
|
|
|
+ _studentId = store.currentTask.studentId;
|
|
|
+ }
|
|
|
|
|
|
- const dataUrl = canvas.toDataURL();
|
|
|
- const sliceImage = new Image();
|
|
|
- sliceImage.src = dataUrl;
|
|
|
- sliceImagesWithTrackList.push({
|
|
|
- url: canvas.toDataURL(),
|
|
|
- indexInSliceUrls: store.currentTask.sliceUrls.indexOf(url) + 1,
|
|
|
- trackList: thisImageTrackList.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;
|
|
|
- }
|
|
|
- }
|
|
|
+ if (hasSliceConfig()) {
|
|
|
+ await processSliceConfig();
|
|
|
+ } else {
|
|
|
+ await processSplitConfig();
|
|
|
}
|
|
|
};
|
|
|
|
|
@@ -262,25 +262,18 @@ export default defineComponent({
|
|
|
// 放大、缩小不影响页面之前的滚动条定位
|
|
|
let percentWidth = 0;
|
|
|
let percentTop = 0;
|
|
|
- if (document.querySelector(".mark-body-container")) {
|
|
|
- const container = document.querySelector(
|
|
|
- ".mark-body-container"
|
|
|
- ) as HTMLDivElement;
|
|
|
- const scrollLeft = container.scrollLeft;
|
|
|
- const scrollTop = container.scrollTop;
|
|
|
- const scrollWidth = container.scrollWidth;
|
|
|
- const scrollHeight = container.scrollHeight;
|
|
|
+ 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 (document.querySelector(".mark-body-container")) {
|
|
|
- const container = document.querySelector(
|
|
|
- ".mark-body-container"
|
|
|
- ) as HTMLDivElement;
|
|
|
- const scrollWidth = container.scrollWidth;
|
|
|
- const scrollHeight = container.scrollHeight;
|
|
|
+ if (container) {
|
|
|
+ const { scrollWidth, scrollHeight } = container;
|
|
|
container.scrollTo({
|
|
|
left: scrollWidth * percentWidth,
|
|
|
top: scrollHeight * percentTop,
|
|
@@ -291,7 +284,7 @@ export default defineComponent({
|
|
|
return scale * 100 + "%";
|
|
|
});
|
|
|
|
|
|
- const makeMark = (event: MouseEvent, item: SliceImage) => {
|
|
|
+ const makeScoreTrack = (event: MouseEvent, item: SliceImage) => {
|
|
|
// console.log(item);
|
|
|
if (!store.currentQuestion || typeof store.currentScore === "undefined")
|
|
|
return;
|
|
@@ -299,7 +292,7 @@ export default defineComponent({
|
|
|
const track = {} as Track;
|
|
|
track.mainNumber = store.currentQuestion?.mainNumber;
|
|
|
track.subNumber = store.currentQuestion?.subNumber;
|
|
|
- track.number = Math.round(Math.random() * 10000000);
|
|
|
+ track.number = Date.now();
|
|
|
track.score = store.currentScore;
|
|
|
track.offsetIndex = item.indexInSliceUrls;
|
|
|
track.offsetX = Math.round(
|
|
@@ -311,34 +304,23 @@ export default defineComponent({
|
|
|
track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
|
|
|
track.positionY =
|
|
|
(track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
|
|
|
- // console.log(
|
|
|
- // track,
|
|
|
- // item.originalImage.naturalWidth,
|
|
|
- // item.originalImage.naturalHeight,
|
|
|
- // target.naturalWidth,
|
|
|
- // target.width,
|
|
|
- // track.offsetX,
|
|
|
- // item.effectiveWidth + item.dx
|
|
|
- // );
|
|
|
if (track.offsetX > item.effectiveWidth + item.dx) {
|
|
|
console.log("不在有效宽度内,轨迹不生效");
|
|
|
return;
|
|
|
}
|
|
|
store.currentScore = undefined;
|
|
|
const markResult = findCurrentTaskMarkResult();
|
|
|
- // console.log("makemark markresult", markResult);
|
|
|
if (markResult) {
|
|
|
markResult.trackList = [...markResult.trackList, track];
|
|
|
}
|
|
|
- // sliceImagesWithTrackList.find(s => s.indexInSliceUrls === item.indexInSliceUrls)
|
|
|
item.trackList.push(track);
|
|
|
};
|
|
|
+
|
|
|
return {
|
|
|
- container,
|
|
|
store,
|
|
|
sliceImagesWithTrackList,
|
|
|
answerPaperScale,
|
|
|
- makeMark,
|
|
|
+ makeScoreTrack,
|
|
|
};
|
|
|
},
|
|
|
// renderTriggered({ key, target, type }) {
|