index.tsx 17 KB

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