|
@@ -1,821 +0,0 @@
|
|
-<template>
|
|
|
|
- <div class="mark-body">
|
|
|
|
- <div v-if="markStatus" class="mark-body-status">
|
|
|
|
- {{ markStatus }}
|
|
|
|
- </div>
|
|
|
|
- <div ref="dragContainer" class="mark-body-container">
|
|
|
|
- <div v-if="!store.currentTask" class="mark-body-none">
|
|
|
|
- <div>
|
|
|
|
- <img src="@/assets/image-none-task.png" />
|
|
|
|
- <p>
|
|
|
|
- {{ store.message }}
|
|
|
|
- </p>
|
|
|
|
- </div>
|
|
|
|
- </div>
|
|
|
|
- <div
|
|
|
|
- v-else-if="store.isScanImage"
|
|
|
|
- :style="{ width: answerPaperScale }"
|
|
|
|
- :class="[`rotate-board-${rotateBoard}`]"
|
|
|
|
- >
|
|
|
|
- <template
|
|
|
|
- v-for="(item, index) in sliceImagesWithTrackList"
|
|
|
|
- :key="index"
|
|
|
|
- >
|
|
|
|
- <div class="single-image-container">
|
|
|
|
- <img
|
|
|
|
- :src="item.url"
|
|
|
|
- draggable="false"
|
|
|
|
- @click="(event) => innerMakeTrack(event, item)"
|
|
|
|
- @contextmenu="showBigImage"
|
|
|
|
- />
|
|
|
|
- <MarkDrawTrack
|
|
|
|
- :trackList="item.trackList"
|
|
|
|
- :specialTagList="item.tagList"
|
|
|
|
- :sliceImageHeight="item.originalImageHeight"
|
|
|
|
- :sliceImageWidth="item.originalImageWidth"
|
|
|
|
- :dx="item.dx"
|
|
|
|
- :dy="item.dy"
|
|
|
|
- @clickSpecialtag="(event) => clickSpecialtag(event, item)"
|
|
|
|
- />
|
|
|
|
- <div
|
|
|
|
- v-if="isCustomSpecialTag"
|
|
|
|
- v-ele-move-directive.stop.prevent="{
|
|
|
|
- moveStart: (event) => specialMouseStart(event, item),
|
|
|
|
- moveElement: specialMouseMove,
|
|
|
|
- moveStop: specialMouseStop,
|
|
|
|
- }"
|
|
|
|
- class="image-canvas"
|
|
|
|
- @click="(event) => canvasClick(event, item)"
|
|
|
|
- >
|
|
|
|
- <template v-if="curSliceImagesWithTrackItem?.url === item.url">
|
|
|
|
- <div
|
|
|
|
- v-if="store.currentSpecialTagType === 'LINE'"
|
|
|
|
- :style="specialLenStyle"
|
|
|
|
- ></div>
|
|
|
|
- <div
|
|
|
|
- v-else-if="store.currentSpecialTagType === 'CIRCLE'"
|
|
|
|
- :style="specialCircleStyle"
|
|
|
|
- ></div>
|
|
|
|
- <div
|
|
|
|
- v-else-if="store.currentSpecialTagType === 'TEXT'"
|
|
|
|
- v-show="cacheTextTrack.id"
|
|
|
|
- :id="`text-edit-box-${cacheTextTrack.id}`"
|
|
|
|
- :key="cacheTextTrack.id"
|
|
|
|
- class="text-edit-box"
|
|
|
|
- contenteditable
|
|
|
|
- :style="specialTextStyle"
|
|
|
|
- @input="textTrackInput"
|
|
|
|
- @blur="textTrackBlur"
|
|
|
|
- @keypress.stop
|
|
|
|
- @keydown.stop
|
|
|
|
- @mousedown.stop
|
|
|
|
- @mousemove.stop
|
|
|
|
- @mouseup.stop
|
|
|
|
- ></div>
|
|
|
|
- </template>
|
|
|
|
- </div>
|
|
|
|
- </div>
|
|
|
|
- <hr class="image-seperator" />
|
|
|
|
- </template>
|
|
|
|
- </div>
|
|
|
|
- <div v-else-if="store.isMultiMedia">
|
|
|
|
- <MultiMediaMarkBody />
|
|
|
|
- </div>
|
|
|
|
- <div v-else>未知数据</div>
|
|
|
|
- </div>
|
|
|
|
- </div>
|
|
|
|
-</template>
|
|
|
|
-
|
|
|
|
-<script setup lang="ts">
|
|
|
|
-import {
|
|
|
|
- onMounted,
|
|
|
|
- onUnmounted,
|
|
|
|
- reactive,
|
|
|
|
- watch,
|
|
|
|
- watchEffect,
|
|
|
|
- nextTick,
|
|
|
|
-} from "vue";
|
|
|
|
-import { store } from "@/store/app";
|
|
|
|
-import MarkDrawTrack from "../mark/MarkDrawTrack.vue";
|
|
|
|
-import type { SliceImage, SpecialTag, Track } from "@/types";
|
|
|
|
-import { useTimers } from "@/setups/useTimers";
|
|
|
|
-import { loadImage, randomCode, addHeaderTrackColorAttr } from "@/utils/utils";
|
|
|
|
-import useDraggable from "../mark/composables/useDraggable";
|
|
|
|
-import MultiMediaMarkBody from "../mark/MultiMediaMarkBody.vue";
|
|
|
|
-import "viewerjs/dist/viewer.css";
|
|
|
|
-import Viewer from "viewerjs";
|
|
|
|
-import { message } from "ant-design-vue";
|
|
|
|
-import EventBus from "@/plugins/eventBus";
|
|
|
|
-import { vEleMoveDirective } from "../../directives/eleMove";
|
|
|
|
-
|
|
|
|
-type MakeTrack = (
|
|
|
|
- event: MouseEvent,
|
|
|
|
- item: SliceImage,
|
|
|
|
- maxSliceWidth: number,
|
|
|
|
- theFinalHeight: number
|
|
|
|
-) => void | (() => void);
|
|
|
|
-
|
|
|
|
-const {
|
|
|
|
- hasMarkResultToRender = false,
|
|
|
|
- makeTrack = () => console.debug("非评卷界面makeTrack没有意义"),
|
|
|
|
-} = defineProps<{
|
|
|
|
- hasMarkResultToRender?: boolean;
|
|
|
|
- makeTrack?: MakeTrack;
|
|
|
|
-}>();
|
|
|
|
-
|
|
|
|
-const emit = defineEmits(["error"]);
|
|
|
|
-
|
|
|
|
-const clickSpecialtag = (event: MouseEvent, item: SliceImage) => {
|
|
|
|
- // console.log(event);
|
|
|
|
- const e = {
|
|
|
|
- target: event.target.offsetParent.childNodes[0],
|
|
|
|
- offsetX: event.offsetX + event.target.offsetLeft,
|
|
|
|
- offsetY: event.offsetY + event.target.offsetTop,
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- makeTrack(e as MouseEvent, item, maxImageWidth, theFinalHeight);
|
|
|
|
-};
|
|
|
|
-
|
|
|
|
-//#region : 图片拖动。在轨迹模式下,仅当没有选择分数时可用。
|
|
|
|
-const { dragContainer } = useDraggable();
|
|
|
|
-//#endregion : 图片拖动
|
|
|
|
-
|
|
|
|
-const { addTimeout } = useTimers();
|
|
|
|
-
|
|
|
|
-//#region : 缩略图定位
|
|
|
|
-watch(
|
|
|
|
- () => [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);
|
|
|
|
- }
|
|
|
|
-);
|
|
|
|
-//#endregion : 缩略图定位
|
|
|
|
-
|
|
|
|
-//#region : 快捷键定位
|
|
|
|
-const scrollContainerByKey = (e: KeyboardEvent) => {
|
|
|
|
- const container = document.querySelector<HTMLDivElement>(
|
|
|
|
- ".mark-body-container"
|
|
|
|
- );
|
|
|
|
- if (!container) {
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- if (e.key === "w") {
|
|
|
|
- container.scrollBy({ top: -100, behavior: "smooth" });
|
|
|
|
- } else if (e.key === "s") {
|
|
|
|
- container.scrollBy({ top: 100, behavior: "smooth" });
|
|
|
|
- } else if (e.key === "a") {
|
|
|
|
- container.scrollBy({ left: -100, behavior: "smooth" });
|
|
|
|
- } else if (e.key === "d") {
|
|
|
|
- container.scrollBy({ left: 100, behavior: "smooth" });
|
|
|
|
- }
|
|
|
|
-};
|
|
|
|
-onMounted(() => {
|
|
|
|
- document.addEventListener("keypress", scrollContainerByKey);
|
|
|
|
-});
|
|
|
|
-onUnmounted(() => {
|
|
|
|
- document.removeEventListener("keypress", scrollContainerByKey);
|
|
|
|
-});
|
|
|
|
-//#endregion : 快捷键定位
|
|
|
|
-
|
|
|
|
-//#region : 计算裁切图和裁切图上的分数轨迹和特殊标记轨迹
|
|
|
|
-let rotateBoard = $ref(0);
|
|
|
|
-let sliceImagesWithTrackList: SliceImage[] = reactive([]);
|
|
|
|
-let maxImageWidth = 0; // 最大的裁切块宽度,图片容器以此为准
|
|
|
|
-let theFinalHeight = 0; // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
|
|
|
|
-
|
|
|
|
-watch(
|
|
|
|
- () => sliceImagesWithTrackList,
|
|
|
|
- () => {
|
|
|
|
- EventBus.emit("draw-change", sliceImagesWithTrackList);
|
|
|
|
- },
|
|
|
|
- { deep: true }
|
|
|
|
-);
|
|
|
|
-
|
|
|
|
-const colors = ["red", "blue", "green"];
|
|
|
|
-let colorMap = {};
|
|
|
|
-function addTrackColorAttr(tList: Track[], groupNumber: number): Track[] {
|
|
|
|
- let userIds: (number | undefined)[] = tList
|
|
|
|
- .map((v) => v.userId)
|
|
|
|
- .filter((x) => !!x);
|
|
|
|
- userIds = Array.from(new Set(userIds));
|
|
|
|
- const isByMultMark = userIds.length > 1;
|
|
|
|
-
|
|
|
|
- tList = tList.map((item) => {
|
|
|
|
- const uid = item.userId;
|
|
|
|
- if (item.headerMarkScore) {
|
|
|
|
- item.color = "green";
|
|
|
|
- } else {
|
|
|
|
- if (!colorMap[groupNumber]) colorMap[groupNumber] = {};
|
|
|
|
- if (!colorMap[groupNumber][uid]) {
|
|
|
|
- colorMap[groupNumber][uid] =
|
|
|
|
- colors[Object.keys(colorMap[groupNumber]).length] || "green";
|
|
|
|
- }
|
|
|
|
- item.color = colorMap[groupNumber][uid];
|
|
|
|
- }
|
|
|
|
- item.isByMultMark = isByMultMark;
|
|
|
|
- return item;
|
|
|
|
- });
|
|
|
|
- return tList;
|
|
|
|
-}
|
|
|
|
-function addSpecialTrackColorAttr(tList: SpecialTag[]): SpecialTag[] {
|
|
|
|
- return tList.map((item) => {
|
|
|
|
- item.color =
|
|
|
|
- colorMap[item.groupNumber] && colorMap[item.groupNumber][item.markerId]
|
|
|
|
- ? colorMap[item.groupNumber][item.markerId]
|
|
|
|
- : "green";
|
|
|
|
- return item;
|
|
|
|
- });
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-async function processImage() {
|
|
|
|
- if (!store.currentTask) return;
|
|
|
|
- colorMap = {};
|
|
|
|
- const images = [];
|
|
|
|
- const urls = store.currentTask.sheetUrls || [];
|
|
|
|
- for (const url of urls) {
|
|
|
|
- const image = await loadImage(url);
|
|
|
|
- images.push(image);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
|
|
|
|
- theFinalHeight = Math.max(...images.map((i) => i.naturalHeight));
|
|
|
|
- sliceImagesWithTrackList.splice(0);
|
|
|
|
-
|
|
|
|
- // let trackLists = store.currentTask.questionList
|
|
|
|
- // .map((q) => addTrackColorAttr(q.trackList, q.groupNumber))
|
|
|
|
- // .flat();
|
|
|
|
- const trackLists = (store.currentTask.questionList || [])
|
|
|
|
- // .map((q) => q.trackList)
|
|
|
|
- .map((q) => {
|
|
|
|
- let tList = q.trackList;
|
|
|
|
- return q.headerTrack?.length
|
|
|
|
- ? addHeaderTrackColorAttr(q.headerTrack)
|
|
|
|
- : addTrackColorAttr(tList, q.groupNumber);
|
|
|
|
- })
|
|
|
|
- .flat();
|
|
|
|
- let tagLists = store.isTrackMode
|
|
|
|
- ? store.currentTask.specialTagList ?? []
|
|
|
|
- : [];
|
|
|
|
- tagLists = addSpecialTrackColorAttr(tagLists);
|
|
|
|
-
|
|
|
|
- let accumTopHeight = 0;
|
|
|
|
- let accumBottomHeight = 0;
|
|
|
|
- for (const url of urls) {
|
|
|
|
- const indexInSliceUrls = urls.indexOf(url) + 1;
|
|
|
|
- const image = images[indexInSliceUrls - 1];
|
|
|
|
- accumBottomHeight += image.naturalHeight;
|
|
|
|
-
|
|
|
|
- const thisImageTrackList = trackLists.filter(
|
|
|
|
- (t) => t.offsetIndex === indexInSliceUrls
|
|
|
|
- );
|
|
|
|
- const thisImageTagList = tagLists.filter(
|
|
|
|
- (t) => t.offsetIndex === indexInSliceUrls
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- sliceImagesWithTrackList.push({
|
|
|
|
- url,
|
|
|
|
- indexInSliceUrls,
|
|
|
|
- trackList: thisImageTrackList,
|
|
|
|
- tagList: thisImageTagList,
|
|
|
|
- originalImageWidth: image.naturalWidth,
|
|
|
|
- originalImageHeight: image.naturalHeight,
|
|
|
|
- width: (image.naturalWidth / maxImageWidth) * 100 + "%",
|
|
|
|
- dx: 0,
|
|
|
|
- dy: 0,
|
|
|
|
- accumTopHeight,
|
|
|
|
- });
|
|
|
|
- accumTopHeight = accumBottomHeight;
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-// should not render twice at the same time
|
|
|
|
-let renderLock = false;
|
|
|
|
-const renderPaperAndMark = async () => {
|
|
|
|
- // console.log("renderPagerAndMark=>store.curTask:", store.currentTask);
|
|
|
|
-
|
|
|
|
- if (!store.currentTask) return;
|
|
|
|
- if (!store.isScanImage) return;
|
|
|
|
- if (renderLock) {
|
|
|
|
- console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
|
|
|
|
- await new Promise((res) => setTimeout(res, 1000));
|
|
|
|
- await renderPaperAndMark();
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- renderLock = true;
|
|
|
|
- try {
|
|
|
|
- store.globalMask = true;
|
|
|
|
- await processImage();
|
|
|
|
- } catch (error) {
|
|
|
|
- sliceImagesWithTrackList.splice(0);
|
|
|
|
- console.trace("render error ", error);
|
|
|
|
- // 图片加载出错,自动加载下一个任务
|
|
|
|
- emit("error");
|
|
|
|
- } finally {
|
|
|
|
- renderLock = false;
|
|
|
|
- store.globalMask = false;
|
|
|
|
- }
|
|
|
|
-};
|
|
|
|
-
|
|
|
|
-watch(
|
|
|
|
- () => store.currentTask,
|
|
|
|
- () => {
|
|
|
|
- setTimeout(renderPaperAndMark, 50);
|
|
|
|
- }
|
|
|
|
-);
|
|
|
|
-//#endregion : 计算裁切图和裁切图上的分数轨迹和特殊标记轨迹
|
|
|
|
-
|
|
|
|
-//#region : 放大缩小和之后的滚动
|
|
|
|
-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 + "%";
|
|
|
|
-});
|
|
|
|
-//#endregion : 放大缩小和之后的滚动
|
|
|
|
-
|
|
|
|
-//#region : 显示评分状态和清除轨迹
|
|
|
|
-let markStatus = $ref("");
|
|
|
|
-if (hasMarkResultToRender) {
|
|
|
|
- watch(
|
|
|
|
- () => store.currentTask,
|
|
|
|
- () => {
|
|
|
|
- markStatus = store.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(() => {
|
|
|
|
- if (!store.currentTask) return;
|
|
|
|
- for (const sliceImage of sliceImagesWithTrackList) {
|
|
|
|
- sliceImage.tagList = sliceImage.tagList.filter((t) =>
|
|
|
|
- store.currentTaskEnsured.markResult?.specialTagList.find(
|
|
|
|
- (st) =>
|
|
|
|
- st.offsetIndex === t.offsetIndex &&
|
|
|
|
- st.offsetX === t.offsetX &&
|
|
|
|
- st.offsetY === t.offsetY
|
|
|
|
- )
|
|
|
|
- );
|
|
|
|
- }
|
|
|
|
- if (store.currentTaskEnsured.markResult?.specialTagList.length === 0) {
|
|
|
|
- for (const sliceImage of sliceImagesWithTrackList) {
|
|
|
|
- sliceImage.tagList = [];
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-}
|
|
|
|
-//#endregion : 显示评分状态和清除轨迹
|
|
|
|
-
|
|
|
|
-//#region : 评分
|
|
|
|
-const checkTrackValid = (event: MouseEvent) => {
|
|
|
|
- const { clientWidth, clientHeight, naturalWidth, naturalHeight } =
|
|
|
|
- event.target;
|
|
|
|
- const { offsetX, offsetY } = event;
|
|
|
|
- const xLimitRate = 10 / naturalWidth;
|
|
|
|
- const yLimitRate = 10 / naturalHeight;
|
|
|
|
- const xRange = [xLimitRate * clientWidth, (1 - xLimitRate) * clientWidth];
|
|
|
|
- const yRange = [yLimitRate * clientHeight, (1 - yLimitRate) * clientHeight];
|
|
|
|
-
|
|
|
|
- return (
|
|
|
|
- offsetX >= xRange[0] &&
|
|
|
|
- offsetX <= xRange[1] &&
|
|
|
|
- offsetY >= yRange[0] &&
|
|
|
|
- offsetY <= yRange[1]
|
|
|
|
- );
|
|
|
|
-};
|
|
|
|
-const innerMakeTrack = (event: MouseEvent, item: SliceImage) => {
|
|
|
|
- if (!checkTrackValid(event)) {
|
|
|
|
- void message.warn("轨迹位置距离边界太近");
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- makeTrack(event, item, maxImageWidth, theFinalHeight);
|
|
|
|
-};
|
|
|
|
-//#endregion : 评分
|
|
|
|
-
|
|
|
|
-//#region : 特殊标记:画线、框、文字
|
|
|
|
-const isCustomSpecialTag = $computed(() => {
|
|
|
|
- return ["CIRCLE", "LINE", "TEXT"].includes(store.currentSpecialTagType);
|
|
|
|
-});
|
|
|
|
-
|
|
|
|
-let specialPoint = $ref({ x: 0, y: 0, ex: 0, ey: 0 });
|
|
|
|
-let curImageTarget: HTMLElement = null;
|
|
|
|
-let curSliceImagesWithTrackItem: SliceImage = $ref(null);
|
|
|
|
-let cacheTextTrack = $ref({
|
|
|
|
- id: "",
|
|
|
|
- x: 0,
|
|
|
|
- y: 0,
|
|
|
|
- maxW: 0,
|
|
|
|
- maxY: 0,
|
|
|
|
- content: "",
|
|
|
|
-});
|
|
|
|
-
|
|
|
|
-const specialLenStyle = $computed(() => {
|
|
|
|
- if (specialPoint.ex <= specialPoint.x) return { display: "none" };
|
|
|
|
-
|
|
|
|
- const width =
|
|
|
|
- specialPoint.ex > specialPoint.x ? specialPoint.ex - specialPoint.x : 0;
|
|
|
|
- return {
|
|
|
|
- top: specialPoint.y + "px",
|
|
|
|
- left: specialPoint.x + "px",
|
|
|
|
- width: width + "px",
|
|
|
|
- position: "absolute",
|
|
|
|
- borderTop: "1px solid red",
|
|
|
|
- zIndex: 9,
|
|
|
|
- };
|
|
|
|
-});
|
|
|
|
-const specialCircleStyle = $computed(() => {
|
|
|
|
- if (specialPoint.ex <= specialPoint.x || specialPoint.ey <= specialPoint.y)
|
|
|
|
- return { display: "none" };
|
|
|
|
-
|
|
|
|
- const width =
|
|
|
|
- specialPoint.ex > specialPoint.x ? specialPoint.ex - specialPoint.x : 0;
|
|
|
|
- const height =
|
|
|
|
- specialPoint.ey > specialPoint.y ? specialPoint.ey - specialPoint.y : 0;
|
|
|
|
- return {
|
|
|
|
- top: specialPoint.y + "px",
|
|
|
|
- left: specialPoint.x + "px",
|
|
|
|
- width: width + "px",
|
|
|
|
- height: height + "px",
|
|
|
|
- position: "absolute",
|
|
|
|
- border: "1px solid red",
|
|
|
|
- borderRadius: "50%",
|
|
|
|
- zIndex: 9,
|
|
|
|
- };
|
|
|
|
-});
|
|
|
|
-const specialTextStyle = $computed(() => {
|
|
|
|
- return {
|
|
|
|
- top: cacheTextTrack.y + "px",
|
|
|
|
- left: cacheTextTrack.x + "px",
|
|
|
|
- minWidth: "30px",
|
|
|
|
- minHeight: "30px",
|
|
|
|
- maxWidth: curImageTarget.width - cacheTextTrack.x + "px",
|
|
|
|
- maxHeight: curImageTarget.height - cacheTextTrack.y + "px",
|
|
|
|
- };
|
|
|
|
-});
|
|
|
|
-
|
|
|
|
-function specialMouseStart(e: MouseEvent, item: SliceImage) {
|
|
|
|
- if (store.currentSpecialTagType === "TEXT") return;
|
|
|
|
-
|
|
|
|
- curImageTarget = e.target.parentElement.childNodes[0];
|
|
|
|
- curSliceImagesWithTrackItem = item;
|
|
|
|
- specialPoint.x = e.offsetX;
|
|
|
|
- specialPoint.y = e.offsetY;
|
|
|
|
-}
|
|
|
|
-function specialMouseMove({ left, top }) {
|
|
|
|
- if (store.currentSpecialTagType === "TEXT") return;
|
|
|
|
-
|
|
|
|
- specialPoint.ex = left + specialPoint.x;
|
|
|
|
- specialPoint.ey = top + specialPoint.y;
|
|
|
|
-}
|
|
|
|
-function specialMouseStop() {
|
|
|
|
- if (store.currentSpecialTagType === "TEXT") return;
|
|
|
|
-
|
|
|
|
- if (
|
|
|
|
- store.currentSpecialTagType === "LINE" &&
|
|
|
|
- specialPoint.ex <= specialPoint.x
|
|
|
|
- ) {
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- if (
|
|
|
|
- store.currentSpecialTagType === "CIRCLE" &&
|
|
|
|
- (specialPoint.ex <= specialPoint.x || specialPoint.ey <= specialPoint.y)
|
|
|
|
- ) {
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- const track: SpecialTag = {
|
|
|
|
- tagName: "",
|
|
|
|
- tagType: store.currentSpecialTagType,
|
|
|
|
- offsetIndex: curSliceImagesWithTrackItem.indexInSliceUrls,
|
|
|
|
- offsetX:
|
|
|
|
- specialPoint.x * (curImageTarget.naturalWidth / curImageTarget.width) +
|
|
|
|
- curSliceImagesWithTrackItem.dx,
|
|
|
|
- offsetY:
|
|
|
|
- specialPoint.y * (curImageTarget.naturalHeight / curImageTarget.height) +
|
|
|
|
- curSliceImagesWithTrackItem.dy,
|
|
|
|
- positionX: -1,
|
|
|
|
- positionY: -1,
|
|
|
|
- groupNumber: store.currentQuestion.groupNumber,
|
|
|
|
- color: "green",
|
|
|
|
- };
|
|
|
|
- track.positionX =
|
|
|
|
- (track.offsetX - curSliceImagesWithTrackItem.dx) / maxImageWidth;
|
|
|
|
- track.positionY =
|
|
|
|
- (track.offsetY -
|
|
|
|
- curSliceImagesWithTrackItem.dy +
|
|
|
|
- curSliceImagesWithTrackItem.accumTopHeight) /
|
|
|
|
- theFinalHeight;
|
|
|
|
-
|
|
|
|
- if (store.currentSpecialTagType === "LINE") {
|
|
|
|
- track.tagName = JSON.stringify({
|
|
|
|
- len:
|
|
|
|
- (specialPoint.ex - specialPoint.x) *
|
|
|
|
- (curImageTarget.naturalWidth / curImageTarget.width),
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
- if (store.currentSpecialTagType === "CIRCLE") {
|
|
|
|
- track.tagName = JSON.stringify({
|
|
|
|
- width:
|
|
|
|
- (specialPoint.ex - specialPoint.x) *
|
|
|
|
- (curImageTarget.naturalWidth / curImageTarget.width),
|
|
|
|
- height:
|
|
|
|
- (specialPoint.ey - specialPoint.y) *
|
|
|
|
- (curImageTarget.naturalHeight / curImageTarget.height),
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- store.currentTaskEnsured.markResult.specialTagList.push(track);
|
|
|
|
- curSliceImagesWithTrackItem.tagList.push(track);
|
|
|
|
- specialPoint = { x: 0, y: 0, ex: 0, ey: 0 };
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-async function canvasClick(e: Event, item: SliceImage) {
|
|
|
|
- if (cacheTextTrack.id) {
|
|
|
|
- textTrackBlur();
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- curImageTarget = e.target.parentElement.childNodes[0];
|
|
|
|
- curSliceImagesWithTrackItem = item;
|
|
|
|
-
|
|
|
|
- cacheTextTrack.x = e.offsetX;
|
|
|
|
- cacheTextTrack.y = e.offsetY;
|
|
|
|
- cacheTextTrack.id = randomCode();
|
|
|
|
- cacheTextTrack.content = "";
|
|
|
|
-
|
|
|
|
- await nextTick(() => {
|
|
|
|
- document.getElementById(`text-edit-box-${cacheTextTrack.id}`).focus();
|
|
|
|
- });
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-function textTrackInput(e: Event) {
|
|
|
|
- cacheTextTrack.content = e.target.outerText;
|
|
|
|
-}
|
|
|
|
-function initCacheTextTrack() {
|
|
|
|
- cacheTextTrack = {
|
|
|
|
- x: 0,
|
|
|
|
- y: 0,
|
|
|
|
- maxW: 0,
|
|
|
|
- maxY: 0,
|
|
|
|
- content: "",
|
|
|
|
- id: "",
|
|
|
|
- };
|
|
|
|
-}
|
|
|
|
-function textTrackBlur() {
|
|
|
|
- if (!cacheTextTrack.content) {
|
|
|
|
- initCacheTextTrack();
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- const textBoxDom = document.getElementById(
|
|
|
|
- `text-edit-box-${cacheTextTrack.id}`
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- // 减去内边距所占宽高
|
|
|
|
- const tagName = JSON.stringify({
|
|
|
|
- width: textBoxDom.offsetWidth - 10,
|
|
|
|
- height: textBoxDom.offsetHeight - 10,
|
|
|
|
- content: cacheTextTrack.content,
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- const track: SpecialTag = {
|
|
|
|
- tagName,
|
|
|
|
- tagType: store.currentSpecialTagType,
|
|
|
|
- offsetIndex: curSliceImagesWithTrackItem.indexInSliceUrls,
|
|
|
|
- offsetX:
|
|
|
|
- cacheTextTrack.x * (curImageTarget.naturalWidth / curImageTarget.width) +
|
|
|
|
- curSliceImagesWithTrackItem.dx,
|
|
|
|
- offsetY:
|
|
|
|
- cacheTextTrack.y *
|
|
|
|
- (curImageTarget.naturalHeight / curImageTarget.height) +
|
|
|
|
- curSliceImagesWithTrackItem.dy,
|
|
|
|
- positionX: -1,
|
|
|
|
- positionY: -1,
|
|
|
|
- groupNumber: store.currentQuestion.groupNumber,
|
|
|
|
- color: "green",
|
|
|
|
- };
|
|
|
|
- track.positionX =
|
|
|
|
- (track.offsetX - curSliceImagesWithTrackItem.dx) / maxImageWidth;
|
|
|
|
- track.positionY =
|
|
|
|
- (track.offsetY -
|
|
|
|
- curSliceImagesWithTrackItem.dy +
|
|
|
|
- curSliceImagesWithTrackItem.accumTopHeight) /
|
|
|
|
- theFinalHeight;
|
|
|
|
-
|
|
|
|
- store.currentTaskEnsured.markResult.specialTagList.push(track);
|
|
|
|
- curSliceImagesWithTrackItem.tagList.push(track);
|
|
|
|
- initCacheTextTrack();
|
|
|
|
-}
|
|
|
|
-watch(
|
|
|
|
- () => store.currentSpecialTagType,
|
|
|
|
- () => {
|
|
|
|
- if (cacheTextTrack.id) {
|
|
|
|
- initCacheTextTrack();
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-);
|
|
|
|
-//#endregion
|
|
|
|
-
|
|
|
|
-//#region : 显示大图,供查看和翻转
|
|
|
|
-const showBigImage = (event: MouseEvent) => {
|
|
|
|
- event.preventDefault();
|
|
|
|
- // console.log(event);
|
|
|
|
- let viewer: Viewer = null as unknown as Viewer;
|
|
|
|
- viewer && viewer.destroy();
|
|
|
|
- viewer = new Viewer(event.target as HTMLElement, {
|
|
|
|
- // inline: true,
|
|
|
|
- viewed() {
|
|
|
|
- viewer.zoomTo(1);
|
|
|
|
- },
|
|
|
|
- hidden() {
|
|
|
|
- viewer.destroy();
|
|
|
|
- },
|
|
|
|
- zIndex: 1000000,
|
|
|
|
- });
|
|
|
|
- viewer.show();
|
|
|
|
-};
|
|
|
|
-//#endregion : 显示大图,供查看和翻转
|
|
|
|
-
|
|
|
|
-// onRenderTriggered(({ key, target, type }) => {
|
|
|
|
-// console.log({ key, target, type });
|
|
|
|
-// });
|
|
|
|
-let topKB = $ref(10);
|
|
|
|
-// const topKBStyle = $computed(() => topKB + "%");
|
|
|
|
-let leftKB = $ref(10);
|
|
|
|
-// const leftKBStyle = $computed(() => leftKB + "%");
|
|
|
|
-function moveCicle(event: KeyboardEvent) {
|
|
|
|
- // query mark-body-container and body to calc max/min topKB and max leftKB
|
|
|
|
- if (event.key === "k") {
|
|
|
|
- if (topKB > 1) topKB--;
|
|
|
|
- }
|
|
|
|
- if (event.key === "j") {
|
|
|
|
- if (topKB < 99) topKB++;
|
|
|
|
- }
|
|
|
|
- if (event.key === "h") {
|
|
|
|
- if (leftKB > 1) leftKB--;
|
|
|
|
- }
|
|
|
|
- if (event.key === "l") {
|
|
|
|
- if (leftKB < 99) leftKB++;
|
|
|
|
- }
|
|
|
|
- if (event.key === "c") {
|
|
|
|
- topKB = 50;
|
|
|
|
- leftKB = 50;
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-function giveScoreCicle(event: KeyboardEvent) {
|
|
|
|
- // console.log(event.key);
|
|
|
|
- event.preventDefault();
|
|
|
|
- // console.log(store.currentScore);
|
|
|
|
- // 接收currentScore间隔时间外才会进入此事件
|
|
|
|
- if (event.key === " " && typeof store.currentScore === "number") {
|
|
|
|
- // topKB--;
|
|
|
|
- const circleElement = document.querySelector(".kb-circle");
|
|
|
|
- let { top, left } = circleElement.getBoundingClientRect();
|
|
|
|
- top = top + 45;
|
|
|
|
- left = left + 45;
|
|
|
|
- // console.log(top, left);
|
|
|
|
- // getBoundingClientRect().top left
|
|
|
|
- // capture => to the specific image
|
|
|
|
- const me = new MouseEvent("click", {
|
|
|
|
- bubbles: true,
|
|
|
|
- cancelable: true,
|
|
|
|
- view: window,
|
|
|
|
- clientY: top,
|
|
|
|
- clientX: left,
|
|
|
|
- });
|
|
|
|
- const eles = document.elementsFromPoint(left, top);
|
|
|
|
- // console.log(eles);
|
|
|
|
- let ele: Element;
|
|
|
|
- // if (eles[0].className === "kb-circle") {
|
|
|
|
- // if (eles[1].tagName == "IMG") {
|
|
|
|
- // ele = eles[1];
|
|
|
|
- // }
|
|
|
|
- // } else
|
|
|
|
- if (eles[0].tagName == "IMG") {
|
|
|
|
- ele = eles[0];
|
|
|
|
- }
|
|
|
|
- if (ele) {
|
|
|
|
- ele.dispatchEvent(me);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-onMounted(() => {
|
|
|
|
- document.addEventListener("keypress", moveCicle);
|
|
|
|
- document.addEventListener("keypress", giveScoreCicle);
|
|
|
|
-});
|
|
|
|
-onUnmounted(() => {
|
|
|
|
- document.removeEventListener("keypress", moveCicle);
|
|
|
|
- document.removeEventListener("keypress", giveScoreCicle);
|
|
|
|
-});
|
|
|
|
-// setInterval(() => {
|
|
|
|
-// _topKB++;
|
|
|
|
-// console.log(topKB);
|
|
|
|
-// }, 1000);
|
|
|
|
-
|
|
|
|
-//#region autoScroll自动跳转
|
|
|
|
-let oldFirstScoreContainer: HTMLDivElement | null;
|
|
|
|
-watch(
|
|
|
|
- () => store.currentTask,
|
|
|
|
- () => {
|
|
|
|
- if (store.setting.autoScroll) {
|
|
|
|
- // 给任务清理和动画留一点时间
|
|
|
|
- oldFirstScoreContainer =
|
|
|
|
- document.querySelector<HTMLDivElement>(".score-container");
|
|
|
|
- oldFirstScoreContainer?.scrollIntoView({ behavior: "smooth" });
|
|
|
|
- addTimeout(scrollToFirstScore, 1000);
|
|
|
|
- } else {
|
|
|
|
- const container = document.querySelector<HTMLDivElement>(
|
|
|
|
- ".mark-body-container"
|
|
|
|
- );
|
|
|
|
- container?.scrollTo({ top: 0, left: 0, behavior: "smooth" });
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-);
|
|
|
|
-function scrollToFirstScore() {
|
|
|
|
- if (renderLock) {
|
|
|
|
- window.requestAnimationFrame(scrollToFirstScore);
|
|
|
|
- }
|
|
|
|
- addTimeout(() => {
|
|
|
|
- const firstScore =
|
|
|
|
- document.querySelector<HTMLDivElement>(".score-container");
|
|
|
|
- firstScore?.scrollIntoView({ behavior: "smooth" });
|
|
|
|
- }, 1000);
|
|
|
|
-}
|
|
|
|
-//#endregion
|
|
|
|
-</script>
|
|
|
|
-
|
|
|
|
-<style scoped>
|
|
|
|
-.image-canvas {
|
|
|
|
- position: absolute;
|
|
|
|
- top: 0;
|
|
|
|
- left: 0;
|
|
|
|
- right: 0;
|
|
|
|
- bottom: 0;
|
|
|
|
- z-index: 9;
|
|
|
|
-}
|
|
|
|
-.text-edit-box {
|
|
|
|
- position: absolute;
|
|
|
|
- border: 1px solid #ff0000;
|
|
|
|
- line-height: 24px;
|
|
|
|
- padding: 5px;
|
|
|
|
- font-size: 20px;
|
|
|
|
- border-radius: 4px;
|
|
|
|
- margin: -15px 0 0 -5px;
|
|
|
|
- outline: none;
|
|
|
|
- z-index: 9;
|
|
|
|
- font-family: 黑体, arial, sans-serif;
|
|
|
|
- color: #ff0000;
|
|
|
|
-}
|
|
|
|
-.text-edit-box:focus {
|
|
|
|
- border-color: #ff5050;
|
|
|
|
-}
|
|
|
|
-</style>
|
|
|