mockExamPlay.vue 23 KB

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