ExamPaperPreview.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <template>
  2. <el-container v-if="showPaper">
  3. <el-header>
  4. <el-row>
  5. <el-col style="border-bottom: 1px solid #f5f5f5">
  6. <span class="startExam">试卷名字: </span>
  7. <span style="color: red;font-size: 18px;">{{ examInfo.name }}</span>
  8. <span class="examTitle">考试时长: </span>
  9. <span style="color: red;font-size: 18px;">{{ examInfo.duration }} 分钟</span>
  10. <div v-if="paperStatus === 2">
  11. <el-button
  12. type="warning"
  13. round
  14. style="background-color: #ffd550;float: right;color: black;font-weight: 800"
  15. @click="uploadExamToAdmin"
  16. >提交试卷
  17. </el-button>
  18. </div>
  19. </el-col>
  20. </el-row>
  21. </el-header>
  22. <el-main>
  23. <el-row>
  24. <el-col :span="16">
  25. <el-row v-for="(item, index) in questionMap" :key="index">
  26. <el-row class="card-style">
  27. <el-card>
  28. <!-- 题目信息 -->
  29. <div>
  30. <span v-if="item[1].questionType === 1">【单选题】</span>
  31. <span v-else-if="item[1].questionType === 2">【多选题】</span>
  32. <span v-else-if="item[1].questionType === 3">【不定项选择题】</span>
  33. <span v-else-if="item[1].questionType === 4">【判断题】</span>
  34. <span v-else-if="item[1].questionType === 5">【填空题】</span>
  35. <span v-else-if="item[1].questionType === 6">【问答题】</span>
  36. <span v-else-if="item[1].questionType === 7">【理解题】</span>
  37. <span v-else>【综合题】</span>
  38. <br>
  39. <br>
  40. <i class="num">{{ item[1].pos }}</i>
  41. <span style="color: red">
  42. ({{ item[1].score }} 分)
  43. </span>
  44. <span v-html="item[1].questionContent" />
  45. <div v-if="item[1].questionType === 8">
  46. <div v-for="(child, childIndex) in item[1].children" :key="childIndex">
  47. <div>
  48. <i class="num">{{ item[1].pos }}</i>
  49. <i class="num">{{ child.pos }}</i>
  50. <span style="color: red">
  51. ({{ child.score }} 分)
  52. </span>
  53. <span v-html="child.questionContent" />
  54. </div>
  55. <!-- 单选和判断题候选答案列表 -->
  56. <div
  57. v-show="child.questionType === 1 || child.questionType === 4"
  58. style="margin-top: 25px"
  59. >
  60. <el-radio-group
  61. v-for="(ans,ans_index) in child.answer"
  62. :key="ans_index"
  63. v-model="singleChecks[ans.questionId]"
  64. @change="singleCheckChange(child)"
  65. >
  66. <el-radio :label="ans.pos">
  67. <span>{{ optionName[ans.pos-1] + '、' + ans.answer }}</span>
  68. </el-radio>
  69. </el-radio-group>
  70. </div>
  71. <!-- 多选和不定项选择题的候选答案列表 -->
  72. <div
  73. v-show="child.questionType === 2 || child.questionType === 3"
  74. style="margin-top: 25px"
  75. >
  76. <el-checkbox-group
  77. v-for="(ans,ans_index) in child.answer"
  78. :key="ans_index"
  79. v-model="multipleChecked[item[1].pos]"
  80. @change="multipleCheckChange(child)"
  81. >
  82. <el-checkbox
  83. :label="ans.pos"
  84. border
  85. style="margin-top: 20px"
  86. >
  87. <span>{{ optionName[ans.pos-1] + '、' + ans.answer }}</span>
  88. </el-checkbox>
  89. </el-checkbox-group>
  90. </div>
  91. <!-- 填空题和问答题的回答区 -->
  92. <div
  93. v-show="child.questionType === 5 || child.questionType === 6"
  94. style="margin-top: 25px"
  95. >
  96. <el-input
  97. v-model="userAnswer[curIndex]"
  98. type="textarea"
  99. :rows="8"
  100. placeholder="请输入答案"
  101. />
  102. </div>
  103. </div>
  104. </div>
  105. </div>
  106. <!-- 单选和判断题候选答案列表 -->
  107. <div
  108. v-show="item[1].questionType === 1 || item[1].questionType === 4"
  109. style="margin-top: 25px"
  110. >
  111. <el-radio-group
  112. v-for="(ans,ans_index) in item[1].answer"
  113. :key="ans_index"
  114. v-model="singleChecks[ans.questionId]"
  115. @change="singleCheckChange(item[1])"
  116. >
  117. <el-radio :label="ans.pos">
  118. <span>{{ optionName[ans.pos-1] + '、' + ans.answer }}</span>
  119. </el-radio>
  120. </el-radio-group>
  121. </div>
  122. <!-- 多选和不定项选择题的候选答案列表 -->
  123. <div
  124. v-show="item[1].questionType === 2 || item[1].questionType === 3"
  125. style="margin-top: 25px"
  126. >
  127. <el-checkbox-group
  128. v-for="(ans,ans_index) in item[1].answer"
  129. :key="ans_index"
  130. v-model="multipleChecked[item[1].pos]"
  131. @change="multipleCheckChange(item[1])"
  132. >
  133. <el-checkbox
  134. :label="ans.pos"
  135. border
  136. style="margin-top: 20px;"
  137. >
  138. <span>{{ optionName[ans.pos-1] + '、' + ans.answer }}</span>
  139. </el-checkbox>
  140. </el-checkbox-group>
  141. </div>
  142. <!-- 填空题的回答区 -->
  143. <div
  144. v-show="item[1].questionType === 5"
  145. style="margin-top: 25px"
  146. >
  147. <el-input
  148. v-model="question5[item[1].questionId]"
  149. type="text"
  150. placeholder="请输入答案"
  151. @blur="onInputBlur(item[1])"
  152. />
  153. </div>
  154. <!-- 问答题的回答区 -->
  155. <div
  156. v-show="item[1].questionType === 6"
  157. style="margin-top: 25px"
  158. >
  159. <el-input
  160. v-model="question6[item[1].questionId]"
  161. type="textarea"
  162. :rows="8"
  163. placeholder="请输入答案"
  164. @blur="onInputBlur(item[1])"
  165. />
  166. </div>
  167. </el-card>
  168. </el-row>
  169. </el-row>
  170. </el-col>
  171. <!-- 答题卡 -->
  172. <el-col :span="8">
  173. <el-row class="card-style">
  174. <el-card>
  175. <div>
  176. <p style="font-size: 18px;">答题卡</p>
  177. </div>
  178. <!-- 单选的答题卡 -->
  179. <div style="margin-top: 25px">
  180. <p style="font-size: 18px;">单选题</p>
  181. <el-button
  182. v-for="item in questionList.length"
  183. v-show="questionList[item-1].questionType === 1"
  184. :key="item"
  185. style="margin-top: 10px;margin-left: 15px"
  186. size="mini"
  187. :class="questionList[item-1].questionType === 1 && userAnswer[item-1] !== undefined ?
  188. 'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
  189. >{{ item }}
  190. </el-button>
  191. </div>
  192. <!-- 多选的答题卡 -->
  193. <div style="margin-top: 25px">
  194. <p style="font-size: 18px;">多选题</p>
  195. <el-button
  196. v-for="item in questionList.length"
  197. v-show="questionList[item-1].questionType === 2"
  198. :key="item"
  199. style="margin-top: 10px;margin-left: 15px"
  200. size="mini"
  201. >{{ item }}
  202. </el-button>
  203. </div>
  204. <!-- 不定项选择题的答题卡 -->
  205. <div style="margin-top: 25px">
  206. <p style="font-size: 18px;">不定项选择题</p>
  207. <el-button
  208. v-for="item in questionList.length"
  209. v-show="questionList[item-1].questionType === 3"
  210. :key="item"
  211. style="margin-top: 10px;margin-left: 15px"
  212. size="mini"
  213. :class="questionList[item-1].questionType === 3 && userAnswer[item-1] !== undefined ?
  214. 'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
  215. >{{ item }}
  216. </el-button>
  217. </div>
  218. <!-- 判断题的答题卡 -->
  219. <div style="margin-top: 25px">
  220. <p style="font-size: 18px;">判断题</p>
  221. <el-button
  222. v-for="item in questionList.length"
  223. v-show="questionList[item-1].questionType === 4"
  224. :key="item"
  225. style="margin-top: 10px;margin-left: 15px"
  226. size="mini"
  227. :class="questionList[item-1].questionType === 4 && userAnswer[item-1] !== undefined ?
  228. 'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
  229. >{{ item }}
  230. </el-button>
  231. </div>
  232. <!-- 填空题的答题卡 -->
  233. <div style="margin-top: 25px">
  234. <p style="font-size: 18px;">填空题</p>
  235. <el-button
  236. v-for="item in questionList.length"
  237. v-show="questionList[item-1].questionType === 5"
  238. :key="item"
  239. style="margin-top: 10px;margin-left: 15px"
  240. size="mini"
  241. :class="questionList[item-1].questionType === 5 && userAnswer[item-1] !== undefined ?
  242. 'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
  243. >{{ item }}
  244. </el-button>
  245. </div>
  246. <!-- 问答题的答题卡 -->
  247. <div style="margin-top: 25px">
  248. <p style="font-size: 18px;">问答题</p>
  249. <el-button
  250. v-for="item in questionList.length"
  251. v-show="questionList[item-1].questionType === 6"
  252. :key="item"
  253. style="margin-top: 10px;margin-left: 15px"
  254. size="mini"
  255. :class="questionList[item-1].questionType === 6 && userAnswer[item-1] !== undefined ?
  256. 'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
  257. >{{ item }}
  258. </el-button>
  259. </div>
  260. <!-- 综合题的答题卡 -->
  261. <div style="margin-top: 25px">
  262. <p style="font-size: 18px;">综合题</p>
  263. <el-button
  264. v-for="item in questionList.length"
  265. v-show="questionList[item-1].questionType === 8"
  266. :key="item"
  267. style="margin-top: 10px;margin-left: 15px"
  268. size="mini"
  269. :class="questionList[item-1].questionType === 8 && userAnswer[item-1] !== undefined ?
  270. 'done' : userAnswer[item-1] === undefined ? curIndex === (item-1) ? 'orange' : 'noAnswer' : 'noAnswer'"
  271. @click="curIndex = item-1"
  272. >{{ item }}
  273. </el-button>
  274. </div>
  275. </el-card>
  276. </el-row>
  277. </el-col>
  278. </el-row>
  279. </el-main>
  280. </el-container>
  281. </template>
  282. <script>
  283. import { getPaper, submitExam } from '@/api/exam'
  284. export default {
  285. name: 'ExamPaperPreview',
  286. data() {
  287. return {
  288. paperStatus: 1,
  289. // 当前考试的信息
  290. examInfo: {
  291. name: '',
  292. description: '',
  293. duration: 0,
  294. totalScore: 0
  295. },
  296. // 当前的考试题目
  297. questionList: [
  298. {
  299. questionType: ''
  300. }
  301. ],
  302. // 当前题目的索引值
  303. curIndex: 0,
  304. // 控制大图的对话框
  305. bigImgDialog: false,
  306. // 当前要展示的大图的url
  307. bigImgUrl: '',
  308. // 用户选择的答案
  309. userAnswer: [],
  310. // 页面数据加载
  311. loading: {},
  312. // 页面绘制是否开始
  313. showPaper: false,
  314. // 答案的选项名abcd数据
  315. optionName: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
  316. // 考试总时长
  317. duration: 0,
  318. answer: {
  319. pos: null,
  320. questionId: null,
  321. questionType: null,
  322. questionAnswer: null,
  323. children: []
  324. },
  325. questionMap: new Map(),
  326. childMap: new Map(),
  327. userAnswers: new Map(),
  328. singleChecks: {},
  329. multipleCheck: [],
  330. multipleChecked: [
  331. [], [], [], [], [], [], [], [], [], [],
  332. [], [], [], [], [], [], [], [], [], [],
  333. [], [], [], [], [], [], [], [], [], [],
  334. [], [], [], [], [], [], [], [], [], [],
  335. [], [], [], [], [], [], [], [], [], [],
  336. [], [], [], [], [], [], [], [], [], [],
  337. [], [], [], [], [], [], [], [], [], [],
  338. [], [], [], [], [], [], [], [], [], [],
  339. [], [], [], [], [], [], [], [], [], [],
  340. [], [], [], [], [], [], [], [], [], []
  341. ],
  342. multipleChecks: {},
  343. question5: {},
  344. question6: {}
  345. }
  346. },
  347. created() {
  348. const path = this.$route.path
  349. if (path.startsWith('/exam/preview')) {
  350. document.title = '试卷预览'
  351. this.paperStatus = 1
  352. } else if (path.startsWith('/exam/start')) {
  353. document.title = '试卷考试'
  354. this.paperStatus = 2
  355. }
  356. const paperId = this.$route.params.paperId
  357. this.getPaperDeatil(paperId)
  358. },
  359. methods: {
  360. renderByMathjax() {
  361. // tinymce 的 mathjax 插件生成的 latex 格式公式放在 className 为 math-tex 的 span 标签内
  362. const className = 'math-tex'
  363. this.$nextTick(function() {
  364. if (this.MathJax.isMathjaxConfig) {
  365. this.MathJax.initMathjaxConfig()
  366. }
  367. this.MathJax.MathQueue1(className)
  368. })
  369. },
  370. // 查询当前考试的信息
  371. getPaperDeatil(paperId) {
  372. getPaper(paperId).then((resp) => {
  373. if (resp.code === 0) {
  374. const respData = resp.data
  375. this.examInfo.paperId = respData.paperId
  376. this.examInfo.name = respData.name
  377. this.examInfo.description = respData.description
  378. this.examInfo.duration = respData.duration
  379. this.examInfo.totalScore = respData.totalScore
  380. this.questionList = respData.questions
  381. // 重置问题的顺序 单选 -> -> 判断 -> 简答
  382. this.questionList = this.questionList.sort(function(a, b) {
  383. return a.questionType - b.questionType
  384. })
  385. for (const item of this.questionList) {
  386. this.questionMap.set(item.questionId, item)
  387. if (item.questionType === 2 || item.questionType === 3) {
  388. this.$set(this.multipleChecks, item.questionId, [])
  389. } else if (item.questionType === 5) {
  390. this.$set(this.question5, item.questionId, null)
  391. } else if (item.questionType === 6) {
  392. this.$set(this.question6, item.questionId, null)
  393. }
  394. if (item.children !== undefined && item.children !== null) {
  395. for (const child of item.children) {
  396. this.childMap.set(child.questionId, item.questionId)
  397. this.questionMap.set(item.questionId, item)
  398. if (child.questionType === 2 || child.questionType === 3) {
  399. this.$set(this.multipleChecks, child.questionId, [])
  400. } else if (child.questionType === 5) {
  401. this.$set(this.question5, child.questionId, null)
  402. } else if (child.questionType === 6) {
  403. this.$set(this.question6, child.questionId, null)
  404. }
  405. }
  406. }
  407. }
  408. this.showPaper = true
  409. this.renderByMathjax()
  410. }
  411. })
  412. },
  413. // ****************************************************************************************************************
  414. // 单选和判断
  415. singleCheckChange(val) {
  416. const questionId = val.questionId
  417. var pid = this.getPid(questionId)
  418. const answer = this.singleChecks[questionId]
  419. this.setUserAnswers(val, answer, pid)
  420. },
  421. // 多选和不定项选择题
  422. multipleCheckChange(val) {
  423. var questionId = val.questionId
  424. var pid = this.getPid(questionId)
  425. var answer = this.multipleChecked[val.pos]
  426. var answerStr = answer.join(',')
  427. this.setUserAnswers(val, answerStr, pid)
  428. },
  429. // 填空和问答题
  430. onInputBlur(val) {
  431. var questionId = val.questionId
  432. var pid = this.getPid(questionId)
  433. var questionType = val.questionType
  434. if (questionType === 5) {
  435. const answer = this.question5[questionId]
  436. this.setUserAnswers(val, answer, pid)
  437. } else if (questionType === 6) {
  438. const answer = this.question6[questionId]
  439. this.setUserAnswers(val, answer, pid)
  440. }
  441. },
  442. getPid(questionId) {
  443. var pid = this.childMap.get(questionId)
  444. if (pid !== undefined && pid !== null) {
  445. return pid
  446. } else {
  447. return 0
  448. }
  449. },
  450. setUserAnswers(question, answer, pid) {
  451. const userAnswer = {}
  452. userAnswer.questionId = question.questionId
  453. userAnswer.questionType = question.questionType
  454. userAnswer.pos = question.pos
  455. userAnswer.answer = answer
  456. userAnswer.pid = pid
  457. this.userAnswers.set(userAnswer.questionId, userAnswer)
  458. },
  459. async uploadExamToAdmin() {
  460. for (const item of this.questionMap) {
  461. const questionId = item[0]
  462. const question = item[1]
  463. const questionType = question.questionType
  464. if (questionType === 8) {
  465. continue
  466. }
  467. const answer = this.userAnswers.get(questionId)
  468. if (answer === undefined || answer === null) {
  469. console.log(questionId)
  470. this.$message.error('试题 ' + question.pos + ' 还未完成')
  471. } else {
  472. if (answer.answer === null || answer.answer === '') {
  473. console.log(questionId)
  474. this.$message.error('试题 ' + question.pos + ' 答案不能为空')
  475. }
  476. }
  477. }
  478. var submitAnswers = []
  479. for (const item of this.userAnswers) {
  480. submitAnswers.push(item[1])
  481. }
  482. const userResult = {}
  483. userResult.paperId = this.examInfo.paperId
  484. userResult.userAnswers = submitAnswers
  485. submitExam(userResult).then((resp) => {
  486. if (resp.code === 0) {
  487. this.$notify({
  488. title: 'Tips',
  489. message: '考试结果已提交',
  490. type: 'success',
  491. duration: 2000
  492. })
  493. this.$router.push('/exam/score')
  494. }
  495. }).catch(error => {
  496. this.$notify({
  497. title: '提示',
  498. message: error.message,
  499. type: 'error',
  500. duration: 3000
  501. })
  502. })
  503. }
  504. }
  505. }
  506. </script>
  507. <style lang="scss" scoped>
  508. * {
  509. font-weight: 800;
  510. }
  511. .el-container {
  512. width: 100%;
  513. height: 100%;
  514. }
  515. .startExam {
  516. color: #160f58;
  517. border-bottom: 4px solid #ffd550;
  518. font-size: 18px;
  519. font-weight: 700;
  520. padding-bottom: 10px
  521. }
  522. .examTitle {
  523. font-size: 18px;
  524. color: #cbcacf;
  525. margin-left: 20px;
  526. font-weight: 700;
  527. }
  528. .el-radio-group label {
  529. display: block;
  530. width: 400px;
  531. padding: 48px 20px 10px 20px;
  532. border-radius: 4px;
  533. border: 1px solid #dcdfe6;
  534. margin-bottom: 10px;
  535. cursor: pointer;
  536. position: relative;
  537. span {
  538. position: absolute;
  539. top: 50%;
  540. transform: translateY(-50%);
  541. font-size: 16px;
  542. }
  543. }
  544. .el-radio-group label:hover {
  545. background-color: rgb(245, 247, 250);
  546. }
  547. /*当前选中的答案*/
  548. .active {
  549. border: 1px solid #1f90ff !important;
  550. opacity: .5;
  551. }
  552. /*做过的题目的高亮颜色*/
  553. .done {
  554. background-color: rgb(87, 148, 247);
  555. }
  556. /*未做题目的高亮颜色*/
  557. .noAnswer {
  558. background-color: rgb(238, 238, 238);
  559. }
  560. /*当前在做的题目高亮的颜色*/
  561. .orange {
  562. background-color: rgb(255, 213, 80);
  563. }
  564. .num {
  565. display: inline-block;
  566. background: url('../../assets/img/examTitle.png') no-repeat 95%;
  567. background-size: contain;
  568. height: 37px;
  569. width: 37px;
  570. line-height: 30px;
  571. color: #fff;
  572. font-size: 20px;
  573. text-align: center;
  574. margin-right: 15px;
  575. }
  576. .card-style {
  577. padding-top: 5px;
  578. padding-bottom: 5px;
  579. padding-left: 5px;
  580. padding-right: 5px;
  581. }
  582. </style>