exam-start.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  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 @submit="beforeSubmit" />
  8. </block>
  9. </ie-page>
  10. <fast-guide v-model:show="guideShow" :list="guideList" v-model:index="guideIndex"
  11. @close="handleGuideClose"></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. import { useAppStore } from '@/store/appStore';
  34. import { useEnv } from '@/hooks/useEnv';
  35. const appStore = useAppStore();
  36. const { platform } = useEnv();
  37. const userStore = useUserStore();
  38. // import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
  39. const { prevData, transferBack, transferTo } = useTransferPage<Transfer.ExamAnalysisPageOptions, {}>();
  40. const examData = useExam();
  41. const { setQuestionList, questionList, flatQuestionList, setSubQuestionIndex,
  42. notDoneCount, isAllDone,
  43. practiceDuration, startTiming, stopTiming, submit, changeIndex, setDuration } = examData;
  44. //
  45. const showSwiperTip = ref(false);
  46. const guideShow = ref(false);
  47. const guideList = ref([
  48. {
  49. target: '#question-calendar-btn',
  50. position: 'top',
  51. msg: '[答题卡]\n查看答题卡,掌握考试进度'
  52. },
  53. {
  54. target: '#question-favorite-btn',
  55. position: 'top',
  56. msg: '[题目收藏]\n收藏的题目可以在收藏夹查看'
  57. },
  58. {
  59. target: '#question-mark-btn',
  60. position: 'top',
  61. msg: '[题目标记]\n标记的题目可以在答题卡中快速找到'
  62. },
  63. {
  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 beforeQuit = () => {
  119. const { paperType } = prevData.value;
  120. if (!isReady.value || isReadOnly.value) {
  121. return;
  122. }
  123. stopTime();
  124. const msg = paperType === EnumPaperType.PRACTICE ? '当前练习未完成,确认退出?' : '当前考试未完成,确认退出?';
  125. confirmShowing.value = true;
  126. uni.$ie.showModal({
  127. title: '提示',
  128. content: msg,
  129. }).then(confirm => {
  130. if (confirm) {
  131. handleSubmit(true);
  132. } else {
  133. confirmQuit.value = false;
  134. confirmShowing.value = false;
  135. startTime();
  136. }
  137. });
  138. };
  139. const startTime = () => {
  140. startTiming();
  141. }
  142. const stopTime = () => {
  143. stopTiming();
  144. }
  145. const beforeSubmit = async () => {
  146. const text = notDoneCount.value > 0 ? `还有${notDoneCount.value}题未做,确认交卷?` : '是否确认交卷?';
  147. stopTime();
  148. uni.$ie.showModal({
  149. title: '提示',
  150. content: text,
  151. }).then(confirm => {
  152. if (confirm) {
  153. handleSubmit(false);
  154. } else {
  155. startTime();
  156. }
  157. });
  158. }
  159. /**
  160. * 提交试卷
  161. * @param tempSave 是否临时保存
  162. */
  163. const handleSubmit = (tempSave: boolean = false) => {
  164. // 执行完后续逻辑
  165. submit();
  166. const msg = tempSave ? '保存中...' : '提交中...';
  167. uni.$ie.showLoading(msg);
  168. setTimeout(async () => {
  169. const params: Study.ExamPaperSubmit = {
  170. ...paperData.value,
  171. questions: questionList.value.map(item => {
  172. return {
  173. ...item,
  174. title: '',
  175. isDone: tempSave ? item.isDone : true,
  176. subQuestions: item.subQuestions.map(subItem => {
  177. return {
  178. ...subItem,
  179. title: '',
  180. };
  181. })
  182. };
  183. }),
  184. examineeId: examineeId.value,
  185. // examineeId: examineerData.value.examineeId,
  186. isDone: tempSave ? isAllDone.value : true,
  187. duration: practiceDuration.value
  188. };
  189. console.log('提交试卷参数', params)
  190. await commitExamineePaper(params);
  191. if (isSimulationExam.value || isTestExam.value) {
  192. if (!tempSave) {
  193. setTimeout(async () => {
  194. uni.$ie.hideLoading();
  195. await nextTick();
  196. confirmQuit.value = true;
  197. confirmShowing.value = false;
  198. transferTo('/pagesStudy/pages/simulation-analysis/simulation-analysis', {
  199. data: {
  200. examineeId: examineeId.value,
  201. paperType: prevData.value.paperType
  202. } as Transfer.SimulationAnalysisPageOptions,
  203. type: 'redirectTo'
  204. });
  205. }, 2500);
  206. } else {
  207. uni.$ie.hideLoading();
  208. confirmQuit.value = true;
  209. confirmShowing.value = false;
  210. nextTick(() => {
  211. transferBack();
  212. });
  213. }
  214. } else if (isPracticeExam.value) {
  215. if (!tempSave) {
  216. setTimeout(async () => {
  217. uni.$ie.hideLoading();
  218. await nextTick();
  219. confirmQuit.value = true;
  220. confirmShowing.value = false;
  221. transferTo('/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail', {
  222. data: {
  223. paperType: prevData.value.paperType,
  224. examineeId: examineeId.value,
  225. name: prevData.value.practiceInfo?.name,
  226. directed: prevData.value.practiceInfo?.directed,
  227. questionType: prevData.value.practiceInfo?.questionType
  228. } as Transfer.PracticeResultPageOptions,
  229. type: 'redirectTo'
  230. });
  231. }, 2500);
  232. } else {
  233. uni.$ie.hideLoading();
  234. confirmQuit.value = true;
  235. confirmShowing.value = false;
  236. nextTick(() => {
  237. transferBack();
  238. });
  239. }
  240. }
  241. }, 300);
  242. }
  243. /**
  244. * 恢复上次做题历史数据
  245. * @param savedQuestion 上次做题历史数据
  246. * @param fullQuestion 当前试卷数据
  247. */
  248. const restoreQuestion = (savedQuestion: Study.ExamineeQuestion[], fullQuestion: Study.ExamineeQuestion[]) => {
  249. if (!savedQuestion) {
  250. return fullQuestion;
  251. }
  252. for (let index = 0; index < fullQuestion.length; index++) {
  253. const item = fullQuestion[index];
  254. const savedQs = savedQuestion[index]
  255. if (savedQs) {
  256. if (savedQs.answers) {
  257. item.answers = savedQs.answers.filter(ans => ans.trim());
  258. }
  259. item.answer1 = savedQs.answer1;
  260. item.answer2 = savedQs.answer2;
  261. item.isMark = savedQs.isMark;
  262. item.isFavorite = savedQs.isFavorite;
  263. item.isNotKnow = savedQs.isNotKnow;
  264. item.parse = savedQs.parse;
  265. item.totalScore = savedQs.totalScore;
  266. if (item.subQuestions) {
  267. restoreQuestion(savedQs.subQuestions, item.subQuestions);
  268. }
  269. }
  270. }
  271. return fullQuestion;
  272. }
  273. // 1、加载知识点练习数据
  274. const loadPracticeData = async () => {
  275. const { paperType, readonly, practiceInfo } = prevData.value;
  276. let data: Study.Examinee | null = null;
  277. if (readonly) {
  278. if (practiceInfo?.examineeId) {
  279. const res = await getExamineeResult(practiceInfo.examineeId);
  280. data = res.data;
  281. }
  282. } else {
  283. const params = {
  284. paperType: paperType,
  285. relateId: practiceInfo?.relateId,
  286. } as Study.OpenExamineeRequestDTO;
  287. if (userStore.isVHS) {
  288. params.questionType = practiceInfo?.questionType;
  289. } else {
  290. params.directed = practiceInfo?.directed || false;
  291. }
  292. const res = await getOpenExaminee(params);
  293. data = res.data || {};
  294. }
  295. if (!data) {
  296. uni.$ie.hideLoading();
  297. transferBack();
  298. return;
  299. }
  300. // 练习没有规定时间,设置为最大值
  301. totalExamTime.value = Number.MAX_SAFE_INTEGER;
  302. combinePaperData(data, paperType);
  303. }
  304. // 2、加载模拟考试数据
  305. const loadExamData = async () => {
  306. const { paperType, readonly, simulationInfo } = prevData.value;
  307. let data: Study.Examinee;
  308. if (simulationInfo?.examineeId) {
  309. if (readonly) {
  310. const res = await getExamineeResult(simulationInfo.examineeId);
  311. data = res.data;
  312. } else {
  313. const res = await beginExaminee(simulationInfo.examineeId);
  314. data = res.data || {};
  315. }
  316. if (!data) {
  317. uni.$ie.hideLoading();
  318. transferBack();
  319. return;
  320. }
  321. totalExamTime.value = data.paperInfo?.time || 0;
  322. combinePaperData(data, paperType);
  323. }
  324. }
  325. // 3、加载对口升学试卷数据
  326. // const loadVHSPaperData = async () => {
  327. // const { paperType, readonly, simulationInfo } = prevData.value;
  328. // let data: Study.Examinee;
  329. // if (simulationInfo?.examineeId) {
  330. // if (readonly) {
  331. // const res = await getExamineeResult(simulationInfo.examineeId);
  332. // data = res.data;
  333. // } else {
  334. // const params = {
  335. // paperType: paperType,
  336. // relateId: simulationInfo?.examineeId,
  337. // } as Study.OpenExamineeRequestDTO;
  338. // const res = await getOpenExaminee(params);
  339. // data = res.data || {};
  340. // }
  341. // if (!data) {
  342. // uni.$ie.hideLoading();
  343. // transferBack();
  344. // return;
  345. // }
  346. // totalExamTime.value = data.paperInfo?.time || Number.MAX_SAFE_INTEGER;
  347. // combinePaperData(data, paperType);
  348. // }
  349. // }
  350. const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperType) => {
  351. examineeId.value = examinee.examineeId;
  352. if (examinee.paperId) {
  353. const res = await getPaper({
  354. type: paperType,
  355. id: examinee.paperId
  356. });
  357. paperData.value = res.data;
  358. paperData.value.questions = restoreQuestion(examinee.questions, res.data.questions);
  359. console.log('初始化数据', paperData.value.questions)
  360. setQuestionList(paperData.value.questions);
  361. setDuration(examinee.duration || 0);
  362. await nextTick();
  363. const targetQuestion = flatQuestionList.value.find(item => item.id === prevData.value.questionId);
  364. if (targetQuestion) {
  365. changeIndex(targetQuestion.index);
  366. } else {
  367. changeIndex(0);
  368. }
  369. setTimeout(() => {
  370. if (targetQuestion?.isSubQuestion) {
  371. setSubQuestionIndex(targetQuestion.subIndex || 0);
  372. }
  373. }, 50);
  374. await new Promise(resolve => setTimeout(resolve, 50));
  375. await nextTick();
  376. // 读取用户练习设置
  377. // setPracticeSettings(userStore.practiceSettings);
  378. isReady.value = true;
  379. console.log('试卷信息', res)
  380. if (!userStore.isExamGuideShow) {
  381. setTimeout(() => {
  382. uni.$ie.hideLoading();
  383. setTimeout(() => {
  384. showSwiperTip.value = true;
  385. }, 100);
  386. }, 300);
  387. } else {
  388. uni.$ie.hideLoading();
  389. if (!isReadOnly.value) {
  390. startTime();
  391. }
  392. }
  393. uni.report('exam-start-success', getReportData());
  394. } else {
  395. uni.report('exam-start-error[no examinee.paperId]', getReportData());
  396. }
  397. }
  398. const handleSwiperTipNext = () => {
  399. showSwiperTip.value = false;
  400. guideShow.value = true;
  401. }
  402. const handleGuideClose = () => {
  403. userStore.isExamGuideShow = true;
  404. if (!isReadOnly.value) {
  405. startTime();
  406. }
  407. }
  408. const loadData = async () => {
  409. uni.$ie.showLoading();
  410. const { paperType } = prevData.value;
  411. if (paperType === EnumPaperType.PRACTICE || paperType === EnumPaperType.COURSE) {
  412. loadPracticeData();
  413. } else if (paperType === EnumPaperType.SIMULATED || paperType === EnumPaperType.TEST) {
  414. // if (paperType === EnumPaperType.SIMULATED && userStore.isVHS) {
  415. // loadVHSPaperData();
  416. // } else {
  417. // loadExamData();
  418. // }
  419. loadExamData();
  420. }
  421. };
  422. const getReportData = () => {
  423. return {
  424. pageOptions: prevData.value,
  425. userInfo: userStore.userInfo,
  426. platform: platform.value,
  427. envInfo: appStore.systemInfo,
  428. }
  429. }
  430. onLoad(() => {
  431. console.log(prevData.value)
  432. uni.report('exam-start-enter', getReportData());
  433. loadData();
  434. uni.addInterceptor('navigateBack', {
  435. invoke: (e) => {
  436. if (confirmShowing.value) {
  437. return false;
  438. }
  439. if (confirmQuit.value) {
  440. return e;
  441. }
  442. handleLeftClick();
  443. return false;
  444. }
  445. })
  446. });
  447. onUnload(() => {
  448. uni.removeInterceptor('navigateBack');
  449. });
  450. </script>
  451. <style lang="scss" scoped></style>