index.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. import {
  2. ActionSheet,
  3. Button,
  4. Cell,
  5. CountDown,
  6. Grid,
  7. GridItem,
  8. Icon,
  9. Image,
  10. Popup,
  11. showDialog,
  12. Swipe,
  13. SwipeItem,
  14. Tag
  15. } from 'vant'
  16. import {
  17. computed,
  18. defineComponent,
  19. nextTick,
  20. onMounted,
  21. onUnmounted,
  22. reactive,
  23. ref,
  24. watch
  25. } from 'vue'
  26. import { useRoute, useRouter } from 'vue-router'
  27. import styles from './index.module.less'
  28. import iconButtonList from '../images/icon-button-list.png'
  29. import { state as baseState } from '@/state'
  30. import OSticky from '@/components/o-sticky'
  31. import ChoiceQuestion from '../model/choice-question'
  32. import AnswerList from '../model/answer-list'
  33. import ODialog from '@/components/o-dialog'
  34. import DragQuestion from '../model/drag-question'
  35. import KeepLookQuestion from '../model/keep-look-question'
  36. import PlayQuestion from '../model/play-question'
  37. import ErrorMode from '../model/error-mode'
  38. import ResultFinish from '../model/result-finish'
  39. import { eventUnit, QuestionType } from '../unit'
  40. import request from '@/helpers/request'
  41. import { useCountDown, useRect } from '@vant/use'
  42. import OHeader from '@/components/o-header'
  43. import { useInterval } from '@vueuse/core'
  44. export default defineComponent({
  45. name: 'unit-detail',
  46. setup() {
  47. const route = useRoute()
  48. const router = useRouter()
  49. const countDownRef = ref()
  50. const swipeRef = ref()
  51. const state = reactive({
  52. examId: route.query.examId,
  53. name: route.query.name,
  54. courseTypeCode: route.query.courseTypeCode,
  55. visiableError: false,
  56. visiableAnswer: false,
  57. visiableResult: false,
  58. id: route.query.id,
  59. currentIndex: 0,
  60. questionList: [],
  61. visiableSure: false,
  62. resultInfo: {} as any,
  63. resultStatusType: 'SUCCESS', // 'SUCCESS' | 'FAIL'
  64. visiableExam: false, // 考试已结束
  65. nextStatus: false,
  66. swipeHeight: 'auto' as any,
  67. answerAnalysis: '',
  68. questionTypeCode: '',
  69. overResult: {
  70. time: '00:00', // 时长
  71. questionLength: 0, // 答题数
  72. errorLength: 0, // 错题数
  73. rate: 0 // 正确率
  74. },
  75. knowledgelist: [] as any, // 知识点列表
  76. quitStatus: false,
  77. dialogMessage: '',
  78. dialogStatus: false
  79. })
  80. // 计时
  81. const { counter, resume, pause } = useInterval(1000, { controls: true })
  82. const getExamDetails = async () => {
  83. try {
  84. const { data } = await request.post('/api-student/examinationQuestion/randomPage', {
  85. data: {
  86. page: 1,
  87. row: 50,
  88. categoryId: state.id
  89. // courseTypeCode: state.courseTypeCode
  90. }
  91. })
  92. const temp = data || []
  93. temp.forEach((item: any) => {
  94. item.showAnalysis = false // 默认不显示解析
  95. item.analysis = {
  96. message: item.answerAnalysis,
  97. topic: true, // 是否显示结果
  98. userResult: false // 用户答题对错
  99. }
  100. item.userAnswer = [] // 用户答题
  101. })
  102. state.questionList = temp
  103. } catch {
  104. //
  105. }
  106. }
  107. // 获取所在知识点
  108. const getDetails = async () => {
  109. try {
  110. const { data } = await request.post('/api-student/unitExamination/queryKnowledgePoint', {
  111. requestType: 'form',
  112. data: {
  113. unitExaminationId: state.examId
  114. }
  115. })
  116. state.knowledgelist = data.lists || []
  117. } catch {
  118. //
  119. }
  120. }
  121. /**
  122. * @description 下一题 | 测试完成
  123. */
  124. const onNextQuestion = async () => {
  125. try {
  126. const questionList = state.questionList || []
  127. let result: any = {}
  128. questionList.forEach((question: any, index: number) => {
  129. // 格式化所有题目的答案
  130. if (index === state.currentIndex) {
  131. result = {
  132. questionId: question.id,
  133. details: question.userAnswer || []
  134. }
  135. }
  136. })
  137. const { data } = await request.post(
  138. '/api-student/studentUnitExamination/submitTrainingAnswer',
  139. {
  140. hideLoading: true,
  141. data: result
  142. }
  143. )
  144. // 初始化是否显示解析
  145. questionList.forEach((question: any, index: number) => {
  146. // 格式化所有题目的答案
  147. if (index === state.currentIndex) {
  148. state.answerAnalysis = question.answerAnalysis
  149. state.questionTypeCode = question.questionTypeCode
  150. question.showAnalysis = true
  151. question.analysis.userResult = data
  152. }
  153. })
  154. // 判断是否是最后一题
  155. if (state.questionList.length === state.currentIndex + 1) {
  156. eventUnit.emit('unitAudioStop')
  157. state.visiableSure = true
  158. return
  159. }
  160. if (data) {
  161. swipeRef.value?.next()
  162. } else {
  163. state.visiableError = true
  164. }
  165. } catch {
  166. //
  167. }
  168. }
  169. //
  170. const getAnswerResult = computed(() => {
  171. const questionList = state.questionList || []
  172. let count = 0
  173. let passCount = 0
  174. let noPassCount = 0
  175. questionList.forEach((item: any) => {
  176. if (item.showAnalysis) {
  177. count += 1
  178. if (item.analysis.userResult) {
  179. passCount += 1
  180. } else {
  181. noPassCount += 1
  182. }
  183. }
  184. })
  185. return {
  186. count,
  187. passCount,
  188. noPassCount
  189. }
  190. })
  191. /**
  192. * @description 重置当前的题目高度
  193. * @param {any} scroll 是否滚动到顶部
  194. */
  195. let size = 0
  196. const resizeSwipeItemHeight = (scroll = true) => {
  197. nextTick(() => {
  198. scroll && window.scrollTo(0, 0)
  199. setTimeout(() => {
  200. const currentItemDom: any = document
  201. .querySelectorAll('.van-swipe-item')
  202. [state.currentIndex]?.querySelector('.swipe-item-question')
  203. console.log('🚀 ~ setTimeout ~ currentItemDom', currentItemDom)
  204. const allImg = currentItemDom.querySelectorAll('.answerTitleImg img')
  205. let status = true
  206. // console.log(allImg)
  207. allImg.forEach((img: any) => {
  208. console.log(img.complete)
  209. if (!img.complete) {
  210. status = false
  211. }
  212. })
  213. // 判断图片是否加载完了
  214. if (!status && size < 3) {
  215. setTimeout(() => {
  216. size += 1
  217. resizeSwipeItemHeight(scroll)
  218. }, 300)
  219. }
  220. if (status) {
  221. size = 0
  222. }
  223. const rect = useRect(currentItemDom)
  224. state.swipeHeight = rect.height
  225. }, 100)
  226. })
  227. }
  228. const onConfirmExam = () => {
  229. const answerResult = getAnswerResult.value
  230. let rate = 0
  231. if (answerResult.count > 0) {
  232. rate = Math.floor((answerResult.passCount / answerResult.count) * 100)
  233. }
  234. const times = counter.value
  235. const minute =
  236. Math.floor(times / 60) >= 10 ? Math.floor(times / 60) : '0' + Math.floor(times / 60)
  237. const seconds = times % 60 >= 10 ? times % 60 : '0' + (times % 60)
  238. state.overResult = {
  239. time: minute + ':' + seconds, // 时长
  240. questionLength: answerResult.count, // 答题数
  241. errorLength: answerResult.noPassCount, // 错题数
  242. rate // 正确率
  243. }
  244. // 重置计时
  245. pause()
  246. counter.value = 0
  247. state.visiableResult = true
  248. }
  249. // 重新练习
  250. const onCloseResult = async () => {
  251. state.questionList = []
  252. await getExamDetails()
  253. setTimeout(async () => {
  254. swipeRef.value?.swipeTo(0, {
  255. immediate: true
  256. })
  257. state.swipeHeight = 'auto'
  258. state.answerAnalysis = ''
  259. state.overResult = {
  260. time: '00:00', // 时长
  261. questionLength: 0, // 答题数
  262. errorLength: 0, // 错题数
  263. rate: 0 // 正确率
  264. }
  265. state.visiableResult = false
  266. // 恢复计时
  267. resume()
  268. resizeSwipeItemHeight()
  269. }, 100)
  270. }
  271. // 下一个考点
  272. const onConfirmResult = () => {
  273. const knowledgelist = state.knowledgelist || []
  274. // console.log('🚀 ~ file: index.tsx:246 ~ onConfirmResult ~ knowledgelist', knowledgelist)
  275. // 当前正在考试的节点
  276. const knownleIndex = knowledgelist.findIndex((item: any) => item.id === state.id)
  277. console.log('🚀 ~ file: index.tsx:249 ~ onConfirmResult ~ knownleIndex', knownleIndex)
  278. let currentKnowle: any = {}
  279. if (knownleIndex + 1 >= knowledgelist.length || knownleIndex < 0) {
  280. currentKnowle = knowledgelist[0]
  281. } else {
  282. currentKnowle = knowledgelist[knownleIndex + 1]
  283. }
  284. state.id = currentKnowle.id
  285. state.visiableResult = false
  286. state.currentIndex = 0
  287. // 重置
  288. onCloseResult()
  289. }
  290. // 拦截
  291. const onBack = () => {
  292. // showDialog({
  293. // title: '提示',
  294. // message: '您是否退出?',
  295. // theme: 'round-button',
  296. // confirmButtonColor: '#ff8057'
  297. // }).then(() => {
  298. // onAfter()
  299. // })
  300. state.quitStatus = true
  301. eventUnit.emit('unitAudioStop')
  302. }
  303. const onAfter = () => {
  304. window.removeEventListener('popstate', onBack, false)
  305. router.back()
  306. }
  307. onMounted(async () => {
  308. await getExamDetails()
  309. await getDetails()
  310. resizeSwipeItemHeight()
  311. if (!baseState.user.data.vipMember) {
  312. pause()
  313. state.dialogStatus = true
  314. state.dialogMessage = '您暂未开通团练宝,请开通后使用'
  315. return
  316. }
  317. window.history.pushState(null, '', document.URL)
  318. window.addEventListener('popstate', onBack, false)
  319. })
  320. onUnmounted(() => {
  321. // 关闭所有音频
  322. eventUnit.emit('unitAudioStop')
  323. })
  324. return () => (
  325. <div class={styles.unitDetail}>
  326. <OSticky position="top">
  327. <OHeader
  328. v-slots={{
  329. right: () => (
  330. <span
  331. style="color: var(--van-primary-color)"
  332. onClick={() => {
  333. eventUnit.emit('unitAudioStop')
  334. state.visiableSure = true
  335. }}
  336. >
  337. 结束练习
  338. </span>
  339. )
  340. }}
  341. />
  342. </OSticky>
  343. <Cell center class={styles.unitSection} border={false}>
  344. {{
  345. title: () => <div class={[styles.unitTitle]}>{state.name}</div>,
  346. value: () => (
  347. <div class={styles.unitCount}>
  348. <div class={styles.countSection}>
  349. <span class={styles.nums}>{getAnswerResult.value.passCount}</span>
  350. <span>答对</span>
  351. </div>
  352. <div class={styles.countSection}>
  353. <span class={styles.nums} style={{ color: '#F44541' }}>
  354. {getAnswerResult.value.noPassCount}
  355. </span>
  356. <span>答错</span>
  357. </div>
  358. </div>
  359. )
  360. }}
  361. </Cell>
  362. <Swipe
  363. loop={false}
  364. showIndicators={false}
  365. ref={swipeRef}
  366. duration={300}
  367. touchable={false}
  368. style={{ paddingBottom: '12px' }}
  369. lazyRender
  370. height={state.swipeHeight}
  371. onChange={(index: number) => {
  372. eventUnit.emit('unitAudioStop')
  373. state.currentIndex = index
  374. resizeSwipeItemHeight()
  375. }}
  376. >
  377. {state.questionList.map((item: any, index: number) => (
  378. <SwipeItem>
  379. <div class="swipe-item-question">
  380. {item.questionTypeCode === QuestionType.RADIO && (
  381. <ChoiceQuestion
  382. v-model:value={item.userAnswer}
  383. index={index + 1}
  384. data={item}
  385. type="radio"
  386. showAnalysis={item.showAnalysis}
  387. analysis={item.analysis}
  388. />
  389. )}
  390. {item.questionTypeCode === QuestionType.CHECKBOX && (
  391. <ChoiceQuestion
  392. v-model:value={item.userAnswer}
  393. index={index + 1}
  394. data={item}
  395. type="checkbox"
  396. showAnalysis={item.showAnalysis}
  397. analysis={item.analysis}
  398. />
  399. )}
  400. {item.questionTypeCode === QuestionType.SORT && (
  401. <DragQuestion
  402. v-model:value={item.userAnswer}
  403. onUpdate:value={() => {
  404. // 如果是空则滑动到顶部
  405. const status = item.userAnswer && item.userAnswer.length > 0 ? false : true
  406. resizeSwipeItemHeight(status)
  407. }}
  408. data={item}
  409. index={index + 1}
  410. showAnalysis={item.showAnalysis}
  411. analysis={item.analysis}
  412. />
  413. )}
  414. {item.questionTypeCode === QuestionType.LINK && (
  415. <KeepLookQuestion
  416. v-model:value={item.userAnswer}
  417. data={item}
  418. index={index + 1}
  419. showAnalysis={item.showAnalysis}
  420. analysis={item.analysis}
  421. />
  422. )}
  423. {item.questionTypeCode === QuestionType.PLAY && (
  424. <PlayQuestion
  425. v-model:value={item.userAnswer}
  426. data={item}
  427. type="practice"
  428. index={index + 1}
  429. unitId={state.id as any}
  430. showAnalysis={item.showAnalysis}
  431. analysis={item.analysis}
  432. />
  433. )}
  434. </div>
  435. </SwipeItem>
  436. ))}
  437. </Swipe>
  438. <OSticky position="bottom" background="white">
  439. <div class={['btnGroup btnMore']}>
  440. {state.currentIndex > 0 && (
  441. <Button
  442. round
  443. block
  444. type="primary"
  445. plain
  446. onClick={() => {
  447. swipeRef.value?.prev()
  448. }}
  449. >
  450. 上一题
  451. </Button>
  452. )}
  453. <Button
  454. block
  455. round
  456. type="primary"
  457. onClick={onNextQuestion}
  458. loading={state.nextStatus}
  459. disabled={state.nextStatus}
  460. >
  461. 提交
  462. </Button>
  463. <Image
  464. src={iconButtonList}
  465. class={[styles.wapList, 'van-haptics-feedback']}
  466. onClick={() => (state.visiableAnswer = true)}
  467. />
  468. </div>
  469. </OSticky>
  470. {/* 题目集合 */}
  471. <ActionSheet v-model:show={state.visiableAnswer} title="题目列表" safeAreaInsetBottom>
  472. <AnswerList
  473. value={state.questionList}
  474. lookType={'PRACTICE'}
  475. statusList={[
  476. {
  477. text: '答对',
  478. color: '#71B0FF'
  479. },
  480. {
  481. text: '答错',
  482. color: '#FF8486'
  483. }
  484. ]}
  485. onSelect={(item: any) => {
  486. // 跳转,并且跳过动画
  487. swipeRef.value?.swipeTo(item, {
  488. immediate: true
  489. })
  490. state.visiableAnswer = false
  491. }}
  492. />
  493. </ActionSheet>
  494. <Popup
  495. v-model:show={state.visiableError}
  496. style={{ width: '90%' }}
  497. round
  498. closeOnClickOverlay={false}
  499. >
  500. <ErrorMode
  501. onClose={() => (state.visiableError = false)}
  502. answerAnalysis={state.answerAnalysis}
  503. questionTypeCode={state.questionTypeCode}
  504. onConform={() => {
  505. swipeRef.value?.next()
  506. state.answerAnalysis = ''
  507. }}
  508. />
  509. </Popup>
  510. <Popup
  511. v-model:show={state.visiableResult}
  512. closeOnClickOverlay={false}
  513. style={{ background: 'transparent', width: '96%' }}
  514. >
  515. <ResultFinish
  516. status="PRACTICE"
  517. confirmButtonText="下一个考点"
  518. cancelButtonText="继续练习本考点"
  519. onClose={onCloseResult}
  520. onConform={onConfirmResult}
  521. v-slots={{
  522. content: () => (
  523. <div class={styles.practiceResult}>
  524. <div class={styles.practiceTitle}>本次练习正确率</div>
  525. <div class={styles.practiceRate}>{state.overResult.rate}%</div>
  526. <Grid border={false} columnNum={3}>
  527. <GridItem>
  528. <p class={styles.title}>{state.overResult.time}</p>
  529. <p class={styles.name}>练习时长</p>
  530. </GridItem>
  531. <GridItem>
  532. <p class={[styles.title]}>{state.overResult.questionLength | 0}</p>
  533. <p class={styles.name}>答题数</p>
  534. </GridItem>
  535. <GridItem>
  536. <p class={styles.title}>{state.overResult.errorLength | 0}</p>
  537. <p class={styles.name}>错题数</p>
  538. </GridItem>
  539. </Grid>
  540. {state.overResult.rate >= 100 ? (
  541. <div class={styles.practiceTips}>
  542. 你真棒!
  543. <br />
  544. 本知识点你已经完全掌握啦!
  545. </div>
  546. ) : (
  547. <div class={styles.practiceTips}>
  548. 继续努力!
  549. <br />
  550. 争取在测验中获得高分!
  551. </div>
  552. )}
  553. </div>
  554. )
  555. }}
  556. />
  557. </Popup>
  558. <ODialog
  559. v-model:show={state.visiableSure}
  560. title="练习完成"
  561. message="确认本次练习的题目都完成了吗?"
  562. messageAlign="left"
  563. showCancelButton
  564. cancelButtonText="再等等"
  565. confirmButtonText="确认完成"
  566. onConfirm={onConfirmExam}
  567. />
  568. <ODialog
  569. v-model:show={state.quitStatus}
  570. title="提示"
  571. message="您是否退出本次练习?"
  572. showCancelButton
  573. cancelButtonText="取消"
  574. onCancel={() => {
  575. window.history.pushState(null, '', document.URL)
  576. window.addEventListener('popstate', onBack, false)
  577. }}
  578. confirmButtonText="确认退出"
  579. onConfirm={onAfter}
  580. />
  581. <ODialog
  582. message={state.dialogMessage}
  583. v-model:show={state.dialogStatus}
  584. showCancelButton
  585. cancelButtonText="返回"
  586. onCancel={() => {
  587. router.back()
  588. }}
  589. confirmButtonText="立即开通"
  590. onConfirm={() => {
  591. router.push('/memberCenter')
  592. }}
  593. />
  594. </div>
  595. )
  596. }
  597. })