useDraw.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. import { ref } from 'vue';
  2. import {
  3. getSingleStudentTaskOfStudentTrack,
  4. studentObjectiveConfirmData,
  5. getSingleStudentCardData,
  6. } from '@/api/task';
  7. import { Task, Track, SpecialTag, Question } from '@/api/types/task';
  8. import { TrackConfigType } from '@/store/modules/app/types';
  9. import { PictureTypeEnum, PICTURE_TYPE } from '@/constants/enumerate';
  10. import { calcSum, deepCopy, maxNum, strGbLen } from '@/utils/utils';
  11. import { useAppStore } from '@/store';
  12. import { DrawTrackItem } from '../../../../electron/preload/types';
  13. import { TrackTaskData } from '../../../../electron/db/models/trackTask';
  14. import { TrackTaskDetailData } from '../../../../electron/db/models/trackTaskDetail';
  15. type AnswerMap = Record<string, { answer: string; isRight: boolean }>;
  16. interface TrackItemType {
  17. url: string;
  18. width: number;
  19. height: number;
  20. drawTrackList: DrawTrackItem[];
  21. }
  22. type ElementType =
  23. | 'FILL_QUESTION'
  24. | 'FILL_LINE'
  25. | 'EXPLAIN'
  26. | 'COMPOSITION'
  27. | 'TOPIC_HEAD'
  28. | 'CARD_HEAD';
  29. interface CardDataItem {
  30. exchange: {
  31. answer_area: Array<{
  32. main_number: number;
  33. sub_number: number | string;
  34. area: [number, number, number, number];
  35. }>;
  36. };
  37. columns: Array<{
  38. elements: Array<{
  39. type: ElementType;
  40. topicNo: number;
  41. startNumber: number;
  42. questionsCount: number;
  43. }>;
  44. }>;
  45. }
  46. interface CardContentType {
  47. pages: CardDataItem[];
  48. }
  49. interface QuestionItem {
  50. mainNumber: number;
  51. subNumber: number | string;
  52. }
  53. interface QuestionArea {
  54. i: number;
  55. x: number;
  56. y: number;
  57. w: number;
  58. h: number;
  59. qStruct: string;
  60. }
  61. type UserMapType = Record<
  62. string,
  63. {
  64. userId: string;
  65. userName: string;
  66. color: string;
  67. scores: Array<{ subNumber: string; score: number }>;
  68. }
  69. >;
  70. interface ImageItem {
  71. url: string;
  72. width: number;
  73. height: number;
  74. }
  75. interface PaperRecogData {
  76. page_index: number;
  77. question: Array<{
  78. index: number;
  79. fill_result: Array<{
  80. main_number: number;
  81. sub_number: number;
  82. single: number;
  83. fill_option: number[];
  84. suspect_flag: number;
  85. fill_position: string[];
  86. fill_size: number[];
  87. }>;
  88. }>;
  89. }
  90. export default function useDraw(winId: number) {
  91. const curWinId = winId;
  92. let answerMap = {} as AnswerMap;
  93. let cardData = [] as CardDataItem[];
  94. let recogDatas: string[] = [];
  95. let rawTask = {} as Task;
  96. let trackData = [] as TrackItemType[];
  97. let originImgs = [] as ImageItem[];
  98. let isOnlyOrigin = false;
  99. let hasPdf = false;
  100. let curStudentId = '';
  101. const defaultColorConfig = {
  102. track: ['red', 'blue', 'gray'],
  103. head: 'green',
  104. };
  105. let colorConfig = { track: ['red', 'blue', 'gray'], head: 'green' };
  106. const task = ref({} as TrackTaskData);
  107. const taskDetail = ref({} as TrackTaskDetailData);
  108. const trackConfig = ref({} as TrackConfigType);
  109. const appStore = useAppStore();
  110. function updateColorConfig() {
  111. if (trackConfig.value.trackColorType === 'ALL_RED') {
  112. colorConfig.head = 'red';
  113. colorConfig.track = ['red', 'red', 'red'];
  114. } else {
  115. colorConfig = deepCopy(defaultColorConfig);
  116. }
  117. }
  118. function addLog(content: string, type?: 'info' | 'error') {
  119. window.api.logger(`win:${curWinId} ${content}`, type);
  120. }
  121. async function getTrackTask(schoolId: string) {
  122. const res = await window.db.getUnfinishTrackTask(schoolId);
  123. if (!res) return Promise.reject(new Error('无任务'));
  124. task.value = res;
  125. trackConfig.value = JSON.parse(res.trackConfig) as TrackConfigType;
  126. isOnlyOrigin = checkOnlyOrigin();
  127. hasPdf = trackConfig.value.pictureType.includes('pdf');
  128. updateColorConfig();
  129. return res.id;
  130. }
  131. async function getTrackTaskDetail() {
  132. const res = await window.db.getUnfinishTrackTaskDetail(task.value.id);
  133. if (!res) return null;
  134. taskDetail.value = res;
  135. return taskDetail.value;
  136. }
  137. async function runTask() {
  138. initData();
  139. curStudentId = taskDetail.value.studentId;
  140. addLog(`[${curStudentId}] 01-开始任务`);
  141. try {
  142. await getTaskData(curStudentId);
  143. addLog(`[${curStudentId}] 02-获取任务数据成功`);
  144. originImgs = await downloadImages(rawTask.sheetUrls);
  145. if (isOnlyOrigin) {
  146. return true;
  147. }
  148. await parseDrawList();
  149. addLog(`[${curStudentId}] 03-解析绘制数据成功`);
  150. const trackFiles = await drawTask();
  151. addLog(`[${curStudentId}] 04-绘制成功`);
  152. if (hasPdf) {
  153. await window.api.imagesToPdf(trackFiles, getOutputPath('pdf'));
  154. addLog(`[${curStudentId}] 05-生成pdf成功`);
  155. }
  156. } catch (error) {
  157. const e = error as Error;
  158. console.log(e);
  159. addLog(
  160. `[${curStudentId}-${rawTask.studentCode}] 08-任务失败,原因:${
  161. e.message || '未知'
  162. }`,
  163. 'error'
  164. );
  165. return Promise.reject(error);
  166. }
  167. return true;
  168. }
  169. function initData() {
  170. cardData = [] as CardDataItem[];
  171. recogDatas = [] as string[];
  172. rawTask = {} as Task;
  173. trackData = [] as TrackItemType[];
  174. answerMap = {} as AnswerMap;
  175. originImgs = [] as ImageItem[];
  176. curStudentId = '';
  177. }
  178. function checkOnlyOrigin() {
  179. return (
  180. trackConfig.value.pictureType.length === 1 &&
  181. trackConfig.value.pictureType[0] === 'origin'
  182. );
  183. }
  184. async function getTaskData(studentId: string) {
  185. rawTask = await getSingleStudentTaskOfStudentTrack(studentId);
  186. if (!rawTask) return;
  187. if (!rawTask.sheetUrls) rawTask.sheetUrls = [];
  188. if (isOnlyOrigin) return;
  189. // 获取客观题选项信息
  190. const objectiveData = await studentObjectiveConfirmData(studentId);
  191. objectiveData.answers.forEach((item) => {
  192. answerMap[`${item.mainNumber}_${item.subNumber}`] = {
  193. answer: item.answer,
  194. isRight: item.answer === item.standardAnswer,
  195. };
  196. });
  197. recogDatas = objectiveData.sheetUrls.map((item) => item.recogData);
  198. // 获取题卡数据
  199. const cardRes = await getSingleStudentCardData(studentId);
  200. const cardContent =
  201. cardRes && cardRes.content
  202. ? (JSON.parse(cardRes.content) as CardContentType)
  203. : { pages: [] };
  204. cardData = cardContent.pages;
  205. }
  206. /**
  207. * 获取文件存储路径,规则:学期/考试/课程/试卷编号/教学班/下载文件类型/学生图片
  208. */
  209. function getOutputPath(type: PictureTypeEnum, index?: number) {
  210. const transfromStr = (str: string) => str.replace(/[*|:?<>]/g, '');
  211. let filename =
  212. trackConfig.value.studentFileRule === 'CODE_NAME'
  213. ? `${rawTask.studentCode}-${rawTask.studentName}`
  214. : rawTask.studentCode;
  215. filename = transfromStr(filename);
  216. if (index !== undefined) {
  217. filename += `-${index}`;
  218. }
  219. filename += type === 'pdf' ? '.pdf' : '.jpg';
  220. const paths = [
  221. trackConfig.value.curOutputDir,
  222. transfromStr(task.value.semesterName),
  223. transfromStr(task.value.examName),
  224. transfromStr(`${rawTask.courseName}(${rawTask.courseCode})`),
  225. transfromStr(rawTask.paperNumber),
  226. transfromStr(taskDetail.value.className),
  227. PICTURE_TYPE[type],
  228. filename,
  229. ];
  230. return window.api.joinPath(paths);
  231. }
  232. async function downloadImages(urls: string[]) {
  233. const downloads: Promise<ImageItem>[] = [];
  234. for (let i = 0; i < urls.length; i++) {
  235. let url = urls[i];
  236. if (!url.startsWith('http://') && !url.startsWith('https://')) {
  237. url = `${appStore.domain}/${url}`;
  238. }
  239. downloads.push(
  240. window.api.downloadImage(url, getOutputPath('origin', i + 1))
  241. );
  242. }
  243. const images = await Promise.all(downloads).catch((error: Error) => {
  244. console.log(error);
  245. addLog(`下载图片错误:${error.toString()}`, 'error');
  246. });
  247. if (!images) {
  248. return Promise.reject(new Error('下载图片失败'));
  249. }
  250. return images;
  251. }
  252. async function parseDrawList() {
  253. trackData = [];
  254. const trackLists = (rawTask.questionList || [])
  255. .map((q) => {
  256. return q.headerTrack?.length
  257. ? addHeaderTrackColorAttr(q.headerTrack)
  258. : addTrackColorAttr(q.trackList);
  259. })
  260. .flat();
  261. const markDeailList = parseMarkDetailList(originImgs);
  262. for (let i = 0; i < originImgs.length; i++) {
  263. const img = originImgs[i];
  264. const drawTrackList = [] as DrawTrackItem[];
  265. trackLists
  266. .filter((item) => item.offsetIndex === i + 1)
  267. .forEach((item) => {
  268. drawTrackList.push(getDrawTrackItem(item));
  269. });
  270. (rawTask.specialTagList || [])
  271. .filter((item) => item.offsetIndex === i + 1)
  272. .forEach((item) => {
  273. drawTrackList.push(getDrawTagTrackItem(item));
  274. });
  275. const answerTags = paserRecogData(i);
  276. drawTrackList.push(...answerTags);
  277. drawTrackList.push(...(markDeailList[i] || []));
  278. drawTrackList.push(getTotalTrack(img));
  279. trackData[i] = {
  280. url: img.url,
  281. width: img.width,
  282. height: img.height,
  283. drawTrackList,
  284. };
  285. }
  286. }
  287. async function drawTask(): Promise<ImageItem[]> {
  288. if (!trackData.length) return [];
  289. const draw = async (item: TrackItemType, index: number) => {
  290. const outpath = getOutputPath('track', index);
  291. const url = await window.api.drawTrack(
  292. item.url,
  293. item.drawTrackList,
  294. outpath
  295. );
  296. return { url, width: item.width, height: item.height };
  297. };
  298. const tasks = trackData.map((item, index) => draw(item, index + 1));
  299. const res = await Promise.all(tasks).catch((error: Error) => {
  300. console.log(error);
  301. addLog(`绘制轨迹错误:${error.toString()}`, 'error');
  302. });
  303. if (!res) {
  304. return Promise.reject(new Error('绘制轨迹失败'));
  305. }
  306. return res;
  307. }
  308. // track ----- start->
  309. const trackTextFontSize = 30;
  310. const trackInfoFontSize = 20;
  311. const trackInfoLineHeight = 20 * 1.2;
  312. function getDrawTrackItem(track: Track): DrawTrackItem {
  313. return {
  314. type: 'text',
  315. option: {
  316. x: track.offsetX,
  317. y: track.offsetY - trackTextFontSize / 2,
  318. text: String(track.score),
  319. color: track.color,
  320. fontSize: trackTextFontSize,
  321. },
  322. };
  323. }
  324. function getDrawTagTrackItem(track: SpecialTag): DrawTrackItem {
  325. if (track.tagType === 'LINE') {
  326. const tagProp = JSON.parse(track.tagName) as {
  327. len: number;
  328. };
  329. return {
  330. type: 'line',
  331. option: {
  332. x0: track.offsetX,
  333. y0: track.offsetY,
  334. x1: track.offsetX + tagProp.len,
  335. y1: track.offsetY,
  336. },
  337. };
  338. }
  339. if (track.tagType === 'CIRCLE') {
  340. const tagProp = JSON.parse(track.tagName) as {
  341. width: number;
  342. height: number;
  343. };
  344. return {
  345. type: 'circle',
  346. option: {
  347. x0: track.offsetX,
  348. y0: track.offsetY,
  349. x1: track.offsetX + tagProp.width,
  350. y1: track.offsetY + tagProp.height,
  351. },
  352. };
  353. }
  354. return {
  355. type: 'text',
  356. option: {
  357. x: track.offsetX,
  358. y: track.offsetY - trackTextFontSize / 2,
  359. text: track.tagName,
  360. color: track.color,
  361. fontSize: trackTextFontSize,
  362. },
  363. };
  364. }
  365. function addHeaderTrackColorAttr(headerTrack: Track[]): Track[] {
  366. return headerTrack.map((item) => {
  367. item.color = colorConfig.head;
  368. return item;
  369. });
  370. }
  371. function addTrackColorAttr(tList: Track[]): Track[] {
  372. let markerIds: string[] = tList.map((v) => v.userId).filter((x) => !!x);
  373. markerIds = Array.from(new Set(markerIds));
  374. // markerIds.sort();
  375. const colorMap: Record<string, string> = {};
  376. for (let i = 0; i < markerIds.length; i++) {
  377. const mId = markerIds[i];
  378. if (i === 0) {
  379. colorMap[mId] = colorConfig.track[0];
  380. } else if (i === 1) {
  381. colorMap[mId] = colorConfig.track[1];
  382. } else if (i > 1) {
  383. colorMap[mId] = colorConfig.track[2];
  384. }
  385. }
  386. type ColorK = keyof typeof colorMap;
  387. tList = tList.map((item: Track) => {
  388. const k = item.userId as ColorK;
  389. item.color = colorMap[k] || 'red';
  390. item.isByMultMark = markerIds.length > 1;
  391. return item;
  392. });
  393. return tList;
  394. }
  395. /*
  396. function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
  397. let markerIds: string[] = tList
  398. .map((v) => String(v.userId))
  399. .filter((x) => !!x);
  400. markerIds = Array.from(new Set(markerIds));
  401. // markerIds.sort();
  402. const colorMap: Record<string, string> = {};
  403. for (let i = 0; i < markerIds.length; i++) {
  404. const mId = markerIds[i];
  405. if (i === 0) {
  406. colorMap[mId] = 'red';
  407. } else if (i === 1) {
  408. colorMap[mId] = 'blue';
  409. } else if (i > 1) {
  410. colorMap[mId] = 'gray';
  411. }
  412. }
  413. type ColorK = keyof typeof colorMap;
  414. tList = tList.map((item: SpecialTag) => {
  415. const k = String(item.userId) as ColorK;
  416. item.color = colorMap[k] || 'red';
  417. item.isByMultMark = markerIds.length > 1;
  418. return item;
  419. });
  420. return tList;
  421. }
  422. */
  423. // track ----- end->
  424. // mark detail ----- start->
  425. // 解析各试题答题区域以及评分
  426. function parseMarkDetailList(images: ImageItem[]): Array<DrawTrackItem[]> {
  427. const dataList: Array<DrawTrackItem[]> = [];
  428. const questions = rawTask.questionList || [];
  429. const fillQues = getFillLines();
  430. let fillQuestions = [] as Question[];
  431. let otherQuestions = questions;
  432. if (Object.keys(fillQues).length) {
  433. const fillQNos = Object.values(fillQues).flat();
  434. fillQuestions = questions.filter((q) =>
  435. fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
  436. );
  437. otherQuestions = questions.filter(
  438. (q) => !fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
  439. );
  440. }
  441. // 填空题:合并所有小题为一个区域
  442. Object.values(fillQues).forEach((qnos) => {
  443. const groupQuestions = fillQuestions.filter((q) =>
  444. qnos.includes(`${q.mainNumber}_${q.subNumber}`)
  445. );
  446. const areas = parseQuestionAreas(groupQuestions);
  447. if (!areas.length) return;
  448. const area = { ...areas[0] };
  449. const imgIndex = area.i - 1;
  450. if (!dataList[imgIndex]) {
  451. dataList[imgIndex] = [];
  452. }
  453. const img = images[imgIndex] as ImageItem;
  454. area.x *= img.width;
  455. area.y *= img.height;
  456. area.w *= img.width;
  457. const dataArr = dataList[imgIndex];
  458. const userMap: UserMapType = {};
  459. const isDoubleMark = !groupQuestions.some((question) => {
  460. const userIds = question.trackList.map((track) => track.userId);
  461. const uids = new Set(userIds);
  462. return uids.size === 1;
  463. });
  464. groupQuestions.forEach((question) => {
  465. question.trackList.forEach((track) => {
  466. if (!userMap[track.userId]) {
  467. userMap[track.userId] = {
  468. userId: track.userId,
  469. userName: track.userName,
  470. color: track.color || '',
  471. scores: [],
  472. };
  473. }
  474. const existUserScore = userMap[track.userId].scores.find(
  475. (s) => s.subNumber === track.subNumber
  476. );
  477. if (existUserScore) {
  478. existUserScore.score += track.score;
  479. } else {
  480. userMap[track.userId].scores.push({
  481. score: track.score,
  482. subNumber: track.subNumber,
  483. });
  484. }
  485. });
  486. });
  487. // 填空题的打分需要自动换行,目前一行只展示最多7个评分
  488. let offsetY = -1 * trackInfoLineHeight;
  489. Object.values(userMap).forEach((user, index) => {
  490. const zhs = ['一', '二', '三'];
  491. const prename = isDoubleMark ? `${zhs[index] || ''}评` : '评卷员';
  492. const userScore = user.scores.map(
  493. (item) => `${item.subNumber}:${item.score}分`
  494. );
  495. const lineScoreCount = 10;
  496. const groupCount = Math.ceil(userScore.length / lineScoreCount);
  497. const groups: string[] = [];
  498. for (let i = 0; i < groupCount; i++) {
  499. groups.push(
  500. userScore
  501. .slice(i * lineScoreCount, (i + 1) * lineScoreCount)
  502. .join(',')
  503. );
  504. }
  505. offsetY += trackInfoLineHeight;
  506. dataArr.push({
  507. type: 'text',
  508. option: {
  509. x: area.x,
  510. y: area.y + offsetY,
  511. text: `${prename}:${user.userName},评分:`,
  512. fontSize: trackInfoFontSize,
  513. color: user.color,
  514. },
  515. });
  516. groups.forEach((group) => {
  517. offsetY += 20;
  518. dataArr.push({
  519. type: 'text',
  520. option: {
  521. x: area.x,
  522. y: area.y + offsetY,
  523. text: group,
  524. fontSize: trackInfoFontSize,
  525. color: user.color,
  526. },
  527. });
  528. });
  529. });
  530. const score = calcSum(groupQuestions.map((item) => item.score || 0));
  531. const maxScore = calcSum(groupQuestions.map((item) => item.maxScore));
  532. const tCont = `得分:${score},满分:${maxScore}`;
  533. const tContLen = strGbLen(tCont) / 2;
  534. dataArr.push({
  535. type: 'text',
  536. option: {
  537. x: area.x + area.w - Math.ceil(tContLen * 30),
  538. y: area.y,
  539. text: tCont,
  540. fontSize: 30,
  541. },
  542. });
  543. });
  544. // 其他试题
  545. otherQuestions.forEach((question) => {
  546. const areas = parseQuestionAreas([question]);
  547. if (!areas.length) return;
  548. const area = { ...areas[0] };
  549. const imgIndex = area.i - 1;
  550. if (!dataList[imgIndex]) {
  551. dataList[imgIndex] = [];
  552. }
  553. const img = images[imgIndex] as ImageItem;
  554. area.x *= img.width;
  555. area.y *= img.height;
  556. area.w *= img.width;
  557. const dataArr = dataList[imgIndex];
  558. const userMap: UserMapType = {};
  559. const isArbitration = Boolean(question.headerTrack?.length);
  560. const tList = isArbitration
  561. ? (question.headerTrack as Track[])
  562. : question.trackList;
  563. tList.forEach((track) => {
  564. if (!userMap[track.userId]) {
  565. userMap[track.userId] = {
  566. userId: track.userId,
  567. userName: track.userName,
  568. color: track.color || '',
  569. scores: [],
  570. };
  571. }
  572. userMap[track.userId].scores.push({
  573. score: track.score,
  574. subNumber: track.subNumber,
  575. });
  576. });
  577. const isDoubleMark = Object.values(userMap).length > 1;
  578. Object.values(userMap).forEach((user, index) => {
  579. const zhs = ['一', '二', '三'];
  580. let prename = '';
  581. if (isArbitration) {
  582. prename = '仲裁';
  583. } else {
  584. prename = isDoubleMark ? `${zhs[index] || ''}评` : '评卷员';
  585. }
  586. const userScore = calcSum(user.scores.map((item) => item.score));
  587. const content = `${prename}:${user.userName},评分:${userScore}`;
  588. dataArr.push({
  589. type: 'text',
  590. option: {
  591. x: area.x,
  592. y: area.y + index * trackInfoLineHeight,
  593. text: content,
  594. fontSize: trackInfoFontSize,
  595. color: user.color,
  596. },
  597. });
  598. });
  599. const tCont = `得分:${question.score},满分:${question.maxScore}`;
  600. const tContLen = strGbLen(tCont) / 2;
  601. dataArr.push({
  602. type: 'text',
  603. option: {
  604. x: area.x + area.w - Math.ceil(tContLen * 30),
  605. y: area.y,
  606. text: tCont,
  607. fontSize: 30,
  608. },
  609. });
  610. });
  611. return dataList;
  612. }
  613. function getTotalTrack(image: ImageItem): DrawTrackItem {
  614. const totalScore = rawTask.markerScore || 0;
  615. const objectiveScore = rawTask.objectiveScore || 0;
  616. const subjectiveScore = totalScore - objectiveScore;
  617. return {
  618. type: 'text',
  619. option: {
  620. x: 0.15 * image.width,
  621. y: 0.01 * image.height,
  622. text: `总分:${totalScore},主观题得分:${subjectiveScore},客观题得分:${objectiveScore}`,
  623. fontSize: 40,
  624. },
  625. };
  626. }
  627. // 获取属于填空题的试题号
  628. function getFillLines() {
  629. const questions: Record<number, string[]> = {};
  630. cardData.forEach((page) => {
  631. page.columns.forEach((column) => {
  632. column.elements.forEach((element) => {
  633. if (element.type !== 'FILL_LINE') return;
  634. if (!questions[element.topicNo]) questions[element.topicNo] = [];
  635. for (let i = 0; i < element.questionsCount; i++) {
  636. questions[element.topicNo].push(
  637. `${element.topicNo}_${element.startNumber + i}`
  638. );
  639. }
  640. });
  641. });
  642. });
  643. return questions;
  644. }
  645. function parseQuestionAreas(questions: QuestionItem[]) {
  646. if (!questions.length || !cardData?.length) return [];
  647. const pictureConfigs: QuestionArea[] = [];
  648. const structs = questions.map(
  649. (item) => `${item.mainNumber}_${item.subNumber}`
  650. );
  651. cardData.forEach((page, pindex) => {
  652. page.exchange.answer_area.forEach((area) => {
  653. const [x, y, w, h] = area.area;
  654. const qStruct = `${area.main_number}_${area.sub_number}`;
  655. const pConfig: QuestionArea = {
  656. i: pindex + 1,
  657. x,
  658. y,
  659. w,
  660. h,
  661. qStruct,
  662. };
  663. if (typeof area.sub_number === 'number') {
  664. if (!structs.includes(qStruct)) return;
  665. pictureConfigs.push(pConfig);
  666. return;
  667. }
  668. // 复合区域处理,比如填空题,多个小题合并为一个区域
  669. if (typeof area.sub_number === 'string') {
  670. const areaStructs = area.sub_number
  671. .split(',')
  672. .map((subNumber) => `${area.main_number}_${subNumber}`);
  673. if (
  674. structs.some((struct) => areaStructs.includes(struct)) &&
  675. !pictureConfigs.find((item) => item.qStruct === qStruct)
  676. ) {
  677. pictureConfigs.push(pConfig);
  678. }
  679. }
  680. });
  681. });
  682. // console.log(pictureConfigs);
  683. // 合并相邻区域
  684. pictureConfigs.sort((a, b) => {
  685. return a.i - b.i || a.x - b.x || a.y - b.y;
  686. });
  687. const combinePictureConfigList: QuestionArea[] = [];
  688. let prevConfig = {} as QuestionArea;
  689. pictureConfigs.forEach((item, index) => {
  690. if (!index) {
  691. prevConfig = { ...item };
  692. combinePictureConfigList.push(prevConfig);
  693. return;
  694. }
  695. const elasticRate = 0.01;
  696. if (
  697. prevConfig.i === item.i &&
  698. prevConfig.y + prevConfig.h + elasticRate >= item.y &&
  699. prevConfig.w === item.w &&
  700. prevConfig.x === item.x
  701. ) {
  702. prevConfig.h = item.y + item.h - prevConfig.y;
  703. } else {
  704. prevConfig = { ...item };
  705. combinePictureConfigList.push(prevConfig);
  706. }
  707. });
  708. // console.log(combinePictureConfigList);
  709. return combinePictureConfigList;
  710. }
  711. // mark detail ----- end->
  712. // answer tag ----- start->
  713. // 解析客观题答案展示位置
  714. function paserRecogData(imageIndex: number): DrawTrackItem[] {
  715. if (!recogDatas.length || !recogDatas[imageIndex]) return [];
  716. const recogData: PaperRecogData = JSON.parse(
  717. window.atob(recogDatas[imageIndex])
  718. );
  719. const answerTags: DrawTrackItem[] = [];
  720. recogData.question.forEach((question) => {
  721. question.fill_result.forEach((result) => {
  722. const fillPositions = result.fill_position.map((pos) => {
  723. return pos.split(',').map((n) => Number(n));
  724. });
  725. const offsetLt = result.fill_size.map((item) => item * 0.4);
  726. const tagLeft =
  727. maxNum(fillPositions.map((pos) => pos[0])) +
  728. result.fill_size[0] -
  729. offsetLt[0];
  730. const tagTop = fillPositions[0][1] - offsetLt[1];
  731. const { answer, isRight } =
  732. answerMap[`${result.main_number}_${result.sub_number}`] || {};
  733. answerTags.push({
  734. type: 'text',
  735. option: {
  736. x: tagLeft,
  737. y: tagTop,
  738. text: answer || '',
  739. color: isRight ? '#05b575' : '#f53f3f',
  740. },
  741. });
  742. });
  743. });
  744. return answerTags;
  745. }
  746. // answer tag ----- end->
  747. return {
  748. runTask,
  749. getTrackTask,
  750. getTrackTaskDetail,
  751. addLog,
  752. };
  753. }