useDraw.ts 36 KB

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