|
@@ -1,1051 +0,0 @@
|
|
|
-<template>
|
|
|
- <div class="mark-body" @scroll="viewScroll">
|
|
|
- <div ref="dragContainer" class="mark-body-container">
|
|
|
- <div v-if="!markStore.currentTask" class="mark-body-none">
|
|
|
- <div>
|
|
|
- <img src="@/assets/image-none-task.png" />
|
|
|
- <p>
|
|
|
- {{ markStore.message }}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div
|
|
|
- v-else-if="markStore.isScanImage"
|
|
|
- :style="{ width: answerPaperScale }"
|
|
|
- >
|
|
|
- <div
|
|
|
- v-for="(item, index) in sliceImagesWithTrackList"
|
|
|
- :key="index"
|
|
|
- class="single-image-container"
|
|
|
- >
|
|
|
- <img :src="item.url" draggable="false" />
|
|
|
- <MarkDrawTrack
|
|
|
- :trackList="item.trackList"
|
|
|
- :specialTagList="item.tagList"
|
|
|
- :sliceImageHeight="item.originalImageHeight"
|
|
|
- :sliceImageWidth="item.originalImageWidth"
|
|
|
- :dx="0"
|
|
|
- :dy="0"
|
|
|
- />
|
|
|
- <template v-if="!onlyTrack">
|
|
|
- <!-- 客观题答案标记 -->
|
|
|
- <template v-if="item.answerTags">
|
|
|
- <div
|
|
|
- v-for="(tag, tindex) in item.answerTags"
|
|
|
- :key="`tag-${tindex}`"
|
|
|
- :style="tag.style"
|
|
|
- >
|
|
|
- {{ tag.answer }}
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <!-- 试题评分明细 -->
|
|
|
- <template v-if="item.markDetail">
|
|
|
- <div
|
|
|
- v-for="(minfo, mindex) in item.markDetail"
|
|
|
- :key="`mark-${mindex}`"
|
|
|
- :style="minfo.style"
|
|
|
- class="mark-info"
|
|
|
- >
|
|
|
- <div v-if="minfo.isFillQuestion">
|
|
|
- <div
|
|
|
- v-for="user in minfo.users"
|
|
|
- :key="user.userId"
|
|
|
- :style="{ color: user.color }"
|
|
|
- >
|
|
|
- <p>{{ user.prename }}:{{ user.userName }},评分:</p>
|
|
|
- <p>
|
|
|
- {{
|
|
|
- user.scores
|
|
|
- .map((s) => `${s.subNumber}:${s.score}分`)
|
|
|
- .join(",")
|
|
|
- }}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div v-else>
|
|
|
- <p
|
|
|
- v-for="user in minfo.users"
|
|
|
- :key="user.userId"
|
|
|
- :style="{ color: user.color }"
|
|
|
- >
|
|
|
- {{ user.prename }}:{{ user.userName }},评分:{{
|
|
|
- user.score
|
|
|
- }}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- <h3>得分:{{ minfo.score }},满分:{{ minfo.maxScore }}</h3>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <!-- 客观题 -->
|
|
|
- <template v-if="item.objectiveAnswerTags">
|
|
|
- <div
|
|
|
- v-for="tag in item.objectiveAnswerTags"
|
|
|
- :key="tag.id"
|
|
|
- class="mark-objective"
|
|
|
- :style="tag.style"
|
|
|
- >
|
|
|
- 得分:{{ tag.score }},满分:{{ tag.totalScore }}
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <!-- 模式4的summary -->
|
|
|
- <template v-if="item.summarys && item.summarys.length">
|
|
|
- <div class="summary-detail">
|
|
|
- <table>
|
|
|
- <tr>
|
|
|
- <th>主观题号</th>
|
|
|
- <th>分数</th>
|
|
|
- <th>评卷员</th>
|
|
|
- </tr>
|
|
|
- <tr v-for="(sinfo, sindex) in item.summarys" :key="sindex">
|
|
|
- <td>{{ sinfo.mainNumber }}-{{ sinfo.subNumber }}</td>
|
|
|
- <td>{{ sinfo.score }}</td>
|
|
|
- <td>{{ sinfo.markerName }}</td>
|
|
|
- </tr>
|
|
|
- </table>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
-
|
|
|
- <!-- 总分 -->
|
|
|
- <div class="mark-total">
|
|
|
- 总分:{{ totalScore }},主观题得分:{{
|
|
|
- subjectiveScore
|
|
|
- }},客观题得分:{{ objectiveScore }}
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <hr class="image-seperator" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div v-else>未知数据</div>
|
|
|
-
|
|
|
- <div v-if="!sliceImagesWithTrackList.length" class="mark-body-none">
|
|
|
- <div>
|
|
|
- <img src="@/assets/image-none-task.png" />
|
|
|
- <p>
|
|
|
- {{ markStore.message }}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup lang="ts">
|
|
|
-import { reactive, watch } from "vue";
|
|
|
-import { useMarkStore } from "@/store";
|
|
|
-import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
|
|
|
-import type {
|
|
|
- SpecialTag,
|
|
|
- Track,
|
|
|
- ColorMap,
|
|
|
- PaperRecogData,
|
|
|
- Question,
|
|
|
-} from "@/types";
|
|
|
-import { useTimers } from "@/setups/useTimers";
|
|
|
-import {
|
|
|
- loadImage,
|
|
|
- addHeaderTrackColorAttr,
|
|
|
- calcSumPrecision,
|
|
|
- maxNum,
|
|
|
- toPrecision,
|
|
|
-} from "@/utils/utils";
|
|
|
-import useDraggable from "@/features/mark/composables/useDraggable";
|
|
|
-
|
|
|
-interface SliceImage {
|
|
|
- url: string;
|
|
|
- trackList: Array<Track>;
|
|
|
- tagList: Array<SpecialTag>;
|
|
|
- originalImageWidth: number;
|
|
|
- originalImageHeight: number;
|
|
|
- width: string; // 图片在整个图片列表里面的宽度比例
|
|
|
- answerTags?: AnswerTagItem[];
|
|
|
- markDetail?: MarkDetailItem[];
|
|
|
- objectiveAnswerTags?: ObjectiveAnswerTagItem[];
|
|
|
- summarys?: SummaryItem[];
|
|
|
-}
|
|
|
-
|
|
|
-const { origImageUrls = "sliceUrls", onlyTrack = false } = defineProps<{
|
|
|
- origImageUrls?: "sheetUrls" | "sliceUrls";
|
|
|
- onlyTrack?: boolean;
|
|
|
-}>();
|
|
|
-const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
|
|
|
-
|
|
|
-const { dragContainer } = useDraggable();
|
|
|
-const markStore = useMarkStore();
|
|
|
-
|
|
|
-const viewScroll = () => {
|
|
|
- if (
|
|
|
- dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
|
|
|
- dragContainer.value.scrollHeight
|
|
|
- ) {
|
|
|
- emit("getScrollStatus");
|
|
|
- }
|
|
|
-};
|
|
|
-const { addTimeout } = useTimers();
|
|
|
-
|
|
|
-const totalScore = $computed(() => {
|
|
|
- return markStore.currentTask?.markerScore || 0;
|
|
|
-});
|
|
|
-const objectiveScore = $computed(() => {
|
|
|
- return markStore.currentTask?.objectiveScore || 0;
|
|
|
-});
|
|
|
-const subjectiveScore = $computed(() => {
|
|
|
- return toPrecision(totalScore - objectiveScore);
|
|
|
-});
|
|
|
-
|
|
|
-let sliceImagesWithTrackList: SliceImage[] = reactive([]);
|
|
|
-let maxImageWidth = 0;
|
|
|
-
|
|
|
-function addTrackColorAttr(tList: Track[]): Track[] {
|
|
|
- let markerIds: (number | undefined)[] = tList
|
|
|
- .map((v) => v.userId)
|
|
|
- .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 + ""] = "gray";
|
|
|
- }
|
|
|
- }
|
|
|
- if (Object.keys(colorMap).length > 1) {
|
|
|
- emit("getIsMultComments", true);
|
|
|
- }
|
|
|
- tList = tList.map((item: Track) => {
|
|
|
- item.color = colorMap[item.userId + ""] || "red";
|
|
|
- item.isByMultMark = markerIds.length > 1;
|
|
|
- return item;
|
|
|
- });
|
|
|
- return tList;
|
|
|
-}
|
|
|
-
|
|
|
-function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
|
|
|
- let markerIds: (number | undefined)[] = tList
|
|
|
- .map((v) => v.userId)
|
|
|
- .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 + ""] = "gray";
|
|
|
- }
|
|
|
- }
|
|
|
- tList = tList.map((item: SpecialTag) => {
|
|
|
- item.color = colorMap[item.userId + ""] || "red";
|
|
|
- item.isByMultMark = markerIds.length > 1;
|
|
|
- return item;
|
|
|
- });
|
|
|
- return tList;
|
|
|
-}
|
|
|
-
|
|
|
-async function processImage() {
|
|
|
- if (!markStore.currentTask) return;
|
|
|
-
|
|
|
- const images = [];
|
|
|
- const urls = markStore.currentTask[origImageUrls] || [];
|
|
|
- if (!urls.length) return;
|
|
|
- for (const url of urls) {
|
|
|
- const image = await loadImage(url);
|
|
|
- images.push(image);
|
|
|
- }
|
|
|
-
|
|
|
- maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
|
|
|
-
|
|
|
- const trackLists = (markStore.currentTask.questionList || [])
|
|
|
- // .map((q) => q.trackList)
|
|
|
- .map((q) => {
|
|
|
- let tList = q.trackList;
|
|
|
- return q.headerTrack?.length
|
|
|
- ? addHeaderTrackColorAttr(q.headerTrack)
|
|
|
- : addTrackColorAttr(tList);
|
|
|
- })
|
|
|
- .flat();
|
|
|
-
|
|
|
- // 解析各试题答题区域以及评分
|
|
|
- const markDetailList = parseMarkDetailList();
|
|
|
- // 解析客观题的得分情况,按大题统计
|
|
|
- const objectiveAnswerTagList = parseObjectiveAnswerTags();
|
|
|
-
|
|
|
- for (const url of urls) {
|
|
|
- const indexInSliceUrls = urls.indexOf(url) + 1;
|
|
|
- const image = images[indexInSliceUrls - 1];
|
|
|
-
|
|
|
- const thisImageTrackList = trackLists.filter(
|
|
|
- (t) => t.offsetIndex === indexInSliceUrls
|
|
|
- );
|
|
|
- const thisImageTagList = markStore.currentTask.headerTagList?.length
|
|
|
- ? addHeaderTrackColorAttr(
|
|
|
- (markStore.currentTask.headerTagList || []).filter(
|
|
|
- (t) => t.offsetIndex === indexInSliceUrls
|
|
|
- )
|
|
|
- )
|
|
|
- : addTagColorAttr(
|
|
|
- (markStore.currentTask.specialTagList || []).filter(
|
|
|
- (t) => t.offsetIndex === indexInSliceUrls
|
|
|
- )
|
|
|
- );
|
|
|
- const answerTags = paserRecogData(image, indexInSliceUrls - 1);
|
|
|
-
|
|
|
- sliceImagesWithTrackList.push({
|
|
|
- url,
|
|
|
- trackList: thisImageTrackList,
|
|
|
- tagList: thisImageTagList,
|
|
|
- originalImageWidth: image.naturalWidth,
|
|
|
- originalImageHeight: image.naturalHeight,
|
|
|
- width: (image.naturalWidth / maxImageWidth) * 100 + "%",
|
|
|
- answerTags,
|
|
|
- markDetail: markDetailList[indexInSliceUrls - 1],
|
|
|
- objectiveAnswerTags: objectiveAnswerTagList[indexInSliceUrls - 1],
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 无答题卡,模式4
|
|
|
- if (!markStore.currentTask.cardData?.length) {
|
|
|
- const summarys = parseMode4Data();
|
|
|
- if (summarys && summarys.length) {
|
|
|
- sliceImagesWithTrackList[0].summarys = summarys;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 解析客观题答案展示位置
|
|
|
-interface AnswerTagItem {
|
|
|
- mainNumber: number;
|
|
|
- subNumber: string;
|
|
|
- answer: string;
|
|
|
- style: Record<string, string>;
|
|
|
-}
|
|
|
-function paserRecogData(imgDom: HTMLImageElement, imageIndex): AnswerTagItem[] {
|
|
|
- if (
|
|
|
- !markStore.currentTask.recogDatas?.length ||
|
|
|
- !markStore.currentTask.recogDatas[imageIndex]
|
|
|
- )
|
|
|
- return [];
|
|
|
-
|
|
|
- const answerMap = markStore.currentTask.answerMap || {};
|
|
|
- const { naturalWidth, naturalHeight } = imgDom;
|
|
|
- const recogData: PaperRecogData = JSON.parse(
|
|
|
- window.atob(markStore.currentTask.recogDatas[imageIndex])
|
|
|
- );
|
|
|
- const answerTags: AnswerTagItem[] = [];
|
|
|
- // const optionsBlocks = [];
|
|
|
- recogData.question.forEach((question) => {
|
|
|
- question.fill_result.forEach((result) => {
|
|
|
- const tagSize = result.fill_size[1];
|
|
|
- const fillPositions = result.fill_position.map((pos) => {
|
|
|
- return pos.split(",").map((n) => n * 1);
|
|
|
- });
|
|
|
-
|
|
|
- const offsetLt = result.fill_size.map((item) => item * 0.4);
|
|
|
- const tagLeft =
|
|
|
- maxNum(fillPositions.map((pos) => pos[0])) +
|
|
|
- result.fill_size[0] -
|
|
|
- offsetLt[0];
|
|
|
- const tagTop = fillPositions[0][1] - offsetLt[1];
|
|
|
-
|
|
|
- const { answer, isRight } =
|
|
|
- answerMap[`${result.main_number}_${result.sub_number}`] || {};
|
|
|
-
|
|
|
- answerTags.push({
|
|
|
- mainNumber: result.main_number,
|
|
|
- subNumber: result.sub_number,
|
|
|
- answer,
|
|
|
- style: {
|
|
|
- height: ((100 * tagSize) / naturalHeight).toFixed(4) + "%",
|
|
|
- fontSize: ((100 * 20) / tagSize).toFixed(4) + "%",
|
|
|
- left: ((100 * tagLeft) / naturalWidth).toFixed(4) + "%",
|
|
|
- top: ((100 * tagTop) / naturalHeight).toFixed(4) + "%",
|
|
|
- position: "absolute",
|
|
|
- color: isRight ? "#05b575" : "#f53f3f",
|
|
|
- fontWeight: 600,
|
|
|
- lineHeight: 1,
|
|
|
- zIndex: 9,
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- // 测试:选项框
|
|
|
- // fillPositions.forEach((fp, index) => {
|
|
|
- // optionsBlocks.push({
|
|
|
- // mainNumber: result.main_number,
|
|
|
- // subNumber: result.sub_number,
|
|
|
- // filled: !!result.fill_option[index],
|
|
|
- // style: {
|
|
|
- // width:
|
|
|
- // ((100 * result.fill_size[0]) / naturalWidth).toFixed(4) + "%",
|
|
|
- // height:
|
|
|
- // ((100 * result.fill_size[1]) / naturalHeight).toFixed(4) + "%",
|
|
|
- // left:
|
|
|
- // ((100 * (fp[0] - offsetLt[0])) / naturalWidth).toFixed(4) + "%",
|
|
|
- // top:
|
|
|
- // ((100 * (fp[1] - offsetLt[1])) / naturalHeight).toFixed(4) + "%",
|
|
|
- // position: "absolute",
|
|
|
- // border: "1px solid #f53f3f",
|
|
|
- // background: result.fill_option[index]
|
|
|
- // ? "rgba(245, 63, 63, 0.5)"
|
|
|
- // : "transparent",
|
|
|
- // zIndex: 9,
|
|
|
- // },
|
|
|
- // });
|
|
|
- // });
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- return answerTags;
|
|
|
-}
|
|
|
-
|
|
|
-interface QuestionItem {
|
|
|
- mainNumber: number;
|
|
|
- subNumber: number | string;
|
|
|
-}
|
|
|
-interface QuestionArea {
|
|
|
- i: number;
|
|
|
- x: number;
|
|
|
- y: number;
|
|
|
- w: number;
|
|
|
- h: number;
|
|
|
- qStruct: string;
|
|
|
-}
|
|
|
-function parseQuestionAreas(questions: QuestionItem[]) {
|
|
|
- if (!questions.length || !markStore.currentTask.cardData?.length) return [];
|
|
|
-
|
|
|
- let pictureConfigs: QuestionArea[] = [];
|
|
|
- const structs = questions.map(
|
|
|
- (item) => `${item.mainNumber}_${item.subNumber}`
|
|
|
- );
|
|
|
- markStore.currentTask.cardData.forEach((page, pindex) => {
|
|
|
- page.exchange.answer_area.forEach((area) => {
|
|
|
- const [x, y, w, h] = area.area;
|
|
|
- const qStruct = `${area.main_number}_${area.sub_number}`;
|
|
|
-
|
|
|
- const pConfig: QuestionArea = {
|
|
|
- i: pindex + 1,
|
|
|
- x,
|
|
|
- y,
|
|
|
- w,
|
|
|
- h,
|
|
|
- qStruct,
|
|
|
- };
|
|
|
-
|
|
|
- if (typeof area.sub_number === "number") {
|
|
|
- if (!structs.includes(qStruct)) return;
|
|
|
- pictureConfigs.push(pConfig);
|
|
|
- return;
|
|
|
- }
|
|
|
- // 复合区域处理,比如填空题,多个小题合并为一个区域
|
|
|
- if (typeof area.sub_number === "string") {
|
|
|
- const areaStructs = area.sub_number
|
|
|
- .split(",")
|
|
|
- .map((subNumber) => `${area.main_number}_${subNumber}`);
|
|
|
- if (
|
|
|
- structs.some((struct) => areaStructs.includes(struct)) &&
|
|
|
- !pictureConfigs.find((item) => item.qStruct === qStruct)
|
|
|
- ) {
|
|
|
- pictureConfigs.push(pConfig);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
- // console.log(pictureConfigs);
|
|
|
-
|
|
|
- // 合并相邻区域
|
|
|
- pictureConfigs.sort((a, b) => {
|
|
|
- return a.i - b.i || a.x - b.x || a.y - b.y;
|
|
|
- });
|
|
|
- let combinePictureConfigList: QuestionArea[] = [];
|
|
|
- let prevConfig = null;
|
|
|
- pictureConfigs.forEach((item, index) => {
|
|
|
- if (!index) {
|
|
|
- prevConfig = { ...item };
|
|
|
- combinePictureConfigList.push(prevConfig);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const elasticRate = 0.01;
|
|
|
- if (
|
|
|
- prevConfig.i === item.i &&
|
|
|
- prevConfig.y + prevConfig.h + elasticRate >= item.y &&
|
|
|
- prevConfig.w === item.w &&
|
|
|
- prevConfig.x === item.x
|
|
|
- ) {
|
|
|
- prevConfig.h = item.y + item.h - prevConfig.y;
|
|
|
- } else {
|
|
|
- prevConfig = { ...item };
|
|
|
- combinePictureConfigList.push(prevConfig);
|
|
|
- }
|
|
|
- });
|
|
|
- // console.log(combinePictureConfigList);
|
|
|
- return combinePictureConfigList;
|
|
|
-}
|
|
|
-
|
|
|
-// 获取属于填空题的试题号
|
|
|
-function getFillLines() {
|
|
|
- if (!markStore.currentTask.cardData?.length) return {};
|
|
|
-
|
|
|
- const questions: Record<number, string[]> = {};
|
|
|
- markStore.currentTask.cardData.forEach((page) => {
|
|
|
- page.columns.forEach((column) => {
|
|
|
- column.elements.forEach((element) => {
|
|
|
- if (element.type !== "FILL_LINE") return;
|
|
|
-
|
|
|
- if (!questions[element.topicNo]) questions[element.topicNo] = [];
|
|
|
-
|
|
|
- for (let i = 0; i < element.questionsCount; i++) {
|
|
|
- questions[element.topicNo].push(
|
|
|
- `${element.topicNo}_${element.startNumber + i}`
|
|
|
- );
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
- });
|
|
|
- return questions;
|
|
|
-}
|
|
|
-
|
|
|
-// 解析各试题答题区域以及评分
|
|
|
-interface MarkDetailUserItem {
|
|
|
- userId: string;
|
|
|
- userName: string;
|
|
|
- prename: string;
|
|
|
- color: string;
|
|
|
- scores: Array<{ subNumber: string; score: number }>;
|
|
|
- score: number;
|
|
|
-}
|
|
|
-type UserMapType = Record<string, MarkDetailUserItem>;
|
|
|
-interface MarkDetailItem {
|
|
|
- mainNumber: number;
|
|
|
- subNumber: string;
|
|
|
- isFillQuestion: boolean;
|
|
|
- score: number;
|
|
|
- maxScore: number;
|
|
|
- users: MarkDetailUserItem[];
|
|
|
- area: QuestionArea;
|
|
|
- style: Record<string, string>;
|
|
|
-}
|
|
|
-
|
|
|
-function parseMarkDetailList(): Array<MarkDetailItem[]> {
|
|
|
- const dataList: Array<MarkDetailItem[]> = [];
|
|
|
- const questions = markStore.currentTask.questionList || [];
|
|
|
-
|
|
|
- const fillQues = getFillLines();
|
|
|
- let fillQuestions = [] as Question[];
|
|
|
- let otherQuestions = questions;
|
|
|
- if (Object.keys(fillQues).length) {
|
|
|
- const fillQNos = Object.values(fillQues).flat();
|
|
|
- fillQuestions = questions.filter((q) =>
|
|
|
- fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
|
|
|
- );
|
|
|
- otherQuestions = questions.filter(
|
|
|
- (q) => !fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- // 填空题:合并所有小题为一个区域
|
|
|
- Object.values(fillQues).forEach((qnos) => {
|
|
|
- const groupQuestions = fillQuestions.filter((q) =>
|
|
|
- qnos.includes(`${q.mainNumber}_${q.subNumber}`)
|
|
|
- );
|
|
|
- const areas = parseQuestionAreas(groupQuestions);
|
|
|
- if (!areas.length) return;
|
|
|
- const area = { ...areas[0] };
|
|
|
- const imgIndex = area.i - 1;
|
|
|
- if (!dataList[imgIndex]) {
|
|
|
- dataList[imgIndex] = [];
|
|
|
- }
|
|
|
-
|
|
|
- const userMap: UserMapType = {};
|
|
|
- // 大题分成两个部分给两个人评 与 大题被两人同时评 是不一样的
|
|
|
- const isDoubleMark = !groupQuestions.some((question) => {
|
|
|
- let userIds = question.trackList.map((track) => track.userId);
|
|
|
- if (
|
|
|
- !userIds.length &&
|
|
|
- question.markerList &&
|
|
|
- question.markerList.length
|
|
|
- ) {
|
|
|
- userIds = question.markerList
|
|
|
- .filter((marker) => !marker.header)
|
|
|
- .map((marker) => marker.userId);
|
|
|
- }
|
|
|
- const uids = new Set(userIds);
|
|
|
- return uids.size === 1;
|
|
|
- });
|
|
|
- groupQuestions.forEach((question) => {
|
|
|
- question.trackList.forEach((track) => {
|
|
|
- if (!userMap[track.userId]) {
|
|
|
- userMap[track.userId] = {
|
|
|
- userId: track.userId,
|
|
|
- userName: track.userName,
|
|
|
- color: track.color || "red",
|
|
|
- prename: "",
|
|
|
- scores: [],
|
|
|
- score: 0,
|
|
|
- };
|
|
|
- }
|
|
|
- const existUserScore = userMap[track.userId].scores.find(
|
|
|
- (s) => s.subNumber === track.subNumber
|
|
|
- );
|
|
|
- if (existUserScore) {
|
|
|
- existUserScore.score += track.score;
|
|
|
- } else {
|
|
|
- userMap[track.userId].scores.push({
|
|
|
- score: track.score,
|
|
|
- subNumber: track.subNumber,
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 普通模式没有轨迹
|
|
|
- if (
|
|
|
- !question.trackList.length &&
|
|
|
- question.markerList &&
|
|
|
- question.markerList.length
|
|
|
- ) {
|
|
|
- question.markerList
|
|
|
- .filter((marker) => !marker.header)
|
|
|
- .forEach((marker) => {
|
|
|
- if (!userMap[marker.userId]) {
|
|
|
- userMap[marker.userId] = {
|
|
|
- userId: marker.userId,
|
|
|
- userName: marker.userName,
|
|
|
- color: marker.header ? "green" : "red",
|
|
|
- prename: "",
|
|
|
- scores: [],
|
|
|
- score: 0,
|
|
|
- };
|
|
|
- }
|
|
|
- userMap[marker.userId].scores.push({
|
|
|
- score: marker.score,
|
|
|
- subNumber: question.subNumber,
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- const users = Object.values(userMap).map((user, index) => {
|
|
|
- const zhs = ["一", "二", "三"];
|
|
|
- const prename = isDoubleMark ? `${zhs[index] || ""}评` : "评卷员";
|
|
|
- return {
|
|
|
- ...user,
|
|
|
- prename,
|
|
|
- score: calcSumPrecision(user.scores.map((s) => s.score)),
|
|
|
- };
|
|
|
- });
|
|
|
-
|
|
|
- const score = calcSumPrecision(
|
|
|
- groupQuestions.map((item) => item.score || 0)
|
|
|
- );
|
|
|
- const maxScore = calcSumPrecision(
|
|
|
- groupQuestions.map((item) => item.maxScore)
|
|
|
- );
|
|
|
-
|
|
|
- dataList[imgIndex].push({
|
|
|
- mainNumber: groupQuestions[0].mainNumber,
|
|
|
- subNumber: "",
|
|
|
- isFillQuestion: true,
|
|
|
- score,
|
|
|
- maxScore,
|
|
|
- users,
|
|
|
- area,
|
|
|
- style: {
|
|
|
- position: "absolute",
|
|
|
- left: (100 * area.x).toFixed(4) + "%",
|
|
|
- top: (100 * area.y).toFixed(4) + "%",
|
|
|
- width: (100 * area.w).toFixed(4) + "%",
|
|
|
- fontSize: "14px",
|
|
|
- lineHeight: 1,
|
|
|
- zIndex: 9,
|
|
|
- },
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- // 其他试题
|
|
|
- otherQuestions.forEach((question) => {
|
|
|
- const areas = parseQuestionAreas([question]);
|
|
|
- const area = { ...areas[0] };
|
|
|
- const imgIndex = area.i - 1;
|
|
|
- if (!dataList[imgIndex]) {
|
|
|
- dataList[imgIndex] = [];
|
|
|
- }
|
|
|
-
|
|
|
- const userMap: UserMapType = {};
|
|
|
- const isArbitration = Boolean(question.headerTrack?.length);
|
|
|
- const tList = isArbitration ? question.headerTrack : question.trackList;
|
|
|
- tList.forEach((track) => {
|
|
|
- if (!userMap[track.userId]) {
|
|
|
- userMap[track.userId] = {
|
|
|
- userId: track.userId,
|
|
|
- userName: track.userName,
|
|
|
- color: track.color || "red",
|
|
|
- prename: "",
|
|
|
- scores: [],
|
|
|
- score: 0,
|
|
|
- };
|
|
|
- }
|
|
|
- userMap[track.userId].scores.push({
|
|
|
- score: track.score,
|
|
|
- subNumber: track.subNumber,
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- const isDoubleMark = Object.keys(userMap).length > 1;
|
|
|
- const zhs = ["一", "二", "三"];
|
|
|
- let users = Object.values(userMap).map((user, index) => {
|
|
|
- let prename = "";
|
|
|
- if (isArbitration) {
|
|
|
- prename = "仲裁";
|
|
|
- } else {
|
|
|
- prename = isDoubleMark ? `${zhs[index] || ""}评` : "评卷员";
|
|
|
- }
|
|
|
- return {
|
|
|
- ...user,
|
|
|
- prename,
|
|
|
- score: calcSumPrecision(user.scores.map((s) => s.score)),
|
|
|
- };
|
|
|
- });
|
|
|
-
|
|
|
- // 普通模式没有轨迹
|
|
|
- if (!tList.length && question.markerList && question.markerList.length) {
|
|
|
- let markers = question.markerList.filter((marker) => marker.header);
|
|
|
- if (!markers.length) {
|
|
|
- markers = question.markerList.filter((marker) => !marker.header);
|
|
|
- }
|
|
|
- users = markers.map((item, index) => {
|
|
|
- return {
|
|
|
- userId: item.userId,
|
|
|
- userName: item.userName,
|
|
|
- color: item.header ? "green" : "red",
|
|
|
- prename: markers.length > 1 ? `${zhs[index] || ""}评` : "评卷员",
|
|
|
- scores: [],
|
|
|
- score: item.score,
|
|
|
- };
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- dataList[imgIndex].push({
|
|
|
- mainNumber: question.mainNumber,
|
|
|
- subNumber: question.subNumber,
|
|
|
- isFillQuestion: false,
|
|
|
- score: question.score,
|
|
|
- maxScore: question.maxScore,
|
|
|
- users,
|
|
|
- area,
|
|
|
- style: {
|
|
|
- position: "absolute",
|
|
|
- left: (100 * area.x).toFixed(4) + "%",
|
|
|
- top: (100 * area.y).toFixed(4) + "%",
|
|
|
- width: (100 * area.w).toFixed(4) + "%",
|
|
|
- fontSize: "14px",
|
|
|
- lineHeight: 1,
|
|
|
- zIndex: 9,
|
|
|
- },
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- return dataList;
|
|
|
-}
|
|
|
-
|
|
|
-// 解析客观题区域总分
|
|
|
-interface ObjectiveAnswerTagItem {
|
|
|
- id: string;
|
|
|
- mainNumber: number;
|
|
|
- subNumbers: string;
|
|
|
- score: number;
|
|
|
- totalScore: number;
|
|
|
- style: Record<string, string | number>;
|
|
|
-}
|
|
|
-function parseObjectiveAnswerTags() {
|
|
|
- const objectiveAnswerTags: Array<ObjectiveAnswerTagItem[]> = [];
|
|
|
-
|
|
|
- if (
|
|
|
- !markStore.currentTask.cardData?.length ||
|
|
|
- !markStore.currentTask.answerMap
|
|
|
- )
|
|
|
- return objectiveAnswerTags;
|
|
|
-
|
|
|
- markStore.currentTask.cardData.forEach((page, pindex) => {
|
|
|
- if (!objectiveAnswerTags[pindex]) objectiveAnswerTags[pindex] = [];
|
|
|
-
|
|
|
- page.columns.forEach((column) => {
|
|
|
- column.elements.forEach((element) => {
|
|
|
- if (element.type !== "FILL_QUESTION") return;
|
|
|
-
|
|
|
- const ogroup = objectiveAnswerTags.find((tgroup) =>
|
|
|
- tgroup.some((oitem) => oitem.id === element.parent.id)
|
|
|
- );
|
|
|
- if (ogroup) return;
|
|
|
-
|
|
|
- const parent = element.parent;
|
|
|
- const oaTagItem: ObjectiveAnswerTagItem = {
|
|
|
- id: parent.id,
|
|
|
- mainNumber: parent.topicNo,
|
|
|
- subNumbers: `${parent.startNumber}~${
|
|
|
- parent.startNumber + parent.questionsCount - 1
|
|
|
- }`,
|
|
|
- score: 0,
|
|
|
- totalScore: 0,
|
|
|
- style: {
|
|
|
- position: "absolute",
|
|
|
- left: 0,
|
|
|
- top: 0,
|
|
|
- textAlign: "right",
|
|
|
- width: "44%",
|
|
|
- fontSize: "20px",
|
|
|
- fontWeight: "bold",
|
|
|
- color: "#f53f3f",
|
|
|
- lineHeight: 1,
|
|
|
- zIndex: 9,
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- let area = [0, 0];
|
|
|
- page.exchange.fill_area.forEach((fa) => {
|
|
|
- fa.items.forEach((fitem) => {
|
|
|
- if (
|
|
|
- fitem.main_number === oaTagItem.mainNumber &&
|
|
|
- fitem.sub_number === parent.startNumber
|
|
|
- ) {
|
|
|
- area = fitem.options[0];
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- const left = (100 * (area[0] - 0.015)).toFixed(4);
|
|
|
- const top = (100 * (area[1] - 0.04)).toFixed(4);
|
|
|
- oaTagItem.style.left = `${left}%`;
|
|
|
- oaTagItem.style.top = `${top}%`;
|
|
|
-
|
|
|
- const questions: Array<{ score: number; totalScore: number }> = [];
|
|
|
- for (let i = 0; i < parent.questionsCount; i++) {
|
|
|
- const qans = markStore.currentTask.answerMap[
|
|
|
- `${parent.topicNo}_${i + parent.startNumber}`
|
|
|
- ] || { score: 0, totalScore: 0 };
|
|
|
- questions[i] = {
|
|
|
- score: qans.score,
|
|
|
- totalScore: qans.totalScore,
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- oaTagItem.score = calcSumPrecision(questions.map((q) => q.score || 0));
|
|
|
- oaTagItem.totalScore = calcSumPrecision(
|
|
|
- questions.map((q) => q.totalScore || 0)
|
|
|
- );
|
|
|
-
|
|
|
- objectiveAnswerTags[pindex].push(oaTagItem);
|
|
|
- });
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- return objectiveAnswerTags;
|
|
|
-}
|
|
|
-
|
|
|
-// 模式4的解析
|
|
|
-interface SummaryItem {
|
|
|
- mainNumber: number;
|
|
|
- subNumber: string;
|
|
|
- score: number;
|
|
|
- markerName: string;
|
|
|
-}
|
|
|
-function parseMode4Data(): SummaryItem[] {
|
|
|
- // 只有单评才展示summary
|
|
|
- const isDoubleMark = (markStore.currentTask.questionList || []).some(
|
|
|
- (question) => {
|
|
|
- let userIds = question.trackList.map((track) => track.userId);
|
|
|
- if (
|
|
|
- !userIds.length &&
|
|
|
- question.markerList &&
|
|
|
- question.markerList.length
|
|
|
- ) {
|
|
|
- userIds = question.markerList
|
|
|
- .filter((marker) => !marker.header)
|
|
|
- .map((marker) => marker.userId);
|
|
|
- }
|
|
|
- const uids = new Set(userIds);
|
|
|
- return uids.size === 2;
|
|
|
- }
|
|
|
- );
|
|
|
- if (isDoubleMark) return [];
|
|
|
-
|
|
|
- return (markStore.currentTask.questionList || []).map((q) => {
|
|
|
- let markerName = "";
|
|
|
- if (q.headerTrack && q.headerTrack.length) {
|
|
|
- markerName = q.headerTrack[0].userName;
|
|
|
- } else if (q.trackList && q.trackList.length) {
|
|
|
- markerName = q.trackList[0].userName;
|
|
|
- } else if (q.markerList && q.markerList.length) {
|
|
|
- let markers = q.markerList.filter((marker) => marker.header);
|
|
|
- if (!markers.length) {
|
|
|
- markers = q.markerList.filter((marker) => !marker.header);
|
|
|
- }
|
|
|
- if (markers.length) markerName = markers[0].userName;
|
|
|
- }
|
|
|
- return {
|
|
|
- mainNumber: q.mainNumber,
|
|
|
- subNumber: q.subNumber,
|
|
|
- score: q.score,
|
|
|
- markerName,
|
|
|
- };
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 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 (!markStore.currentTask) {
|
|
|
- renderLock = false;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- markStore.globalMask = true;
|
|
|
- await processImage();
|
|
|
- } catch (error) {
|
|
|
- sliceImagesWithTrackList.splice(0);
|
|
|
- console.log("render error ", error);
|
|
|
- // 图片加载出错,自动加载下一个任务
|
|
|
- emit("error");
|
|
|
- } finally {
|
|
|
- await new Promise((res) => setTimeout(res, 500));
|
|
|
- markStore.globalMask = false;
|
|
|
- renderLock = false;
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-watch(() => markStore.currentTask, renderPaperAndMark);
|
|
|
-
|
|
|
-watch(
|
|
|
- (): (number | undefined)[] => [
|
|
|
- markStore.minimapScrollToX,
|
|
|
- markStore.minimapScrollToY,
|
|
|
- ],
|
|
|
- () => {
|
|
|
- const container = document.querySelector<HTMLDivElement>(
|
|
|
- ".mark-body-container"
|
|
|
- );
|
|
|
- addTimeout(() => {
|
|
|
- if (
|
|
|
- container &&
|
|
|
- typeof markStore.minimapScrollToX === "number" &&
|
|
|
- typeof markStore.minimapScrollToY === "number"
|
|
|
- ) {
|
|
|
- const { scrollWidth, scrollHeight } = container;
|
|
|
- container.scrollTo({
|
|
|
- top: scrollHeight * markStore.minimapScrollToY,
|
|
|
- left: scrollWidth * markStore.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 = markStore.setting.uiSetting["answer.paper.scale"];
|
|
|
- return scale * 100 + "%";
|
|
|
-});
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-.mark-body-container {
|
|
|
- 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);
|
|
|
-}
|
|
|
-.mark-info {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
-}
|
|
|
-.mark-info h3 {
|
|
|
- font-size: 20px;
|
|
|
- font-weight: bold;
|
|
|
- line-height: 1;
|
|
|
- color: #f53f3f;
|
|
|
-}
|
|
|
-.mark-info p {
|
|
|
- margin: 0;
|
|
|
- line-height: 20px;
|
|
|
- font-weight: bold;
|
|
|
-}
|
|
|
-.mark-total {
|
|
|
- font-size: 20px;
|
|
|
- font-weight: bold;
|
|
|
- position: absolute;
|
|
|
- top: 1%;
|
|
|
- left: 15%;
|
|
|
- z-index: 9;
|
|
|
- color: #f53f3f;
|
|
|
-}
|
|
|
-.summary-detail {
|
|
|
- position: absolute;
|
|
|
- width: 45%;
|
|
|
- left: 5%;
|
|
|
- top: 11%;
|
|
|
- height: 84%;
|
|
|
- z-index: 9;
|
|
|
- color: #f53f3f;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.summary-detail table {
|
|
|
- border-spacing: 0;
|
|
|
- border-collapse: collapse;
|
|
|
- text-align: left;
|
|
|
-}
|
|
|
-.summary-detail table td,
|
|
|
-.summary-detail table th {
|
|
|
- padding: 0 10px;
|
|
|
- line-height: 24px;
|
|
|
-}
|
|
|
-</style>
|