index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. import {
  2. ActionSheet,
  3. Button,
  4. Cell,
  5. CountDown,
  6. Icon,
  7. Image,
  8. Popup,
  9. showDialog,
  10. Swipe,
  11. SwipeItem,
  12. Tag
  13. } from 'vant'
  14. import { defineComponent, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
  15. import { useRoute, useRouter } from 'vue-router'
  16. import styles from './index.module.less'
  17. import iconQuestionNums from '../images/icon-question-nums.png'
  18. import iconCountDown from '../images/icon-count-down.png'
  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 request from '@/helpers/request'
  28. import dayjs from 'dayjs'
  29. import ResultFinish from '../model/result-finish'
  30. import { eventUnit, QuestionType } from '../unit'
  31. import { useRect } from '@vant/use'
  32. export default defineComponent({
  33. name: 'unit-detail',
  34. setup() {
  35. const route = useRoute()
  36. const router = useRouter()
  37. const countDownRef = ref()
  38. const swipeRef = ref()
  39. const state = reactive({
  40. id: route.query.id,
  41. examDetail: {} as any,
  42. visiableAnswer: false,
  43. currentIndex: 0,
  44. questionList: [],
  45. time: 0,
  46. visiableSure: false,
  47. visiableResult: false,
  48. resultInfo: {} as any,
  49. resultStatusType: 'SUCCESS', // 'SUCCESS' | 'FAIL'
  50. visiableExam: false, // 考试已结束
  51. nextStatus: false,
  52. swipeHeight: 'auto' as any,
  53. quitStatus: false
  54. })
  55. const getExamDetails = async () => {
  56. try {
  57. const { data } = await request.post(
  58. '/api-student/studentUnitExamination/startExamination',
  59. {
  60. requestType: 'form',
  61. data: {
  62. studentUnitExaminationId: state.id
  63. }
  64. }
  65. )
  66. const { questionJson, studentAnswerJson, ...res } = data
  67. const temp = questionJson || []
  68. temp.forEach((item: any) => {
  69. item.userAnswer = formatUserAnswers(item, studentAnswerJson)
  70. })
  71. state.questionList = temp
  72. state.examDetail = { ...res } || {}
  73. calcTime()
  74. } catch {
  75. //
  76. }
  77. }
  78. /**
  79. * @description 计算考试时间剩余时间
  80. */
  81. const calcTime = async () => {
  82. const examDetail = state.examDetail || {}
  83. const startTime = examDetail.startTime
  84. const nowTime = examDetail.now
  85. const timeMinutes = examDetail.timeMinutes || 0 // 测验时间
  86. // 返回秒
  87. const minu = dayjs(startTime).add(timeMinutes, 'minute').diff(dayjs(nowTime))
  88. // 时间到了考试结束
  89. if (minu <= 0) {
  90. eventUnit.emit('unitAudioStop')
  91. await onConfirmExam()
  92. state.visiableExam = true
  93. } else {
  94. state.time = Math.ceil(minu / 1000) * 1000
  95. setTimeout(() => {
  96. countDownRef.value?.start()
  97. }, 10)
  98. }
  99. }
  100. /**
  101. * @description 初始化用户答案
  102. */
  103. const formatUserAnswers = (item: any, userAnswer: any) => {
  104. // 判断是否有结果
  105. if (!userAnswer) return []
  106. const answers = userAnswer || []
  107. return answers[item.id] ? answers[item.id] : []
  108. }
  109. /**
  110. * @description 重置当前的题目高度
  111. */
  112. let size = 0
  113. const resizeSwipeItemHeight = (scroll = true) => {
  114. nextTick(() => {
  115. scroll && window.scrollTo(0, 0)
  116. setTimeout(() => {
  117. // const currentItemDom: Element =
  118. // document.querySelectorAll('.swipe-item-question')[state.currentIndex]
  119. const currentItemDom: any = document
  120. .querySelectorAll('.van-swipe-item')
  121. [state.currentIndex]?.querySelector('.swipe-item-question')
  122. const allImg = currentItemDom.querySelectorAll('.answerTitleImg img')
  123. let status = true
  124. // console.log(allImg)
  125. allImg.forEach((img: any) => {
  126. console.log(img.complete)
  127. if (!img.complete) {
  128. status = false
  129. }
  130. })
  131. // 判断图片是否加载完了
  132. if (!status && size < 3) {
  133. setTimeout(() => {
  134. size += 1
  135. resizeSwipeItemHeight(scroll)
  136. }, 300)
  137. }
  138. if (status) {
  139. size = 0
  140. }
  141. const rect = useRect(currentItemDom)
  142. state.swipeHeight = rect.height
  143. }, 100)
  144. })
  145. }
  146. /**
  147. * @description 下一题 | 测试完成
  148. */
  149. const onNextQuestion = async () => {
  150. try {
  151. const questionList = state.questionList || []
  152. const userAnswerList: any = [] // 所有题目的答案
  153. // let currentResult = false // 当前题目是否已经答题
  154. questionList.forEach((question: any, index: number) => {
  155. // 格式化所有题目的答案
  156. if (question.userAnswer && question.userAnswer.length > 0) {
  157. userAnswerList.push({
  158. questionId: question.id,
  159. details: question.userAnswer
  160. })
  161. }
  162. })
  163. // 判断是否是最后一题
  164. // console.log(state.questionList.length, state.currentIndex, userAnswerList, '-----')
  165. if (state.questionList.length === state.currentIndex + 1) {
  166. eventUnit.emit('unitAudioStop')
  167. state.visiableSure = true
  168. return
  169. }
  170. state.nextStatus = true
  171. await request.post('/api-student/studentUnitExamination/submitAnswer', {
  172. hideLoading: true,
  173. data: {
  174. answers: userAnswerList,
  175. studentUnitExaminationId: state.id
  176. }
  177. })
  178. swipeRef.value?.next()
  179. state.nextStatus = false
  180. } catch {
  181. //
  182. state.nextStatus = false
  183. }
  184. }
  185. /**
  186. * @description 提交最终答案
  187. */
  188. const onConfirmExam = async () => {
  189. try {
  190. const questionList = state.questionList || []
  191. const userAnswerList: any = [] // 所有题目的答案
  192. questionList.forEach((question: any) => {
  193. // 格式化所有题目的答案
  194. if (question.userAnswer && question.userAnswer.length > 0) {
  195. userAnswerList.push({
  196. questionId: question.id,
  197. details: question.userAnswer
  198. })
  199. }
  200. })
  201. const { data } = await request.post(
  202. '/api-student/studentUnitExamination/completionExamination',
  203. {
  204. data: {
  205. answers: userAnswerList,
  206. studentUnitExaminationId: state.id
  207. }
  208. }
  209. )
  210. if (data.status === 'A_PASS') {
  211. state.resultStatusType = 'SUCCESS'
  212. state.resultInfo = {
  213. tips: '恭喜你,测验通过!',
  214. score: data.score,
  215. examName: state.examDetail.unitExaminationName
  216. }
  217. } else {
  218. state.resultStatusType = 'FAIL'
  219. state.resultInfo = {
  220. tips: '本次测验不合格!',
  221. score: data.score,
  222. examName: state.examDetail.unitExaminationName
  223. }
  224. }
  225. onAfter()
  226. state.visiableResult = true
  227. } catch {
  228. //
  229. }
  230. }
  231. // 拦截
  232. const onBack = () => {
  233. // showDialog({
  234. // title: '提示',
  235. // message: '您考试还未提交,是否退出?',
  236. // theme: 'round-button',
  237. // confirmButtonColor: '#ff8057'
  238. // }).then(() => {
  239. // onAfter()
  240. // router.back()
  241. // })
  242. state.quitStatus = true
  243. eventUnit.emit('unitAudioStop')
  244. }
  245. const onAfter = () => {
  246. window.removeEventListener('popstate', onBack, false)
  247. router.back()
  248. }
  249. onMounted(async () => {
  250. await getExamDetails()
  251. // 初始化高度
  252. resizeSwipeItemHeight()
  253. window.history.pushState(null, '', document.URL)
  254. window.addEventListener('popstate', onBack, false)
  255. })
  256. onUnmounted(() => {
  257. eventUnit.emit('unitAudioStop')
  258. })
  259. return () => (
  260. <div class={styles.unitDetail}>
  261. <Cell center class={styles.unitSection} border={false}>
  262. {{
  263. title: () => <div class={styles.unitTitle}>{state.examDetail.unitExaminationName}</div>,
  264. label: () => (
  265. <div class={styles.unitCount}>
  266. <div class={styles.qNums}>
  267. <Icon class={styles.icon} name={iconQuestionNums} />
  268. 题目数量{' '}
  269. <span class={styles.num} style={{ paddingLeft: '6px' }}>
  270. {state.currentIndex + 1}
  271. </span>
  272. /{state.examDetail.questionNum}
  273. </div>
  274. <div class={styles.qNums}>
  275. <Icon class={styles.icon} name={iconCountDown} />
  276. 剩余时长:
  277. <CountDown
  278. ref={countDownRef}
  279. v-model:time={state.time}
  280. format={'mm:ss'}
  281. autoStart={false}
  282. onFinish={async () => {
  283. eventUnit.emit('unitAudioStop')
  284. await onConfirmExam()
  285. state.visiableExam = true
  286. }}
  287. />
  288. </div>
  289. </div>
  290. )
  291. }}
  292. </Cell>
  293. <Swipe
  294. loop={false}
  295. showIndicators={false}
  296. ref={swipeRef}
  297. duration={300}
  298. touchable={false}
  299. height={state.swipeHeight}
  300. style={{ marginBottom: '12px' }}
  301. lazyRender
  302. onChange={(index: number) => {
  303. eventUnit.emit('unitAudioStop')
  304. state.currentIndex = index
  305. resizeSwipeItemHeight()
  306. }}
  307. >
  308. {state.questionList.map((item: any, index: number) => (
  309. // item.questionTypeCode === QuestionType.LINK && (
  310. // <SwipeItem>
  311. // <KeepLookQuestion v-model:value={item.userAnswer} data={item} index={index + 1} />
  312. // </SwipeItem>
  313. // )
  314. <SwipeItem>
  315. <div class="swipe-item-question">
  316. {item.questionTypeCode === QuestionType.RADIO && (
  317. <ChoiceQuestion
  318. v-model:value={item.userAnswer}
  319. index={index + 1}
  320. data={item}
  321. type="radio"
  322. />
  323. )}
  324. {item.questionTypeCode === QuestionType.CHECKBOX && (
  325. <ChoiceQuestion
  326. v-model:value={item.userAnswer}
  327. index={index + 1}
  328. data={item}
  329. type="checkbox"
  330. />
  331. )}
  332. {item.questionTypeCode === QuestionType.SORT && (
  333. <DragQuestion
  334. v-model:value={item.userAnswer}
  335. onUpdate:value={() => {
  336. resizeSwipeItemHeight(false)
  337. }}
  338. data={item}
  339. index={index + 1}
  340. />
  341. )}
  342. {item.questionTypeCode === QuestionType.LINK && (
  343. <KeepLookQuestion v-model:value={item.userAnswer} data={item} index={index + 1} />
  344. )}
  345. {item.questionTypeCode === QuestionType.PLAY && (
  346. <PlayQuestion
  347. v-model:value={item.userAnswer}
  348. data={item}
  349. index={index + 1}
  350. unitId={state.id as any}
  351. />
  352. )}
  353. </div>
  354. </SwipeItem>
  355. ))}
  356. </Swipe>
  357. <OSticky position="bottom" background="white">
  358. <div class={['btnGroup btnMore']}>
  359. {state.currentIndex > 0 && (
  360. <Button
  361. round
  362. block
  363. type="primary"
  364. plain
  365. onClick={() => {
  366. swipeRef.value?.prev()
  367. }}
  368. >
  369. 上一题
  370. </Button>
  371. )}
  372. <Button
  373. block
  374. round
  375. type="primary"
  376. onClick={onNextQuestion}
  377. loading={state.nextStatus}
  378. disabled={state.nextStatus}
  379. >
  380. {state.questionList.length === state.currentIndex + 1 ? '测试完成' : '下一题'}
  381. </Button>
  382. <Image
  383. src={iconButtonList}
  384. class={[styles.wapList, 'van-haptics-feedback']}
  385. onClick={() => (state.visiableAnswer = true)}
  386. />
  387. </div>
  388. </OSticky>
  389. {/* 题目集合 */}
  390. <ActionSheet v-model:show={state.visiableAnswer} title="题目列表" safeAreaInsetBottom>
  391. <AnswerList
  392. value={state.questionList}
  393. onSelect={(item: any) => {
  394. // 跳转,并且跳过动画
  395. swipeRef.value?.swipeTo(item, {
  396. immediate: true
  397. })
  398. state.visiableAnswer = false
  399. }}
  400. />
  401. </ActionSheet>
  402. <Popup
  403. v-model:show={state.visiableResult}
  404. closeOnClickOverlay={false}
  405. style={{ background: 'transparent', width: '96%' }}
  406. >
  407. <ResultFinish
  408. status={state.resultStatusType as any}
  409. result={state.resultInfo}
  410. confirmButtonText="去练习"
  411. cancelButtonText="我知道了"
  412. onClose={() => {
  413. state.visiableResult = false
  414. router.back()
  415. router.back()
  416. }}
  417. onConform={() => {
  418. state.visiableResult = false
  419. router.back()
  420. router.back()
  421. }}
  422. />
  423. </Popup>
  424. <ODialog
  425. v-model:show={state.visiableSure}
  426. title="测验完成"
  427. message="确认本次测验的题目都完成了吗?\n提交后不可修改哦"
  428. messageAlign="left"
  429. showCancelButton
  430. cancelButtonText="再等等"
  431. confirmButtonText="确认完成"
  432. onConfirm={onConfirmExam}
  433. />
  434. <ODialog
  435. v-model:show={state.visiableExam}
  436. message="考试已结束"
  437. messageAlign="center"
  438. onConfirm={async () => {
  439. onAfter()
  440. state.visiableResult = true
  441. }}
  442. />
  443. <ODialog
  444. v-model:show={state.quitStatus}
  445. title="提示"
  446. message="您是否退出本次测验?"
  447. confirmButtonText="确认完成"
  448. showCancelButton
  449. cancelButtonText="取消"
  450. onCancel={() => {
  451. window.history.pushState(null, '', document.URL)
  452. window.addEventListener('popstate', onBack, false)
  453. }}
  454. onConfirm={() => {
  455. onAfter()
  456. }}
  457. />
  458. </div>
  459. )
  460. }
  461. })