examDetails.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <!-- 试卷详情 -->
  2. <template>
  3. <div class="page-container examDetails-addPage">
  4. <!-- 左侧面板 -->
  5. <div class="left-panel">
  6. <div class="exam-title">{{ examInfo.examTypeName }}</div>
  7. <div class="exam-subtitle">总{{ examInfo.questionCount }}题 / 共{{ examInfo.totalScore }}分</div>
  8. <div class="exam-name-box">{{ examInfo.examName }}</div>
  9. <div class="exam-info-box">
  10. <div class="info-item"><span class="label">试卷总分:</span>{{ examInfo.totalScore }}分</div>
  11. <div class="info-item"><span class="label">及格分数:</span>{{ examInfo.passScore }}分</div>
  12. <div class="info-item"><span class="label">我的得分:</span>{{ examInfo.earnedScore }}分</div>
  13. <div class="info-item"><span class="label">考试用时:</span>{{ examInfo.durationText }}</div>
  14. <div class="info-item">
  15. <span class="label">考试结果:</span>
  16. <span :class="examInfo.passStatus == 1 ? 'result-pass' : 'result-fail'">
  17. {{ examInfo.passStatus == 1?'通过':'无效' }}
  18. </span>
  19. </div>
  20. </div>
  21. <!-- 题目导航 -->
  22. <div v-for="group in questionTypeGroups" :key="group.type" class="question-group">
  23. <div class="group-header">
  24. <span class="group-title">{{ group.questionTypeName }}</span>
  25. <span class="group-count">{{ group.questionCount }}题 / 共{{ group.totalScore }}分</span>
  26. </div>
  27. <div class="question-nums">
  28. <div
  29. v-for="(item,index) in group.questions"
  30. :key="item.questionNo"
  31. :class="['num-item', getNumClass(item), currentIndex === item.questionNo ? 'num-active' : '']"
  32. @click="selectQuestion(item)">
  33. {{ item.questionNo }}
  34. </div>
  35. </div>
  36. </div>
  37. </div>
  38. <!-- 右侧内容区:只展示当前选中题目 -->
  39. <div class="right-panel" v-if="currentQuestion">
  40. <div class="question-card">
  41. <!-- 题目头部 -->
  42. <div class="question-header">
  43. <div class="question-index-box">{{ currentQuestion.questionNo }}</div>
  44. <div class="question-type-score">{{ currentQuestion.questionTypeName }}({{ currentQuestion.questionScore }}分)</div>
  45. <div class="question-stars">
  46. <i
  47. v-for="s in 4"
  48. :key="s"
  49. :class="['star-icon', s <= currentQuestion.difficulty ? 'star-active' : 'star-empty']"
  50. >★</i>
  51. </div>
  52. </div>
  53. <!-- 题目内容(富文本) -->
  54. <div class="question-content" v-html="currentQuestion.questionContent"></div>
  55. <!-- 单选题选项/多选题选项 -->
  56. <div class="options-list">
  57. <div v-for="(minItem,minIndex) in currentQuestion.options"
  58. :key="minIndex" class="option-item"
  59. :class="isValueInList(minItem.optionLabel,currentQuestion.correctAnswerTags)?'option-selected':''">
  60. <span class="option-radio">
  61. <i v-if="isValueInList(minItem.optionLabel,currentQuestion.correctAnswerTags)" class="el-icon-check radio-checked"></i>
  62. <i v-else class="radio-empty"></i>
  63. </span>
  64. <span class="option-key" v-if="currentQuestion.questionType == '1' || currentQuestion.questionType == '2'">{{ minItem.optionLabel }}</span>
  65. <span class="option-text" v-html="minItem.optionContent"></span>
  66. </div>
  67. </div>
  68. <!-- 答题结果 -->
  69. <div class="answer-result-box">
  70. <div class="answer-status">
  71. <span v-if="currentQuestion.isCorrect == 1" class="status-correct">✅ 回答正确</span>
  72. <span v-else-if="currentQuestion.isCorrect == 0" class="status-wrong">❌ 回答错误</span>
  73. <span v-else class="status-wrong">❌ 未作答</span>
  74. </div>
  75. <div class="answer-detail">
  76. <div class="answer-row"><span class="answer-label">回答选项:</span>{{ arrayToString(currentQuestion.answerTags) }}</div>
  77. <div class="analysis-title">试题解析</div>
  78. <div class="analysis-content" v-html="currentQuestion.analysis"></div>
  79. </div>
  80. </div>
  81. </div>
  82. </div>
  83. </div>
  84. </template>
  85. <script>
  86. import {
  87. examElExamAttemptReviewSidebar, examElExamAttemptReviewQuestion
  88. } from '@/api/safetyEducationExaminationNew/index'
  89. export default {
  90. name: 'examDetails',
  91. props: {
  92. examDetailsPropsData: {}
  93. },
  94. data() {
  95. return {
  96. newData:{},
  97. currentIndex: null,
  98. examInfo: {
  99. examTypeName: '',
  100. examName: '',
  101. questionCount: 0,
  102. totalScore: 0,
  103. passScore: 0,
  104. earnedScore: 0,
  105. durationText: '',
  106. passStatus: null,
  107. },
  108. questionTypeGroups: [],
  109. questionCache: {},
  110. currentQuestion: null,
  111. // 右侧加载中状态
  112. questionLoading: false,
  113. mockQuestions: {}
  114. }
  115. },
  116. computed: {},
  117. created() {},
  118. mounted() {
  119. this.initialize()
  120. },
  121. methods: {
  122. initialize() {
  123. this.$set(this,'newData',this.examDetailsPropsData);
  124. //左侧数据
  125. examElExamAttemptReviewSidebar({attemptId:this.newData.attemptId}).then(response => {
  126. this.$set(this,'examInfo',{
  127. examTypeName: response.data.examTypeName?response.data.examTypeName:'',
  128. examName: response.data.examName?response.data.examName:'',
  129. questionCount: response.data.questionCount?response.data.questionCount:0,
  130. totalScore: response.data.totalScore?response.data.totalScore:0,
  131. passScore: response.data.passScore?response.data.passScore:0,
  132. earnedScore: response.data.earnedScore?response.data.earnedScore:0,
  133. durationText: response.data.durationText?response.data.durationText:'',
  134. passStatus: response.data.passStatus?response.data.passStatus:null,
  135. });
  136. this.$set(this,'questionTypeGroups',response.data.questionTypeGroups);
  137. // 默认选中第一题(假数据直接从 data 里读取,无需接口)
  138. const firstGroup = this.questionTypeGroups[0]
  139. if (firstGroup && firstGroup.questions.length) {
  140. this.$set(this, 'currentIndex', firstGroup.questions[0].questionNo);
  141. this.examElExamAttemptReviewQuestion(firstGroup.questions[0].snapshotQuestionId);
  142. }
  143. });
  144. },
  145. //右侧数据
  146. examElExamAttemptReviewQuestion(id){
  147. examElExamAttemptReviewQuestion({attemptId:this.newData.attemptId,snapshotQuestionId:id}).then(response => {
  148. this.$set(this,'currentQuestion',response.data);
  149. });
  150. },
  151. backPage() {
  152. this.$parent.tableButton(6)
  153. },
  154. // 点击左侧题号:切换当前题目,按需从接口加载详情
  155. selectQuestion(item) {
  156. this.$set(this, 'currentIndex', item.questionNo);
  157. this.examElExamAttemptReviewQuestion(item.snapshotQuestionId);
  158. },
  159. // 判断选项是否被选中
  160. isSelectedOption(q, key) {
  161. if (!q.userAnswer) return false
  162. return q.userAnswer.split(',').includes(key)
  163. },
  164. // 根据左侧题目列表的 status 字段获取题号样式
  165. // status: '1' → 绿色,'0' → 红色,'null' → 灰色
  166. getNumClass(item) {
  167. if (item.isCorrect === 1) return 'num-correct'
  168. if (item.isCorrect === 0) return 'num-wrong'
  169. return 'num-unanswered'
  170. },
  171. //匹配用户选择
  172. isValueInList(target, list) {
  173. if (!Array.isArray(list)) return false;
  174. return list.some(item => item === target);
  175. },
  176. //返回用户选择答案
  177. arrayToString(list) {
  178. // 防御性编程:如果传入的不是数组,返回空字符串防止报错
  179. if (!Array.isArray(list)) return '';
  180. return list.join(',');
  181. },
  182. }
  183. }
  184. </script>
  185. <style scoped lang="scss">
  186. .examDetails-addPage {
  187. flex: 1;
  188. display: flex;
  189. overflow: hidden;
  190. background: #f5f5f5;
  191. /* ========== 左侧面板 ========== */
  192. .left-panel {
  193. width: 400px;
  194. min-width: 400px;
  195. background: #fff;
  196. display: flex;
  197. flex-direction: column;
  198. overflow-y: auto;
  199. padding: 20px 16px;
  200. border-right: 1px solid #e8e8e8;
  201. &::-webkit-scrollbar { width: 4px; }
  202. &::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }
  203. .exam-title {
  204. font-size: 16px;
  205. font-weight: 700;
  206. color: #1890ff;
  207. margin-bottom: 4px;
  208. }
  209. .exam-subtitle {
  210. font-size: 13px;
  211. color: #666;
  212. margin-bottom: 16px;
  213. }
  214. .exam-name-box {
  215. font-size: 13px;
  216. color: #333;
  217. margin-bottom: 12px;
  218. line-height: 1.6;
  219. }
  220. .exam-info-box {
  221. background: #f5f5f5;
  222. border-radius: 6px;
  223. padding: 12px;
  224. margin-bottom: 20px;
  225. .info-item {
  226. font-size: 13px;
  227. color: #333;
  228. margin-bottom: 6px;
  229. line-height: 1.5;
  230. &:last-child { margin-bottom: 0; }
  231. .label { font-weight: 500; }
  232. .result-pass { color: #52c41a; font-weight: 700; }
  233. .result-fail { color: #faad14; font-weight: 700; }
  234. }
  235. }
  236. .question-group {
  237. margin-bottom: 16px;
  238. .group-header {
  239. display: flex;
  240. justify-content: space-between;
  241. align-items: center;
  242. margin-bottom: 8px;
  243. .group-title { font-size: 14px; font-weight: 600; color: #333; }
  244. .group-count { font-size: 12px; color: #999; }
  245. }
  246. .question-nums {
  247. display: flex;
  248. flex-wrap: wrap;
  249. gap: 6px;
  250. .num-item {
  251. width: 36px;
  252. height: 36px;
  253. border-radius: 4px;
  254. display: flex;
  255. align-items: center;
  256. justify-content: center;
  257. font-size: 13px;
  258. font-weight: 600;
  259. cursor: pointer;
  260. position: relative;
  261. color: #fff;
  262. transition: opacity 0.2s;
  263. &:hover { opacity: 0.85; }
  264. &.num-correct { background: #52c41a; }
  265. &.num-wrong { background: #ff4d4f; }
  266. &.num-unanswered { background: #999; color: #fff; }
  267. &.num-active {
  268. outline: 3px solid #1890ff;
  269. outline-offset: 1px;
  270. }
  271. }
  272. }
  273. }
  274. }
  275. /* ========== 右侧内容区 ========== */
  276. .right-panel {
  277. flex: 1;
  278. overflow-y: auto;
  279. padding: 20px;
  280. &::-webkit-scrollbar { width: 6px; }
  281. &::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }
  282. .question-card {
  283. background: #fff;
  284. border-radius: 8px;
  285. padding: 20px;
  286. .question-header {
  287. display: flex;
  288. align-items: center;
  289. margin-bottom: 12px;
  290. .question-index-box {
  291. width: 28px;
  292. height: 28px;
  293. background: #1890ff;
  294. color: #fff;
  295. border-radius: 4px;
  296. display: flex;
  297. align-items: center;
  298. justify-content: center;
  299. font-size: 13px;
  300. font-weight: 700;
  301. margin-right: 10px;
  302. flex-shrink: 0;
  303. }
  304. .question-type-score {
  305. font-size: 15px;
  306. font-weight: 600;
  307. color: #333;
  308. flex: 1;
  309. }
  310. .question-stars {
  311. display: flex;
  312. gap: 2px;
  313. .star-icon {
  314. font-size: 18px;
  315. font-style: normal;
  316. &.star-active { color: #fadb14; }
  317. &.star-empty { color: #d9d9d9; }
  318. }
  319. }
  320. }
  321. .question-content {
  322. font-size: 14px;
  323. font-weight: 600;
  324. color: #333;
  325. margin-bottom: 16px;
  326. line-height: 1.6;
  327. }
  328. .options-list {
  329. display: flex;
  330. flex-direction: column;
  331. gap: 8px;
  332. margin-bottom: 16px;
  333. .option-item {
  334. display: flex;
  335. align-items: center;
  336. border: 1px solid #e8e8e8;
  337. border-radius: 6px;
  338. padding: 10px 14px;
  339. cursor: default;
  340. transition: border-color 0.2s;
  341. &.option-selected {
  342. border-color: #1890ff;
  343. background: #e6f7ff;
  344. .option-key { color: #1890ff; }
  345. .option-text { color: #1890ff; }
  346. }
  347. .option-radio {
  348. width: 16px;
  349. height: 16px;
  350. border-radius: 50%;
  351. border: 1px solid #d9d9d9;
  352. margin-right: 10px;
  353. display: flex;
  354. align-items: center;
  355. justify-content: center;
  356. flex-shrink: 0;
  357. .radio-checked { color: #1890ff; font-size: 12px; }
  358. .radio-empty { display: block; width: 8px; height: 8px; }
  359. }
  360. .option-checkbox {
  361. margin-right: 10px;
  362. flex-shrink: 0;
  363. }
  364. .option-key {
  365. font-size: 14px;
  366. font-weight: 600;
  367. color: #666;
  368. margin-right: 8px;
  369. width: 16px;
  370. flex-shrink: 0;
  371. }
  372. .option-text {
  373. font-size: 14px;
  374. color: #333;
  375. flex: 1;
  376. }
  377. }
  378. }
  379. .answer-result-box {
  380. background: #f5f5f5;
  381. border-radius: 6px;
  382. padding: 14px 16px;
  383. .answer-status {
  384. margin-bottom: 10px;
  385. font-size: 14px;
  386. font-weight: 600;
  387. .status-correct { color: #52c41a; }
  388. .status-wrong { color: #ff4d4f; }
  389. }
  390. .answer-detail {
  391. .answer-row {
  392. font-size: 14px;
  393. color: #333;
  394. margin-bottom: 10px;
  395. .answer-label { font-weight: 600; }
  396. }
  397. .analysis-title {
  398. font-size: 14px;
  399. font-weight: 600;
  400. color: #333;
  401. margin-bottom: 6px;
  402. }
  403. .analysis-content {
  404. font-size: 13px;
  405. /*color: #fa8c16;*/
  406. line-height: 1.7;
  407. }
  408. }
  409. }
  410. }
  411. }
  412. }
  413. </style>
  414. <style lang="scss">
  415. .viewGrades-addPage-drawer{
  416. .el-drawer__body{
  417. padding:0;
  418. }
  419. .el-drawer__header{
  420. margin:0;
  421. }
  422. }
  423. </style>