123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767 |
- <template>
- <div class="mark-body" @scroll="viewScroll">
- <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 }">
- <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="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>
- <!-- 总分 -->
- <div class="mark-total">
- 总分:{{ totalScore }},主观题得分:{{
- subjectiveScore
- }},客观题得分:{{ objectiveScore }}
- </div>
- <hr class="image-seperator" />
- </div>
- </div>
- <div v-else>未知数据</div>
- </div>
- </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,
- PaperRecogData,
- Question,
- } from "@/types";
- import { useTimers } from "@/setups/useTimers";
- import { loadImage, addHeaderTrackColorAttr, calcSum } from "@/utils/utils";
- import { dragImage } from "@/features/mark/use/draggable";
- import { maxNum } from "@/utils/utils";
- interface SliceImage {
- url: string;
- trackList: Array<Track>;
- tagList: Array<SpecialTag>;
- originalImageWidth: number;
- originalImageHeight: number;
- width: string; // 图片在整个图片列表里面的宽度比例
- answerTags?: AnswerTagItem[];
- markDetail?: MarkDetailItem[];
- }
- 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();
- const totalScore = $computed(() => {
- return store.currentTask?.markerScore || 0;
- });
- const objectiveScore = $computed(() => {
- return store.currentTask?.objectiveScore || 0;
- });
- const subjectiveScore = $computed(() => {
- return 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 (!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));
- const trackLists = (store.currentTask.questionList || [])
- // .map((q) => q.trackList)
- .map((q) => {
- let tList = q.trackList;
- return q.headerTrack?.length
- ? addHeaderTrackColorAttr(q.headerTrack)
- : addTrackColorAttr(tList);
- })
- .flat();
- store.setting.doubleTrack = trackLists.some((item) => item.isByMultMark);
- // 解析各试题答题区域以及评分
- const markDetailList = parseMarkDetailList();
- 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 = store.currentTask.headerTagList?.length
- ? addHeaderTrackColorAttr(
- (store.currentTask.headerTagList || []).filter(
- (t) => t.offsetIndex === indexInSliceUrls
- )
- )
- : addTagColorAttr(
- (store.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],
- });
- }
- }
- // 解析客观题答案展示位置
- interface AnswerTagItem {
- mainNumber: number;
- subNumber: string;
- answer: string;
- style: Record<string, string>;
- }
- function paserRecogData(imgDom: HTMLImageElement, imageIndex): AnswerTagItem[] {
- if (
- !store.currentTask.recogDatas?.length ||
- !store.currentTask.recogDatas[imageIndex]
- )
- return [];
- const answerMap = store.currentTask.answerMap || {};
- const { naturalWidth, naturalHeight } = imgDom;
- const recogData: PaperRecogData = JSON.parse(
- window.atob(store.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 || !store.currentTask.cardData?.length) return [];
- let pictureConfigs: QuestionArea[] = [];
- const structs = questions.map(
- (item) => `${item.mainNumber}_${item.subNumber}`
- );
- store.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 (!store.currentTask.cardData?.length) return {};
- const questions: Record<number, string[]> = {};
- store.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 = store.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) => {
- const userIds = question.trackList.map((track) => track.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,
- });
- }
- });
- });
- const users = Object.values(userMap).map((user, index) => {
- const zhs = ["一", "二", "三"];
- const prename = isDoubleMark ? `${zhs[index] || ""}评` : "评卷员";
- return {
- ...user,
- prename,
- score: calcSum(user.scores.map((s) => s.score)),
- };
- });
- const score = calcSum(groupQuestions.map((item) => item.score || 0));
- const maxScore = calcSum(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 users = Object.values(userMap).map((user, index) => {
- const zhs = ["一", "二", "三"];
- let prename = "";
- if (isArbitration) {
- prename = "仲裁";
- } else {
- prename = isDoubleMark ? `${zhs[index] || ""}评` : "评卷员";
- }
- return {
- ...user,
- prename,
- score: calcSum(user.scores.map((s) => s.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;
- }
- // 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 {
- 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;
- }
- </style>
|