index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  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, 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 { 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 = () => {
  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. if (minu <= 0) {
  89. state.visiableExam = true
  90. } else {
  91. state.time = Math.ceil(minu / 1000) * 1000
  92. setTimeout(() => {
  93. countDownRef.value?.start()
  94. }, 10)
  95. }
  96. }
  97. /**
  98. * @description 初始化用户答案
  99. */
  100. const formatUserAnswers = (item: any, userAnswer: any) => {
  101. // 判断是否有结果
  102. if (!userAnswer) return []
  103. const answers = userAnswer || []
  104. return answers[item.id] ? answers[item.id] : []
  105. }
  106. /**
  107. * @description 重置当前的题目高度
  108. */
  109. const resizeSwipeItemHeight = (scroll = true) => {
  110. nextTick(() => {
  111. scroll && window.scrollTo(0, 0)
  112. setTimeout(() => {
  113. // const currentItemDom: Element =
  114. // document.querySelectorAll('.swipe-item-question')[state.currentIndex]
  115. const currentItemDom: any = document
  116. .querySelectorAll('.van-swipe-item')
  117. [state.currentIndex]?.querySelector('.swipe-item-question')
  118. const rect = useRect(currentItemDom)
  119. state.swipeHeight = rect.height
  120. }, 100)
  121. })
  122. }
  123. /**
  124. * @description 下一题 | 测试完成
  125. */
  126. const onNextQuestion = async () => {
  127. try {
  128. const questionList = state.questionList || []
  129. const userAnswerList: any = [] // 所有题目的答案
  130. // let currentResult = false // 当前题目是否已经答题
  131. questionList.forEach((question: any, index: number) => {
  132. // 格式化所有题目的答案
  133. if (question.userAnswer && question.userAnswer.length > 0) {
  134. userAnswerList.push({
  135. questionId: question.id,
  136. details: question.userAnswer
  137. })
  138. }
  139. })
  140. // 判断是否是最后一题
  141. // console.log(state.questionList.length, state.currentIndex, userAnswerList, '-----')
  142. if (state.questionList.length === state.currentIndex + 1) {
  143. state.visiableSure = true
  144. return
  145. }
  146. state.nextStatus = true
  147. await request.post('/api-student/studentUnitExamination/submitAnswer', {
  148. hideLoading: true,
  149. data: {
  150. answers: userAnswerList,
  151. studentUnitExaminationId: state.id
  152. }
  153. })
  154. swipeRef.value?.next()
  155. state.nextStatus = false
  156. } catch {
  157. //
  158. state.nextStatus = false
  159. }
  160. }
  161. /**
  162. * @description 提交最终答案
  163. */
  164. const onConfirmExam = async () => {
  165. try {
  166. const questionList = state.questionList || []
  167. const userAnswerList: any = [] // 所有题目的答案
  168. questionList.forEach((question: any) => {
  169. // 格式化所有题目的答案
  170. if (question.userAnswer && question.userAnswer.length > 0) {
  171. userAnswerList.push({
  172. questionId: question.id,
  173. details: question.userAnswer
  174. })
  175. }
  176. })
  177. const { data } = await request.post(
  178. '/api-student/studentUnitExamination/completionExamination',
  179. {
  180. data: {
  181. answers: userAnswerList,
  182. studentUnitExaminationId: state.id
  183. }
  184. }
  185. )
  186. if (data.status === 'A_PASS') {
  187. state.resultStatusType = 'SUCCESS'
  188. state.resultInfo = {
  189. tips: '恭喜你,测验通过!',
  190. score: data.score,
  191. examName: state.examDetail.unitExaminationName
  192. }
  193. } else {
  194. state.resultStatusType = 'FAIL'
  195. state.resultInfo = {
  196. tips: '本次测验不合格!',
  197. score: data.score,
  198. examName: state.examDetail.unitExaminationName
  199. }
  200. }
  201. state.visiableResult = true
  202. } catch {
  203. //
  204. }
  205. }
  206. // 拦截
  207. const onBack = () => {
  208. // showDialog({
  209. // title: '提示',
  210. // message: '您考试还未提交,是否退出?',
  211. // theme: 'round-button',
  212. // confirmButtonColor: '#ff8057'
  213. // }).then(() => {
  214. // onAfter()
  215. // router.back()
  216. // })
  217. state.quitStatus = true
  218. }
  219. const onAfter = () => {
  220. window.removeEventListener('popstate', onBack, false)
  221. }
  222. onMounted(async () => {
  223. await getExamDetails()
  224. // 初始化高度
  225. resizeSwipeItemHeight()
  226. window.history.pushState(null, '', document.URL)
  227. window.addEventListener('popstate', onBack, false)
  228. })
  229. return () => (
  230. <div class={styles.unitDetail}>
  231. <Cell center class={styles.unitSection} border={false}>
  232. {{
  233. title: () => <div class={styles.unitTitle}>{state.examDetail.unitExaminationName}</div>,
  234. label: () => (
  235. <div class={styles.unitCount}>
  236. <div class={styles.qNums}>
  237. <Icon class={styles.icon} name={iconQuestionNums} />
  238. 题目数量{' '}
  239. <span class={styles.num} style={{ paddingLeft: '6px' }}>
  240. {state.currentIndex + 1}
  241. </span>
  242. /{state.examDetail.questionNum}
  243. </div>
  244. <div class={styles.qNums}>
  245. <Icon class={styles.icon} name={iconCountDown} />
  246. 剩余时长:
  247. <CountDown
  248. ref={countDownRef}
  249. v-model:time={state.time}
  250. format={'mm:ss'}
  251. autoStart={false}
  252. onFinish={() => {
  253. state.visiableExam = true
  254. }}
  255. />
  256. </div>
  257. </div>
  258. )
  259. }}
  260. </Cell>
  261. <Swipe
  262. loop={false}
  263. showIndicators={false}
  264. ref={swipeRef}
  265. duration={300}
  266. touchable={false}
  267. height={state.swipeHeight}
  268. style={{ marginBottom: '12px' }}
  269. lazyRender
  270. onChange={(index: number) => {
  271. state.currentIndex = index
  272. resizeSwipeItemHeight()
  273. }}
  274. >
  275. {state.questionList.map((item: any, index: number) => (
  276. // item.questionTypeCode === QuestionType.LINK && (
  277. // <SwipeItem>
  278. // <KeepLookQuestion v-model:value={item.userAnswer} data={item} index={index + 1} />
  279. // </SwipeItem>
  280. // )
  281. <SwipeItem>
  282. <div class="swipe-item-question">
  283. {item.questionTypeCode === QuestionType.RADIO && (
  284. <ChoiceQuestion
  285. v-model:value={item.userAnswer}
  286. index={index + 1}
  287. data={item}
  288. type="radio"
  289. />
  290. )}
  291. {item.questionTypeCode === QuestionType.CHECKBOX && (
  292. <ChoiceQuestion
  293. v-model:value={item.userAnswer}
  294. index={index + 1}
  295. data={item}
  296. type="checkbox"
  297. />
  298. )}
  299. {item.questionTypeCode === QuestionType.SORT && (
  300. <DragQuestion
  301. v-model:value={item.userAnswer}
  302. onUpdate:value={() => {
  303. resizeSwipeItemHeight(false)
  304. }}
  305. data={item}
  306. index={index + 1}
  307. />
  308. )}
  309. {item.questionTypeCode === QuestionType.LINK && (
  310. <KeepLookQuestion v-model:value={item.userAnswer} data={item} index={index + 1} />
  311. )}
  312. {item.questionTypeCode === QuestionType.PLAY && (
  313. <PlayQuestion
  314. v-model:value={item.userAnswer}
  315. data={item}
  316. index={index + 1}
  317. unitId={state.id as any}
  318. />
  319. )}
  320. </div>
  321. </SwipeItem>
  322. ))}
  323. </Swipe>
  324. <OSticky position="bottom" background="white">
  325. <div class={['btnGroup btnMore']}>
  326. {state.currentIndex > 0 && (
  327. <Button
  328. round
  329. block
  330. type="primary"
  331. plain
  332. onClick={() => {
  333. swipeRef.value?.prev()
  334. }}
  335. >
  336. 上一题
  337. </Button>
  338. )}
  339. <Button
  340. block
  341. round
  342. type="primary"
  343. onClick={onNextQuestion}
  344. loading={state.nextStatus}
  345. disabled={state.nextStatus}
  346. >
  347. {state.questionList.length === state.currentIndex + 1 ? '测试完成' : '下一题'}
  348. </Button>
  349. <Image
  350. src={iconButtonList}
  351. class={[styles.wapList, 'van-haptics-feedback']}
  352. onClick={() => (state.visiableAnswer = true)}
  353. />
  354. </div>
  355. </OSticky>
  356. {/* 题目集合 */}
  357. <ActionSheet v-model:show={state.visiableAnswer} title="题目列表" safeAreaInsetBottom>
  358. <AnswerList
  359. value={state.questionList}
  360. onSelect={(item: any) => {
  361. // 跳转,并且跳过动画
  362. swipeRef.value?.swipeTo(item, {
  363. immediate: true
  364. })
  365. state.visiableAnswer = false
  366. }}
  367. />
  368. </ActionSheet>
  369. <Popup
  370. v-model:show={state.visiableResult}
  371. closeOnClickOverlay={false}
  372. style={{ background: 'transparent', width: '96%' }}
  373. >
  374. <ResultFinish
  375. status={state.resultStatusType as any}
  376. result={state.resultInfo}
  377. confirmButtonText="去练习"
  378. cancelButtonText="我知道了"
  379. onClose={() => {
  380. state.visiableResult = false
  381. onAfter()
  382. router.back()
  383. router.back()
  384. }}
  385. onConform={() => {
  386. state.visiableResult = false
  387. onAfter()
  388. router.back()
  389. router.back()
  390. }}
  391. />
  392. </Popup>
  393. <ODialog
  394. v-model:show={state.visiableSure}
  395. title="测验完成"
  396. message="确认本次测验的题目都完成了吗?\n提交后不可修改哦"
  397. messageAlign="left"
  398. showCancelButton
  399. cancelButtonText="再等等"
  400. confirmButtonText="确认完成"
  401. onConfirm={onConfirmExam}
  402. />
  403. <ODialog
  404. v-model:show={state.visiableExam}
  405. message="考试已结束"
  406. messageAlign="center"
  407. onConfirm={() => {
  408. state.visiableResult = true
  409. }}
  410. />
  411. <ODialog
  412. v-model:show={state.quitStatus}
  413. title="提示"
  414. message="您是否退出本次练习?"
  415. confirmButtonText="确认完成"
  416. onConfirm={() => {
  417. onAfter()
  418. router.back()
  419. }}
  420. />
  421. </div>
  422. )
  423. }
  424. })