|
@@ -24,6 +24,32 @@
|
|
:dx="0"
|
|
:dx="0"
|
|
:dy="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>
|
|
|
|
+ <p v-for="user in minfo.users" :key="user.userId">
|
|
|
|
+ 评卷员:{{ user.userName }},评分:{{ user.score }}
|
|
|
|
+ </p>
|
|
|
|
+ </div>
|
|
|
|
+ <h3>得分:{{ minfo.score }},满分:{{ minfo.maxScore }}</h3>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
<hr class="image-seperator" />
|
|
<hr class="image-seperator" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@@ -36,10 +62,11 @@
|
|
import { reactive, watch } from "vue";
|
|
import { reactive, watch } from "vue";
|
|
import { store } from "@/store/store";
|
|
import { store } from "@/store/store";
|
|
import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
|
|
import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
|
|
-import type { SpecialTag, Track, ColorMap } from "@/types";
|
|
|
|
|
|
+import type { SpecialTag, Track, ColorMap, PaperRecogData } from "@/types";
|
|
import { useTimers } from "@/setups/useTimers";
|
|
import { useTimers } from "@/setups/useTimers";
|
|
-import { loadImage, addHeaderTrackColorAttr } from "@/utils/utils";
|
|
|
|
|
|
+import { loadImage, addHeaderTrackColorAttr, calcSum } from "@/utils/utils";
|
|
import { dragImage } from "@/features/mark/use/draggable";
|
|
import { dragImage } from "@/features/mark/use/draggable";
|
|
|
|
+import { maxNum } from "@/utils/utils";
|
|
|
|
|
|
interface SliceImage {
|
|
interface SliceImage {
|
|
url: string;
|
|
url: string;
|
|
@@ -48,6 +75,8 @@ interface SliceImage {
|
|
originalImageWidth: number;
|
|
originalImageWidth: number;
|
|
originalImageHeight: number;
|
|
originalImageHeight: number;
|
|
width: string; // 图片在整个图片列表里面的宽度比例
|
|
width: string; // 图片在整个图片列表里面的宽度比例
|
|
|
|
+ answerTags?: AnswerTagItem[];
|
|
|
|
+ markDetail?: MarkDetailItem[];
|
|
}
|
|
}
|
|
|
|
|
|
const { origImageUrls = "sliceUrls" } = defineProps<{
|
|
const { origImageUrls = "sliceUrls" } = defineProps<{
|
|
@@ -134,6 +163,9 @@ async function processImage() {
|
|
|
|
|
|
maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
|
|
maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
|
|
|
|
|
|
|
|
+ // 解析各试题答题区域以及评分
|
|
|
|
+ const markDetailList = parseMarkDetailList();
|
|
|
|
+
|
|
const trackLists = (store.currentTask.questionList || [])
|
|
const trackLists = (store.currentTask.questionList || [])
|
|
// .map((q) => q.trackList)
|
|
// .map((q) => q.trackList)
|
|
.map((q) => {
|
|
.map((q) => {
|
|
@@ -163,6 +195,7 @@ async function processImage() {
|
|
(t) => t.offsetIndex === indexInSliceUrls
|
|
(t) => t.offsetIndex === indexInSliceUrls
|
|
)
|
|
)
|
|
);
|
|
);
|
|
|
|
+ const answerTags = paserRecogData(image, indexInSliceUrls - 1);
|
|
|
|
|
|
sliceImagesWithTrackList.push({
|
|
sliceImagesWithTrackList.push({
|
|
url,
|
|
url,
|
|
@@ -171,10 +204,242 @@ async function processImage() {
|
|
originalImageWidth: image.naturalWidth,
|
|
originalImageWidth: image.naturalWidth,
|
|
originalImageHeight: image.naturalHeight,
|
|
originalImageHeight: image.naturalHeight,
|
|
width: (image.naturalWidth / maxImageWidth) * 100 + "%",
|
|
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];
|
|
|
|
+
|
|
|
|
+ answerTags.push({
|
|
|
|
+ mainNumber: result.main_number,
|
|
|
|
+ subNumber: result.sub_number,
|
|
|
|
+ answer: answerMap[`${result.main_number}_${result.sub_number}`],
|
|
|
|
+ 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: "#f53f3f",
|
|
|
|
+ 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;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 解析各试题答题区域以及评分
|
|
|
|
+interface MarkDetailItem {
|
|
|
|
+ mainNumber: number;
|
|
|
|
+ subNumber: string;
|
|
|
|
+ score: number;
|
|
|
|
+ maxScore: number;
|
|
|
|
+ users: Array<{
|
|
|
|
+ userId: string;
|
|
|
|
+ userName: string;
|
|
|
|
+ scores: number[];
|
|
|
|
+ score: number;
|
|
|
|
+ }>;
|
|
|
|
+ area: QuestionArea;
|
|
|
|
+ style: Record<string, string>;
|
|
|
|
+}
|
|
|
|
+function parseMarkDetailList(): Array<MarkDetailItem[]> {
|
|
|
|
+ const dataList: Array<MarkDetailItem[]> = [];
|
|
|
|
+
|
|
|
|
+ (store.currentTask.questionList || []).forEach((question) => {
|
|
|
|
+ const areas = parseQuestionAreas([question]);
|
|
|
|
+ if (!areas.length) return;
|
|
|
|
+ const area = areas[0];
|
|
|
|
+ if (!dataList[area.i - 1]) {
|
|
|
|
+ dataList[area.i - 1] = [];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const userMap = {};
|
|
|
|
+ question.trackList.forEach((track) => {
|
|
|
|
+ if (!userMap[track.userId]) {
|
|
|
|
+ userMap[track.userId] = {
|
|
|
|
+ userId: track.userId,
|
|
|
|
+ userName: track.userName,
|
|
|
|
+ scores: [],
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ userMap[track.userId].scores.push(track.score);
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ const users = Object.values(userMap).map((user) => {
|
|
|
|
+ return { ...user, score: calcSum(user.scores) };
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ dataList[area.i - 1].push({
|
|
|
|
+ mainNumber: question.mainNumber,
|
|
|
|
+ subNumber: question.subNumber,
|
|
|
|
+ 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) + "%",
|
|
|
|
+ color: "#f53f3f",
|
|
|
|
+ fontSize: "14px",
|
|
|
|
+ lineHeight: 1,
|
|
|
|
+ zIndex: 9,
|
|
|
|
+ },
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return dataList;
|
|
|
|
+}
|
|
|
|
+
|
|
// should not render twice at the same time
|
|
// should not render twice at the same time
|
|
let renderLock = false;
|
|
let renderLock = false;
|
|
const renderPaperAndMark = async () => {
|
|
const renderPaperAndMark = async () => {
|
|
@@ -284,4 +549,19 @@ const answerPaperScale = $computed(() => {
|
|
.image-seperator {
|
|
.image-seperator {
|
|
border: 2px solid rgba(120, 120, 120, 0.1);
|
|
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;
|
|
|
|
+}
|
|
</style>
|
|
</style>
|