index.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import { ActionSheet, Button, Image, Popup, Swipe, SwipeItem } from 'vant';
  2. import {
  3. computed,
  4. defineComponent,
  5. nextTick,
  6. onMounted,
  7. onUnmounted,
  8. reactive,
  9. ref
  10. } from 'vue';
  11. import { useRoute, useRouter } from 'vue-router';
  12. import styles from './index.module.less';
  13. import iconButtonList from '../images/icon-button-list.png';
  14. import MSticky from '@/components/m-sticky';
  15. import ChoiceQuestion from '../model/choice-question';
  16. import AnswerList from '../model/answer-list';
  17. import DragQuestion from '../model/drag-question';
  18. import KeepLookQuestion from '../model/keep-look-question';
  19. import PlayQuestion from '../model/play-question';
  20. import ErrorMode from '../model/error-mode';
  21. import ResultFinish from '../model/result-finish';
  22. import { eventUnit, QuestionType } from '../unit';
  23. import request from '@/helpers/request';
  24. import { useRect } from '@vant/use';
  25. import MHeader from '@/components/m-header';
  26. import { useEventListener, useInterval, useWindowScroll } from '@vueuse/core';
  27. export default defineComponent({
  28. name: 'unit-detail',
  29. setup() {
  30. const route = useRoute();
  31. const router = useRouter();
  32. const swipeRef = ref();
  33. const state = reactive({
  34. type: route.query.type, // 类型
  35. knowledgePointIds: route.query.knowledgePointIds, // 智能组卷 多个编号
  36. lessonCoursewareId: route.query.lessonCoursewareId, // 教材编号
  37. background: 'transparent',
  38. color: '#fff',
  39. visiableError: false,
  40. visiableAnswer: false,
  41. id: route.query.id,
  42. currentIndex: 0,
  43. questionList: [],
  44. visiableInfo: {
  45. show: false,
  46. operationType: 'RESULT' as 'RESULT' | 'BACK' | 'CONTINUE',
  47. type: 'DEFAULT' as 'DEFAULT' | 'FAIL' | 'PASS' | 'GOOD' | 'COUNTDOWN',
  48. content: '',
  49. showCancelButton: false,
  50. confirmButtonText: '',
  51. cancelButtonText: '',
  52. title: ''
  53. },
  54. nextStatus: false,
  55. swipeHeight: 'auto' as any,
  56. answerAnalysis: '',
  57. questionTypeCode: '',
  58. overResult: {
  59. time: '00:00', // 时长
  60. questionLength: 0, // 答题数
  61. errorLength: 0, // 错题数
  62. rate: 0 // 正确率
  63. }
  64. });
  65. // 计时
  66. const { counter, resume, pause } = useInterval(1000, { controls: true });
  67. const getExamDetails = async () => {
  68. try {
  69. const { data } = await request.post(
  70. '/edu-app/studentUnitExamination/queryPracticeModeExam',
  71. {
  72. data: {
  73. lessonCoursewareId: state.lessonCoursewareId
  74. }
  75. }
  76. );
  77. const temp = data || {};
  78. temp.examinationQuestionAdds.forEach((item: any) => {
  79. item.showAnalysis = false; // 默认不显示解析
  80. item.analysis = {
  81. message: item.answerAnalysis,
  82. topic: true, // 是否显示结果
  83. isAnswer: false, // 是否答题
  84. userResult: false // 用户答题对错
  85. };
  86. item.userAnswer = []; // 用户答题
  87. });
  88. state.questionList = temp.examinationQuestionAdds || [];
  89. } catch {
  90. //
  91. }
  92. };
  93. /**
  94. * @description 下一题 | 测试完成
  95. */
  96. const onNextQuestion = async () => {
  97. try {
  98. const questionList: any = state.questionList || [];
  99. let currentStatus = true;
  100. if (!questionList[state.currentIndex]?.analysis.isAnswer) {
  101. let result: any = {};
  102. questionList.forEach((question: any, index: number) => {
  103. // 格式化所有题目的答案
  104. if (index === state.currentIndex) {
  105. result = {
  106. questionId: question.id,
  107. details: question.userAnswer || []
  108. };
  109. }
  110. });
  111. const { data } = await request.post(
  112. '/edu-app/studentUnitExamination/submitTrainingAnswer',
  113. {
  114. hideLoading: true,
  115. data: result
  116. }
  117. );
  118. currentStatus = data;
  119. // 初始化是否显示解析
  120. questionList.forEach((question: any, index: number) => {
  121. // 格式化所有题目的答案
  122. if (index === state.currentIndex) {
  123. state.answerAnalysis = question.answerAnalysis;
  124. state.questionTypeCode = question.questionTypeCode;
  125. question.showAnalysis = true;
  126. question.analysis.userResult = data;
  127. question.analysis.isAnswer = true; // 是否答题
  128. }
  129. });
  130. }
  131. // 判断是否是最后一题
  132. if (state.questionList.length === state.currentIndex + 1) {
  133. eventUnit.emit('unitAudioStop');
  134. state.visiableInfo.show = true;
  135. state.visiableInfo.title = '练习完成';
  136. state.visiableInfo.showCancelButton = true;
  137. state.visiableInfo.operationType = 'CONTINUE';
  138. state.visiableInfo.cancelButtonText = '再等等';
  139. state.visiableInfo.confirmButtonText = '确认完成';
  140. state.visiableInfo.content = `确认本次练习的题目都完成了吗?`;
  141. return;
  142. }
  143. if (currentStatus) {
  144. swipeRef.value?.next();
  145. } else {
  146. state.visiableError = true;
  147. }
  148. } catch {
  149. //
  150. }
  151. };
  152. //
  153. const getAnswerResult = computed(() => {
  154. const questionList = state.questionList || [];
  155. let count = 0;
  156. let passCount = 0;
  157. let noPassCount = 0;
  158. questionList.forEach((item: any) => {
  159. if (item.showAnalysis) {
  160. count += 1;
  161. if (item.analysis.userResult) {
  162. passCount += 1;
  163. } else {
  164. noPassCount += 1;
  165. }
  166. }
  167. });
  168. return {
  169. count,
  170. passCount,
  171. noPassCount
  172. };
  173. });
  174. /**
  175. * @description 重置当前的题目高度
  176. * @param {any} scroll 是否滚动到顶部
  177. */
  178. let size = 0;
  179. const resizeSwipeItemHeight = (scroll = true) => {
  180. nextTick(() => {
  181. scroll && window.scrollTo(0, 0);
  182. setTimeout(() => {
  183. const currentItemDom: any = document
  184. .querySelectorAll('.van-swipe-item')
  185. [state.currentIndex]?.querySelector('.swipe-item-question');
  186. const allImg = currentItemDom?.querySelectorAll(
  187. '.answerTitleImg img'
  188. );
  189. let status = true;
  190. // console.log(allImg)
  191. allImg?.forEach((img: any) => {
  192. console.log(img.complete);
  193. if (!img.complete) {
  194. status = false;
  195. }
  196. });
  197. // 判断图片是否加载完了
  198. if (!status && size < 3) {
  199. setTimeout(() => {
  200. size += 1;
  201. resizeSwipeItemHeight(scroll);
  202. }, 300);
  203. }
  204. if (status) {
  205. size = 0;
  206. }
  207. const rect = useRect(currentItemDom);
  208. state.swipeHeight = rect.height;
  209. }, 100);
  210. });
  211. };
  212. const onConfirmExam = () => {
  213. //
  214. };
  215. const onConfirmResult = () => {
  216. if (state.visiableInfo.operationType === 'RESULT') {
  217. state.visiableInfo.show = false;
  218. router.back();
  219. onAfter();
  220. } else if (state.visiableInfo.operationType === 'BACK') {
  221. state.visiableInfo.show = false;
  222. onAfter();
  223. } else if (state.visiableInfo.operationType === 'CONTINUE') {
  224. onResultPopup();
  225. }
  226. };
  227. const onCloseResult = async () => {
  228. if (state.visiableInfo.operationType === 'RESULT') {
  229. } else if (state.visiableInfo.operationType === 'BACK') {
  230. state.visiableInfo.show = false;
  231. window.history.pushState(null, '', document.URL);
  232. window.addEventListener('popstate', onBack, false);
  233. } else if (state.visiableInfo.operationType === 'CONTINUE') {
  234. state.visiableInfo.show = false;
  235. }
  236. };
  237. /** 结果页面弹窗 */
  238. const onResultPopup = () => {
  239. const answerResult = getAnswerResult.value;
  240. let rate = 0;
  241. if (answerResult.count > 0) {
  242. rate = Math.floor((answerResult.passCount / answerResult.count) * 100);
  243. }
  244. const times = counter.value;
  245. const minute =
  246. Math.floor(times / 60) >= 10
  247. ? Math.floor(times / 60)
  248. : '0' + Math.floor(times / 60);
  249. const seconds = times % 60 >= 10 ? times % 60 : '0' + (times % 60);
  250. state.overResult = {
  251. time: minute + ':' + seconds, // 时长
  252. questionLength: answerResult.count, // 答题数
  253. errorLength: answerResult.noPassCount, // 错题数
  254. rate // 正确率
  255. };
  256. // 重置计时
  257. pause();
  258. counter.value = 0;
  259. // 60 及格
  260. // 85 及以上优秀
  261. state.visiableInfo.show = true;
  262. state.visiableInfo.title = '已完成';
  263. state.visiableInfo.showCancelButton = false;
  264. state.visiableInfo.operationType = 'RESULT';
  265. state.visiableInfo.confirmButtonText = '确认';
  266. state.visiableInfo.content = `<div>您已完成本次测试,答对<span class='${
  267. styles.right
  268. }'>${answerResult.passCount}</span>,答错<span class='${styles.error}'>${
  269. answerResult.count - answerResult.passCount
  270. }</span>,正确率${rate}%~</div>`;
  271. };
  272. // 拦截
  273. const onBack = () => {
  274. const answerResult = getAnswerResult.value;
  275. state.visiableInfo.show = true;
  276. state.visiableInfo.title = '确认退出吗?';
  277. state.visiableInfo.showCancelButton = true;
  278. state.visiableInfo.operationType = 'BACK';
  279. state.visiableInfo.cancelButtonText = '取消';
  280. state.visiableInfo.confirmButtonText = '确定';
  281. state.visiableInfo.content = `您已经完成${
  282. answerResult.passCount + answerResult.noPassCount
  283. }道题了,继续做题可以巩固所学知识哦~`;
  284. eventUnit.emit('unitAudioStop');
  285. };
  286. const onAfter = () => {
  287. window.removeEventListener('popstate', onBack, false);
  288. router.back();
  289. };
  290. onMounted(async () => {
  291. useEventListener(document, 'scroll', () => {
  292. const { y } = useWindowScroll();
  293. if (y.value > 52) {
  294. state.background = '#fff';
  295. state.color = '#323333';
  296. } else {
  297. state.background = 'transparent';
  298. state.color = '#fff';
  299. }
  300. });
  301. await getExamDetails();
  302. resizeSwipeItemHeight();
  303. window.history.pushState(null, '', document.URL);
  304. window.addEventListener('popstate', onBack, false);
  305. });
  306. onUnmounted(() => {
  307. // 关闭所有音频
  308. eventUnit.emit('unitAudioStop');
  309. });
  310. return () => (
  311. <div class={styles.unitDetail}>
  312. <MSticky position="top">
  313. <MHeader
  314. border={false}
  315. background={state.background}
  316. color={state.color}
  317. />
  318. </MSticky>
  319. <Swipe
  320. loop={false}
  321. showIndicators={false}
  322. ref={swipeRef}
  323. duration={300}
  324. touchable={false}
  325. class={styles.unitSwipe}
  326. style={{ paddingBottom: '12px' }}
  327. lazyRender
  328. height={state.swipeHeight}
  329. onChange={(index: number) => {
  330. eventUnit.emit('unitAudioStop');
  331. state.currentIndex = index;
  332. resizeSwipeItemHeight();
  333. }}>
  334. {state.questionList.map((item: any, index: number) => (
  335. <SwipeItem>
  336. <div class="swipe-item-question">
  337. {item.questionTypeCode === QuestionType.RADIO && (
  338. <ChoiceQuestion
  339. v-model:value={item.userAnswer}
  340. index={index + 1}
  341. data={item}
  342. type="radio"
  343. showAnalysis={item.showAnalysis}
  344. analysis={item.analysis}>
  345. {{
  346. title: () => (
  347. <div class={styles.questionTitle}>
  348. <div class={styles.questionNum}>
  349. <span>{state.currentIndex + 1}</span>/
  350. {state.questionList.length}
  351. </div>
  352. <div class={styles.questionType}>
  353. <i></i>
  354. <span>{item.knowledgePointName}</span>
  355. </div>
  356. </div>
  357. )
  358. }}
  359. </ChoiceQuestion>
  360. )}
  361. {item.questionTypeCode === QuestionType.CHECKBOX && (
  362. <ChoiceQuestion
  363. v-model:value={item.userAnswer}
  364. index={index + 1}
  365. data={item}
  366. type="checkbox"
  367. showAnalysis={item.showAnalysis}
  368. analysis={item.analysis}>
  369. {{
  370. title: () => (
  371. <div class={styles.questionTitle}>
  372. <div class={styles.questionNum}>
  373. <span>{state.currentIndex + 1}</span>/
  374. {state.questionList.length}
  375. </div>
  376. <div class={styles.questionType}>
  377. <i></i>
  378. <span>{item.knowledgePointName}</span>
  379. </div>
  380. </div>
  381. )
  382. }}
  383. </ChoiceQuestion>
  384. )}
  385. {item.questionTypeCode === QuestionType.SORT && (
  386. <DragQuestion
  387. v-model:value={item.userAnswer}
  388. onUpdate:value={() => {
  389. // 如果是空则滑动到顶部
  390. const status =
  391. item.userAnswer && item.userAnswer.length > 0
  392. ? false
  393. : true;
  394. resizeSwipeItemHeight(status);
  395. }}
  396. data={item}
  397. index={index + 1}
  398. showAnalysis={item.showAnalysis}
  399. analysis={item.analysis}>
  400. {{
  401. title: () => (
  402. <div class={styles.questionTitle}>
  403. <div class={styles.questionNum}>
  404. <span>{state.currentIndex + 1}</span>/
  405. {state.questionList.length}
  406. </div>
  407. <div class={styles.questionType}>
  408. <i></i>
  409. <span>{item.knowledgePointName}</span>
  410. </div>
  411. </div>
  412. )
  413. }}
  414. </DragQuestion>
  415. )}
  416. {item.questionTypeCode === QuestionType.LINK && (
  417. <KeepLookQuestion
  418. v-model:value={item.userAnswer}
  419. data={item}
  420. index={index + 1}
  421. showAnalysis={item.showAnalysis}
  422. analysis={item.analysis}>
  423. {{
  424. title: () => (
  425. <div class={styles.questionTitle}>
  426. <div class={styles.questionNum}>
  427. <span>{state.currentIndex + 1}</span>/
  428. {state.questionList.length}
  429. </div>
  430. <div class={styles.questionType}>
  431. <i></i>
  432. <span>{item.knowledgePointName}</span>
  433. </div>
  434. </div>
  435. )
  436. }}
  437. </KeepLookQuestion>
  438. )}
  439. {item.questionTypeCode === QuestionType.PLAY && (
  440. <PlayQuestion
  441. v-model:value={item.userAnswer}
  442. data={item}
  443. index={index + 1}
  444. unitId={state.id as any}
  445. showAnalysis={item.showAnalysis}
  446. analysis={item.analysis}>
  447. {{
  448. title: () => (
  449. <div class={styles.questionTitle}>
  450. <div class={styles.questionNum}>
  451. <span>{state.currentIndex + 1}</span>/
  452. {state.questionList.length}
  453. </div>
  454. <div class={styles.questionType}>
  455. <i></i>
  456. <span>{item.knowledgePointName}</span>
  457. </div>
  458. </div>
  459. )
  460. }}
  461. </PlayQuestion>
  462. )}
  463. </div>
  464. </SwipeItem>
  465. ))}
  466. </Swipe>
  467. <MSticky position="bottom">
  468. <div class={['btnGroup btnMore', styles.btnSection]}>
  469. <Button
  470. round
  471. block
  472. class={
  473. state.currentIndex > 0 ? styles.activePrevBtn : styles.prevBtn
  474. }
  475. disabled={state.currentIndex > 0 ? false : true}
  476. onClick={() => {
  477. swipeRef.value?.prev();
  478. }}>
  479. 上一题
  480. </Button>
  481. <Button
  482. block
  483. round
  484. class={styles.nextBtn}
  485. onClick={onNextQuestion}
  486. loading={state.nextStatus}
  487. disabled={state.nextStatus}>
  488. {state.questionList.length === state.currentIndex + 1
  489. ? '提交'
  490. : '下一题'}
  491. </Button>
  492. <Image
  493. src={iconButtonList}
  494. class={[styles.wapList, 'van-haptics-feedback']}
  495. onClick={() => (state.visiableAnswer = true)}
  496. />
  497. </div>
  498. </MSticky>
  499. {/* 题目集合 */}
  500. <ActionSheet
  501. v-model:show={state.visiableAnswer}
  502. title="题目列表"
  503. safeAreaInsetBottom>
  504. <AnswerList
  505. value={state.questionList}
  506. // lookType={'ANSWER'}
  507. onSelect={(item: any) => {
  508. // 跳转,并且跳过动画
  509. swipeRef.value?.swipeTo(item, {
  510. immediate: true
  511. });
  512. state.visiableAnswer = false;
  513. }}
  514. />
  515. </ActionSheet>
  516. <Popup
  517. v-model:show={state.visiableError}
  518. style={{ width: '90%' }}
  519. round
  520. closeOnClickOverlay={false}>
  521. <ErrorMode
  522. onClose={() => (state.visiableError = false)}
  523. answerAnalysis={state.answerAnalysis}
  524. questionTypeCode={state.questionTypeCode}
  525. onConform={() => {
  526. swipeRef.value?.next();
  527. state.answerAnalysis = '';
  528. }}
  529. />
  530. </Popup>
  531. <Popup
  532. v-model:show={state.visiableInfo.show}
  533. closeOnClickOverlay={false}
  534. style={{
  535. background: 'transparent',
  536. width: '100%',
  537. maxWidth: '100%',
  538. transform: 'translateY(-55%)'
  539. }}>
  540. <ResultFinish
  541. title={state.visiableInfo.title}
  542. showCancelButton={state.visiableInfo.showCancelButton}
  543. cancelButtonText={state.visiableInfo.cancelButtonText}
  544. confirmButtonText={state.visiableInfo.confirmButtonText}
  545. status={state.visiableInfo.type}
  546. content={state.visiableInfo.content}
  547. contentHtml
  548. onConform={onConfirmResult}
  549. onClose={onCloseResult}
  550. />
  551. </Popup>
  552. </div>
  553. );
  554. }
  555. });