useExam.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. import { EnumQuestionType, EnumReviewMode } from "@/common/enum";
  2. import { getPaper } from '@/api/modules/study';
  3. import { Study } from "@/types";
  4. import { Question } from "@/types/study";
  5. /**
  6. * @description 解码 HTML 实体
  7. * 由于 uv-parse 的 decodeEntity 只支持有限的实体,需要手动解码音标等特殊实体
  8. * 使用手动映射表,兼容所有 uni-app 平台(包括小程序)
  9. */
  10. export const decodeHtmlEntities = (str: string): string => {
  11. if (!str) return str;
  12. // 音标和常用 HTML 实体映射表
  13. const entityMap: Record<string, string> = {
  14. // 音标相关 - 锐音符 (acute)
  15. 'aacute': 'á',
  16. 'eacute': 'é',
  17. 'iacute': 'í',
  18. 'oacute': 'ó',
  19. 'uacute': 'ú',
  20. // 音标相关 - 重音符 (grave)
  21. 'agrave': 'à',
  22. 'egrave': 'è',
  23. 'igrave': 'ì',
  24. 'ograve': 'ò',
  25. 'ugrave': 'ù',
  26. // 音标相关 - 扬抑符 (circumflex)
  27. 'acirc': 'â',
  28. 'ecirc': 'ê',
  29. 'icirc': 'î',
  30. 'ocirc': 'ô',
  31. 'ucirc': 'û',
  32. // 音标相关 - 分音符 (umlaut/diaeresis)
  33. 'auml': 'ä',
  34. 'euml': 'ë',
  35. 'iuml': 'ï',
  36. 'ouml': 'ö',
  37. 'uuml': 'ü',
  38. // 音标相关 - 波浪符 (tilde)
  39. 'ntilde': 'ñ',
  40. 'atilde': 'ã',
  41. 'otilde': 'õ',
  42. // 其他常用实体
  43. 'amp': '&',
  44. 'lt': '<',
  45. 'gt': '>',
  46. 'quot': '"',
  47. 'apos': "'",
  48. 'nbsp': '\u00A0',
  49. 'copy': '©',
  50. 'reg': '®',
  51. 'trade': '™',
  52. 'mdash': '—',
  53. 'ndash': '–',
  54. 'hellip': '…',
  55. // 数学符号
  56. 'times': '×',
  57. 'divide': '÷',
  58. 'plusmn': '±',
  59. };
  60. // 处理命名实体(如 &iacute;)
  61. // 使用 [a-z0-9] 以支持包含数字的实体名称
  62. let result = str.replace(/&([a-z0-9]+);/gi, (match, entity) => {
  63. const lowerEntity = entity.toLowerCase();
  64. if (entityMap[lowerEntity]) {
  65. return entityMap[lowerEntity];
  66. }
  67. return match; // 如果找不到映射,保持原样
  68. });
  69. // 处理数字实体(如 &#237; 或 &#xED;)
  70. result = result.replace(/&#(\d+);/g, (match, num) => {
  71. return String.fromCharCode(parseInt(num, 10));
  72. });
  73. result = result.replace(/&#x([0-9a-f]+);/gi, (match, hex) => {
  74. return String.fromCharCode(parseInt(hex, 16));
  75. });
  76. return result;
  77. }
  78. export const useExam = () => {
  79. const questionTypeDesc: Record<EnumQuestionType, string> = {
  80. [EnumQuestionType.SINGLE_CHOICE]: '单选题',
  81. [EnumQuestionType.MULTIPLE_CHOICE]: '多选题',
  82. [EnumQuestionType.JUDGMENT]: '判断题',
  83. [EnumQuestionType.FILL_IN_THE_BLANK]: '填空题',
  84. [EnumQuestionType.SUBJECTIVE]: '主观题',
  85. [EnumQuestionType.SHORT_ANSWER]: '简答题',
  86. [EnumQuestionType.ESSAY]: '问答题',
  87. [EnumQuestionType.ANALYSIS]: '分析题',
  88. [EnumQuestionType.OTHER]: '阅读题'
  89. }
  90. // 题型顺序
  91. const questionTypeOrder = [
  92. EnumQuestionType.SINGLE_CHOICE,
  93. EnumQuestionType.MULTIPLE_CHOICE,
  94. EnumQuestionType.JUDGMENT,
  95. EnumQuestionType.FILL_IN_THE_BLANK,
  96. EnumQuestionType.SUBJECTIVE,
  97. EnumQuestionType.SHORT_ANSWER,
  98. EnumQuestionType.ESSAY,
  99. EnumQuestionType.ANALYSIS,
  100. EnumQuestionType.OTHER
  101. ];
  102. let interval: NodeJS.Timeout | null = null;
  103. let animationFrameId: number | null = null; // requestAnimationFrame 的 ID
  104. let countStart: number = 0;
  105. let countTime: number = 0;
  106. const countDownCallback = ref<() => void>(() => { });
  107. // 练习计时相关变量
  108. let practiceStartTime: number = 0; // 练习开始时间戳(毫秒)
  109. let practiceAccumulatedTime: number = 0; // 累计的练习时间(秒)
  110. // 练习时长
  111. const practiceDuration = ref<number>(0);
  112. const formatPracticeDuration = computed(() => {
  113. const hours = Math.floor(practiceDuration.value / 3600);
  114. const minutes = Math.floor((practiceDuration.value % 3600) / 60);
  115. const seconds = practiceDuration.value % 60;
  116. if (hours > 0) {
  117. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  118. } else {
  119. return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  120. }
  121. });
  122. // 考试时长
  123. const examDuration = ref<number>(0);
  124. const formatExamDuration = computed(() => {
  125. const hours = Math.floor(examDuration.value / 3600);
  126. const minutes = Math.floor((examDuration.value % 3600) / 60);
  127. const seconds = examDuration.value % 60;
  128. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  129. });
  130. const swiperDuration = ref<number>(300);
  131. const questionList = ref<Study.Question[]>([]);
  132. // 练习设置
  133. const practiceSettings = ref<Study.PracticeSettings>({
  134. reviewMode: EnumReviewMode.AFTER_SUBMIT,
  135. autoNext: false
  136. });
  137. // 收藏列表
  138. const favoriteList = ref<Study.Question[]>([]);
  139. // 不会列表
  140. const notKnowList = ref<Study.Question[]>([]);
  141. // 重点标记列表
  142. const markList = ref<Study.Question[]>([]);
  143. /// 虚拟题目索引,包含子题
  144. const virtualCurrentIndex = ref<number>(0);
  145. /// 虚拟总题量,包含子题
  146. const virtualTotalCount = computed(() => {
  147. return questionList.value.reduce((acc, item) => {
  148. if (item.subQuestions && item.subQuestions.length > 0) {
  149. return acc + item.subQuestions.length;
  150. }
  151. return acc + 1;
  152. }, 0);
  153. });
  154. // 包含状态的问题列表
  155. const stateQuestionList = computed(() => {
  156. const parseQuestion = (qs: Study.Question) => {
  157. if (qs.subQuestions && qs.subQuestions.length > 0) {
  158. qs.isLeaf = false;
  159. qs.subQuestions.forEach((subQs, index) => {
  160. subQs.isDone = isDone(subQs);
  161. subQs.isCorrect = isQuestionCorrect(subQs);
  162. subQs.isNotAnswer = isQuestionNotAnswer(subQs);
  163. subQs.isLeaf = true;
  164. subQs.options.forEach(option => {
  165. option.isCorrect = isOptionCorrect(subQs, option);
  166. option.isSelected = isOptionSelected(subQs, option);
  167. option.isMissed = !option.isSelected && option.isCorrect;
  168. option.isIncorrect = !option.isCorrect && option.isSelected;
  169. });
  170. });
  171. } else {
  172. qs.isSubQuestion = false;
  173. qs.isDone = isDone(qs);
  174. qs.isCorrect = isQuestionCorrect(qs);
  175. qs.isNotAnswer = isQuestionNotAnswer(qs);
  176. qs.isLeaf = true;
  177. qs.options.forEach(option => {
  178. option.isCorrect = isOptionCorrect(qs, option);
  179. option.isSelected = isOptionSelected(qs, option);
  180. option.isMissed = !option.isSelected && option.isCorrect;
  181. option.isIncorrect = !option.isCorrect && option.isSelected;
  182. });
  183. }
  184. return qs;
  185. }
  186. return questionList.value.map((item, index) => {
  187. return parseQuestion(item)
  188. });
  189. });
  190. /// 扁平化题目列表,用于答题卡
  191. const flatQuestionList = computed(() => {
  192. return stateQuestionList.value.flatMap(item => {
  193. if (item.subQuestions && item.subQuestions.length > 0) {
  194. return item.subQuestions.flat();
  195. }
  196. return item;
  197. });
  198. });
  199. /// 按照题型分组,用于答题卡
  200. const groupedQuestionList = computed(() => {
  201. const state = questionTypeOrder.map(type => {
  202. return {
  203. type,
  204. list: [] as {
  205. question: Study.Question;
  206. index: number
  207. }[]
  208. }
  209. });
  210. flatQuestionList.value.forEach((qs, index) => {
  211. let group;
  212. if (qs.isSubQuestion) {
  213. group = state.find(item => item.type === qs.parentTypeId);
  214. } else {
  215. group = state.find(item => item.type === qs.typeId);
  216. }
  217. if (group) {
  218. group.list.push({
  219. question: qs,
  220. index
  221. });
  222. } else {
  223. state.push({
  224. type: qs.typeId,
  225. list: [{
  226. question: qs,
  227. index
  228. }]
  229. });
  230. }
  231. });
  232. return state;
  233. });
  234. /// 是否全部做完
  235. const isAllDone = computed(() => {
  236. return doneCount.value === virtualTotalCount.value;
  237. });
  238. // 当前下标
  239. const currentIndex = ref<number>(0);
  240. // 子题下标
  241. const subQuestionIndex = ref<number>(0);
  242. // 当前题目
  243. const currentQuestion = computed(() => {
  244. return questionList.value[currentIndex.value];
  245. });
  246. // 当前子题
  247. const currentSubQuestion = computed(() => {
  248. if (currentQuestion.value.subQuestions.length === 0) {
  249. return null;
  250. }
  251. if (subQuestionIndex.value >= currentQuestion.value.subQuestions.length) {
  252. return null;
  253. }
  254. // return currentQuestion.value.subQuestions[subQuestionIndex.value];
  255. return currentQuestion.value.subQuestions[currentQuestion.value.activeSubIndex];
  256. });
  257. /// 总题量,不区分子题,等同于接口返回的题目列表
  258. const totalCount = computed(() => {
  259. return questionList.value.length;
  260. });
  261. /// 已做题的数量 --> stateQuestionList
  262. const doneCount = computed(() => {
  263. // 有答案的或者不会做的,都认为是做了
  264. let count = 0;
  265. for (let i = 0; i <= stateQuestionList.value.length - 1; i++) {
  266. const qs = stateQuestionList.value[i];
  267. if (qs.subQuestions && qs.subQuestions.length > 0) {
  268. qs.subQuestions.forEach(subQs => {
  269. if (subQs.isDone) {
  270. count++;
  271. }
  272. });
  273. } else {
  274. if (qs.isDone) {
  275. count++;
  276. }
  277. }
  278. }
  279. return count;
  280. });
  281. /// 未做题的数量
  282. const notDoneCount = computed(() => {
  283. return virtualTotalCount.value - doneCount.value;
  284. });
  285. watch(() => virtualCurrentIndex.value, (newVal, oldVal) => {
  286. updateQuestionDuration(oldVal);
  287. });
  288. // 更新单个题目做题时间
  289. const updateQuestionDuration = (index: number, continueCount = true) => {
  290. const question = flatQuestionList.value[index];
  291. const time = stopCount();
  292. question.duration += time;
  293. // 每次结算后都清空累计时长,避免多次提交或多次 stop 导致重复累加
  294. clearCount();
  295. if (continueCount) {
  296. startCount();
  297. }
  298. }
  299. /// 包含子题的题目计算整体做题进度
  300. const calcProgress = (qs: Study.Question): number => {
  301. if (qs.subQuestions && qs.subQuestions.length > 0) {
  302. return qs.subQuestions.reduce((acc, q) => acc + calcProgress(q), 0) / qs.subQuestions.length;
  303. }
  304. return qs.isDone ? 100 : 0;
  305. }
  306. /// 题目是否做完
  307. const isDone = (qs: Study.Question): boolean => {
  308. if (qs.subQuestions && qs.subQuestions.length > 0) {
  309. return qs.subQuestions.every(q => isDone(q));
  310. }
  311. return (qs.answers && qs.answers.filter(item => !!item).length > 0) || !!qs.isNotKnow;
  312. }
  313. /// 题目是否正确
  314. const isQuestionCorrect = (qs: Study.Question): boolean => {
  315. let { answers, answer1, answer2, typeId } = qs;
  316. answers = answers?.filter(item => !!item) || [];
  317. answer1 = answer1 || '';
  318. answer2 = answer2 || '';
  319. if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(typeId)) {
  320. return answer1.includes(answers[0]);
  321. } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(typeId)) {
  322. return answers.length === answer1.length && answers.every(item => answer1.includes(item));
  323. } else {
  324. // 主观题 A 对 B 错
  325. return answers.includes('A') && !answers.includes('B');
  326. }
  327. };
  328. /// 题目是否未作答
  329. const isQuestionNotAnswer = (qs: Study.Question): boolean => {
  330. if (qs.subQuestions && qs.subQuestions.length > 0) {
  331. return qs.subQuestions.every(q => isQuestionNotAnswer(q));
  332. }
  333. return !qs.answers || qs.answers.filter(item => !!item).length === 0;
  334. }
  335. /// 选项是否正确
  336. const isOptionCorrect = (question: Study.Question, option: Study.QuestionOption) => {
  337. const { answers, answer1, typeId } = question;
  338. if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(typeId)) {
  339. return answer1?.includes(option.no);
  340. } else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(typeId)) {
  341. return answer1?.includes(option.no);
  342. } else {
  343. return answers?.includes(option.no) && option.no === 'A';
  344. }
  345. }
  346. /// 选项是否选中
  347. const isOptionSelected = (question: Study.Question, option: Study.QuestionOption) => {
  348. return question.answers.includes(option.no);
  349. }
  350. // 是否可以切换上一题
  351. const prevEnable = computed(() => {
  352. return currentIndex.value > 0 || currentQuestion.value.activeSubIndex > 0;
  353. });
  354. // 是否可以切换下一题
  355. const nextEnable = computed(() => {
  356. return currentIndex.value < questionList.value.length - 1 || currentQuestion.value.activeSubIndex < currentQuestion.value.subQuestions.length - 1;
  357. });
  358. // 下一题
  359. const nextQuestion = () => {
  360. if (!nextEnable.value) {
  361. return;
  362. }
  363. if (currentQuestion.value.activeSubIndex < currentQuestion.value.subQuestions.length - 1) {
  364. currentQuestion.value.activeSubIndex++;
  365. } else {
  366. currentIndex.value++;
  367. }
  368. }
  369. // 上一题
  370. const prevQuestion = () => {
  371. if (!prevEnable.value) {
  372. return;
  373. }
  374. if (currentQuestion.value.subQuestions && currentQuestion.value.subQuestions.length > 0) {
  375. if (currentQuestion.value.activeSubIndex > 0) {
  376. currentQuestion.value.activeSubIndex--;
  377. } else {
  378. currentIndex.value--;
  379. }
  380. } else {
  381. if (currentIndex.value > 0) {
  382. currentIndex.value--;
  383. }
  384. }
  385. }
  386. // 快速下一题
  387. const nextQuestionQuickly = () => {
  388. if (!nextEnable) {
  389. return;
  390. }
  391. swiperDuration.value = 0;
  392. setTimeout(() => {
  393. nextQuestion();
  394. setTimeout(() => {
  395. swiperDuration.value = 300;
  396. }, 0);
  397. }, 0);
  398. }
  399. // 快速上一题
  400. const prevQuestionQuickly = () => {
  401. if (!prevEnable.value) {
  402. return;
  403. }
  404. swiperDuration.value = 0;
  405. setTimeout(() => {
  406. prevQuestion();
  407. setTimeout(() => {
  408. swiperDuration.value = 300;
  409. }, 0);
  410. }, 0);
  411. }
  412. // 通过下标切换题目
  413. const changeIndex = (index: number, subIndex?: number) => {
  414. swiperDuration.value = 0;
  415. setTimeout(() => {
  416. currentIndex.value = index;
  417. if (!subIndex !== undefined) {
  418. if (subIndex !== undefined) {
  419. questionList.value[index].activeSubIndex = subIndex || 0;
  420. }
  421. }
  422. setTimeout(() => {
  423. swiperDuration.value = 300;
  424. }, 0);
  425. }, 0);
  426. }
  427. // 开始计时
  428. const startTiming = () => {
  429. startCount();
  430. // 记录开始时间戳(毫秒)
  431. if (practiceStartTime === 0) {
  432. practiceStartTime = performance.now();
  433. }
  434. // 使用 requestAnimationFrame 更新显示,更流畅且性能更好
  435. const updatePracticeDuration = () => {
  436. if (practiceStartTime > 0) {
  437. // 计算实际经过的时间(秒)
  438. const elapsed = (performance.now() - practiceStartTime) / 1000;
  439. practiceDuration.value = Math.floor(practiceAccumulatedTime + elapsed);
  440. // 继续下一帧更新
  441. animationFrameId = requestAnimationFrame(updatePracticeDuration);
  442. }
  443. };
  444. // 开始动画帧循环
  445. animationFrameId = requestAnimationFrame(updatePracticeDuration);
  446. }
  447. // 停止计时
  448. const stopTiming = () => {
  449. stopCount();
  450. // 取消动画帧
  451. if (animationFrameId !== null) {
  452. cancelAnimationFrame(animationFrameId);
  453. animationFrameId = null;
  454. }
  455. // 如果正在计时,累加经过的时间
  456. if (practiceStartTime > 0) {
  457. const elapsed = (performance.now() - practiceStartTime) / 1000;
  458. practiceAccumulatedTime += elapsed;
  459. practiceDuration.value = Math.floor(practiceAccumulatedTime);
  460. practiceStartTime = 0;
  461. }
  462. }
  463. const startCount = () => {
  464. if (countStart === 0) {
  465. countStart = performance.now();
  466. }
  467. }
  468. const stopCount = () => {
  469. // 如果当前没有在计时(countStart 为 0),说明已经 stop 过了,直接返回累计时长,避免重复累加
  470. if (countStart === 0) {
  471. return countTime;
  472. }
  473. countTime += (performance.now() - countStart);
  474. countStart = 0;
  475. return countTime;
  476. }
  477. const clearCount = () => {
  478. countTime = 0;
  479. }
  480. // 开始倒计时
  481. const startCountdown = () => {
  482. interval = setInterval(() => {
  483. if (examDuration.value <= 0) {
  484. console.log('停止倒计时')
  485. stopExamDuration();
  486. return;
  487. }
  488. examDuration.value -= 1;
  489. }, 1000);
  490. }
  491. // 停止倒计时
  492. const stopExamDuration = () => {
  493. interval && clearInterval(interval);
  494. interval = null;
  495. countDownCallback.value && countDownCallback.value();
  496. }
  497. const setExamDuration = (duration: number) => {
  498. examDuration.value = duration;
  499. }
  500. const setPracticeDuration = (duration: number) => {
  501. practiceDuration.value = duration;
  502. practiceAccumulatedTime = duration;
  503. practiceStartTime = 0;
  504. }
  505. const setCountDownCallback = (callback: () => void) => {
  506. countDownCallback.value = callback;
  507. }
  508. const setDuration = (duration: number) => {
  509. examDuration.value = duration;
  510. practiceDuration.value = duration;
  511. practiceAccumulatedTime = duration;
  512. practiceStartTime = 0;
  513. }
  514. /// 整理题目结构
  515. const transerQuestions = (arr: Study.Question[]) => {
  516. let offset = 0;
  517. return arr.map((item: Study.Question, index: number) => {
  518. const result = {
  519. ...item,
  520. index: index
  521. };
  522. // 如果有子节点,处理子节点并计算subIndex
  523. if (item.subQuestions && Array.isArray(item.subQuestions) && item.subQuestions.length > 0) {
  524. // 为当前节点设置offset
  525. result.offset = offset;
  526. result.subQuestions = item.subQuestions.map((child, childIndex) => ({
  527. ...child,
  528. subIndex: childIndex,
  529. isSubQuestion: true,
  530. parentId: item.id,
  531. parentTypeId: item.typeId,
  532. parentIndex: index,
  533. index: index,
  534. virtualIndex: index + result.offset + childIndex
  535. }));
  536. // 更新offset,累加当前节点的子节点数量
  537. offset += (item.subQuestions.length - 1);
  538. } else {
  539. // 如果没有子节点,设置offset为当前累计值
  540. result.offset = offset;
  541. result.isSubQuestion = false;
  542. result.virtualIndex = result.index + offset;
  543. }
  544. return result;
  545. });
  546. }
  547. // 将ExamineeQuestion转为Question
  548. const setQuestionList = (list: Study.ExamineeQuestion[]) => {
  549. const orders = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
  550. // 数据预处理
  551. // 1、给每个项目补充额外字段
  552. const transerQuestion = (item: Study.ExamineeQuestion, index: number): Study.Question => {
  553. return {
  554. ...item,
  555. // 处理没有题型的大题,统一作为阅读题
  556. typeId: (item.typeId === null || item.typeId === undefined) ? EnumQuestionType.OTHER : item.typeId,
  557. answers: item.answers || [],
  558. subQuestions: item.subQuestions?.map(transerQuestion) || [],
  559. options: item.options?.map((option, index) => {
  560. // 移除选项编号(如 A.)并解码 HTML 实体(如 &iacute; → í)
  561. const cleanedOption = option.replace(/[A-Z]\./g, '').replace(/\s/g, ' ');
  562. return {
  563. name: decodeHtmlEntities(cleanedOption),
  564. no: orders[index],
  565. id: index,
  566. isAnswer: false,
  567. isCorrect: false,
  568. isSelected: false
  569. } as Study.QuestionOption
  570. }) || [],
  571. totalScore: item.totalScore || 0,
  572. offset: 0,
  573. index: index,
  574. virtualIndex: 0,
  575. duration: 0,
  576. activeSubIndex: 0,
  577. hasSubQuestions: item.subQuestions?.length > 0
  578. } as Study.Question
  579. }
  580. questionList.value = transerQuestions(list.map((item, index) => transerQuestion(item, index)));
  581. }
  582. /// 重置题目状态
  583. const reset = () => {
  584. questionList.value = questionList.value.map(item => {
  585. return {
  586. ...item,
  587. answers: [],
  588. isMark: false,
  589. isNotKnow: false,
  590. hasParsed: false,
  591. showParse: false,
  592. subQuestions: item.subQuestions.map(subItem => {
  593. return {
  594. ...subItem,
  595. answers: [],
  596. isMark: false,
  597. isNotKnow: false,
  598. hasParsed: false,
  599. showParse: false
  600. }
  601. }),
  602. options: item.options.map(option => {
  603. return {
  604. ...option,
  605. isAnswer: false,
  606. isCorrect: false,
  607. isSelected: false,
  608. isMissed: false,
  609. isIncorrect: false
  610. }
  611. })
  612. }
  613. });
  614. changeIndex(0);
  615. practiceDuration.value = 0;
  616. practiceAccumulatedTime = 0;
  617. practiceStartTime = 0;
  618. examDuration.value = 0;
  619. interval && clearInterval(interval);
  620. interval = null;
  621. if (animationFrameId !== null) {
  622. cancelAnimationFrame(animationFrameId);
  623. animationFrameId = null;
  624. }
  625. }
  626. /// 设置子题下标
  627. const setSubQuestionIndex = (index: number) => {
  628. currentQuestion.value.activeSubIndex = index;
  629. }
  630. // 切换阅卷模式
  631. const setPracticeSettings = (settings: Study.PracticeSettings) => {
  632. practiceSettings.value = settings;
  633. }
  634. const submit = () => {
  635. updateQuestionDuration(virtualCurrentIndex.value, false);
  636. }
  637. watch([() => currentIndex.value, () => currentQuestion.value?.activeSubIndex], (val) => {
  638. const qs = questionList.value[val[0]];
  639. virtualCurrentIndex.value = qs.index + qs.offset + val[1];
  640. console.log(virtualCurrentIndex.value, 777)
  641. }, {
  642. immediate: false
  643. });
  644. return {
  645. practiceSettings,
  646. questionList,
  647. groupedQuestionList,
  648. stateQuestionList,
  649. flatQuestionList,
  650. favoriteList,
  651. notKnowList,
  652. markList,
  653. currentIndex,
  654. isAllDone,
  655. totalCount,
  656. virtualCurrentIndex,
  657. virtualTotalCount,
  658. subQuestionIndex,
  659. setSubQuestionIndex,
  660. doneCount,
  661. notDoneCount,
  662. questionTypeDesc,
  663. currentSubQuestion,
  664. prevEnable,
  665. nextEnable,
  666. nextQuestion,
  667. prevQuestion,
  668. nextQuestionQuickly,
  669. prevQuestionQuickly,
  670. swiperDuration,
  671. practiceDuration,
  672. examDuration,
  673. formatExamDuration,
  674. formatPracticeDuration,
  675. startTiming,
  676. stopTiming,
  677. setDuration,
  678. startCountdown,
  679. stopExamDuration,
  680. setExamDuration,
  681. setPracticeDuration,
  682. setCountDownCallback,
  683. setQuestionList,
  684. changeIndex,
  685. reset,
  686. submit,
  687. isQuestionCorrect,
  688. isOptionCorrect,
  689. setPracticeSettings,
  690. }
  691. }