index.vue 28 KB

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