mockExamPlay.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990
  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. if (response.data && response.data.data) {
  284. this.$set(this, 'currentQuestion', response.data.data)
  285. this.$set(this, 'viewAnswerShowType', false)
  286. } else {
  287. throw new Error((response.data && response.data.message) || '加载题目失败')
  288. }
  289. } catch (e) {
  290. console.error('加载题目失败', e)
  291. throw e
  292. }
  293. },
  294. // 同步剩余时间并启动倒计时
  295. async syncRemainingTime() {
  296. try {
  297. const response = await examUserExamGetExamSurplusTime({ attemptId: this.attemptId })
  298. this.remainingSeconds = Number(response.data.data) || 0
  299. this.clearTimer()
  300. this.startTimer()
  301. } catch (e) {
  302. console.error('获取剩余时间失败', e)
  303. }
  304. },
  305. startTimer() {
  306. if (this.timer) return
  307. this.timer = setInterval(() => {
  308. if (this.remainingSeconds <= 0) {
  309. this.clearTimer()
  310. this.autoSubmit()
  311. return
  312. }
  313. this.remainingSeconds--
  314. if (this.remainingSeconds === 60 && !this.hasWarnedTime) {
  315. this.hasWarnedTime = true
  316. uni.showToast({ title: '请注意剩余时间', icon: 'none', duration: 2000 })
  317. }
  318. }, 1000)
  319. },
  320. clearTimer() {
  321. if (this.timer) {
  322. clearInterval(this.timer)
  323. this.timer = null
  324. }
  325. },
  326. // 选择答案
  327. selectAnswer(optIndex) {
  328. const opt = (this.currentQuestion.options || [])[optIndex]
  329. if (!opt || !this.currentQuestion.snapshotQuestionId) return
  330. let tags = Array.isArray(this.currentQuestion.answerTags) ? [...this.currentQuestion.answerTags] : []
  331. const qType = this.currentQuestion.questionType
  332. if (qType == 2) {
  333. const idx = tags.indexOf(opt.optionLabel)
  334. if (idx === -1) { tags.push(opt.optionLabel) } else { tags.splice(idx, 1) }
  335. } else {
  336. tags = [opt.optionLabel]
  337. }
  338. this.$set(this.currentQuestion, 'answerTags', tags)
  339. },
  340. onOptionTap(e) {
  341. const oi = Number(e.currentTarget.dataset.oi)
  342. this.selectAnswer(oi)
  343. },
  344. isSelected(label) {
  345. const tags = this.currentQuestion.answerTags
  346. return Array.isArray(tags) && tags.indexOf(label) !== -1
  347. },
  348. // 保存当前答案,同时回写 isCorrect 到题号列表
  349. saveCurrentAnswer() {
  350. const q = this.currentQuestion
  351. if (!q || !q.snapshotQuestionId) return Promise.resolve()
  352. const tags = Array.isArray(q.answerTags) ? q.answerTags.filter(Boolean) : []
  353. if (tags.length === 0) return Promise.resolve()
  354. // 快照当前 index,避免 .then 回调时 index 已切换导致回写到错误的题
  355. const gi = this.groupIndex
  356. const qi = this.questionIndex
  357. return examUserExamAnswerSave({
  358. attemptId: this.attemptId,
  359. snapshotQuestionId: q.snapshotQuestionId,
  360. userAnswer: tags.join(','),
  361. }).then(response => {
  362. const groups = this.questionTypeGroups
  363. if (groups[gi] && groups[gi].questions[qi]) {
  364. this.$set(groups[gi].questions[qi], 'isCorrect', response.data.data)
  365. }
  366. }).catch(e => {
  367. console.error('保存答案失败', e)
  368. })
  369. },
  370. // 切换题目(先保存当前答案)
  371. async changeQuestion(targetGi, targetQi) {
  372. if (this.isSaving) return
  373. if (targetGi === this.groupIndex && targetQi === this.questionIndex) return
  374. const group = this.questionTypeGroups[targetGi]
  375. if (!group || !group.questions || !group.questions[targetQi]) return
  376. const prevGi = this.groupIndex
  377. const prevQi = this.questionIndex
  378. this.isSaving = true
  379. try {
  380. await this.saveCurrentAnswer()
  381. this.groupIndex = targetGi
  382. this.questionIndex = targetQi
  383. await this.loadQuestion(group.questions[targetQi].snapshotQuestionId)
  384. } catch (e) {
  385. this.groupIndex = prevGi
  386. this.questionIndex = prevQi
  387. uni.showToast({ title: '题目加载失败,请重试', icon: 'none' })
  388. } finally {
  389. this.isSaving = false
  390. }
  391. },
  392. selectQuestion(gi, qi) {
  393. this.changeQuestion(gi, qi)
  394. this.showQuestionNav = false
  395. },
  396. prevQuestion() {
  397. if (this.isFirstQuestion || this.isSaving) return
  398. if (this.questionIndex > 0) {
  399. this.changeQuestion(this.groupIndex, this.questionIndex - 1)
  400. } else {
  401. const prevGi = this.groupIndex - 1
  402. const prevGroup = this.questionTypeGroups[prevGi]
  403. if (prevGroup) this.changeQuestion(prevGi, prevGroup.questions.length - 1)
  404. }
  405. },
  406. nextQuestion() {
  407. if (this.isLastQuestion || this.isSaving) return
  408. const currentGroup = this.questionTypeGroups[this.groupIndex]
  409. if (this.questionIndex < currentGroup.questions.length - 1) {
  410. this.changeQuestion(this.groupIndex, this.questionIndex + 1)
  411. } else {
  412. this.changeQuestion(this.groupIndex + 1, 0)
  413. }
  414. },
  415. toggleQuestionNav() {
  416. this.showQuestionNav = !this.showQuestionNav
  417. },
  418. // 题号样式:答对绿、答错红、当前蓝、未答灰
  419. getNumClass(gi, qi, q) {
  420. if (gi === this.groupIndex && qi === this.questionIndex) return 'current'
  421. if (q.isCorrect === 1) return 'correct'
  422. if (q.isCorrect === 0) return 'wrong'
  423. return ''
  424. },
  425. // 提交前确认
  426. async requestSubmit(isAuto) {
  427. if (this.isSubmitting || this.hasSubmitted) return
  428. this.isSubmitting = true
  429. await this.saveCurrentAnswer()
  430. // 统计未答题数
  431. let unanswered = 0
  432. this.questionTypeGroups.forEach(g => {
  433. ;(g.questions || []).forEach(q => { if (q.isCorrect == null) unanswered++ })
  434. })
  435. if (isAuto) {
  436. await this.doSubmit()
  437. this.isSubmitting = false
  438. return
  439. }
  440. const content = unanswered > 0 ? `还有${unanswered}题未答,确认要交卷吗?` : '确认要交卷吗?'
  441. uni.showModal({
  442. title: '提示',
  443. content: content,
  444. success: async (res) => {
  445. if (res.confirm) { await this.doSubmit() }
  446. this.isSubmitting = false
  447. },
  448. fail: () => { this.isSubmitting = false }
  449. })
  450. },
  451. async autoSubmit() {
  452. if (this.isSubmitting || this.hasSubmitted) return
  453. this.isSubmitting = true
  454. await this.saveCurrentAnswer()
  455. uni.showToast({ title: '考试时间结束,正在提交…', icon: 'none' })
  456. await this.doSubmit()
  457. this.isSubmitting = false
  458. },
  459. async doSubmit() {
  460. try {
  461. const response = await examUserExamSubmit({ attemptId: this.attemptId })
  462. this.hasSubmitted = true
  463. this.clearTimer()
  464. this.buildResult(response.data.data)
  465. this.showResult = true
  466. } catch (e) {
  467. uni.showToast({ title: '交卷失败', icon: 'none' })
  468. }
  469. },
  470. buildResult(resp) {
  471. const score = resp.totalScore || 0
  472. const pass = score >= (this.examInfo.passScore || 0)
  473. if (pass) {
  474. this.resultData = {
  475. title: '模拟考试通过',
  476. score: score,
  477. desc: '本次模拟考试成绩有效',
  478. canRetake: false,
  479. }
  480. } else {
  481. this.resultData = {
  482. title: '本次模拟考试不及格',
  483. score: score,
  484. desc: `未达及格线 ${this.examInfo.passScore} 分,可重新考试`,
  485. canRetake: true,
  486. }
  487. }
  488. },
  489. async retakeExam() {
  490. try {
  491. const response = await examUserExamStart({ examId: this.examId, examScene: 2 })
  492. const data = response.data.data
  493. if (data && data.attemptId) {
  494. this.attemptId = data.attemptId
  495. this.showResult = false
  496. this.hasSubmitted = false
  497. this.isSubmitting = false
  498. this.hasWarnedTime = false
  499. this.viewAnswerShowType = false
  500. this.$set(this, 'questionTypeGroups', [])
  501. this.$set(this, 'currentQuestion', {})
  502. this.groupIndex = 0
  503. this.questionIndex = 0
  504. this.remainingSeconds = 0
  505. this.pageLoading = true
  506. this.loadSidebar()
  507. } else {
  508. uni.showToast({ title: response.data.message || '重新考试失败', icon: 'none' })
  509. uni.navigateBack()
  510. }
  511. } catch (e) {
  512. uni.showToast({ title: '重新考试失败', icon: 'none' })
  513. }
  514. },
  515. closeResult() {
  516. this.showResult = false
  517. uni.navigateBack()
  518. },
  519. // 答案标签转显示文字(Y→对 N→错)
  520. arrayToString(list) {
  521. if (!Array.isArray(list)) return ''
  522. return list.map(item => {
  523. if (item === 'Y') return '对'
  524. if (item === 'N') return '错'
  525. return item
  526. }).join(',')
  527. },
  528. }
  529. }
  530. </script>
  531. <style lang="stylus" scoped>
  532. .exam-screen
  533. height 100%
  534. display flex
  535. flex-direction column
  536. background #f5f7fa
  537. position relative
  538. .topbar
  539. display flex
  540. align-items center
  541. justify-content space-between
  542. height 88rpx
  543. padding 0 28rpx
  544. background #fff
  545. border-bottom 1rpx solid #e8edf3
  546. flex-shrink 0
  547. .topbar-left
  548. font-size 28rpx
  549. color #1677ff
  550. min-width 80rpx
  551. .topbar-title
  552. font-size 28rpx
  553. font-weight 600
  554. color #222
  555. flex 1
  556. text-align center
  557. .topbar-right
  558. font-size 28rpx
  559. color #1677ff
  560. min-width 80rpx
  561. text-align right
  562. .time-bar
  563. display flex
  564. align-items center
  565. justify-content space-between
  566. padding 20rpx 28rpx
  567. background linear-gradient(90deg, #1677ff 0%, #0958d9 100%)
  568. flex-shrink 0
  569. .time-bar-left
  570. display flex
  571. align-items baseline
  572. .time-label
  573. font-size 24rpx
  574. color rgba(255,255,255,0.85)
  575. margin-right 16rpx
  576. .time-value
  577. font-size 44rpx
  578. font-weight 700
  579. color #fff
  580. .time-tip
  581. font-size 22rpx
  582. color rgba(255,255,255,0.75)
  583. .stat-row
  584. display flex
  585. align-items center
  586. background #fff
  587. padding 20rpx 0
  588. flex-shrink 0
  589. border-bottom 1rpx solid #e8edf3
  590. .stat-item
  591. flex 1
  592. display flex
  593. flex-direction column
  594. align-items center
  595. .stat-divider
  596. width 1rpx
  597. height 40rpx
  598. background #eee
  599. .stat-num
  600. font-size 36rpx
  601. font-weight 700
  602. color #333
  603. line-height 1
  604. .stat-correct
  605. color #52c41a
  606. .stat-wrong
  607. color #ff4d4f
  608. .stat-label
  609. font-size 22rpx
  610. color #999
  611. margin-top 6rpx
  612. .q-nav-panel
  613. position fixed
  614. top 0
  615. left 0
  616. right 0
  617. bottom 0
  618. background rgba(0,0,0,0.45)
  619. z-index 100
  620. display flex
  621. flex-direction column
  622. justify-content flex-end
  623. .q-nav-scroll-wrapper
  624. background #fff
  625. border-radius 32rpx 32rpx 0 0
  626. max-height 70vh
  627. display flex
  628. flex-direction column
  629. .q-nav-header
  630. display flex
  631. align-items center
  632. justify-content space-between
  633. padding 28rpx 28rpx 16rpx
  634. flex-shrink 0
  635. .q-nav-title
  636. font-size 30rpx
  637. font-weight 600
  638. color #222
  639. .q-nav-close
  640. font-size 32rpx
  641. color #999
  642. padding 10rpx
  643. .q-nav-scroll
  644. flex 1
  645. overflow hidden
  646. padding-bottom 40rpx
  647. .q-group
  648. padding 0 28rpx 16rpx
  649. .q-group-title
  650. font-size 24rpx
  651. color #666
  652. line-height 60rpx
  653. .q-num-row
  654. display flex
  655. flex-wrap wrap
  656. .q-num
  657. width 72rpx
  658. height 72rpx
  659. line-height 72rpx
  660. text-align center
  661. font-size 24rpx
  662. border-radius 12rpx
  663. margin 8rpx
  664. background #f4f6fa
  665. color #333
  666. border 1rpx solid transparent
  667. .q-num.correct
  668. background #f6ffed
  669. color #52c41a
  670. border-color #b7eb8f
  671. .q-num.wrong
  672. background #fff2f0
  673. color #ff4d4f
  674. border-color #ffa39e
  675. .q-num.current
  676. background #1677ff
  677. color #fff
  678. .q-scroll-content
  679. flex 1
  680. overflow hidden
  681. .q-card
  682. margin 24rpx
  683. padding 28rpx
  684. background #fff
  685. border-radius 20rpx
  686. border 1rpx solid #dde3ed
  687. .q-header
  688. display flex
  689. align-items center
  690. justify-content space-between
  691. margin-bottom 20rpx
  692. .q-header-left
  693. display flex
  694. align-items center
  695. flex 1
  696. flex-wrap wrap
  697. .q-header-right
  698. display flex
  699. align-items center
  700. flex-shrink 0
  701. margin-left 12rpx
  702. .q-type-badge
  703. font-size 20rpx
  704. padding 4rpx 14rpx
  705. background #e6f4ff
  706. color #1677ff
  707. border 1rpx solid #91caff
  708. border-radius 8rpx
  709. margin-right 12rpx
  710. .q-num-text
  711. font-size 26rpx
  712. color #333
  713. .q-score-text
  714. font-size 22rpx
  715. color #999
  716. .q-stars
  717. display flex
  718. align-items center
  719. margin-right 16rpx
  720. .star-icon
  721. font-size 28rpx
  722. font-style normal
  723. .star-active
  724. color #fadb14
  725. .star-empty
  726. color #d9d9d9
  727. .view-answer-btn
  728. font-size 22rpx
  729. padding 6rpx 18rpx
  730. background #0183fa
  731. color #fff
  732. border-radius 8rpx
  733. white-space nowrap
  734. .q-content
  735. font-size 28rpx
  736. line-height 1.8
  737. color #222
  738. margin-bottom 24rpx
  739. .q-options
  740. display flex
  741. flex-direction column
  742. .q-option
  743. display flex
  744. align-items center
  745. padding 22rpx 20rpx
  746. margin-bottom 16rpx
  747. border 2rpx solid #dde3ed
  748. border-radius 16rpx
  749. background #fff
  750. .q-option.selected
  751. border-color #1677ff
  752. background #e6f4ff
  753. .q-option-label
  754. width 48rpx
  755. height 48rpx
  756. line-height 48rpx
  757. text-align center
  758. border-radius 50%
  759. background #f4f6fa
  760. color #333
  761. font-size 24rpx
  762. font-weight 600
  763. margin-right 20rpx
  764. flex-shrink 0
  765. .q-option.selected .q-option-label
  766. background #1677ff
  767. color #fff
  768. .q-option-content
  769. flex 1
  770. font-size 26rpx
  771. color #333
  772. line-height 1.6
  773. .answer-result-box
  774. background #f5f5f5
  775. border-radius 12rpx
  776. padding 24rpx 28rpx
  777. margin-top 20rpx
  778. .answer-row
  779. display flex
  780. align-items center
  781. margin-bottom 16rpx
  782. .answer-label
  783. font-size 26rpx
  784. font-weight 600
  785. color #333
  786. .answer-value
  787. font-size 26rpx
  788. color #1677ff
  789. font-weight 700
  790. .analysis-title
  791. font-size 26rpx
  792. font-weight 600
  793. color #333
  794. margin-bottom 12rpx
  795. .analysis-content
  796. font-size 24rpx
  797. color #666
  798. line-height 1.7
  799. padding 0 8rpx
  800. .analysis-empty
  801. font-size 24rpx
  802. color #aaa
  803. .scroll-bottom-space
  804. height 200rpx
  805. .q-nav
  806. display flex
  807. background #fff
  808. border-top 1rpx solid #e8edf3
  809. flex-shrink 0
  810. .q-nav-btn
  811. flex 1
  812. height 88rpx
  813. line-height 88rpx
  814. text-align center
  815. font-size 28rpx
  816. color #1677ff
  817. .q-nav-btn.q-nav-btn-next
  818. border-left 1rpx solid #e8edf3
  819. .q-nav-btn.disabled
  820. color #ccc
  821. .exam-bottom
  822. padding 20rpx 24rpx
  823. padding-bottom calc(20rpx + env(safe-area-inset-bottom))
  824. background #fff
  825. border-top 1rpx solid #e8edf3
  826. flex-shrink 0
  827. .btn-primary
  828. background #1677ff
  829. color #fff
  830. border-radius 16rpx
  831. font-size 30rpx
  832. font-weight 600
  833. height 88rpx
  834. line-height 88rpx
  835. text-align center
  836. .result-shade
  837. position fixed
  838. top 0
  839. left 0
  840. right 0
  841. bottom 0
  842. background rgba(0,0,0,0.55)
  843. z-index 200
  844. display flex
  845. align-items center
  846. justify-content center
  847. .result-card
  848. width 580rpx
  849. background #fff
  850. border-radius 24rpx
  851. padding 48rpx 40rpx 36rpx
  852. display flex
  853. flex-direction column
  854. align-items center
  855. .result-title
  856. font-size 32rpx
  857. font-weight 700
  858. color #222
  859. margin-bottom 20rpx
  860. .result-score
  861. display flex
  862. align-items baseline
  863. margin-bottom 16rpx
  864. .result-score-num
  865. font-size 80rpx
  866. font-weight 700
  867. color #F59A23
  868. line-height 1
  869. .result-score-unit
  870. font-size 30rpx
  871. color #F59A23
  872. margin-left 8rpx
  873. .result-desc
  874. font-size 26rpx
  875. color #666
  876. text-align center
  877. margin-bottom 36rpx
  878. line-height 1.6
  879. .result-btns
  880. display flex
  881. width 100%
  882. .result-btn
  883. flex 1
  884. height 80rpx
  885. line-height 80rpx
  886. text-align center
  887. border-radius 14rpx
  888. font-size 28rpx
  889. .result-btn-outline
  890. border 1rpx solid #1677ff
  891. color #1677ff
  892. margin-right 20rpx
  893. .result-btn-primary
  894. background #1677ff
  895. color #fff
  896. .empty-tip
  897. text-align center
  898. padding 80rpx 0
  899. color #aaa
  900. font-size 26rpx
  901. </style>