exam-start.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. <template>
  2. <ie-page :fix-height="true" :safe-area-inset-bottom="false">
  3. <block v-if="isReady">
  4. <exam-navbar :total-exam-time="totalExamTime" @left-click="handleLeftClick" @right-click="handleRightClick" />
  5. <exam-subtitle />
  6. <exam-swiper @submit="beforeSubmit" />
  7. <exam-toolbar ref="examToolbarRef" @submit="beforeSubmit" />
  8. </block>
  9. </ie-page>
  10. <fast-guide v-model:show="guideShow" :list="guideList" v-model:index="guideIndex" @close="handleGuideClose"
  11. @getQuery="getQuery"></fast-guide>
  12. <question-swiper-tip :visible="showSwiperTip" @next="handleSwiperTipNext" />
  13. <exam-mode ref="examModeRef" />
  14. </template>
  15. <script lang="ts" setup>
  16. import ExamNavbar from './components/exam-navbar.vue';
  17. import ExamSubtitle from './components/exam-subtitle.vue';
  18. import ExamSwiper from './components/exam-swiper.vue';
  19. import ExamToolbar from './components/exam-toolbar.vue';
  20. import ExamMode from './components/exam-mode.vue';
  21. import QuestionSwiperTip from './components/question-swiper-tip.vue';
  22. import { useTransferPage } from '@/hooks/useTransferPage';
  23. import { useUserStore } from '@/store/userStore';
  24. import { EnumPaperType, EnumQuestionType } from '@/common/enum';
  25. import { getOpenExaminee, getPaper, commitExamineePaper, beginExaminee, getExamineeResult } from '@/api/modules/study';
  26. import { useExam } from '@/composables/useExam';
  27. import { Study, Transfer } from '@/types';
  28. import {
  29. EXAM_AUTO_SUBMIT,
  30. EXAM_PAGE_OPTIONS,
  31. EXAM_DATA
  32. } from '@/types/injectionSymbols';
  33. const userStore = useUserStore();
  34. // import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
  35. const { prevData, transferBack, transferTo } = useTransferPage<Transfer.ExamAnalysisPageOptions, {}>();
  36. const examData = useExam();
  37. const { setQuestionList, questionList, flatQuestionList, setSubQuestionIndex,
  38. notDoneCount, isAllDone,
  39. practiceDuration, startTiming, stopTiming, submit, changeIndex, setDuration } = examData;
  40. //
  41. const showSwiperTip = ref(false);
  42. const guideShow = ref(false);
  43. const guideList = ref([
  44. {
  45. ref: 'examToolbarRef',
  46. target: '#question-calendar-btn',
  47. position: 'top',
  48. msg: '[答题卡]\n查看答题卡,掌握考试进度'
  49. },
  50. {
  51. ref: 'examToolbarRef',
  52. target: '#question-favorite-btn',
  53. position: 'top',
  54. msg: '[题目收藏]\n收藏的题目可以在收藏夹查看'
  55. },
  56. {
  57. ref: 'examToolbarRef',
  58. target: '#question-mark-btn',
  59. position: 'top',
  60. msg: '[题目标记]\n标记的题目可以在答题卡中快速找到'
  61. },
  62. {
  63. ref: 'examToolbarRef',
  64. target: '#question-correct-btn',
  65. position: 'top',
  66. msg: '[试题纠错]\n点击试题纠错,帮助我们改进题目'
  67. }
  68. ]);
  69. const guideIndex = ref(0);
  70. const isReady = ref(false);
  71. // 考试规定时间
  72. const totalExamTime = ref<number>(0);
  73. // 自动提交只提醒1次
  74. const hasShowSubmitConfirm = ref(false);
  75. const examineeId = ref<number | undefined>(undefined);
  76. const paperData = ref<Study.ExamPaper>({} as Study.ExamPaper);
  77. // 是否确认退出
  78. const confirmQuit = ref(false);
  79. const confirmShowing = ref(false);
  80. /**
  81. * 自动提交
  82. */
  83. const autoSubmit = () => {
  84. if (hasShowSubmitConfirm.value) {
  85. return;
  86. }
  87. hasShowSubmitConfirm.value = true;
  88. beforeSubmit();
  89. }
  90. provide(EXAM_PAGE_OPTIONS, prevData.value);
  91. provide(EXAM_DATA, examData);
  92. provide(EXAM_AUTO_SUBMIT, autoSubmit);
  93. const isPracticeExam = computed(() => {
  94. return prevData.value.paperType === EnumPaperType.PRACTICE || prevData.value.paperType === EnumPaperType.COURSE;
  95. });
  96. const isSimulationExam = computed(() => {
  97. // prevData.value
  98. return prevData.value.paperType === EnumPaperType.SIMULATED;
  99. });
  100. const isTestExam = computed(() => {
  101. return prevData.value.paperType === EnumPaperType.TEST;
  102. });
  103. const isReadOnly = computed(() => {
  104. return prevData.value.readonly || false;
  105. });
  106. const handleLeftClick = () => {
  107. if (!isReady.value || isReadOnly.value) {
  108. confirmQuit.value = true;
  109. transferBack();
  110. return;
  111. }
  112. beforeQuit();
  113. };
  114. const examModeRef = ref();
  115. const handleRightClick = () => {
  116. examModeRef.value.open();
  117. }
  118. const instance = getCurrentInstance();
  119. const getQuery = (fn: () => void) => {
  120. fn.call(instance?.proxy)
  121. }
  122. const beforeQuit = () => {
  123. const { paperType } = prevData.value;
  124. if (!isReady.value || isReadOnly.value) {
  125. return;
  126. }
  127. stopTime();
  128. const msg = paperType === EnumPaperType.PRACTICE ? '当前练习未完成,确认退出?' : '当前考试未完成,确认退出?';
  129. confirmShowing.value = true;
  130. uni.$ie.showModal({
  131. title: '提示',
  132. content: msg,
  133. }).then(confirm => {
  134. if (confirm) {
  135. handleSubmit(true);
  136. } else {
  137. confirmQuit.value = false;
  138. confirmShowing.value = false;
  139. startTime();
  140. }
  141. });
  142. };
  143. const startTime = () => {
  144. startTiming();
  145. }
  146. const stopTime = () => {
  147. stopTiming();
  148. }
  149. const beforeSubmit = async () => {
  150. const text = notDoneCount.value > 0 ? `还有${notDoneCount.value}题未做,确认交卷?` : '是否确认交卷?';
  151. stopTime();
  152. uni.$ie.showModal({
  153. title: '提示',
  154. content: text,
  155. }).then(confirm => {
  156. if (confirm) {
  157. handleSubmit(false);
  158. } else {
  159. startTime();
  160. }
  161. });
  162. }
  163. /**
  164. * 提交试卷
  165. * @param tempSave 是否临时保存
  166. */
  167. const handleSubmit = (tempSave: boolean = false) => {
  168. // 执行完后续逻辑
  169. submit();
  170. const msg = tempSave ? '保存中...' : '提交中...';
  171. uni.$ie.showLoading(msg);
  172. setTimeout(async () => {
  173. const params: Study.ExamPaperSubmit = {
  174. ...paperData.value,
  175. questions: questionList.value.map(item => {
  176. return {
  177. ...item,
  178. title: '',
  179. isDone: tempSave ? item.isDone : true,
  180. subQuestions: item.subQuestions.map(subItem => {
  181. return {
  182. ...subItem,
  183. title: '',
  184. };
  185. })
  186. };
  187. }),
  188. examineeId: examineeId.value,
  189. // examineeId: examineerData.value.examineeId,
  190. isDone: tempSave ? isAllDone.value : true,
  191. duration: practiceDuration.value
  192. };
  193. console.log('提交试卷参数', params)
  194. await commitExamineePaper(params);
  195. if (isSimulationExam.value || isTestExam.value) {
  196. if (!tempSave) {
  197. setTimeout(async () => {
  198. uni.$ie.hideLoading();
  199. await nextTick();
  200. confirmQuit.value = true;
  201. confirmShowing.value = false;
  202. transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
  203. data: {
  204. examineeId: examineeId.value,
  205. paperType: prevData.value.paperType
  206. } as Transfer.SimulationAnalysisPageOptions,
  207. type: 'redirectTo'
  208. });
  209. }, 2500);
  210. } else {
  211. uni.$ie.hideLoading();
  212. confirmQuit.value = true;
  213. confirmShowing.value = false;
  214. nextTick(() => {
  215. transferBack();
  216. });
  217. }
  218. } else if (isPracticeExam.value) {
  219. if (!tempSave) {
  220. setTimeout(async () => {
  221. uni.$ie.hideLoading();
  222. await nextTick();
  223. confirmQuit.value = true;
  224. confirmShowing.value = false;
  225. transferTo('/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail', {
  226. data: {
  227. paperType: prevData.value.paperType,
  228. examineeId: examineeId.value,
  229. name: prevData.value.practiceInfo?.name,
  230. directed: prevData.value.practiceInfo?.directed,
  231. questionType: prevData.value.practiceInfo?.questionType
  232. } as Transfer.PracticeResultPageOptions,
  233. type: 'redirectTo'
  234. });
  235. }, 2500);
  236. } else {
  237. uni.$ie.hideLoading();
  238. confirmQuit.value = true;
  239. confirmShowing.value = false;
  240. nextTick(() => {
  241. transferBack();
  242. });
  243. }
  244. }
  245. }, 300);
  246. }
  247. /**
  248. * 恢复上次做题历史数据
  249. * @param savedQuestion 上次做题历史数据
  250. * @param fullQuestion 当前试卷数据
  251. */
  252. const restoreQuestion = (savedQuestion: Study.ExamineeQuestion[], fullQuestion: Study.ExamineeQuestion[]) => {
  253. if (!savedQuestion) {
  254. return fullQuestion;
  255. }
  256. for (let index = 0; index < fullQuestion.length; index++) {
  257. const item = fullQuestion[index];
  258. const savedQs = savedQuestion[index]
  259. if (savedQs) {
  260. if (savedQs.answers) {
  261. item.answers = savedQs.answers.filter(ans => ans.trim());
  262. }
  263. item.answer1 = savedQs.answer1;
  264. item.answer2 = savedQs.answer2;
  265. item.isMark = savedQs.isMark;
  266. item.isFavorite = savedQs.isFavorite;
  267. item.isNotKnow = savedQs.isNotKnow;
  268. item.parse = savedQs.parse;
  269. item.totalScore = savedQs.totalScore;
  270. if (item.subQuestions) {
  271. restoreQuestion(savedQs.subQuestions, item.subQuestions);
  272. }
  273. }
  274. }
  275. return fullQuestion;
  276. }
  277. // 1、加载知识点练习数据
  278. const loadPracticeData = async () => {
  279. const { paperType, readonly, practiceInfo } = prevData.value;
  280. let data: Study.Examinee | null = null;
  281. if (readonly) {
  282. if (practiceInfo?.examineeId) {
  283. const res = await getExamineeResult(practiceInfo.examineeId);
  284. data = res.data;
  285. }
  286. } else {
  287. const params = {
  288. paperType: paperType,
  289. relateId: practiceInfo?.relateId,
  290. } as Study.OpenExamineeRequestDTO;
  291. if (userStore.isVHS) {
  292. params.questionType = practiceInfo?.questionType;
  293. } else {
  294. params.directed = practiceInfo?.directed || false;
  295. }
  296. const res = await getOpenExaminee(params);
  297. data = res.data || {};
  298. }
  299. if (!data) {
  300. uni.$ie.hideLoading();
  301. transferBack();
  302. return;
  303. }
  304. // 练习没有规定时间,设置为最大值
  305. totalExamTime.value = Number.MAX_SAFE_INTEGER;
  306. combinePaperData(data, paperType);
  307. }
  308. // 2、加载模拟考试数据
  309. const loadExamData = async () => {
  310. const { paperType, readonly, simulationInfo } = prevData.value;
  311. let data: Study.Examinee;
  312. if (simulationInfo?.examineeId) {
  313. if (readonly) {
  314. const res = await getExamineeResult(simulationInfo.examineeId);
  315. data = res.data;
  316. } else {
  317. const res = await beginExaminee(simulationInfo.examineeId);
  318. data = res.data || {};
  319. }
  320. if (!data) {
  321. uni.$ie.hideLoading();
  322. transferBack();
  323. return;
  324. }
  325. totalExamTime.value = data.paperInfo?.time || 0;
  326. combinePaperData(data, paperType);
  327. }
  328. }
  329. // 3、加载对口升学试卷数据
  330. // const loadVHSPaperData = async () => {
  331. // const { paperType, readonly, simulationInfo } = prevData.value;
  332. // let data: Study.Examinee;
  333. // if (simulationInfo?.examineeId) {
  334. // if (readonly) {
  335. // const res = await getExamineeResult(simulationInfo.examineeId);
  336. // data = res.data;
  337. // } else {
  338. // const params = {
  339. // paperType: paperType,
  340. // relateId: simulationInfo?.examineeId,
  341. // } as Study.OpenExamineeRequestDTO;
  342. // const res = await getOpenExaminee(params);
  343. // data = res.data || {};
  344. // }
  345. // if (!data) {
  346. // uni.$ie.hideLoading();
  347. // transferBack();
  348. // return;
  349. // }
  350. // totalExamTime.value = data.paperInfo?.time || Number.MAX_SAFE_INTEGER;
  351. // combinePaperData(data, paperType);
  352. // }
  353. // }
  354. const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperType) => {
  355. examineeId.value = examinee.examineeId;
  356. if (examinee.paperId) {
  357. const res = await getPaper({
  358. type: paperType,
  359. id: examinee.paperId
  360. });
  361. paperData.value = res.data;
  362. paperData.value.questions = restoreQuestion(examinee.questions, res.data.questions);
  363. console.log('初始化数据', paperData.value.questions)
  364. setQuestionList(paperData.value.questions);
  365. setDuration(examinee.duration || 0);
  366. await nextTick();
  367. const targetQuestion = flatQuestionList.value.find(item => item.id === prevData.value.questionId);
  368. if (targetQuestion) {
  369. changeIndex(targetQuestion.index);
  370. } else {
  371. changeIndex(0);
  372. }
  373. setTimeout(() => {
  374. if (targetQuestion?.isSubQuestion) {
  375. setSubQuestionIndex(targetQuestion.subIndex || 0);
  376. }
  377. }, 50);
  378. await new Promise(resolve => setTimeout(resolve, 50));
  379. await nextTick();
  380. // 读取用户练习设置
  381. // setPracticeSettings(userStore.practiceSettings);
  382. isReady.value = true;
  383. console.log('试卷信息', res)
  384. if (!userStore.isExamGuideShow) {
  385. setTimeout(() => {
  386. uni.$ie.hideLoading();
  387. setTimeout(() => {
  388. showSwiperTip.value = true;
  389. }, 100);
  390. }, 300);
  391. } else {
  392. uni.$ie.hideLoading();
  393. if (!isReadOnly.value) {
  394. startTime();
  395. }
  396. }
  397. }
  398. }
  399. const handleSwiperTipNext = () => {
  400. showSwiperTip.value = false;
  401. guideShow.value = true;
  402. }
  403. const handleGuideClose = () => {
  404. userStore.isExamGuideShow = true;
  405. if (!isReadOnly.value) {
  406. startTime();
  407. }
  408. }
  409. const loadData = async () => {
  410. uni.$ie.showLoading();
  411. const { paperType } = prevData.value;
  412. if (paperType === EnumPaperType.PRACTICE || paperType === EnumPaperType.COURSE) {
  413. loadPracticeData();
  414. } else if (paperType === EnumPaperType.SIMULATED || paperType === EnumPaperType.TEST) {
  415. // if (paperType === EnumPaperType.SIMULATED && userStore.isVHS) {
  416. // loadVHSPaperData();
  417. // } else {
  418. // loadExamData();
  419. // }
  420. loadExamData();
  421. }
  422. };
  423. onLoad(() => {
  424. console.log(prevData.value)
  425. loadData();
  426. uni.addInterceptor('navigateBack', {
  427. invoke: (e) => {
  428. if (confirmShowing.value) {
  429. return false;
  430. }
  431. if (confirmQuit.value) {
  432. return e;
  433. }
  434. handleLeftClick();
  435. return false;
  436. }
  437. })
  438. });
  439. onUnload(() => {
  440. uni.removeInterceptor('navigateBack');
  441. });
  442. </script>
  443. <style lang="scss" scoped></style>