|
@@ -0,0 +1,505 @@
|
|
|
+import {
|
|
|
+ getSingleStudentTaskOfStudentTrack,
|
|
|
+ studentObjectiveConfirmData,
|
|
|
+ getSingleStudentCardData,
|
|
|
+} from '@/api/task';
|
|
|
+import { Task, Track, SpecialTag } from '@/api/types/task';
|
|
|
+import { calcSum, maxNum, randomCode, strGbLen } from '@/utils/utils';
|
|
|
+import { DrawTrackItem } from '../../../electron/preload/types';
|
|
|
+
|
|
|
+type AnswerMap = Record<string, { answer: string; isRight: boolean }>;
|
|
|
+
|
|
|
+interface TrackTtemType {
|
|
|
+ url: string;
|
|
|
+ drawTrackList: DrawTrackItem[];
|
|
|
+}
|
|
|
+
|
|
|
+interface CardDataItem {
|
|
|
+ exchange: {
|
|
|
+ answer_area: Array<{
|
|
|
+ main_number: number;
|
|
|
+ sub_number: number | string;
|
|
|
+ area: [number, number, number, number];
|
|
|
+ }>;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+interface CardContentType {
|
|
|
+ pages: CardDataItem[];
|
|
|
+}
|
|
|
+
|
|
|
+interface QuestionItem {
|
|
|
+ mainNumber: number;
|
|
|
+ subNumber: number | string;
|
|
|
+}
|
|
|
+interface QuestionArea {
|
|
|
+ i: number;
|
|
|
+ x: number;
|
|
|
+ y: number;
|
|
|
+ w: number;
|
|
|
+ h: number;
|
|
|
+ qStruct: string;
|
|
|
+}
|
|
|
+
|
|
|
+type UserMapType = Record<
|
|
|
+ string,
|
|
|
+ { userId: string; userName: string; scores: number[] }
|
|
|
+>;
|
|
|
+
|
|
|
+interface ImageItem {
|
|
|
+ url: string;
|
|
|
+ width: number;
|
|
|
+ height: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface PaperRecogData {
|
|
|
+ page_index: number;
|
|
|
+ question: Array<{
|
|
|
+ index: number;
|
|
|
+ fill_result: Array<{
|
|
|
+ main_number: number;
|
|
|
+ sub_number: number;
|
|
|
+ single: number;
|
|
|
+ fill_option: number[];
|
|
|
+ suspect_flag: number;
|
|
|
+ fill_position: string[];
|
|
|
+ fill_size: number[];
|
|
|
+ }>;
|
|
|
+ }>;
|
|
|
+}
|
|
|
+
|
|
|
+export default function useTrack() {
|
|
|
+ let answerMap = {} as AnswerMap;
|
|
|
+ let cardData = [] as CardDataItem[];
|
|
|
+ let recogDatas: string[] = [];
|
|
|
+ let rawTask = {} as Task;
|
|
|
+ let trackData = [] as TrackTtemType[];
|
|
|
+
|
|
|
+ async function runTask(studentId: string) {
|
|
|
+ initData();
|
|
|
+ try {
|
|
|
+ await getTaskData(studentId);
|
|
|
+ await parseDrawList();
|
|
|
+ await drawTask();
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error);
|
|
|
+ return Promise.reject(error);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ function initData() {
|
|
|
+ cardData = [] as CardDataItem[];
|
|
|
+ recogDatas = [] as string[];
|
|
|
+ rawTask = {} as Task;
|
|
|
+ trackData = [] as TrackTtemType[];
|
|
|
+ answerMap = {} as AnswerMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function getTaskData(studentId: string) {
|
|
|
+ rawTask = await getSingleStudentTaskOfStudentTrack(studentId);
|
|
|
+ if (!rawTask) return;
|
|
|
+
|
|
|
+ // rawTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
|
|
|
+ if (!rawTask.sheetUrls) rawTask.sheetUrls = [];
|
|
|
+
|
|
|
+ // 获取客观题选项信息
|
|
|
+ const objectiveData = await studentObjectiveConfirmData(studentId);
|
|
|
+ objectiveData.answers.forEach((item) => {
|
|
|
+ answerMap[`${item.mainNumber}_${item.subNumber}`] = {
|
|
|
+ answer: item.answer,
|
|
|
+ isRight: item.answer === item.standardAnswer,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ recogDatas = objectiveData.sheetUrls.map((item) => item.recogData);
|
|
|
+
|
|
|
+ // 获取题卡数据
|
|
|
+ const cardRes = await getSingleStudentCardData(studentId);
|
|
|
+ const cardContent = cardRes.content
|
|
|
+ ? (JSON.parse(cardRes.content) as CardContentType)
|
|
|
+ : { pages: [] };
|
|
|
+ cardData = cardContent.pages;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function downloadImages(urls: string[]) {
|
|
|
+ const downloads: Promise<ImageItem>[] = [];
|
|
|
+ for (let i = 0; i < urls.length; i++) {
|
|
|
+ downloads.push(
|
|
|
+ window.api.downloadImage(
|
|
|
+ urls[i],
|
|
|
+ `${rawTask.studentId}-${i}-${randomCode(8)}.jpg`
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const images = await Promise.all(downloads).catch((error) => {
|
|
|
+ console.log(error);
|
|
|
+ });
|
|
|
+ if (!images) {
|
|
|
+ return Promise.reject(new Error('下载图片失败'));
|
|
|
+ }
|
|
|
+ return images;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function parseDrawList() {
|
|
|
+ trackData = [];
|
|
|
+
|
|
|
+ const trackLists = (rawTask.questionList || [])
|
|
|
+ .map((q) => {
|
|
|
+ return q.headerTrack?.length
|
|
|
+ ? addHeaderTrackColorAttr(q.headerTrack)
|
|
|
+ : addTrackColorAttr(q.trackList);
|
|
|
+ })
|
|
|
+ .flat();
|
|
|
+
|
|
|
+ const images = await downloadImages(rawTask.sheetUrls);
|
|
|
+
|
|
|
+ const markDeailList = parseMarkDetailList(images);
|
|
|
+
|
|
|
+ for (let i = 0; i < images.length; i++) {
|
|
|
+ const img = images[i];
|
|
|
+ const drawTrackList = [] as DrawTrackItem[];
|
|
|
+ trackLists
|
|
|
+ .filter((item) => item.offsetIndex === i + 1)
|
|
|
+ .forEach((item) => {
|
|
|
+ drawTrackList.push(getDrawTrackItem(item));
|
|
|
+ });
|
|
|
+ rawTask.specialTagList
|
|
|
+ .filter((item) => item.offsetIndex === i + 1)
|
|
|
+ .forEach((item) => {
|
|
|
+ drawTrackList.push(getDrawTagTrackItem(item));
|
|
|
+ });
|
|
|
+ const answerTags = paserRecogData(i);
|
|
|
+ drawTrackList.push(...answerTags);
|
|
|
+ drawTrackList.push(...markDeailList[i]);
|
|
|
+
|
|
|
+ trackData[i] = {
|
|
|
+ url: img.url,
|
|
|
+ drawTrackList,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function drawTask(): Promise<string[]> {
|
|
|
+ if (!trackData.length) return [];
|
|
|
+
|
|
|
+ const tasks: Promise<string>[] = [];
|
|
|
+ for (let i = 0; i < trackData.length; i++) {
|
|
|
+ const item = trackData[i];
|
|
|
+ const outpath = getOutputPath(i + 1);
|
|
|
+ tasks.push(window.api.drawTrack(item.url, item.drawTrackList, outpath));
|
|
|
+ }
|
|
|
+ const res = await Promise.all(tasks).catch((error) => {
|
|
|
+ console.log(error);
|
|
|
+ });
|
|
|
+ if (!res) {
|
|
|
+ return Promise.reject(res);
|
|
|
+ }
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+
|
|
|
+ function getOutputPath(ind: number) {
|
|
|
+ // TODO:
|
|
|
+ const no = `000${ind}`.slice(-3);
|
|
|
+ return window.api.joinPath(['', `${no}.jpg`]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // track ----- start->
|
|
|
+ function getDrawTrackItem(track: Track): DrawTrackItem {
|
|
|
+ return {
|
|
|
+ type: 'text',
|
|
|
+ option: {
|
|
|
+ x: track.offsetX,
|
|
|
+ y: track.offsetY,
|
|
|
+ text: String(track.score),
|
|
|
+ color: track.color,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function getDrawTagTrackItem(track: SpecialTag): DrawTrackItem {
|
|
|
+ if (track.tagType === 'LINE') {
|
|
|
+ const tagProp = JSON.parse(track.tagName) as {
|
|
|
+ len: number;
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ type: 'line',
|
|
|
+ option: {
|
|
|
+ x0: track.offsetX,
|
|
|
+ y0: track.offsetY,
|
|
|
+ x1: track.offsetX + tagProp.len,
|
|
|
+ y1: track.offsetY,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (track.tagType === 'CIRCLE') {
|
|
|
+ const tagProp = JSON.parse(track.tagName) as {
|
|
|
+ width: number;
|
|
|
+ height: number;
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ type: 'circle',
|
|
|
+ option: {
|
|
|
+ x0: track.offsetX,
|
|
|
+ y0: track.offsetY,
|
|
|
+ x1: track.offsetX + tagProp.width,
|
|
|
+ y1: track.offsetY + tagProp.height,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ type: 'text',
|
|
|
+ option: {
|
|
|
+ x: track.offsetX,
|
|
|
+ y: track.offsetY,
|
|
|
+ text: track.tagName,
|
|
|
+ color: track.color,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function addHeaderTrackColorAttr(headerTrack: Track[]): Track[] {
|
|
|
+ return headerTrack.map((item) => {
|
|
|
+ item.color = 'green';
|
|
|
+ return item;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function addTrackColorAttr(tList: Track[]): Track[] {
|
|
|
+ let markerIds: string[] = tList.map((v) => v.userId).filter((x) => !!x);
|
|
|
+ markerIds = Array.from(new Set(markerIds));
|
|
|
+ // markerIds.sort();
|
|
|
+ const colorMap: Record<string, string> = {};
|
|
|
+ for (let i = 0; i < markerIds.length; i++) {
|
|
|
+ const mId = markerIds[i];
|
|
|
+ if (i === 0) {
|
|
|
+ colorMap[mId] = 'red';
|
|
|
+ } else if (i === 1) {
|
|
|
+ colorMap[mId] = 'blue';
|
|
|
+ } else if (i > 1) {
|
|
|
+ colorMap[mId] = 'gray';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ type ColorK = keyof typeof colorMap;
|
|
|
+ tList = tList.map((item: Track) => {
|
|
|
+ const k = item.userId as ColorK;
|
|
|
+ item.color = colorMap[k] || 'red';
|
|
|
+ item.isByMultMark = markerIds.length > 1;
|
|
|
+ return item;
|
|
|
+ });
|
|
|
+ return tList;
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
|
|
|
+ let markerIds: string[] = tList
|
|
|
+ .map((v) => String(v.userId))
|
|
|
+ .filter((x) => !!x);
|
|
|
+ markerIds = Array.from(new Set(markerIds));
|
|
|
+ // markerIds.sort();
|
|
|
+ const colorMap: Record<string, string> = {};
|
|
|
+ for (let i = 0; i < markerIds.length; i++) {
|
|
|
+ const mId = markerIds[i];
|
|
|
+ if (i === 0) {
|
|
|
+ colorMap[mId] = 'red';
|
|
|
+ } else if (i === 1) {
|
|
|
+ colorMap[mId] = 'blue';
|
|
|
+ } else if (i > 1) {
|
|
|
+ colorMap[mId] = 'gray';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ type ColorK = keyof typeof colorMap;
|
|
|
+ tList = tList.map((item: SpecialTag) => {
|
|
|
+ const k = String(item.userId) as ColorK;
|
|
|
+ item.color = colorMap[k] || 'red';
|
|
|
+ item.isByMultMark = markerIds.length > 1;
|
|
|
+ return item;
|
|
|
+ });
|
|
|
+ return tList;
|
|
|
+ }
|
|
|
+ */
|
|
|
+
|
|
|
+ // track ----- end->
|
|
|
+
|
|
|
+ // mark detail ----- start->
|
|
|
+ // 解析各试题答题区域以及评分
|
|
|
+ function parseMarkDetailList(images: ImageItem[]): Array<DrawTrackItem[]> {
|
|
|
+ const dataList: Array<DrawTrackItem[]> = [];
|
|
|
+
|
|
|
+ (rawTask.questionList || []).forEach((question) => {
|
|
|
+ const areas = parseQuestionAreas([question]);
|
|
|
+ if (!areas.length) return;
|
|
|
+ const area = { ...areas[0] };
|
|
|
+ const imgIndex = area.i - 1;
|
|
|
+ if (!dataList[imgIndex]) {
|
|
|
+ dataList[imgIndex] = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ const img = images[imgIndex] as ImageItem;
|
|
|
+ area.x *= img.width;
|
|
|
+ area.y *= img.height;
|
|
|
+ area.w *= img.width;
|
|
|
+
|
|
|
+ const dataArr = dataList[imgIndex];
|
|
|
+ const userMap: UserMapType = {};
|
|
|
+ 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);
|
|
|
+ });
|
|
|
+
|
|
|
+ Object.values(userMap).forEach((user, index) => {
|
|
|
+ const userScore = calcSum(user.scores);
|
|
|
+ const content = `评卷员:${user.userName},评分:${userScore}`;
|
|
|
+ dataArr.push({
|
|
|
+ type: 'text',
|
|
|
+ option: {
|
|
|
+ x: area.x,
|
|
|
+ y: area.y + index * 20,
|
|
|
+ text: content,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+ const tCont = `得分:${question.score},满分:${question.maxScore}`;
|
|
|
+ const tContLen = strGbLen(tCont) / 2;
|
|
|
+ dataArr.push({
|
|
|
+ type: 'text',
|
|
|
+ option: {
|
|
|
+ x: area.x + area.w - Math.ceil(tContLen * 20),
|
|
|
+ y: area.y,
|
|
|
+ text: tCont,
|
|
|
+ fontSize: 20,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ return dataList;
|
|
|
+ }
|
|
|
+
|
|
|
+ function parseQuestionAreas(questions: QuestionItem[]) {
|
|
|
+ if (!questions.length || !cardData?.length) return [];
|
|
|
+
|
|
|
+ const pictureConfigs: QuestionArea[] = [];
|
|
|
+ const structs = questions.map(
|
|
|
+ (item) => `${item.mainNumber}_${item.subNumber}`
|
|
|
+ );
|
|
|
+ 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;
|
|
|
+ });
|
|
|
+ const combinePictureConfigList: QuestionArea[] = [];
|
|
|
+ let prevConfig = {} as QuestionArea;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ // mark detail ----- end->
|
|
|
+
|
|
|
+ // answer tag ----- start->
|
|
|
+ // 解析客观题答案展示位置
|
|
|
+ function paserRecogData(imageIndex: number): DrawTrackItem[] {
|
|
|
+ if (!recogDatas.length || !recogDatas[imageIndex]) return [];
|
|
|
+
|
|
|
+ const recogData: PaperRecogData = JSON.parse(
|
|
|
+ window.atob(recogDatas[imageIndex])
|
|
|
+ );
|
|
|
+ const answerTags: DrawTrackItem[] = [];
|
|
|
+ recogData.question.forEach((question) => {
|
|
|
+ question.fill_result.forEach((result) => {
|
|
|
+ const fillPositions = result.fill_position.map((pos) => {
|
|
|
+ return pos.split(',').map((n) => Number(n));
|
|
|
+ });
|
|
|
+
|
|
|
+ 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({
|
|
|
+ type: 'text',
|
|
|
+ option: {
|
|
|
+ x: tagLeft,
|
|
|
+ y: tagTop,
|
|
|
+ text: answer,
|
|
|
+ color: isRight ? '#05b575' : '#f53f3f',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ return answerTags;
|
|
|
+ }
|
|
|
+ // answer tag ----- end->
|
|
|
+
|
|
|
+ return {
|
|
|
+ runTask,
|
|
|
+ };
|
|
|
+}
|