index.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. <template>
  2. <div v-loading="loading" class="answer">
  3. <header class="header">
  4. <div class="back round" @click="goBack">
  5. <i class="el-icon-arrow-left"></i>
  6. <span>返回</span>
  7. </div>
  8. <div v-if="isAnnotations && is_objective" class="user-answer-info">
  9. <template v-if="user_answer.answer_status === 1">
  10. <span class="answer-status right"><SvgIcon :size="10" icon-class="check-mark" />回答正确</span>
  11. </template>
  12. <template v-else-if="user_answer.answer_status === 2">
  13. <span class="answer-status error"><SvgIcon :size="10" icon-class="cross" />回答错误</span>
  14. </template>
  15. </div>
  16. <div class="question-info">
  17. <el-popover
  18. v-if="!isStart && !isSubmit"
  19. v-model="isShowQuestionList"
  20. :width="200"
  21. :disabled="isStart"
  22. trigger="click"
  23. popper-class="question-wrapper"
  24. >
  25. <ul class="question-list">
  26. <li
  27. v-for="({ id, type: question_type, additional_type }, i) in questionList"
  28. :key="id"
  29. :class="[{ active: i === curQuestionIndex }]"
  30. @click="selectQuestion(i)"
  31. >
  32. <span>{{ i + 1 }}.</span>
  33. <span>{{ getExerciseName('list', question_type, additional_type) }}</span>
  34. </li>
  35. </ul>
  36. <div
  37. slot="reference"
  38. :style="{ backgroundColor: isShowQuestionList ? '#E9E8EA' : '' }"
  39. class="round question-index"
  40. >
  41. <SvgIcon icon-class="list" />
  42. <span>{{ curQuestionIndex + 1 }} / {{ questionList.length }}</span>
  43. <span>{{ getExerciseName('cur') }}</span>
  44. </div>
  45. </el-popover>
  46. <div v-if="!isTeacherAnnotations && !isSubmit" class="round primary">
  47. <SvgIcon icon-class="hourglass" />{{ secondFormatConversion(time) }}
  48. </div>
  49. </div>
  50. </header>
  51. <main class="main">
  52. <StartQuestion
  53. v-if="isStart"
  54. :question-length="questionList.length"
  55. :answer-time-limit-minute="answer_time_limit_minute"
  56. @startAnswer="startAnswer"
  57. />
  58. <AnswerReport v-else-if="isSubmit" :record-report="recordReport" @selectQuestion="selectQuestion" />
  59. <template v-for="({ id }, i) in questionList" v-else>
  60. <component
  61. :is="curQuestionPage"
  62. v-if="i === curQuestionIndex"
  63. :key="id"
  64. ref="exercise"
  65. :data="currentQuestion"
  66. />
  67. </template>
  68. </main>
  69. <footer class="footer" :style="{ justifyContent: isAnnotations ? 'space-between' : 'center' }">
  70. <el-popover v-model="isPopover" placement="top-start" trigger="click">
  71. <!-- 学生查看批注 -->
  72. <div v-if="isEnable(remark.is_remarked) && !isTeacher" class="remark-container">
  73. <div class="remark-info">
  74. <el-avatar :size="24" :src="remark.remark_person_image_url" />
  75. <span class="remark-name">{{ remark.remark_person_name }}</span>
  76. <span class="remark-time">{{ remark.remark_time }}</span>
  77. </div>
  78. <div v-if="currentQuestion.type === 'read'" class="read-score">
  79. <div v-for="(item, i) in remark.child_question_remark_list" :key="i">
  80. <span>小题 {{ i + 1 }} 分数:</span>
  81. <span v-if="isEnable(item?.is_objective)">得分 {{ item.score }}</span>
  82. </div>
  83. </div>
  84. <div v-else>得分:{{ remark.score }}</div>
  85. <div class="remark">
  86. {{ remark.remark }}
  87. </div>
  88. <div class="file">
  89. <el-image v-for="{ file_url, file_id } in remark.file_list" :key="file_id" :src="file_url" fit="contain" />
  90. </div>
  91. </div>
  92. <!-- 教师填写批注 -->
  93. <div v-else class="annotations-container">
  94. <div class="title">增加批注</div>
  95. <div v-if="currentQuestion.type === 'read'" class="read-score">
  96. <div v-for="(item, i) in remark.child_question_remark_list" :key="i">
  97. <span>小题 {{ i + 1 }} 分数:</span>
  98. <span v-if="isEnable(item?.is_objective)">得分 {{ item.score }}</span>
  99. <el-input-number v-else v-model="item.score" :min="0" :max="item.score_question" :step="1" />
  100. </div>
  101. </div>
  102. <div v-else-if="!is_objective" class="score">
  103. <span>分数</span>
  104. <el-input-number
  105. v-model="remark.score"
  106. :min="0"
  107. :max="question.score"
  108. :step="question.score_type === scoreTypeList[0].value ? 1 : 0.1"
  109. />
  110. </div>
  111. <el-input v-model="remark.remark" type="textarea" rows="6" resize="none" class="remark" />
  112. <div>图片/视频</div>
  113. <el-upload action="no" accept="video/*,image/*" :show-file-list="false" :http-request="upload">
  114. <div class="upload">
  115. <i class="el-icon-plus avatar-uploader-icon"></i>
  116. <span>Upload</span>
  117. </div>
  118. </el-upload>
  119. <ul class="file-list">
  120. <li v-for="({ file_name, file_id }, i) in remark.file_list" :key="file_id" @click="removeFile(i)">
  121. <span>{{ file_name }}</span>
  122. <SvgIcon icon-class="delete" />
  123. </li>
  124. </ul>
  125. <div class="popover-footer">
  126. <el-button @click="isPopover = false">取消</el-button>
  127. <el-button type="primary" @click="fillQuestionAnswerRemark">确定</el-button>
  128. </div>
  129. </div>
  130. <div
  131. v-show="isAnnotations"
  132. slot="reference"
  133. :class="['annotations', { has: isEnable(remark.is_remarked) && !isTeacher }]"
  134. >
  135. <template v-if="isEnable(remark.is_remarked) && !isTeacher">
  136. <span>有一条教师批注</span>
  137. </template>
  138. <template v-else><i class="el-icon-plus"></i><span>批注</span></template>
  139. </div>
  140. </el-popover>
  141. <div class="footer-opeartion">
  142. <template v-if="curQuestionIndex === -1 && !(user_answer_record_info.is_exist_answer_record === 'true')">
  143. <el-button type="primary" round @click="startAnswer">开始答题</el-button>
  144. </template>
  145. <template v-else-if="isSubmit">
  146. <el-button v-if="answer_mode === 1" round type="primary" @click="startAnswer">开始答题</el-button>
  147. </template>
  148. <template v-else>
  149. <el-button v-if="curQuestionIndex > 0" type="primary" round @click="fillQuestionAnswer('pre')">
  150. 上一题
  151. </el-button>
  152. <el-button
  153. v-if="
  154. curQuestionIndex === questionList.length - 1 &&
  155. !isTeacherAnnotations &&
  156. !isShow &&
  157. user_answer_record_info.is_exist_answer_record !== 'true'
  158. "
  159. class="confirm"
  160. round
  161. @click="confirmSubmitAnswer"
  162. >
  163. {{ curQuestionIsSubmit ? '完成答题' : '提交' }}
  164. </el-button>
  165. <el-button
  166. v-else-if="curQuestionIndex < questionList.length - 1"
  167. :type="curQuestionIsSubmit ? 'primary' : ''"
  168. :class="curQuestionIsSubmit ? '' : 'confirm'"
  169. round
  170. @click="fillQuestionAnswer('next')"
  171. >
  172. {{ curQuestionIsSubmit ? '下一题' : '提交' }}
  173. </el-button>
  174. </template>
  175. </div>
  176. <div v-if="isAnnotations" class="score_type">
  177. 本题分数:{{
  178. question.score_type === scoreTypeList[0].value || currentQuestion.type === 'read'
  179. ? `总分${question.score}分`
  180. : `总分${question.score}分 每小题${question.score_item}分`
  181. }}
  182. <template v-if="is_objective">
  183. <span>得分{{ remark.score }}分</span>
  184. </template>
  185. </div>
  186. </footer>
  187. </div>
  188. </template>
  189. <script>
  190. import { secondFormatConversion } from '@/utils/transform';
  191. import {
  192. GetExerciseQuestionIndexList,
  193. GetShareRecordInfo,
  194. StartAnswer,
  195. FillQuestionAnswer,
  196. SubmitAnswer,
  197. GetQuestionInfo_AnswerRecord,
  198. FillQuestionAnswerRemark,
  199. GetAnswerRecordReport,
  200. GetQuestionInfo,
  201. EndAnswer,
  202. } from '@/api/exercise';
  203. import { fileUpload } from '@/api/app';
  204. import { exerciseNames } from '@/views/exercise_questions/data/questionType';
  205. import { scoreTypeList } from '@/views/exercise_questions/data/common';
  206. import StartQuestion from './components/StartQuestion.vue';
  207. import AnswerReport from './components/AnswerReport.vue';
  208. import PreviewQuestionTypeMixin from '../data/PreviewQuestionTypeMixin';
  209. export default {
  210. name: 'AnswerPage',
  211. components: {
  212. StartQuestion,
  213. AnswerReport,
  214. },
  215. mixins: [PreviewQuestionTypeMixin],
  216. data() {
  217. const {
  218. id,
  219. share_record_id,
  220. answer_record_id,
  221. exercise_id,
  222. question_index,
  223. back_url,
  224. type = 'answer',
  225. } = this.$route.query;
  226. let questionIndex = Number(question_index);
  227. return {
  228. type, // 类型:answer【答题】show【展示】
  229. isShow: type === 'show', // 是否是展示模式
  230. exercise_id: id || exercise_id, // 练习题id
  231. share_record_id, // 分享记录id
  232. answer_record_id: answer_record_id ?? '', // 答题记录id
  233. back_url:
  234. back_url?.length > 0
  235. ? decodeURIComponent(back_url)
  236. : `${window.location.origin}/GCLS-Learn/#/main?tab=ExerciseList`, // 返回链接
  237. secondFormatConversion,
  238. isTeacher: this.$store.getters.isTeacher, // 是否是教师
  239. user_answer_record_info: {}, // 当前用户的答题记录信息
  240. correct_answer_show_mode: 1,
  241. scoreTypeList, // 分数类型列表
  242. // 问题列表
  243. questionList: [],
  244. // 当前问题
  245. currentQuestion: {},
  246. // 当前问题索引
  247. curQuestionIndex: -1,
  248. question_index: questionIndex >= 0 ? questionIndex : -1, // 跳转的问题索引
  249. loading: false,
  250. // 倒计时
  251. countDownTimer: null,
  252. answer_mode: 1, // 答题模式
  253. answer_time_limit_minute: 30, // 答题时间限制
  254. time: 1800,
  255. isSubmit: false,
  256. isView: false, // 是否从答题报告跳转到题目
  257. curQuestionPage: '', // 当前问题页面
  258. remark: {
  259. is_remarked: 'false',
  260. score: 0,
  261. remark: '',
  262. remark_person_image_url: '',
  263. remark_person_name: '',
  264. remark_time: '',
  265. file_list: [],
  266. child_question_remark_list: [], // 子题批注列表
  267. }, // 批注
  268. isPopover: false,
  269. recordReport: {
  270. answer_record: {
  271. answer_duration: 0,
  272. right_count: 0,
  273. error_count: 0,
  274. is_remarked: 'false',
  275. total_score: 0,
  276. },
  277. question_list: [],
  278. }, // 答题报告
  279. exerciseNames,
  280. isShowQuestionList: false, // 是否显示题目列表
  281. is_objective: false, // 是否客观题
  282. question: {
  283. score: 1,
  284. score_item: 1,
  285. score_type: 'aggregate',
  286. }, // 题目信息
  287. // 用户答案
  288. user_answer: {
  289. answer_status: 0,
  290. },
  291. };
  292. },
  293. computed: {
  294. isStart() {
  295. return (
  296. this.curQuestionIndex === -1 &&
  297. !(this.user_answer_record_info.is_exist_answer_record === 'true') &&
  298. !this.isShow &&
  299. !this.isSubmit
  300. );
  301. },
  302. // 是否教师批改
  303. isTeacherAnnotations() {
  304. return this.question_index >= 0 && this.isTeacher && this.type !== 'show';
  305. },
  306. // 是否显示批注
  307. isAnnotations() {
  308. return (
  309. (this.remark.is_remarked === 'true' || this.isTeacherAnnotations) &&
  310. this.curQuestionIndex >= 0 &&
  311. !this.isSubmit
  312. );
  313. },
  314. // 是否考试模式
  315. isExamMode() {
  316. return this.answer_mode === 2;
  317. },
  318. // 当前题目是否可提交答题
  319. curQuestionIsSubmit() {
  320. return this.questionList[this.curQuestionIndex].isFill || this.isShow || this.isExamMode;
  321. },
  322. },
  323. watch: {
  324. curQuestionIndex(val) {
  325. if (val === -1) {
  326. this.curQuestionPage = '';
  327. this.currentQuestion = {};
  328. return;
  329. }
  330. if (this.isShow) {
  331. this.getQuestionInfo();
  332. return;
  333. }
  334. this.getQuestionInfo_AnswerRecord();
  335. },
  336. isSubmit(val) {
  337. if (val) {
  338. this.getAnswerRecordReport();
  339. }
  340. },
  341. },
  342. created() {
  343. this.init();
  344. },
  345. beforeDestroy() {
  346. if (this.countDownTimer) clearInterval(this.countDownTimer);
  347. },
  348. methods: {
  349. // 初始化
  350. init() {
  351. if (this.exercise_id) {
  352. this.getExerciseQuestionIndexList();
  353. }
  354. if (this.share_record_id && !this.exercise_id) {
  355. this.loading = true;
  356. GetShareRecordInfo({ share_record_id: this.share_record_id }).then(
  357. ({
  358. user_answer_record_info,
  359. share_record: { exercise_id, answer_mode, answer_time_limit_minute, correct_answer_show_mode },
  360. }) => {
  361. this.user_answer_record_info = user_answer_record_info;
  362. this.exercise_id = exercise_id;
  363. this.getExerciseQuestionIndexList();
  364. this.answer_time_limit_minute = answer_time_limit_minute;
  365. this.time = answer_time_limit_minute * 60;
  366. this.correct_answer_show_mode = correct_answer_show_mode;
  367. this.loading = false;
  368. this.answer_mode = answer_mode;
  369. // 如果已经存在答题记录,则直接显示答题报告
  370. if (this.user_answer_record_info.is_exist_answer_record === 'true') {
  371. this.answer_record_id = this.user_answer_record_info.answer_record_id;
  372. this.isSubmit = true;
  373. }
  374. if (!this.isTeacher) {
  375. this.getAnswerRecordReport();
  376. }
  377. },
  378. );
  379. }
  380. },
  381. // 获取答题报告
  382. getAnswerRecordReport() {
  383. if (!this.answer_record_id) return;
  384. GetAnswerRecordReport({ answer_record_id: this.answer_record_id })
  385. .then(({ answer_record, question_list }) => {
  386. if (answer_record.is_remarked === 'true') {
  387. this.isSubmit = true;
  388. }
  389. this.recordReport = {
  390. answer_record,
  391. question_list,
  392. };
  393. })
  394. .catch(() => {});
  395. },
  396. // 得到练习的题目索引列表
  397. getExerciseQuestionIndexList() {
  398. GetExerciseQuestionIndexList({ exercise_id: this.exercise_id }).then(({ index_list }) => {
  399. this.questionList = index_list.map((item) => ({
  400. ...item,
  401. isFill: this.isTeacherAnnotations || this.user_answer_record_info.is_exist_answer_record === 'true',
  402. }));
  403. if (this.isShow) {
  404. this.curQuestionIndex = this.question_index || 0;
  405. return;
  406. }
  407. if (this.question_index >= 0) {
  408. this.curQuestionIndex = this.question_index;
  409. return;
  410. }
  411. });
  412. },
  413. goBack() {
  414. if (this.isView) {
  415. this.isSubmit = true;
  416. this.isView = false;
  417. return;
  418. }
  419. if (this.back_url === 'not-return') return;
  420. window.location.href = this.back_url;
  421. },
  422. // 倒计时
  423. countDown() {
  424. this.countDownTimer = setInterval(() => {
  425. this.time -= 1;
  426. if (this.time === 0) {
  427. clearInterval(this.countDownTimer);
  428. this.endAnswer();
  429. }
  430. }, 1000);
  431. },
  432. endAnswer() {
  433. EndAnswer({ answer_record_id: this.answer_record_id }).then(() => {
  434. this.isSubmit = true;
  435. });
  436. },
  437. startAnswer() {
  438. if (!this.share_record_id) {
  439. this.curQuestionIndex = 0;
  440. return;
  441. }
  442. StartAnswer({ exercise_id: this.exercise_id, share_record_id: this.share_record_id })
  443. .then(({ answer_mode, answer_record_id, answer_time_limit_minute }) => {
  444. this.questionList = this.questionList.map((item) => ({
  445. ...item,
  446. isFill: false,
  447. }));
  448. this.answer_record_id = answer_record_id;
  449. this.answer_time_limit_minute = answer_time_limit_minute;
  450. this.time = answer_time_limit_minute * 60;
  451. this.countDown();
  452. this.answer_mode = answer_mode;
  453. this.curQuestionIndex = 0;
  454. this.isSubmit = false;
  455. this.user_answer_record_info.is_exist_answer_record = 'false';
  456. })
  457. .catch(() => {});
  458. },
  459. preQuestion() {
  460. if (this.curQuestionIndex === 0) return;
  461. this.curQuestionIndex -= 1;
  462. },
  463. nextQuestion() {
  464. if (this.curQuestionIndex === this.questionList.length - 1) return;
  465. this.curQuestionIndex += 1;
  466. },
  467. /**
  468. * 填写答案
  469. * @param {'pre' | 'next'} type 上一题/下一题
  470. */
  471. fillQuestionAnswer(type) {
  472. if (type === 'pre' && this.curQuestionIndex <= 0) return;
  473. if (type === 'next' && this.curQuestionIndex > this.questionList.length - 1) return;
  474. if (!this.answer_record_id) {
  475. this.curQuestionIndex =
  476. type === 'pre'
  477. ? Math.max(0, this.curQuestionIndex - 1)
  478. : Math.min(this.questionList.length - 1, this.curQuestionIndex + 1);
  479. return;
  480. }
  481. // 如果是上一题,直接跳转
  482. if (type === 'pre') {
  483. return this.preQuestion();
  484. }
  485. // 如果已填写或展示预览模式,直接跳转
  486. if (this.questionList[this.curQuestionIndex].isFill || this.isShow) {
  487. if (type === 'next') return this.nextQuestion();
  488. }
  489. return FillQuestionAnswer({
  490. answer_record_id: this.answer_record_id,
  491. question_id: this.questionList[this.curQuestionIndex].id,
  492. answer: JSON.stringify(this.$refs.exercise[0].answer),
  493. }).then(() => {
  494. // 考试模式下,直接跳转下一题
  495. if (this.isExamMode) {
  496. if (type === 'next') return this.nextQuestion();
  497. } else {
  498. // 练习模式下,将当前题目标记为已填写
  499. this.questionList[this.curQuestionIndex].isFill = true;
  500. }
  501. this.$refs.exercise[0].showAnswer(this.answer_mode === 1, this.correct_answer_show_mode === 1, null, true);
  502. });
  503. },
  504. getQuestionInfo() {
  505. if (this.questionList.length === 0) return;
  506. GetQuestionInfo({ question_id: this.questionList[this.curQuestionIndex].id }).then(({ question, file_list }) => {
  507. if (!question.content) return;
  508. this.curQuestionPage =
  509. this.curQuestionIndex < 0 ? '' : this.previewComponents[this.questionList[this.curQuestionIndex].type];
  510. // 将题目文件id列表添加到题目内容中
  511. let file_id_list = file_list.map(({ file_id }) => file_id);
  512. let content = JSON.parse(question.content);
  513. content.file_id_list = file_id_list;
  514. this.currentQuestion = content;
  515. });
  516. },
  517. // 得到答题记录题目信息
  518. getQuestionInfo_AnswerRecord() {
  519. if (this.questionList.length === 0) return;
  520. GetQuestionInfo_AnswerRecord({
  521. answer_record_id: this.answer_record_id,
  522. question_id: this.questionList[this.curQuestionIndex].id,
  523. }).then(({ question, user_answer: { is_fill_answer, content, is_objective, answer_status }, remark }) => {
  524. if (question.type === 'read') {
  525. let question_list = JSON.parse(question.content)?.question_list ?? [];
  526. let child_question_remark_list = question_list
  527. .map(({ id }) => {
  528. return remark.child_question_remark_list.find((item) => item.question_id === id);
  529. })
  530. .filter((item) => item);
  531. remark.child_question_remark_list = child_question_remark_list;
  532. }
  533. // 批注
  534. this.remark = remark;
  535. this.question = question;
  536. // 题目内容
  537. if (question.content) {
  538. this.currentQuestion = JSON.parse(question.content);
  539. this.curQuestionPage =
  540. this.questionList.length === 0 || this.curQuestionIndex < 0
  541. ? ''
  542. : this.previewComponents[this.questionList[this.curQuestionIndex].type];
  543. }
  544. this.is_objective = this.isEnable(is_objective);
  545. this.user_answer.answer_status = answer_status;
  546. // 如果已经填写过答案,直接显示答案
  547. if (is_fill_answer === 'true') {
  548. this.$nextTick().then(() => {
  549. /**
  550. * 是否判断对错
  551. * 1. 答题模式为练习模式
  552. * 2. 教师批改
  553. * 3. 答题模式为考试模式,且已经批改过
  554. * 4. 从答题报告跳转到题目
  555. */
  556. let isJudgingRightWrong =
  557. this.answer_mode === 1 ||
  558. this.isTeacherAnnotations ||
  559. (this.isExamMode && this.recordReport.answer_record.is_remarked === 'true') ||
  560. this.isView;
  561. /**
  562. * 是否显示正确答案
  563. * 1. 答题模式为练习模式,且正确答案显示模式为答题后显示
  564. * 2. 教师批改
  565. * 3. 从答题报告跳转到题目
  566. */
  567. let isShowRightAnswer =
  568. (this.answer_mode === 1 && this.correct_answer_show_mode === 1) ||
  569. this.isTeacherAnnotations ||
  570. this.isView;
  571. /**
  572. * 是否禁用答题
  573. * 1. 教师批改
  574. * 2. 答题模式为练习模式,且正确答案显示模式为答题后显示
  575. * 3. 教师已经批改过
  576. * 4. 从测试报告跳转到题目
  577. */
  578. let disabled =
  579. this.isTeacherAnnotations ||
  580. (this.answer_mode === 1 && this.correct_answer_show_mode === 1) ||
  581. this.recordReport.answer_record.is_remarked === 'true' ||
  582. this.isView;
  583. this.$refs.exercise?.[0].showAnswer(
  584. isJudgingRightWrong,
  585. isShowRightAnswer,
  586. content.length > 0 ? JSON.parse(content) : null,
  587. disabled,
  588. );
  589. });
  590. }
  591. // 在有批注且为主观题时,弹出批注框
  592. if (this.isAnnotations && !this.is_objective) {
  593. this.isPopover = true;
  594. }
  595. });
  596. },
  597. // 提交答题
  598. confirmSubmitAnswer() {
  599. if (!this.answer_record_id) return;
  600. // 在非考试模式下,当前题目未填写答案先填写答案
  601. if (!this.questionList[this.curQuestionIndex].isFill && !this.isExamMode) {
  602. this.fillQuestionAnswer('next');
  603. return;
  604. }
  605. this.$confirm('是否确认提交答题?', '提示', {
  606. confirmButtonText: '确定',
  607. cancelButtonText: '取消',
  608. type: 'warning',
  609. })
  610. .then(() => {
  611. this.handleSubmitAnswer();
  612. })
  613. .catch(() => {});
  614. },
  615. handleSubmitAnswer() {
  616. clearInterval(this.countDownTimer);
  617. // 如果已经填写过答案,直接提交
  618. if (this.questionList[this.curQuestionIndex].isFill) {
  619. this.submitAnswer();
  620. return;
  621. }
  622. this.fillQuestionAnswer('next').then(() => {
  623. this.submitAnswer();
  624. });
  625. },
  626. submitAnswer() {
  627. SubmitAnswer({ answer_record_id: this.answer_record_id })
  628. .then(() => {
  629. this.isSubmit = true;
  630. this.curQuestionIndex = -1;
  631. this.user_answer_record_info.is_exist_answer_record = 'true';
  632. })
  633. .catch(() => {});
  634. },
  635. selectQuestion(i) {
  636. if (this.isSubmit) {
  637. this.isSubmit = false;
  638. this.isView = true;
  639. }
  640. this.curQuestionIndex = i;
  641. },
  642. upload(file) {
  643. fileUpload('Mid', file).then(({ file_info_list }) => {
  644. if (file_info_list.length > 0) {
  645. const { file_id, file_url, file_name } = file_info_list[0];
  646. this.remark.file_list.push({ file_id, file_url, file_name });
  647. }
  648. });
  649. },
  650. removeFile(i) {
  651. this.remark.file_list.splice(i, 1);
  652. },
  653. // 填写批注
  654. fillQuestionAnswerRemark() {
  655. FillQuestionAnswerRemark({
  656. answer_record_id: this.answer_record_id,
  657. question_id: this.questionList[this.curQuestionIndex].id,
  658. file_id_list: this.remark.file_list.map(({ file_id }) => file_id),
  659. child_question_remark_list: this.remark.child_question_remark_list,
  660. score: this.remark.score,
  661. remark: this.remark.remark,
  662. }).then(() => {
  663. this.$message.success('批注成功');
  664. this.isPopover = false;
  665. });
  666. },
  667. getExerciseName(type, question_type, additional_type) {
  668. if (this.questionList.length <= 0) return;
  669. if (type === 'cur') {
  670. if (this.curQuestionIndex < 0) return '';
  671. let { type: _type, additional_type: _additional_type } = this.questionList[this.curQuestionIndex];
  672. if (_type === 'select') {
  673. return _additional_type === 'single' ? '单选题' : '多选题';
  674. }
  675. return this.exerciseNames[_type];
  676. }
  677. if (type === 'list') {
  678. if (question_type === 'select') {
  679. return additional_type === 'single' ? '单选题' : '多选题';
  680. }
  681. return this.exerciseNames[question_type];
  682. }
  683. },
  684. },
  685. };
  686. </script>
  687. <style lang="scss" scoped>
  688. .answer {
  689. display: flex;
  690. flex-direction: column;
  691. row-gap: 16px;
  692. max-width: 1200px;
  693. min-height: 100%;
  694. padding: 16px;
  695. margin: 0 auto;
  696. background-color: #fff;
  697. border-radius: 24px;
  698. box-shadow: 0 6px 30px 5px #0000000d;
  699. .header {
  700. display: flex;
  701. align-items: center;
  702. justify-content: space-between;
  703. height: 38px;
  704. font-size: 14px;
  705. .back {
  706. cursor: pointer;
  707. }
  708. .user-answer-info {
  709. .answer-status {
  710. display: flex;
  711. column-gap: 8px;
  712. align-items: center;
  713. padding: 8px 16px;
  714. color: #fff;
  715. border-radius: 40px;
  716. &.right {
  717. background-color: #3acb85;
  718. }
  719. &.error {
  720. background-color: #e65656;
  721. }
  722. }
  723. }
  724. .question-info {
  725. display: flex;
  726. column-gap: 12px;
  727. .question-index {
  728. cursor: pointer;
  729. }
  730. }
  731. }
  732. .main {
  733. flex: 1;
  734. }
  735. .footer {
  736. position: relative;
  737. display: flex;
  738. align-items: center;
  739. .annotations {
  740. display: flex;
  741. column-gap: 8px;
  742. align-items: center;
  743. padding: 7px 16px;
  744. font-size: 14px;
  745. cursor: pointer;
  746. background-color: $fill-color;
  747. border-radius: 20px;
  748. &.has {
  749. color: $danger-color;
  750. background-color: #ffece8;
  751. }
  752. }
  753. &-opeartion {
  754. .el-button {
  755. font-size: 16px;
  756. }
  757. .confirm {
  758. color: #fff;
  759. background-color: #5ac448;
  760. }
  761. }
  762. .el-button {
  763. padding: 9px 40px;
  764. }
  765. .score_type {
  766. font-size: 14px;
  767. font-weight: bold;
  768. color: $main-color;
  769. }
  770. }
  771. }
  772. </style>
  773. <style lang="scss">
  774. .el-popover {
  775. display: flex;
  776. flex-direction: column;
  777. row-gap: 8px;
  778. padding: 8px 8px 14px;
  779. font-size: 14px;
  780. %read-score {
  781. display: flex;
  782. flex-direction: column;
  783. row-gap: 8px;
  784. > div {
  785. display: flex;
  786. column-gap: 8px;
  787. align-items: center;
  788. color: #000;
  789. :first-child {
  790. color: #999;
  791. }
  792. .el-input-number {
  793. flex: 1;
  794. }
  795. }
  796. }
  797. .remark-container {
  798. display: flex;
  799. flex-direction: column;
  800. row-gap: 8px;
  801. .remark-info {
  802. display: flex;
  803. column-gap: 8px;
  804. align-items: center;
  805. .remark-name {
  806. flex: 1;
  807. }
  808. .remark-time {
  809. font-size: 12px;
  810. color: #999;
  811. }
  812. }
  813. .file {
  814. display: flex;
  815. flex-wrap: wrap;
  816. gap: 8px;
  817. .el-image {
  818. width: 80px;
  819. height: 80px;
  820. background-color: #d9d9d9;
  821. }
  822. }
  823. .read-score {
  824. @extend %read-score;
  825. }
  826. }
  827. .annotations-container {
  828. display: flex;
  829. flex-direction: column;
  830. row-gap: 8px;
  831. .title {
  832. color: #000;
  833. }
  834. .read-score {
  835. @extend %read-score;
  836. }
  837. .score {
  838. display: flex;
  839. column-gap: 8px;
  840. align-items: center;
  841. :first-child {
  842. color: #999;
  843. }
  844. }
  845. .remark {
  846. width: 350px;
  847. }
  848. .upload {
  849. display: flex;
  850. flex-direction: column;
  851. align-items: center;
  852. justify-content: space-around;
  853. width: 80px;
  854. height: 80px;
  855. padding: 8px;
  856. background-color: $fill-color;
  857. border: 1px solid $border-color;
  858. }
  859. .file-list {
  860. display: flex;
  861. flex-direction: column;
  862. row-gap: 4px;
  863. > li {
  864. display: flex;
  865. column-gap: 4px;
  866. :first-child {
  867. flex: 1;
  868. }
  869. :last-child {
  870. cursor: pointer;
  871. }
  872. }
  873. }
  874. .popover-footer {
  875. display: flex;
  876. justify-content: flex-end;
  877. }
  878. }
  879. }
  880. .question-wrapper {
  881. max-height: 60vh;
  882. padding: 8px;
  883. overflow: auto;
  884. border-radius: 8px;
  885. box-shadow: 0 2px 8px 0 #00000040;
  886. .question-list {
  887. li {
  888. display: flex;
  889. column-gap: 8px;
  890. align-items: center;
  891. padding: 8px 16px;
  892. cursor: pointer;
  893. &.active {
  894. color: $main-color;
  895. background-color: #f4f8ff;
  896. border-radius: 2px;
  897. }
  898. }
  899. }
  900. }
  901. </style>