mockExamPlay.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  1. <!-- 模拟考试答题页 -->
  2. <template>
  3. <view class="exam-screen">
  4. <!-- 顶部栏 -->
  5. <view class="topbar">
  6. <view class="topbar-left" @click="requestSubmit(false)">‹ 交卷</view>
  7. <view class="topbar-title">{{ examInfo.examName || '模拟考试' }}</view>
  8. <view class="topbar-right" @click="toggleQuestionNav">题号</view>
  9. </view>
  10. <!-- 剩余时间条 -->
  11. <view class="time-bar">
  12. <view class="time-bar-left">
  13. <text class="time-label">剩余时间</text>
  14. <text class="time-value">{{ formattedTime }}</text>
  15. </view>
  16. <text class="time-tip">请合理安排答题时间</text>
  17. </view>
  18. <!-- 答题统计(答对/答错/正确率) -->
  19. <view class="stat-row">
  20. <view class="stat-item">
  21. <text class="stat-num stat-correct">{{ correct }}</text>
  22. <text class="stat-label">答对</text>
  23. </view>
  24. <view class="stat-divider"></view>
  25. <view class="stat-item">
  26. <text class="stat-num stat-wrong">{{ incorrect }}</text>
  27. <text class="stat-label">答错</text>
  28. </view>
  29. <view class="stat-divider"></view>
  30. <view class="stat-item">
  31. <text class="stat-num">{{ (accuracy * 100).toFixed(0) }}%</text>
  32. <text class="stat-label">正确率</text>
  33. </view>
  34. <view class="stat-divider"></view>
  35. <view class="stat-item">
  36. <text class="stat-num">{{ totalCount }}</text>
  37. <text class="stat-label">总题</text>
  38. </view>
  39. </view>
  40. <!-- 题号导航面板 -->
  41. <view v-if="showQuestionNav" class="q-nav-panel">
  42. <view class="q-nav-scroll-wrapper">
  43. <view class="q-nav-header">
  44. <text class="q-nav-title">题目列表</text>
  45. <text class="q-nav-close" @click="toggleQuestionNav">✕</text>
  46. </view>
  47. <scroll-view scroll-y class="q-nav-scroll">
  48. <view v-for="(group, gi) in questionTypeGroups" :key="gi" class="q-group">
  49. <view class="q-group-title">{{ group.questionTypeName }}({{ group.questionCount }}题 {{ group.totalScore }}分)</view>
  50. <view class="q-num-row">
  51. <view
  52. v-for="(q, qi) in group.questions"
  53. :key="qi"
  54. class="q-num"
  55. :class="getNumClass(gi, qi, q)"
  56. @click="selectQuestion(gi, qi)"
  57. >{{ q.questionNo }}</view>
  58. </view>
  59. </view>
  60. </scroll-view>
  61. </view>
  62. </view>
  63. <!-- 题目内容 -->
  64. <scroll-view scroll-y class="q-scroll-content">
  65. <view v-if="currentQuestion.snapshotQuestionId" class="q-card">
  66. <!-- 题目头部 -->
  67. <view class="q-header">
  68. <view class="q-header-left">
  69. <text class="q-type-badge">{{ currentQuestion.questionTypeName }}</text>
  70. <text class="q-num-text">第 {{ currentQuestion.questionNo }} 题</text>
  71. <text class="q-score-text">({{ currentQuestion.questionScore }}分)</text>
  72. </view>
  73. <view class="q-header-right">
  74. <view v-if="currentQuestion.difficulty" class="q-stars">
  75. <text
  76. v-for="s in 4"
  77. :key="s"
  78. :class="['star-icon', s <= currentQuestion.difficulty ? 'star-active' : 'star-empty']"
  79. >★</text>
  80. </view>
  81. <view class="view-answer-btn" @click="viewAnswerShowType = !viewAnswerShowType">
  82. {{ viewAnswerShowType ? '关闭答案' : '查看答案' }}
  83. </view>
  84. </view>
  85. </view>
  86. <!-- 题目内容 -->
  87. <view class="q-content">
  88. <rich-text :nodes="currentQuestion.questionContent || ''"></rich-text>
  89. </view>
  90. <!-- 选项 -->
  91. <view class="q-options">
  92. <view
  93. v-for="(opt, oi) in (currentQuestion.options || [])"
  94. :key="oi"
  95. class="q-option"
  96. :class="{ selected: isSelected(opt.optionLabel) }"
  97. @click="selectAnswer(opt)"
  98. >
  99. <view class="q-option-label">{{ opt.optionLabel }}</view>
  100. <view class="q-option-content">
  101. <rich-text :nodes="opt.optionContent || opt.optionLabel"></rich-text>
  102. </view>
  103. </view>
  104. </view>
  105. <!-- 查看答案区域 -->
  106. <view v-if="viewAnswerShowType" class="answer-result-box">
  107. <view class="answer-row">
  108. <text class="answer-label">答案:</text>
  109. <text class="answer-value">{{ arrayToString(currentQuestion.correctAnswerTags) }}</text>
  110. </view>
  111. <view class="analysis-title">试题解析</view>
  112. <view class="analysis-content">
  113. <rich-text v-if="currentQuestion.analysis" :nodes="currentQuestion.analysis"></rich-text>
  114. <text v-else class="analysis-empty">无</text>
  115. </view>
  116. </view>
  117. </view>
  118. <view v-else-if="pageLoading" class="empty-tip">加载中...</view>
  119. <view v-else class="empty-tip">暂无题目</view>
  120. <view class="scroll-bottom-space"></view>
  121. </scroll-view>
  122. <!-- 上一题/下一题 -->
  123. <view class="q-nav">
  124. <view class="q-nav-btn" :class="{ disabled: isFirstQuestion }" @click="prevQuestion">上一题</view>
  125. <view class="q-nav-btn q-nav-btn-next" :class="{ disabled: isLastQuestion }" @click="nextQuestion">下一题</view>
  126. </view>
  127. <!-- 底部提交 -->
  128. <view class="exam-bottom">
  129. <view class="btn-primary" @click="requestSubmit(false)">提交试卷</view>
  130. </view>
  131. <!-- 交卷结果遮罩 -->
  132. <view v-if="showResult" class="result-shade">
  133. <view class="result-card">
  134. <view class="result-title">{{ resultData.title }}</view>
  135. <view class="result-score">
  136. <text class="result-score-num">{{ resultData.score }}</text>
  137. <text class="result-score-unit">分</text>
  138. </view>
  139. <view class="result-desc">{{ resultData.desc }}</view>
  140. <view class="result-btns">
  141. <view v-if="resultData.canRetake" class="result-btn result-btn-outline" @click="retakeExam">重新考试</view>
  142. <view class="result-btn result-btn-primary" @click="closeResult">{{ resultData.canRetake ? '返回' : '确认' }}</view>
  143. </view>
  144. </view>
  145. </view>
  146. </view>
  147. </template>
  148. <script>
  149. import {
  150. examElExamAttemptReviewSidebar,
  151. examElExamAttemptReviewQuestion,
  152. examUserExamGetExamSurplusTime,
  153. examUserExamAnswerSave,
  154. examUserExamSubmit,
  155. examUserExamStart,
  156. } from '@/pages_safetyEducationExamination/api/index.js'
  157. export default {
  158. data() {
  159. return {
  160. examId: '',
  161. attemptId: '',
  162. examInfo: {},
  163. questionTypeGroups: [],
  164. currentQuestion: {},
  165. groupIndex: 0,
  166. questionIndex: 0,
  167. remainingSeconds: 0,
  168. timer: null,
  169. hasWarnedTime: false,
  170. showQuestionNav: false,
  171. pageLoading: false,
  172. isSaving: false,
  173. isSubmitting: false,
  174. hasSubmitted: false,
  175. showResult: false,
  176. resultData: {},
  177. viewAnswerShowType: false,
  178. }
  179. },
  180. computed: {
  181. isFirstQuestion() {
  182. return this.groupIndex === 0 && this.questionIndex === 0
  183. },
  184. isLastQuestion() {
  185. const lastGi = this.questionTypeGroups.length - 1
  186. if (lastGi < 0) return true
  187. const lastGroup = this.questionTypeGroups[lastGi]
  188. return this.groupIndex === lastGi && this.questionIndex === (lastGroup.questions.length - 1)
  189. },
  190. totalCount() {
  191. return this.questionTypeGroups.reduce((s, g) => s + (g.questions ? g.questions.length : 0), 0)
  192. },
  193. // 答对:isCorrect === 1
  194. correct() {
  195. let count = 0
  196. this.questionTypeGroups.forEach(g => {
  197. ;(g.questions || []).forEach(q => { if (q.isCorrect === 1) count++ })
  198. })
  199. return count
  200. },
  201. // 答错:isCorrect === 0
  202. incorrect() {
  203. let count = 0
  204. this.questionTypeGroups.forEach(g => {
  205. ;(g.questions || []).forEach(q => { if (q.isCorrect === 0) count++ })
  206. })
  207. return count
  208. },
  209. // 正确率
  210. accuracy() {
  211. if (this.totalCount === 0) return 0
  212. return parseFloat((this.correct / this.totalCount).toFixed(2))
  213. },
  214. formattedTime() {
  215. const s = Math.max(0, Math.floor(this.remainingSeconds))
  216. const m = Math.floor(s / 60)
  217. const sec = s % 60
  218. return m + '分' + (sec < 10 ? '0' : '') + sec + '秒'
  219. },
  220. },
  221. onLoad(options) {
  222. console.log('options',options)
  223. this.examId = options.examId || ''
  224. this.attemptId = options.attemptId || ''
  225. if (!this.attemptId) {
  226. uni.showToast({ title: '考试信息无效', icon: 'none' })
  227. setTimeout(() => uni.navigateBack(), 1500)
  228. return
  229. }
  230. this.pageLoading = true
  231. this.loadSidebar()
  232. },
  233. onShow() {
  234. if (this.hasSubmitted || !this.attemptId) return
  235. if (this.questionTypeGroups.length > 0) {
  236. this.syncRemainingTime()
  237. }
  238. },
  239. onHide() {
  240. this.clearTimer()
  241. this.saveCurrentAnswer()
  242. },
  243. onUnload() {
  244. this.clearTimer()
  245. this.saveCurrentAnswer()
  246. },
  247. methods: {
  248. // 加载左侧题号导航
  249. async loadSidebar() {
  250. try {
  251. const response = await examElExamAttemptReviewSidebar({ attemptId: this.attemptId })
  252. const data = response.data
  253. this.examInfo = {
  254. examTypeName: data.examTypeName || '',
  255. examName: data.examName || '',
  256. questionCount: data.questionCount || 0,
  257. totalScore: data.totalScore || 0,
  258. passScore: data.passScore || 0,
  259. }
  260. this.$set(this, 'questionTypeGroups', data.questionTypeGroups || [])
  261. this.groupIndex = 0
  262. this.questionIndex = 0
  263. const firstGroup = this.questionTypeGroups[0]
  264. if (firstGroup && firstGroup.questions && firstGroup.questions.length > 0) {
  265. await this.loadQuestion(firstGroup.questions[0].snapshotQuestionId)
  266. this.syncRemainingTime()
  267. }
  268. } catch (e) {
  269. uni.showToast({ title: '加载失败', icon: 'none' })
  270. } finally {
  271. this.pageLoading = false
  272. }
  273. },
  274. // 加载题目详情(examScene:2 模拟考试)
  275. async loadQuestion(snapshotQuestionId) {
  276. try {
  277. const response = await examElExamAttemptReviewQuestion({
  278. attemptId: this.attemptId,
  279. snapshotQuestionId: snapshotQuestionId,
  280. examScene: 2,
  281. })
  282. this.$set(this, 'currentQuestion', response.data)
  283. this.$set(this, 'viewAnswerShowType', false)
  284. } catch (e) {
  285. uni.showToast({ title: '题目加载失败', icon: 'none' })
  286. }
  287. },
  288. // 同步剩余时间并启动倒计时
  289. async syncRemainingTime() {
  290. try {
  291. const response = await examUserExamGetExamSurplusTime({ attemptId: this.attemptId })
  292. this.remainingSeconds = Number(response.data) || 0
  293. this.clearTimer()
  294. this.startTimer()
  295. } catch (e) {
  296. console.error('获取剩余时间失败', e)
  297. }
  298. },
  299. startTimer() {
  300. if (this.timer) return
  301. this.timer = setInterval(() => {
  302. if (this.remainingSeconds <= 0) {
  303. this.clearTimer()
  304. this.autoSubmit()
  305. return
  306. }
  307. this.remainingSeconds--
  308. if (this.remainingSeconds === 60 && !this.hasWarnedTime) {
  309. this.hasWarnedTime = true
  310. uni.showToast({ title: '请注意剩余时间', icon: 'none', duration: 2000 })
  311. }
  312. }, 1000)
  313. },
  314. clearTimer() {
  315. if (this.timer) {
  316. clearInterval(this.timer)
  317. this.timer = null
  318. }
  319. },
  320. // 选择答案
  321. selectAnswer(opt) {
  322. if (!this.currentQuestion.snapshotQuestionId) return
  323. let tags = Array.isArray(this.currentQuestion.answerTags) ? [...this.currentQuestion.answerTags] : []
  324. const qType = this.currentQuestion.questionType
  325. if (qType == 2) {
  326. // 多选
  327. const idx = tags.indexOf(opt.optionLabel)
  328. if (idx === -1) { tags.push(opt.optionLabel) } else { tags.splice(idx, 1) }
  329. } else {
  330. // 单选 / 判断
  331. tags = [opt.optionLabel]
  332. }
  333. this.$set(this.currentQuestion, 'answerTags', tags)
  334. },
  335. isSelected(label) {
  336. const tags = this.currentQuestion.answerTags
  337. return Array.isArray(tags) && tags.indexOf(label) !== -1
  338. },
  339. // 保存当前答案,同时回写 isCorrect 到题号列表
  340. saveCurrentAnswer() {
  341. const q = this.currentQuestion
  342. if (!q || !q.snapshotQuestionId) return Promise.resolve()
  343. const tags = Array.isArray(q.answerTags) ? q.answerTags.filter(Boolean) : []
  344. if (tags.length === 0) return Promise.resolve()
  345. return examUserExamAnswerSave({
  346. attemptId: this.attemptId,
  347. snapshotQuestionId: q.snapshotQuestionId,
  348. userAnswer: tags.join(','),
  349. }).then(response => {
  350. // response.data 是 isCorrect 值(1正确 0错误)
  351. const groups = this.questionTypeGroups
  352. if (groups[this.groupIndex] && groups[this.groupIndex].questions[this.questionIndex]) {
  353. this.$set(groups[this.groupIndex].questions[this.questionIndex], 'isCorrect', response.data)
  354. }
  355. }).catch(e => {
  356. console.error('保存答案失败', e)
  357. })
  358. },
  359. // 切换题目(先保存当前答案)
  360. async changeQuestion(targetGi, targetQi) {
  361. if (this.isSaving) return
  362. if (targetGi === this.groupIndex && targetQi === this.questionIndex) return
  363. const group = this.questionTypeGroups[targetGi]
  364. if (!group || !group.questions || !group.questions[targetQi]) return
  365. this.isSaving = true
  366. await this.saveCurrentAnswer()
  367. this.isSaving = false
  368. this.groupIndex = targetGi
  369. this.questionIndex = targetQi
  370. await this.loadQuestion(group.questions[targetQi].snapshotQuestionId)
  371. },
  372. selectQuestion(gi, qi) {
  373. this.changeQuestion(gi, qi)
  374. this.showQuestionNav = false
  375. },
  376. prevQuestion() {
  377. if (this.isFirstQuestion || this.isSaving) return
  378. if (this.questionIndex > 0) {
  379. this.changeQuestion(this.groupIndex, this.questionIndex - 1)
  380. } else {
  381. const prevGi = this.groupIndex - 1
  382. const prevGroup = this.questionTypeGroups[prevGi]
  383. if (prevGroup) this.changeQuestion(prevGi, prevGroup.questions.length - 1)
  384. }
  385. },
  386. nextQuestion() {
  387. if (this.isLastQuestion || this.isSaving) return
  388. const currentGroup = this.questionTypeGroups[this.groupIndex]
  389. if (this.questionIndex < currentGroup.questions.length - 1) {
  390. this.changeQuestion(this.groupIndex, this.questionIndex + 1)
  391. } else {
  392. this.changeQuestion(this.groupIndex + 1, 0)
  393. }
  394. },
  395. toggleQuestionNav() {
  396. this.showQuestionNav = !this.showQuestionNav
  397. },
  398. // 题号样式:答对绿、答错红、当前蓝、未答灰
  399. getNumClass(gi, qi, q) {
  400. if (gi === this.groupIndex && qi === this.questionIndex) return 'current'
  401. if (q.isCorrect === 1) return 'correct'
  402. if (q.isCorrect === 0) return 'wrong'
  403. return ''
  404. },
  405. // 提交前确认
  406. async requestSubmit(isAuto) {
  407. if (this.isSubmitting || this.hasSubmitted) return
  408. this.isSubmitting = true
  409. await this.saveCurrentAnswer()
  410. // 统计未答题数
  411. let unanswered = 0
  412. this.questionTypeGroups.forEach(g => {
  413. ;(g.questions || []).forEach(q => { if (q.isCorrect == null) unanswered++ })
  414. })
  415. if (isAuto) {
  416. await this.doSubmit()
  417. this.isSubmitting = false
  418. return
  419. }
  420. const content = unanswered > 0 ? `还有${unanswered}题未答,确认要交卷吗?` : '确认要交卷吗?'
  421. uni.showModal({
  422. title: '提示',
  423. content: content,
  424. success: async (res) => {
  425. if (res.confirm) { await this.doSubmit() }
  426. this.isSubmitting = false
  427. },
  428. fail: () => { this.isSubmitting = false }
  429. })
  430. },
  431. async autoSubmit() {
  432. if (this.isSubmitting || this.hasSubmitted) return
  433. this.isSubmitting = true
  434. await this.saveCurrentAnswer()
  435. uni.showToast({ title: '考试时间结束,正在提交…', icon: 'none' })
  436. await this.doSubmit()
  437. this.isSubmitting = false
  438. },
  439. async doSubmit() {
  440. try {
  441. const response = await examUserExamSubmit({ attemptId: this.attemptId })
  442. this.hasSubmitted = true
  443. this.clearTimer()
  444. this.buildResult(response.data)
  445. this.showResult = true
  446. } catch (e) {
  447. uni.showToast({ title: '交卷失败', icon: 'none' })
  448. }
  449. },
  450. buildResult(resp) {
  451. const score = resp.totalScore || 0
  452. const pass = score >= (this.examInfo.passScore || 0)
  453. if (pass) {
  454. this.resultData = {
  455. title: '模拟考试通过',
  456. score: score,
  457. desc: '本次模拟考试成绩有效',
  458. canRetake: false,
  459. }
  460. } else {
  461. this.resultData = {
  462. title: '本次模拟考试不及格',
  463. score: score,
  464. desc: `未达及格线 ${this.examInfo.passScore} 分,可重新考试`,
  465. canRetake: true,
  466. }
  467. }
  468. },
  469. async retakeExam() {
  470. try {
  471. const response = await examUserExamStart({ examId: this.examId, examScene: 2 })
  472. const data = response.data
  473. if (data && data.attemptId) {
  474. this.attemptId = data.attemptId
  475. this.showResult = false
  476. this.hasSubmitted = false
  477. this.isSubmitting = false
  478. this.hasWarnedTime = false
  479. this.viewAnswerShowType = false
  480. this.$set(this, 'questionTypeGroups', [])
  481. this.$set(this, 'currentQuestion', {})
  482. this.groupIndex = 0
  483. this.questionIndex = 0
  484. this.remainingSeconds = 0
  485. this.pageLoading = true
  486. this.loadSidebar()
  487. } else {
  488. uni.showToast({ title: response.message || '重新考试失败', icon: 'none' })
  489. uni.navigateBack()
  490. }
  491. } catch (e) {
  492. uni.showToast({ title: '重新考试失败', icon: 'none' })
  493. }
  494. },
  495. closeResult() {
  496. this.showResult = false
  497. uni.navigateBack()
  498. },
  499. // 答案标签转显示文字(Y→对 N→错)
  500. arrayToString(list) {
  501. if (!Array.isArray(list)) return ''
  502. return list.map(item => {
  503. if (item === 'Y') return '对'
  504. if (item === 'N') return '错'
  505. return item
  506. }).join(',')
  507. },
  508. }
  509. }
  510. </script>
  511. <style lang="stylus" scoped>
  512. .exam-screen
  513. height 100%
  514. display flex
  515. flex-direction column
  516. background #f5f7fa
  517. position relative
  518. .topbar
  519. display flex
  520. align-items center
  521. justify-content space-between
  522. height 88rpx
  523. padding 0 28rpx
  524. background #fff
  525. border-bottom 1rpx solid #e8edf3
  526. flex-shrink 0
  527. .topbar-left
  528. font-size 28rpx
  529. color #1677ff
  530. min-width 80rpx
  531. .topbar-title
  532. font-size 28rpx
  533. font-weight 600
  534. color #222
  535. flex 1
  536. text-align center
  537. .topbar-right
  538. font-size 28rpx
  539. color #1677ff
  540. min-width 80rpx
  541. text-align right
  542. .time-bar
  543. display flex
  544. align-items center
  545. justify-content space-between
  546. padding 20rpx 28rpx
  547. background linear-gradient(90deg, #1677ff 0%, #0958d9 100%)
  548. flex-shrink 0
  549. .time-bar-left
  550. display flex
  551. align-items baseline
  552. .time-label
  553. font-size 24rpx
  554. color rgba(255,255,255,0.85)
  555. margin-right 16rpx
  556. .time-value
  557. font-size 44rpx
  558. font-weight 700
  559. color #fff
  560. .time-tip
  561. font-size 22rpx
  562. color rgba(255,255,255,0.75)
  563. .stat-row
  564. display flex
  565. align-items center
  566. background #fff
  567. padding 20rpx 0
  568. flex-shrink 0
  569. border-bottom 1rpx solid #e8edf3
  570. .stat-item
  571. flex 1
  572. display flex
  573. flex-direction column
  574. align-items center
  575. .stat-divider
  576. width 1rpx
  577. height 40rpx
  578. background #eee
  579. .stat-num
  580. font-size 36rpx
  581. font-weight 700
  582. color #333
  583. line-height 1
  584. .stat-correct
  585. color #52c41a
  586. .stat-wrong
  587. color #ff4d4f
  588. .stat-label
  589. font-size 22rpx
  590. color #999
  591. margin-top 6rpx
  592. .q-nav-panel
  593. position fixed
  594. top 0
  595. left 0
  596. right 0
  597. bottom 0
  598. background rgba(0,0,0,0.45)
  599. z-index 100
  600. display flex
  601. flex-direction column
  602. justify-content flex-end
  603. .q-nav-scroll-wrapper
  604. background #fff
  605. border-radius 32rpx 32rpx 0 0
  606. max-height 70vh
  607. display flex
  608. flex-direction column
  609. .q-nav-header
  610. display flex
  611. align-items center
  612. justify-content space-between
  613. padding 28rpx 28rpx 16rpx
  614. flex-shrink 0
  615. .q-nav-title
  616. font-size 30rpx
  617. font-weight 600
  618. color #222
  619. .q-nav-close
  620. font-size 32rpx
  621. color #999
  622. padding 10rpx
  623. .q-nav-scroll
  624. flex 1
  625. overflow hidden
  626. padding-bottom 40rpx
  627. .q-group
  628. padding 0 28rpx 16rpx
  629. .q-group-title
  630. font-size 24rpx
  631. color #666
  632. line-height 60rpx
  633. .q-num-row
  634. display flex
  635. flex-wrap wrap
  636. .q-num
  637. width 72rpx
  638. height 72rpx
  639. line-height 72rpx
  640. text-align center
  641. font-size 24rpx
  642. border-radius 12rpx
  643. margin 8rpx
  644. background #f4f6fa
  645. color #333
  646. border 1rpx solid transparent
  647. .q-num.correct
  648. background #f6ffed
  649. color #52c41a
  650. border-color #b7eb8f
  651. .q-num.wrong
  652. background #fff2f0
  653. color #ff4d4f
  654. border-color #ffa39e
  655. .q-num.current
  656. background #1677ff
  657. color #fff
  658. .q-scroll-content
  659. flex 1
  660. overflow hidden
  661. .q-card
  662. margin 24rpx
  663. padding 28rpx
  664. background #fff
  665. border-radius 20rpx
  666. border 1rpx solid #dde3ed
  667. .q-header
  668. display flex
  669. align-items center
  670. justify-content space-between
  671. margin-bottom 20rpx
  672. .q-header-left
  673. display flex
  674. align-items center
  675. flex 1
  676. flex-wrap wrap
  677. .q-header-right
  678. display flex
  679. align-items center
  680. flex-shrink 0
  681. margin-left 12rpx
  682. .q-type-badge
  683. font-size 20rpx
  684. padding 4rpx 14rpx
  685. background #e6f4ff
  686. color #1677ff
  687. border 1rpx solid #91caff
  688. border-radius 8rpx
  689. margin-right 12rpx
  690. .q-num-text
  691. font-size 26rpx
  692. color #333
  693. .q-score-text
  694. font-size 22rpx
  695. color #999
  696. .q-stars
  697. display flex
  698. align-items center
  699. margin-right 16rpx
  700. .star-icon
  701. font-size 28rpx
  702. font-style normal
  703. .star-active
  704. color #fadb14
  705. .star-empty
  706. color #d9d9d9
  707. .view-answer-btn
  708. font-size 22rpx
  709. padding 6rpx 18rpx
  710. background #0183fa
  711. color #fff
  712. border-radius 8rpx
  713. white-space nowrap
  714. .q-content
  715. font-size 28rpx
  716. line-height 1.8
  717. color #222
  718. margin-bottom 24rpx
  719. .q-options
  720. display flex
  721. flex-direction column
  722. .q-option
  723. display flex
  724. align-items center
  725. padding 22rpx 20rpx
  726. margin-bottom 16rpx
  727. border 2rpx solid #dde3ed
  728. border-radius 16rpx
  729. background #fff
  730. .q-option.selected
  731. border-color #1677ff
  732. background #e6f4ff
  733. .q-option-label
  734. width 48rpx
  735. height 48rpx
  736. line-height 48rpx
  737. text-align center
  738. border-radius 50%
  739. background #f4f6fa
  740. color #333
  741. font-size 24rpx
  742. font-weight 600
  743. margin-right 20rpx
  744. flex-shrink 0
  745. .q-option.selected .q-option-label
  746. background #1677ff
  747. color #fff
  748. .q-option-content
  749. flex 1
  750. font-size 26rpx
  751. color #333
  752. line-height 1.6
  753. .answer-result-box
  754. background #f5f5f5
  755. border-radius 12rpx
  756. padding 24rpx 28rpx
  757. margin-top 20rpx
  758. .answer-row
  759. display flex
  760. align-items center
  761. margin-bottom 16rpx
  762. .answer-label
  763. font-size 26rpx
  764. font-weight 600
  765. color #333
  766. .answer-value
  767. font-size 26rpx
  768. color #1677ff
  769. font-weight 700
  770. .analysis-title
  771. font-size 26rpx
  772. font-weight 600
  773. color #333
  774. margin-bottom 12rpx
  775. .analysis-content
  776. font-size 24rpx
  777. color #666
  778. line-height 1.7
  779. padding 0 8rpx
  780. .analysis-empty
  781. font-size 24rpx
  782. color #aaa
  783. .scroll-bottom-space
  784. height 200rpx
  785. .q-nav
  786. display flex
  787. background #fff
  788. border-top 1rpx solid #e8edf3
  789. flex-shrink 0
  790. .q-nav-btn
  791. flex 1
  792. height 88rpx
  793. line-height 88rpx
  794. text-align center
  795. font-size 28rpx
  796. color #1677ff
  797. .q-nav-btn.q-nav-btn-next
  798. border-left 1rpx solid #e8edf3
  799. .q-nav-btn.disabled
  800. color #ccc
  801. .exam-bottom
  802. padding 20rpx 24rpx
  803. padding-bottom calc(20rpx + env(safe-area-inset-bottom))
  804. background #fff
  805. border-top 1rpx solid #e8edf3
  806. flex-shrink 0
  807. .btn-primary
  808. background #1677ff
  809. color #fff
  810. border-radius 16rpx
  811. font-size 30rpx
  812. font-weight 600
  813. height 88rpx
  814. line-height 88rpx
  815. text-align center
  816. .result-shade
  817. position fixed
  818. top 0
  819. left 0
  820. right 0
  821. bottom 0
  822. background rgba(0,0,0,0.55)
  823. z-index 200
  824. display flex
  825. align-items center
  826. justify-content center
  827. .result-card
  828. width 580rpx
  829. background #fff
  830. border-radius 24rpx
  831. padding 48rpx 40rpx 36rpx
  832. display flex
  833. flex-direction column
  834. align-items center
  835. .result-title
  836. font-size 32rpx
  837. font-weight 700
  838. color #222
  839. margin-bottom 20rpx
  840. .result-score
  841. display flex
  842. align-items baseline
  843. margin-bottom 16rpx
  844. .result-score-num
  845. font-size 80rpx
  846. font-weight 700
  847. color #F59A23
  848. line-height 1
  849. .result-score-unit
  850. font-size 30rpx
  851. color #F59A23
  852. margin-left 8rpx
  853. .result-desc
  854. font-size 26rpx
  855. color #666
  856. text-align center
  857. margin-bottom 36rpx
  858. line-height 1.6
  859. .result-btns
  860. display flex
  861. width 100%
  862. .result-btn
  863. flex 1
  864. height 80rpx
  865. line-height 80rpx
  866. text-align center
  867. border-radius 14rpx
  868. font-size 28rpx
  869. .result-btn-outline
  870. border 1rpx solid #1677ff
  871. color #1677ff
  872. margin-right 20rpx
  873. .result-btn-primary
  874. background #1677ff
  875. color #fff
  876. .empty-tip
  877. text-align center
  878. padding 80rpx 0
  879. color #aaa
  880. font-size 26rpx
  881. </style>