useDraw.ts 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210
  1. import { getStudentTrack } from '@/api/task';
  2. import { Task, Track, SpecialTag, Question, MarkArea } from '@/api/types/task';
  3. import { TrackConfigType } from '@/store/modules/app/types';
  4. import { PictureTypeEnum, PICTURE_TYPE } from '@/constants/enumerate';
  5. import {
  6. calcSumPrecision,
  7. deepCopy,
  8. maxNum,
  9. strGbLen,
  10. toPrecision,
  11. } from '@/utils/utils';
  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<
  16. string,
  17. { answer: string; isRight: boolean; score: number; totalScore: number }
  18. >;
  19. interface TrackItemType {
  20. url: string;
  21. width: number;
  22. height: number;
  23. outpath: string;
  24. drawTrackList: DrawTrackItem[];
  25. }
  26. type ElementType =
  27. | 'FILL_QUESTION'
  28. | 'FILL_LINE'
  29. | 'EXPLAIN'
  30. | 'COMPOSITION'
  31. | 'TOPIC_HEAD'
  32. | 'CARD_HEAD';
  33. interface CardBaseElement {
  34. id: string;
  35. type: ElementType;
  36. topicNo: number;
  37. startNumber: number;
  38. questionsCount: number;
  39. }
  40. interface CardElement extends CardBaseElement {
  41. parent: CardBaseElement;
  42. }
  43. interface CardDataItem {
  44. exchange: {
  45. answer_area: Array<{
  46. main_number: number;
  47. sub_number: number | string;
  48. area: [number, number, number, number];
  49. }>;
  50. fill_area: Array<{
  51. field: 'question' | 'examNumber';
  52. index: number;
  53. single: boolean;
  54. horizontal: boolean;
  55. items: Array<{
  56. main_number: number;
  57. sub_number: number | string;
  58. options: [number, number, number, number][];
  59. }>;
  60. }>;
  61. };
  62. columns: Array<{
  63. elements: CardElement[];
  64. }>;
  65. }
  66. interface CardContentType {
  67. pages: CardDataItem[];
  68. }
  69. interface QuestionItem {
  70. mainNumber: number;
  71. subNumber: number | string;
  72. }
  73. interface QuestionArea {
  74. i: number;
  75. x: number;
  76. y: number;
  77. w: number;
  78. h: number;
  79. qStruct: string;
  80. }
  81. type UserMapType = Record<
  82. string,
  83. {
  84. userId: string;
  85. userName: string;
  86. color: string;
  87. scores: Array<{ subNumber: number; score: number }>;
  88. prename: string;
  89. score: number;
  90. }
  91. >;
  92. interface ImageItem {
  93. url: string;
  94. width: number;
  95. height: number;
  96. }
  97. interface PaperRecogData {
  98. page_index: number;
  99. question: Array<{
  100. index: number;
  101. fill_result: Array<{
  102. main_number: number;
  103. sub_number: number;
  104. single: number;
  105. fill_option: number[];
  106. suspect_flag: number;
  107. fill_position: string[];
  108. fill_size: number[];
  109. }>;
  110. }>;
  111. }
  112. interface DrawConfig {
  113. domain: string;
  114. task: TrackTaskData;
  115. taskDetail: TrackTaskDetailData;
  116. trackConfig: TrackConfigType;
  117. winId: number;
  118. }
  119. export default function useDraw(drawConfig: DrawConfig) {
  120. const { domain, task, taskDetail, trackConfig, winId } = drawConfig;
  121. let answerMap = {} as AnswerMap;
  122. let cardData = [] as CardDataItem[];
  123. let markAreas = [] as MarkArea[];
  124. let recogDatas: string[] = [];
  125. let rawTask = {} as Task;
  126. let trackData = [] as TrackItemType[];
  127. let originImgs = [] as ImageItem[];
  128. let trackFiles = [] as ImageItem[];
  129. let curStudentId = '';
  130. const hasOrigin = trackConfig.pictureType.includes('origin');
  131. const hasTrack = trackConfig.pictureType.includes('track');
  132. const hasPdf = trackConfig.pictureType.includes('pdf');
  133. const defaultColorConfig = {
  134. track: ['red', 'blue', 'gray'],
  135. head: 'green',
  136. };
  137. let colorConfig = { track: ['red', 'blue', 'gray'], head: 'green' };
  138. function updateColorConfig() {
  139. if (trackConfig.trackColorType === 'ALL_RED') {
  140. colorConfig.head = 'red';
  141. colorConfig.track = ['red', 'red', 'red'];
  142. } else {
  143. colorConfig = deepCopy(defaultColorConfig);
  144. }
  145. }
  146. updateColorConfig();
  147. function addLog(content: string, type?: 'info' | 'error') {
  148. window.api.logger(`win:${winId} ${content}`, type);
  149. }
  150. async function runTask() {
  151. initData();
  152. curStudentId = taskDetail.studentId;
  153. addLog(`[${curStudentId}] 01-开始任务`);
  154. let result = true;
  155. try {
  156. await getTaskData(curStudentId);
  157. addLog(`[${curStudentId}] 02-获取任务数据成功`);
  158. originImgs = await downloadImages(rawTask.sheetUrls);
  159. addLog(`[${curStudentId}] 02-1-图片下载成功`);
  160. if (hasTrack || hasPdf) {
  161. await parseDrawList();
  162. addLog(`[${curStudentId}] 03-解析绘制数据成功`);
  163. trackFiles = await drawTask();
  164. addLog(`[${curStudentId}] 04-绘制成功`);
  165. if (hasPdf) {
  166. await window.api.imagesToPdf(trackFiles, getOutputPath('pdf'));
  167. addLog(`[${curStudentId}] 05-生成pdf成功`);
  168. }
  169. }
  170. clearResult();
  171. } catch (error) {
  172. const e = error as Error;
  173. console.log(e);
  174. addLog(
  175. `[${curStudentId}-${rawTask.studentCode}] 08-任务失败,原因:${
  176. e.message || '未知'
  177. }`,
  178. 'error'
  179. );
  180. result = false;
  181. }
  182. const status = result ? 'FINISH' : 'INIT';
  183. await window.db.updateTrackTaskDetailStatus({
  184. id: taskDetail.id,
  185. status,
  186. });
  187. addLog(`[${curStudentId}] 09-任务结束`);
  188. return true;
  189. }
  190. function initData() {
  191. cardData = [] as CardDataItem[];
  192. recogDatas = [] as string[];
  193. rawTask = {} as Task;
  194. trackData = [] as TrackItemType[];
  195. answerMap = {} as AnswerMap;
  196. originImgs = [] as ImageItem[];
  197. trackFiles = [] as ImageItem[];
  198. curStudentId = '';
  199. }
  200. function clearResult() {
  201. if (!hasOrigin) {
  202. window.api.clearFilesSync(originImgs.map((item) => item.url));
  203. }
  204. if (!hasTrack && trackFiles.length) {
  205. window.api.clearFilesSync(trackFiles.map((item) => item.url));
  206. }
  207. }
  208. async function getTaskData(studentId: string) {
  209. const res = await getStudentTrack(studentId);
  210. if (!res?.studentId) return;
  211. rawTask = {
  212. examId: res.examId,
  213. studentId: res.studentId,
  214. secretNumber: res.secretNumber,
  215. courseCode: res.courseCode,
  216. courseName: res.courseName,
  217. paperNumber: res.paperNumber,
  218. studentCode: res.studentCode,
  219. studentName: res.studentName,
  220. paperType: res.paperType,
  221. objectiveScore: res.objectiveScore || 0,
  222. markerScore: (res.objectiveScore || 0) + (res.subjectiveScore || 0),
  223. sheetUrls: res.sheetUrls ? res.sheetUrls.map((item) => item.url) : [],
  224. questionList: res.subjectiveQuestions,
  225. sliceConfig: [],
  226. jsonUrl: '',
  227. markerTime: 0,
  228. };
  229. recogDatas = (res.sheetUrls || []).map((item) => item.recogData);
  230. markAreas = res.subjectiveQuestions.map((item) => {
  231. return {
  232. mainNumber: item.mainNumber,
  233. subNumber: item.subNumber,
  234. questionType: item.questionType,
  235. splitConfig: item.picList || [],
  236. };
  237. });
  238. // 获取客观题选项信息
  239. res.objectiveQuestions.forEach((item) => {
  240. answerMap[`${item.mainNumber}_${item.subNumber}`] = {
  241. answer: item.answer,
  242. totalScore: item.totalScore,
  243. score: item.score,
  244. isRight: item.answer === item.standardAnswer,
  245. };
  246. });
  247. // 获取题卡数据
  248. const cardCont: CardContentType = res.cardContent
  249. ? JSON.parse(res.cardContent)
  250. : { pages: [] };
  251. cardData = cardCont.pages;
  252. }
  253. /**
  254. * 获取文件存储路径,规则:学期/考试/课程/试卷编号/教学班/下载文件类型/学生图片
  255. */
  256. function getOutputPath(type: PictureTypeEnum, index?: number) {
  257. const transfromStr = (str: string) => str.replace(/[*|:?<>]/g, '');
  258. let filename =
  259. trackConfig.studentFileRule === 'CODE_NAME'
  260. ? `${rawTask.studentCode}-${rawTask.studentName}`
  261. : rawTask.studentCode;
  262. filename = transfromStr(filename);
  263. if (index !== undefined) {
  264. filename += `-${index}`;
  265. }
  266. filename += type === 'pdf' ? '.pdf' : '.jpg';
  267. const paths = [
  268. trackConfig.curOutputDir,
  269. transfromStr(task.semesterName),
  270. transfromStr(task.examName),
  271. transfromStr(`${rawTask.courseName}(${rawTask.courseCode})`),
  272. transfromStr(rawTask.paperNumber),
  273. transfromStr(taskDetail.className),
  274. ];
  275. if (trackConfig.pictureType.includes(type)) {
  276. paths.push(PICTURE_TYPE[type]);
  277. } else {
  278. filename = `${type}-${filename}`;
  279. }
  280. paths.push(filename);
  281. return window.api.joinPath(paths);
  282. }
  283. async function downloadImages(urls: string[]) {
  284. const downloads: Promise<ImageItem>[] = [];
  285. for (let i = 0; i < urls.length; i++) {
  286. let url = urls[i];
  287. if (!url.startsWith('http://') && !url.startsWith('https://')) {
  288. url = `${domain}/${url}`;
  289. }
  290. // url += `&t=${Date.now()}`;
  291. downloads.push(
  292. window.api.downloadImage(url, getOutputPath('origin', i + 1))
  293. );
  294. }
  295. const images = await Promise.all(downloads).catch((error) => {
  296. console.log(error);
  297. });
  298. if (!images) {
  299. return Promise.reject(new Error('下载图片失败'));
  300. }
  301. return images;
  302. }
  303. async function parseDrawList() {
  304. trackData = [];
  305. const trackLists = (rawTask.questionList || [])
  306. .map((q) => {
  307. return q.headerTrackList?.length
  308. ? addHeaderTrackColorAttr<Track>(q.headerTrackList)
  309. : addTrackColorAttr<Track>(q.markerTrackList);
  310. })
  311. .flat();
  312. const tagLists = (rawTask.questionList || [])
  313. .map((q) =>
  314. q.headerTagList?.length
  315. ? addHeaderTrackColorAttr<SpecialTag>(q.headerTagList)
  316. : addTrackColorAttr<SpecialTag>(q.markerTagList || [])
  317. )
  318. .flat();
  319. const markDeailList = parseMarkDetailList(originImgs);
  320. const objectiveAnswerTagList = parseObjectiveAnswerTags(originImgs);
  321. for (let i = 0; i < originImgs.length; i++) {
  322. const img = originImgs[i];
  323. const drawTrackList = [] as DrawTrackItem[];
  324. trackLists
  325. .filter((item) => item.offsetIndex === i + 1)
  326. .forEach((item) => {
  327. drawTrackList.push(getDrawTrackItem(item));
  328. });
  329. tagLists
  330. .filter((item) => item.offsetIndex === i + 1)
  331. .forEach((item) => {
  332. drawTrackList.push(getDrawTagTrackItem(item));
  333. });
  334. const answerTags = paserRecogData(i);
  335. drawTrackList.push(...answerTags);
  336. drawTrackList.push(...(markDeailList[i] || []));
  337. const oTags = (objectiveAnswerTagList[i] || []).map(
  338. (tag) => tag.trackItem
  339. );
  340. drawTrackList.push(...oTags);
  341. drawTrackList.push(getTotalTrack(img));
  342. trackData[i] = {
  343. url: img.url,
  344. width: img.width,
  345. height: img.height,
  346. outpath: getOutputPath('track', i + 1),
  347. drawTrackList,
  348. };
  349. }
  350. }
  351. async function drawTask(): Promise<ImageItem[]> {
  352. if (!trackData.length) return [];
  353. await window.api.drawTracks(trackData);
  354. return trackData.map((item) => {
  355. return {
  356. url: item.outpath,
  357. width: item.width,
  358. height: item.height,
  359. };
  360. });
  361. }
  362. // track ----- start->
  363. const trackTextFontSize = 30;
  364. const trackInfoFontSize = 20;
  365. const trackInfoLineHeight = 20 * 1.2;
  366. function getDrawTrackItem(track: Track): DrawTrackItem {
  367. return {
  368. type: 'text',
  369. option: {
  370. x: track.offsetX,
  371. y: track.offsetY - trackTextFontSize / 2,
  372. text: String(track.score),
  373. color: track.color,
  374. fontSize: trackTextFontSize,
  375. },
  376. };
  377. }
  378. function getDrawTagTrackItem(track: SpecialTag): DrawTrackItem {
  379. if (track.tagType === 'LINE') {
  380. const tagProp = JSON.parse(track.tagName) as {
  381. len: number;
  382. };
  383. return {
  384. type: 'line',
  385. option: {
  386. x0: track.offsetX,
  387. y0: track.offsetY,
  388. x1: track.offsetX + tagProp.len,
  389. y1: track.offsetY,
  390. },
  391. };
  392. }
  393. if (track.tagType === 'CIRCLE') {
  394. const tagProp = JSON.parse(track.tagName) as {
  395. width: number;
  396. height: number;
  397. };
  398. return {
  399. type: 'circle',
  400. option: {
  401. x0: track.offsetX,
  402. y0: track.offsetY,
  403. x1: track.offsetX + tagProp.width,
  404. y1: track.offsetY + tagProp.height,
  405. },
  406. };
  407. }
  408. return {
  409. type: 'text',
  410. option: {
  411. x: track.offsetX,
  412. y: track.offsetY - trackTextFontSize / 2,
  413. text: track.tagName,
  414. color: track.color,
  415. fontSize: trackTextFontSize,
  416. },
  417. };
  418. }
  419. function addHeaderTrackColorAttr<T extends { color?: string }>(
  420. headerTrackList: T[]
  421. ): T[] {
  422. return headerTrackList.map((item) => {
  423. item.color = colorConfig.head;
  424. return item;
  425. });
  426. }
  427. function addTrackColorAttr<
  428. T extends {
  429. userId: string;
  430. color?: string;
  431. isByMultMark?: boolean;
  432. }
  433. >(tList: T[]): T[] {
  434. let markerIds: string[] = tList.map((v) => v.userId).filter((x) => !!x);
  435. markerIds = Array.from(new Set(markerIds));
  436. // markerIds.sort();
  437. const colorMap: Record<string, string> = {};
  438. for (let i = 0; i < markerIds.length; i++) {
  439. const mId = markerIds[i];
  440. if (i === 0) {
  441. colorMap[mId] = colorConfig.track[0];
  442. } else if (i === 1) {
  443. colorMap[mId] = colorConfig.track[1];
  444. } else if (i > 1) {
  445. colorMap[mId] = colorConfig.track[2];
  446. }
  447. }
  448. type ColorK = keyof typeof colorMap;
  449. tList = tList.map((item: T) => {
  450. const k = item.userId as ColorK;
  451. item.color = colorMap[k] || 'red';
  452. item.isByMultMark = markerIds.length > 1;
  453. return item;
  454. });
  455. return tList;
  456. }
  457. // track ----- end->
  458. // mark detail ----- start->
  459. // 解析各试题答题区域以及评分
  460. function parseMarkDetailList(images: ImageItem[]): Array<DrawTrackItem[]> {
  461. const dataList: Array<DrawTrackItem[]> = [];
  462. const questions = rawTask.questionList || [];
  463. const fillQues = getFillLines();
  464. let fillQuestions = [] as Question[];
  465. let otherQuestions = questions;
  466. if (Object.keys(fillQues).length) {
  467. const fillQNos = Object.values(fillQues).flat();
  468. fillQuestions = questions.filter((q) =>
  469. fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
  470. );
  471. otherQuestions = questions.filter(
  472. (q) => !fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
  473. );
  474. }
  475. // 填空题:合并所有小题为一个区域
  476. Object.values(fillQues).forEach((qnos) => {
  477. const groupQuestions = fillQuestions.filter((q) =>
  478. qnos.includes(`${q.mainNumber}_${q.subNumber}`)
  479. );
  480. const areas = parseQuestionAreas(groupQuestions);
  481. if (!areas.length) return;
  482. const area = { ...areas[0] };
  483. const imgIndex = area.i - 1;
  484. if (!dataList[imgIndex]) {
  485. dataList[imgIndex] = [];
  486. }
  487. const img = images[imgIndex] as ImageItem;
  488. area.x *= img.width;
  489. area.y *= img.height;
  490. area.w *= img.width;
  491. const dataArr = dataList[imgIndex];
  492. const userMap: UserMapType = {};
  493. const isDoubleMark = !groupQuestions.some((question) => {
  494. let userIds = question.markerTrackList.map((track) => track.userId);
  495. if (
  496. !userIds.length &&
  497. question.markerList &&
  498. question.markerList.length
  499. ) {
  500. userIds = question.markerList
  501. .filter((marker) => !marker.header)
  502. .map((marker) => marker.userId);
  503. }
  504. const uids = new Set(userIds);
  505. return uids.size === 1;
  506. });
  507. groupQuestions.forEach((question) => {
  508. question.markerTrackList.forEach((track) => {
  509. if (!userMap[track.userId]) {
  510. userMap[track.userId] = {
  511. userId: track.userId,
  512. userName: track.userName,
  513. color: track.color || '',
  514. prename: '',
  515. scores: [],
  516. score: 0,
  517. };
  518. }
  519. const existUserScore = userMap[track.userId].scores.find(
  520. (s) => s.subNumber === track.subNumber
  521. );
  522. if (existUserScore) {
  523. existUserScore.score += track.score;
  524. } else {
  525. userMap[track.userId].scores.push({
  526. score: track.score,
  527. subNumber: track.subNumber,
  528. });
  529. }
  530. });
  531. // 普通模式没有轨迹
  532. if (
  533. !question.markerTrackList.length &&
  534. question.markerList &&
  535. question.markerList.length
  536. ) {
  537. question.markerList
  538. .filter((marker) => !marker.header)
  539. .forEach((marker) => {
  540. if (!userMap[marker.userId]) {
  541. userMap[marker.userId] = {
  542. userId: marker.userId,
  543. userName: marker.userName,
  544. color: marker.header ? 'green' : 'red',
  545. prename: '',
  546. scores: [],
  547. score: 0,
  548. };
  549. }
  550. userMap[marker.userId].scores.push({
  551. score: marker.score,
  552. subNumber: question.subNumber,
  553. });
  554. });
  555. }
  556. });
  557. const users = Object.values(userMap).map((user, index) => {
  558. const zhs = ['一', '二', '三'];
  559. const prename = isDoubleMark ? `${zhs[index] || ''}评` : '评卷员';
  560. return {
  561. ...user,
  562. prename,
  563. score: calcSumPrecision(user.scores.map((s) => s.score)),
  564. };
  565. });
  566. // 新增仲裁和复核记录
  567. const headerTrackQuestions = groupQuestions.filter(
  568. (question) => question.headerTrackList?.length
  569. );
  570. if (headerTrackQuestions.length) {
  571. const updateTypeUser = (type: 'ARBITRATED' | 'MARKED') => {
  572. const typeQuestions = headerTrackQuestions.filter(
  573. (question) => question.headerTrackList[0].headerType === type
  574. );
  575. typeQuestions.forEach((question) => {
  576. const scores = question.headerTrackList.map((track) => {
  577. return {
  578. score: track.score,
  579. subNumber: track.subNumber,
  580. };
  581. });
  582. const score = calcSumPrecision(scores.map((s) => s.score));
  583. const user = {
  584. userId: question.headerTrackList[0].userId,
  585. userName: question.headerTrackList[0].userName,
  586. color: 'green',
  587. prename: type === 'ARBITRATED' ? '仲裁' : '复核',
  588. scores,
  589. score,
  590. };
  591. users.push(user);
  592. });
  593. };
  594. // 仲裁记录
  595. updateTypeUser('ARBITRATED');
  596. // 复核记录
  597. updateTypeUser('MARKED');
  598. }
  599. // 填空题的打分需要自动换行,目前一行只展示最多10个评分
  600. let offsetY = -1 * trackInfoLineHeight;
  601. const lineScoreCount = 10;
  602. const groupLineScore = (userScore: string[], color: string) => {
  603. const groupCount = Math.ceil(userScore.length / lineScoreCount);
  604. const groups: string[] = [];
  605. for (let i = 0; i < groupCount; i++) {
  606. groups.push(
  607. userScore
  608. .slice(i * lineScoreCount, (i + 1) * lineScoreCount)
  609. .join(',')
  610. );
  611. }
  612. groups.forEach((group) => {
  613. offsetY += 20;
  614. dataArr.push({
  615. type: 'text',
  616. option: {
  617. x: area.x,
  618. y: area.y + offsetY,
  619. text: group,
  620. fontSize: trackInfoFontSize,
  621. color,
  622. },
  623. });
  624. });
  625. };
  626. users.forEach((user) => {
  627. offsetY += trackInfoLineHeight;
  628. dataArr.push({
  629. type: 'text',
  630. option: {
  631. x: area.x,
  632. y: area.y + offsetY,
  633. text: `${user.prename}:${user.userName},评分:`,
  634. fontSize: trackInfoFontSize,
  635. color: user.color,
  636. },
  637. });
  638. const userScore = user.scores.map(
  639. (item) => `${item.subNumber}:${item.score}分`
  640. );
  641. groupLineScore(userScore, user.color);
  642. });
  643. const score = calcSumPrecision(
  644. groupQuestions.map((item) => item.markerScore || 0)
  645. );
  646. const maxScore = calcSumPrecision(
  647. groupQuestions.map((item) => item.maxScore)
  648. );
  649. const tCont = `得分:${score},满分:${maxScore}`;
  650. const tContLen = strGbLen(tCont) / 2;
  651. dataArr.push({
  652. type: 'text',
  653. option: {
  654. x: area.x + area.w - Math.ceil(tContLen * trackTextFontSize),
  655. y: area.y,
  656. text: tCont,
  657. fontSize: trackTextFontSize,
  658. },
  659. });
  660. });
  661. // 其他试题
  662. otherQuestions.forEach((question) => {
  663. const areas = parseQuestionAreas([question]);
  664. if (!areas.length) return;
  665. const area = { ...areas[0] };
  666. const imgIndex = area.i - 1;
  667. if (!dataList[imgIndex]) {
  668. dataList[imgIndex] = [];
  669. }
  670. const img = images[imgIndex] as ImageItem;
  671. area.x *= img.width;
  672. area.y *= img.height;
  673. area.w *= img.width;
  674. const dataArr = dataList[imgIndex];
  675. const userMap: UserMapType = {};
  676. const hasHeaderTrack = question.headerTrackList?.length;
  677. // 是否仲裁
  678. const isArbitration =
  679. hasHeaderTrack &&
  680. question.headerTrackList[0].headerType === 'ARBITRATED';
  681. // 是否复核
  682. const isReview =
  683. hasHeaderTrack && question.headerTrackList[0].headerType === 'MARKED';
  684. const tList = hasHeaderTrack
  685. ? question.headerTrackList || []
  686. : question.markerTrackList || [];
  687. tList.forEach((track) => {
  688. if (!userMap[track.userId]) {
  689. userMap[track.userId] = {
  690. userId: track.userId,
  691. userName: track.userName,
  692. color: hasHeaderTrack ? 'green' : track.color || 'red',
  693. prename: '',
  694. scores: [],
  695. score: 0,
  696. };
  697. }
  698. userMap[track.userId].scores.push({
  699. score: track.score,
  700. subNumber: track.subNumber,
  701. });
  702. });
  703. // 是否双评,复核或者仲裁只会是一个人
  704. const isDoubleMark = Object.keys(userMap).length > 1;
  705. const zhs = ['一', '二', '三'];
  706. let users = Object.values(userMap).map((user, index) => {
  707. let prename = '';
  708. if (isArbitration) {
  709. prename = '仲裁';
  710. } else if (isReview) {
  711. prename = '复核';
  712. } else {
  713. prename = isDoubleMark ? `${zhs[index] || ''}评` : '评卷员';
  714. }
  715. return {
  716. ...user,
  717. prename,
  718. score: calcSumPrecision(user.scores.map((s) => s.score)),
  719. };
  720. });
  721. // 普通模式没有轨迹
  722. if (!tList.length && question.markerList && question.markerList.length) {
  723. let markers = question.markerList.filter((marker) => marker.header);
  724. if (!markers.length) {
  725. markers = question.markerList.filter((marker) => !marker.header);
  726. }
  727. users = markers.map((item, index) => {
  728. return {
  729. userId: item.userId,
  730. userName: item.userName,
  731. color: item.header ? 'green' : 'red',
  732. prename: markers.length > 1 ? `${zhs[index] || ''}评` : '评卷员',
  733. scores: [],
  734. score: item.score,
  735. };
  736. });
  737. }
  738. users.forEach((user, index) => {
  739. const content = `${user.prename}:${user.userName},评分:${user.score}`;
  740. dataArr.push({
  741. type: 'text',
  742. option: {
  743. x: area.x,
  744. y: area.y + index * trackInfoLineHeight,
  745. text: content,
  746. fontSize: trackInfoFontSize,
  747. color: user.color,
  748. },
  749. });
  750. });
  751. const tCont = `得分:${question.markerScore},满分:${question.maxScore}`;
  752. const tContLen = strGbLen(tCont) / 2;
  753. dataArr.push({
  754. type: 'text',
  755. option: {
  756. x: area.x + area.w - Math.ceil(tContLen * trackTextFontSize),
  757. y: area.y,
  758. text: tCont,
  759. fontSize: trackTextFontSize,
  760. },
  761. });
  762. });
  763. return dataList;
  764. }
  765. function getTotalTrack(image: ImageItem): DrawTrackItem {
  766. const totalScore = rawTask.markerScore || 0;
  767. const objectiveScore = rawTask.objectiveScore || 0;
  768. const subjectiveScore = toPrecision(totalScore - objectiveScore);
  769. return {
  770. type: 'text',
  771. option: {
  772. x: 0.15 * image.width,
  773. y: 0.01 * image.height,
  774. text: `总分:${totalScore},主观题得分:${subjectiveScore},客观题得分:${objectiveScore}`,
  775. fontSize: 40,
  776. },
  777. };
  778. }
  779. // 通过题卡获取属于填空题的试题号
  780. // function getFillLinesFromCard() {
  781. // const questions: Record<number, string[]> = {};
  782. // cardData.forEach((page) => {
  783. // page.columns.forEach((column) => {
  784. // column.elements.forEach((element) => {
  785. // if (element.type !== 'FILL_LINE') return;
  786. // if (!questions[element.topicNo]) questions[element.topicNo] = [];
  787. // for (let i = 0; i < element.questionsCount; i++) {
  788. // questions[element.topicNo].push(
  789. // `${element.topicNo}_${element.startNumber + i}`
  790. // );
  791. // }
  792. // });
  793. // });
  794. // });
  795. // return questions;
  796. // }
  797. // 通过评卷区获取属于填空题的试题号
  798. function getFillLines() {
  799. if (!markAreas?.length) return {};
  800. const questions: Record<number, string[]> = {};
  801. markAreas.forEach((markArea) => {
  802. const { mainNumber, subNumber, questionType } = markArea;
  803. if (questionType !== 4) return;
  804. if (!questions[mainNumber]) questions[mainNumber] = [];
  805. questions[mainNumber].push(`${mainNumber}_${subNumber}`);
  806. });
  807. return questions;
  808. }
  809. // 获取题型的评卷区
  810. function parseQuestionAreas(questions: QuestionItem[]) {
  811. if (!questions.length || !markAreas?.length) return [];
  812. const pictureConfigs: QuestionArea[] = [];
  813. const structs = questions.map(
  814. (item) => `${item.mainNumber}_${item.subNumber}`
  815. );
  816. markAreas.forEach((markArea) => {
  817. const qStruct = `${markArea.mainNumber}_${markArea.subNumber}`;
  818. if (!structs.includes(qStruct)) return;
  819. (markArea.splitConfig || []).forEach((area) => {
  820. pictureConfigs.push({
  821. i: area.i,
  822. x: area.x,
  823. y: area.y,
  824. w: area.w,
  825. h: area.h,
  826. qStruct,
  827. });
  828. });
  829. });
  830. // 合并相邻区域
  831. const combinePictureConfigList = combinePictureConfig(pictureConfigs);
  832. // console.log(combinePictureConfigList);
  833. return combinePictureConfigList;
  834. }
  835. // 通过题卡获取试题评卷区
  836. // function parseCardQuestionAreas(questions: QuestionItem[]) {
  837. // if (!questions.length || !cardData?.length) return [];
  838. // const pictureConfigs: QuestionArea[] = [];
  839. // const structs = questions.map(
  840. // (item) => `${item.mainNumber}_${item.subNumber}`
  841. // );
  842. // cardData.forEach((page, pindex) => {
  843. // page.exchange.answer_area.forEach((area) => {
  844. // const [x, y, w, h] = area.area;
  845. // const qStruct = `${area.main_number}_${area.sub_number}`;
  846. // const pConfig: QuestionArea = {
  847. // i: pindex + 1,
  848. // x,
  849. // y,
  850. // w,
  851. // h,
  852. // qStruct,
  853. // };
  854. // if (typeof area.sub_number === 'number') {
  855. // if (!structs.includes(qStruct)) return;
  856. // pictureConfigs.push(pConfig);
  857. // return;
  858. // }
  859. // // 复合区域处理,比如填空题,多个小题合并为一个区域
  860. // if (typeof area.sub_number === 'string') {
  861. // const areaStructs = area.sub_number
  862. // .split(',')
  863. // .map((subNumber) => `${area.main_number}_${subNumber}`);
  864. // if (
  865. // structs.some((struct) => areaStructs.includes(struct)) &&
  866. // !pictureConfigs.find((item) => item.qStruct === qStruct)
  867. // ) {
  868. // pictureConfigs.push(pConfig);
  869. // }
  870. // }
  871. // });
  872. // });
  873. // // console.log(pictureConfigs);
  874. // // 合并相邻区域
  875. // const combinePictureConfigList: QuestionArea[] =
  876. // combinePictureConfig(pictureConfigs);
  877. // // console.log(combinePictureConfigList);
  878. // return combinePictureConfigList;
  879. // }
  880. function combinePictureConfig(pictureConfigs: QuestionArea[]) {
  881. pictureConfigs.sort((a, b) => {
  882. return a.i - b.i || a.x - b.x || a.y - b.y;
  883. });
  884. const combinePictureConfigList: QuestionArea[] = [];
  885. const elasticRate = 0.01;
  886. let prevConfig = {} as QuestionArea;
  887. pictureConfigs.forEach((item, index) => {
  888. if (!index) {
  889. prevConfig = { ...item };
  890. combinePictureConfigList.push(prevConfig);
  891. return;
  892. }
  893. if (
  894. prevConfig.i === item.i &&
  895. prevConfig.y + prevConfig.h + elasticRate >= item.y &&
  896. prevConfig.w === item.w &&
  897. prevConfig.x === item.x
  898. ) {
  899. prevConfig.h = item.y + item.h - prevConfig.y;
  900. } else {
  901. prevConfig = { ...item };
  902. combinePictureConfigList.push(prevConfig);
  903. }
  904. });
  905. // console.log(combinePictureConfigList);
  906. return combinePictureConfigList;
  907. }
  908. // mark detail ----- end->
  909. // answer tag ----- start->
  910. // 解析客观题答案展示位置
  911. function paserRecogData(imageIndex: number): DrawTrackItem[] {
  912. if (!recogDatas.length || !recogDatas[imageIndex]) return [];
  913. const recogData: PaperRecogData = JSON.parse(
  914. window.atob(recogDatas[imageIndex])
  915. );
  916. const answerTags: DrawTrackItem[] = [];
  917. recogData.question.forEach((question) => {
  918. question.fill_result.forEach((result) => {
  919. const fillPositions = result.fill_position.map((pos) => {
  920. return pos.split(',').map((n) => Number(n));
  921. });
  922. const offsetLt = result.fill_size.map((item) => item * 0.4);
  923. const tagLeft =
  924. maxNum(fillPositions.map((pos) => pos[0])) +
  925. result.fill_size[0] -
  926. offsetLt[0];
  927. const tagTop = fillPositions[0][1] - offsetLt[1];
  928. const { answer, isRight } =
  929. answerMap[`${result.main_number}_${result.sub_number}`] || {};
  930. answerTags.push({
  931. type: 'text',
  932. option: {
  933. x: tagLeft,
  934. y: tagTop,
  935. text: answer || '',
  936. color: isRight ? '#05b575' : '#f53f3f',
  937. },
  938. });
  939. });
  940. });
  941. return answerTags;
  942. }
  943. // answer tag ----- end->
  944. // objective answer tag ----- start->
  945. interface ObjectiveAnswerTagItem {
  946. id: string;
  947. mainNumber: number;
  948. subNumbers: string;
  949. score: number;
  950. totalScore: number;
  951. trackItem: DrawTrackItem;
  952. }
  953. function parseObjectiveAnswerTags(images: ImageItem[]) {
  954. const objectiveAnswerTags: Array<ObjectiveAnswerTagItem[]> = [];
  955. if (!cardData?.length) return objectiveAnswerTags;
  956. cardData.forEach((page, pindex) => {
  957. if (!objectiveAnswerTags[pindex]) objectiveAnswerTags[pindex] = [];
  958. const img = images[pindex] as ImageItem;
  959. page.columns.forEach((column) => {
  960. column.elements.forEach((element) => {
  961. if (element.type !== 'FILL_QUESTION') return;
  962. const ogroup = objectiveAnswerTags.find((tgroup) =>
  963. tgroup.some((oitem) => oitem.id === element.parent.id)
  964. );
  965. if (ogroup) return;
  966. const parent = element.parent;
  967. const oaTagItem: ObjectiveAnswerTagItem = {
  968. id: parent.id,
  969. mainNumber: parent.topicNo,
  970. subNumbers: `${parent.startNumber}~${
  971. parent.startNumber + parent.questionsCount - 1
  972. }`,
  973. score: 0,
  974. totalScore: 0,
  975. trackItem: {} as DrawTrackItem,
  976. };
  977. // 位置
  978. const area = { x: 0, y: 0, w: 0.44 };
  979. page.exchange.fill_area.forEach((fa) => {
  980. fa.items.forEach((fitem) => {
  981. if (
  982. fitem.main_number === oaTagItem.mainNumber &&
  983. fitem.sub_number === parent.startNumber
  984. ) {
  985. const [x, y] = fitem.options[0];
  986. area.x = x;
  987. area.y = y;
  988. }
  989. });
  990. });
  991. area.x = (area.x - 0.015) * img.width;
  992. area.y = (area.y - 0.04) * img.height;
  993. area.w *= img.width;
  994. // 分数统计
  995. const questions: Array<{ score: number; totalScore: number }> = [];
  996. for (let i = 0; i < parent.questionsCount; i++) {
  997. const qans = answerMap[
  998. `${parent.topicNo}_${i + parent.startNumber}`
  999. ] || { score: 0, totalScore: 0 };
  1000. questions[i] = {
  1001. score: qans.score,
  1002. totalScore: qans.totalScore,
  1003. };
  1004. }
  1005. oaTagItem.score = calcSumPrecision(
  1006. questions.map((q) => q.score || 0)
  1007. );
  1008. oaTagItem.totalScore = calcSumPrecision(
  1009. questions.map((q) => q.totalScore || 0)
  1010. );
  1011. const tCont = `得分:${oaTagItem.score},满分:${oaTagItem.totalScore}`;
  1012. const tContLen = strGbLen(tCont) / 2;
  1013. oaTagItem.trackItem = {
  1014. type: 'text',
  1015. option: {
  1016. x: area.x + area.w - Math.ceil(tContLen * trackTextFontSize),
  1017. y: area.y,
  1018. text: tCont,
  1019. fontSize: trackTextFontSize,
  1020. },
  1021. };
  1022. objectiveAnswerTags[pindex].push(oaTagItem);
  1023. });
  1024. });
  1025. });
  1026. return objectiveAnswerTags;
  1027. }
  1028. // objective answer tag ----- end->
  1029. /* 首页汇总信息
  1030. // 获取汇总记录中评卷员信息
  1031. function getSummaryMarkerName(q: Question): string {
  1032. let markerName = '';
  1033. if (q.headerTrackList && q.headerTrackList.length) {
  1034. markerName = q.headerTrackList[0].userName;
  1035. } else if (q.markerTrackList && q.markerTrackList.length) {
  1036. markerName = q.markerTrackList[0].userName;
  1037. } else if (q.markerList && q.markerList.length) {
  1038. let markers = q.markerList.filter((marker) => marker.header);
  1039. if (!markers.length) {
  1040. markers = q.markerList.filter((marker) => !marker.header);
  1041. }
  1042. if (markers.length) markerName = markers[0].userName;
  1043. }
  1044. return markerName;
  1045. }
  1046. // 解析首页汇总信息
  1047. function parseSummaryData(img: ImageItem): DrawTrackItem[] {
  1048. const isDoubleMark = (rawTask.questionList || []).some((question) => {
  1049. let userIds = question.markerTrackList.map((track) => track.userId);
  1050. if (
  1051. !userIds.length &&
  1052. question.markerList &&
  1053. question.markerList.length
  1054. ) {
  1055. userIds = question.markerList
  1056. .filter((marker) => !marker.header)
  1057. .map((marker) => marker.userId);
  1058. }
  1059. const uids = new Set(userIds);
  1060. return uids.size === 2;
  1061. });
  1062. if (isDoubleMark) return [];
  1063. const dataList: DrawTrackItem[] = [];
  1064. const sources: string[][] = [['主观题号', '分数', '评卷员']];
  1065. (rawTask.questionList || []).forEach((q) => {
  1066. sources.push([
  1067. `${q.mainNumber}-${q.subNumber}`,
  1068. `${q.score}`,
  1069. getSummaryMarkerName(q),
  1070. ]);
  1071. });
  1072. const rowX = img.width * 0.05;
  1073. const rowY = img.height * 0.11;
  1074. // const rowW = img.width * 0.45;
  1075. const columnOffsetLeft = [0, 150, 80 + 150];
  1076. sources.forEach((source, sindex) => {
  1077. source.forEach((cont, cindex) => {
  1078. dataList.push({
  1079. type: 'text',
  1080. option: {
  1081. x: rowX + columnOffsetLeft[cindex],
  1082. y: rowY + sindex * trackInfoLineHeight,
  1083. text: cont,
  1084. color: 'red',
  1085. fontSize: trackInfoFontSize,
  1086. },
  1087. });
  1088. });
  1089. });
  1090. return dataList;
  1091. }
  1092. */
  1093. return {
  1094. runTask,
  1095. };
  1096. }