mockExamPlay.vue 23 KB

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