mockExamPlay.vue 24 KB

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