QuestionImportEdit.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. <template>
  2. <div class="question-export-edit">
  3. <el-dialog
  4. custom-class="question-export-edit-dialog"
  5. :visible.sync="modalIsShow"
  6. :close-on-click-modal="false"
  7. :close-on-press-escape="false"
  8. append-to-body
  9. fullscreen
  10. destroy-on-close
  11. :show-close="false"
  12. @opened="visibleChange"
  13. @closed="initData"
  14. >
  15. <div slot="title" class="box-justify">
  16. <div>
  17. <h2>文件上传</h2>
  18. </div>
  19. <div>
  20. <el-button
  21. size="small"
  22. type="danger"
  23. plain
  24. icon="icon icon-back"
  25. @click="cancel"
  26. >返回</el-button
  27. >
  28. </div>
  29. </div>
  30. <div class="qe-body">
  31. <div class="qe-part qe-part-edit">
  32. <div class="qe-part-main">
  33. <div class="qe-part-head">
  34. <h3>题目编辑</h3>
  35. <div>
  36. <i class="icon icon-tips"></i>
  37. 提示:若识别有误,可点击左侧题目按格式进行修改后重新识别
  38. </div>
  39. </div>
  40. <div class="qe-part-body">
  41. <v-editor
  42. v-model="paperRichJson"
  43. :enable-formula="false"
  44. :enable-audio="false"
  45. ></v-editor>
  46. </div>
  47. </div>
  48. </div>
  49. <div class="qe-part qe-part-view">
  50. <div class="qe-part-main">
  51. <div class="qe-part-head">
  52. <h3>题目阅览</h3>
  53. <div>
  54. <el-button
  55. size="small"
  56. type="primary"
  57. plain
  58. icon="icon icon-export-answer"
  59. @click="toImportAnswer"
  60. >导入答案属性</el-button
  61. >
  62. <el-button
  63. size="small"
  64. type="primary"
  65. icon="icon icon-save-white"
  66. @click="confirm"
  67. >识别无误,加入题库</el-button
  68. >
  69. </div>
  70. </div>
  71. <div class="qe-part-body">
  72. <question-import-paper-edit
  73. v-if="paperData.length"
  74. ref="QuestionImportPaperEdit"
  75. :key="questionKey"
  76. :paper="paperData"
  77. :course-id="data.importData.courseId"
  78. ></question-import-paper-edit>
  79. </div>
  80. </div>
  81. </div>
  82. <div class="qe-middle">
  83. <div class="qe-middle-arrow"></div>
  84. <el-button
  85. size="small"
  86. type="primary"
  87. :loading="loading"
  88. @click="toParse"
  89. >识别</el-button
  90. >
  91. </div>
  92. </div>
  93. </el-dialog>
  94. <!-- 上传答案文件 -->
  95. <import-file-dialog
  96. ref="ImportAnswerDialog"
  97. dialog-title="导入答案"
  98. :template-download-handle="answerTemplateDownload"
  99. :upload-url="uploadAnswerUrl"
  100. :upload-data="uploadAnswerData"
  101. add-file-param="dataFile"
  102. @uploaded="answerUploaded"
  103. ></import-file-dialog>
  104. </div>
  105. </template>
  106. <script>
  107. // import paperRichTextJson from "../datas/paperRichText.json";
  108. // import paperParseData from "../datas/paperParseData.json";
  109. import { calcSum, deepCopy, objTypeOf, randomCode } from "@/plugins/utils";
  110. import QuestionImportPaperEdit from "./QuestionImportPaperEdit.vue";
  111. import { isAnEmptyRichText } from "@/utils/utils";
  112. import {
  113. questionImportPaperSave,
  114. questionImportParseRichText,
  115. questionImportDownloadTemplate,
  116. } from "../api";
  117. import ImportFileDialog from "@/components/ImportFileDialog.vue";
  118. import { QUESTION_API } from "@/constants/constants";
  119. import { propertyNameQueryApi } from "@/modules/question/api";
  120. import { downloadByApi } from "@/plugins/download";
  121. const questionInfoField = [
  122. "courseId",
  123. "difficulty",
  124. "quesProperties",
  125. "score",
  126. "publicity",
  127. "control",
  128. "answerAnalysis",
  129. "quesAnswer",
  130. ];
  131. export default {
  132. name: "QuestionExportEdit",
  133. components: { QuestionImportPaperEdit, ImportFileDialog },
  134. props: {
  135. data: {
  136. type: Object,
  137. default() {
  138. return {
  139. richText: { sections: [] },
  140. detailInfo: [],
  141. importData: {
  142. courseId: "",
  143. courseName: "",
  144. name: "",
  145. checkTotalScore: false,
  146. useOriginalPaper: false,
  147. totalScore: 0,
  148. },
  149. };
  150. },
  151. },
  152. },
  153. data() {
  154. return {
  155. modalIsShow: false,
  156. loading: false,
  157. questionKey: "",
  158. paperData: [],
  159. paperRichJson: { sections: [] },
  160. // upload answer
  161. uploadAnswerUrl: `${QUESTION_API}/word/parse/import`,
  162. uploadAnswerData: {},
  163. };
  164. },
  165. methods: {
  166. async visibleChange() {
  167. await this.getCourseProperty();
  168. // this.paperData = deepCopy(paperParseData);
  169. // this.paperRichJson = deepCopy(paperRichTextJson);
  170. this.paperRichJson = deepCopy(this.data.richText);
  171. this.paperData = deepCopy(this.data.detailInfo);
  172. this.transformRichImg();
  173. this.questionKey = randomCode();
  174. },
  175. async getCourseProperty() {
  176. const res = await propertyNameQueryApi(this.data.importData.courseId, "");
  177. const optionList = res.data || [];
  178. window.sessionStorage.setItem(
  179. "coursePropertys",
  180. JSON.stringify({ optionList, courseId: this.data.importData.courseId })
  181. );
  182. },
  183. transformRichImg() {
  184. const rate = 96 / 200;
  185. this.paperRichJson.sections.forEach((section) => {
  186. section.blocks.forEach((block) => {
  187. if (block.type !== "image" || !block.param) return;
  188. block.param.width = block.param.width * rate;
  189. block.param.height = block.param.height * rate;
  190. });
  191. });
  192. },
  193. initData() {
  194. this.paperData = [];
  195. this.paperRichJson = { sections: [] };
  196. window.sessionStorage.removeItem("coursePropertys");
  197. this.$message.closeAll();
  198. },
  199. cancel() {
  200. this.modalIsShow = false;
  201. },
  202. open() {
  203. this.modalIsShow = true;
  204. },
  205. async toParse() {
  206. if (isAnEmptyRichText(this.paperRichJson)) {
  207. this.$message.error("请输入试卷内容!");
  208. return;
  209. }
  210. if (this.loading) return;
  211. this.loading = true;
  212. const res = await questionImportParseRichText({
  213. richText: this.paperRichJson,
  214. courseId: this.data.importData.courseId,
  215. }).catch(() => {});
  216. this.loading = false;
  217. if (!res) return;
  218. const cacheData = this.getCachePaperInfo(
  219. this.getImportPaperData(),
  220. questionInfoField
  221. );
  222. // console.log(cacheData);
  223. this.paperData = this.assignCachePaperData(res.data, cacheData);
  224. this.questionKey = randomCode();
  225. },
  226. getCachePaperInfo(paperData, cacheFields = []) {
  227. let cachePaperInfo = {};
  228. paperData.forEach((detail, dIndex) => {
  229. detail.questionInfo.forEach((question, qIndex) => {
  230. let info = {};
  231. let k = `${dIndex + 1}_${qIndex + 1}`;
  232. if (cacheFields.length) {
  233. cacheFields.forEach((field) => {
  234. info[field] = question[field];
  235. });
  236. } else {
  237. info = { ...question };
  238. }
  239. cachePaperInfo[k] = info;
  240. if (question.subQuestions && question.subQuestions.length) {
  241. question.subQuestions.forEach((subq, subqIndex) => {
  242. let info = {};
  243. let k = `${dIndex + 1}_${qIndex + 1}_${subqIndex + 1}`;
  244. if (cacheFields.length) {
  245. cacheFields.forEach((field) => {
  246. info[field] = subq[field];
  247. });
  248. } else {
  249. info = { ...subq };
  250. }
  251. cachePaperInfo[k] = info;
  252. });
  253. }
  254. });
  255. });
  256. // console.log(cachePaperInfo);
  257. return cachePaperInfo;
  258. },
  259. assignCachePaperData(paperData, cacheData, mergeReverse = false) {
  260. return paperData.map((detail, dIndex) => {
  261. detail.questions = detail.questions.map((question, qIndex) => {
  262. let k = `${dIndex + 1}_${qIndex + 1}`;
  263. let nq = this.mergeObjData(
  264. question,
  265. cacheData[k] || {},
  266. mergeReverse
  267. );
  268. if (question.subQuestions && question.subQuestions.length) {
  269. nq.subQuestions = question.subQuestions.map((subq, subqIndex) => {
  270. let k = `${dIndex + 1}_${qIndex + 1}_${subqIndex + 1}`;
  271. return this.mergeObjData(subq, cacheData[k] || {}, mergeReverse);
  272. });
  273. }
  274. return nq;
  275. });
  276. return detail;
  277. });
  278. },
  279. isNull(val) {
  280. if (val) {
  281. if (val === "[]") return true;
  282. if (objTypeOf(val) === "array" && !val.length) return true;
  283. }
  284. return val === null || val === "" || val === undefined;
  285. },
  286. mergeObjData(targetObj, cacheObj, mergeReverse) {
  287. let data = { ...targetObj };
  288. Object.keys(cacheObj).forEach((k) => {
  289. if (mergeReverse) {
  290. data[k] = this.isNull(cacheObj[k]) ? targetObj[k] : cacheObj[k];
  291. } else {
  292. data[k] = this.isNull(targetObj[k]) ? cacheObj[k] : targetObj[k];
  293. }
  294. });
  295. return data;
  296. },
  297. getImportPaperData() {
  298. if (!this.$refs.QuestionImportPaperEdit) return [];
  299. let paperData = deepCopy(this.$refs.QuestionImportPaperEdit.getData());
  300. const transformFieldMap = { body: "quesBody", options: "quesOptions" };
  301. const fields = Object.keys(transformFieldMap);
  302. const course = {
  303. id: this.data.importData.courseId,
  304. name: this.data.importData.courseName,
  305. };
  306. const transformQuestion = (question) => {
  307. question.id = null;
  308. question.course = course;
  309. fields.forEach((field) => {
  310. question[transformFieldMap[field]] = question[field];
  311. delete question[field];
  312. });
  313. if (question.quesOptions && question.quesOptions.length) {
  314. question.quesOptions = question.quesOptions.map((option) => {
  315. option.optionBody = option.body;
  316. delete option.body;
  317. return option;
  318. });
  319. }
  320. return question;
  321. };
  322. const detailInfo = paperData.map((detail) => {
  323. const questionInfo = detail.questions.map((question) => {
  324. transformQuestion(question);
  325. if (question.subQuestions && question.subQuestions.length) {
  326. question.subQuestions = question.subQuestions.map((subq) =>
  327. transformQuestion(subq)
  328. );
  329. question.score = calcSum(
  330. question.subQuestions.map((q) => q.score || 0)
  331. );
  332. }
  333. return question;
  334. });
  335. return {
  336. name: detail.name,
  337. number: detail.number,
  338. questionCount: questionInfo.length,
  339. questionInfo,
  340. questionScore: detail.questionScore,
  341. totalScore: calcSum(questionInfo.map((q) => q.score || 0)),
  342. };
  343. });
  344. // console.log(detailInfo);
  345. return detailInfo;
  346. },
  347. checkImportPaperData(paperData) {
  348. this.$message.closeAll();
  349. // 题目内容校验
  350. const MATCHING_QUESTION = ["PARAGRAPH_MATCHING", "BANKED_CLOZE"];
  351. const SELECT_QUESTION = [
  352. "SINGLE_ANSWER_QUESTION",
  353. "MULTIPLE_ANSWER_QUESTION",
  354. ...MATCHING_QUESTION,
  355. ];
  356. const NESTED_QUESTION = [
  357. ...MATCHING_QUESTION,
  358. "READING_COMPREHENSION",
  359. "CLOZE",
  360. ];
  361. let errInfos = [];
  362. paperData.forEach((detail) => {
  363. detail.questionInfo.forEach((question) => {
  364. const { questionType, quesBody } = question;
  365. const questionTitle = `第${detail.number}大题第${question.number}小题`;
  366. let qErrInfo = [];
  367. // 题干
  368. if (
  369. !MATCHING_QUESTION.includes(questionType) &&
  370. (!quesBody || isAnEmptyRichText(quesBody))
  371. ) {
  372. qErrInfo.push(`没有题干`);
  373. }
  374. // 选项
  375. if (SELECT_QUESTION.includes(questionType)) {
  376. if (!question.quesOptions.length) {
  377. qErrInfo.push(`没有选项`);
  378. }
  379. if (
  380. question.quesOptions.some((option) =>
  381. isAnEmptyRichText(option.optionBody)
  382. )
  383. ) {
  384. qErrInfo.push(`有选择内容为空`);
  385. }
  386. }
  387. // 小题数
  388. if (
  389. NESTED_QUESTION.includes(questionType) &&
  390. !question.subQuestions.length
  391. ) {
  392. qErrInfo.push(`没有小题`);
  393. }
  394. if (qErrInfo.length) {
  395. errInfos.push(`${questionTitle}${qErrInfo.join("、")}`);
  396. qErrInfo = [];
  397. }
  398. // 选词填空、段落匹配,单用模式时校验输入答案是否重复
  399. if (
  400. MATCHING_QUESTION.includes(questionType) &&
  401. question.quesParam.matchingMode === 1
  402. ) {
  403. let selectedAnswer = [],
  404. errorQuestionIndexs = [];
  405. question.subQuestions.forEach((subq, sindex) => {
  406. if (selectedAnswer.includes(subq.quesAnswer)) {
  407. errorQuestionIndexs.push(`${question.number}-${sindex + 1}`);
  408. } else {
  409. if (subq.quesAnswer !== "[]")
  410. selectedAnswer.push(subq.quesAnswer);
  411. }
  412. });
  413. if (errorQuestionIndexs.length) {
  414. errInfos.push(
  415. `第${
  416. detail.number
  417. }大题${errorQuestionIndexs.join()}小题答案重复!`
  418. );
  419. }
  420. }
  421. if (!NESTED_QUESTION.includes(questionType)) return;
  422. // 套题小题校验
  423. question.subQuestions.forEach((subq, sindex) => {
  424. const subqTitle = `第${detail.number}大题第${question.number}-${
  425. sindex + 1
  426. }小题`;
  427. if (
  428. questionType === "READING_COMPREHENSION" &&
  429. (!subq.quesBody || isAnEmptyRichText(subq.quesBody))
  430. ) {
  431. qErrInfo.push(`没有题干`);
  432. }
  433. if (
  434. SELECT_QUESTION.includes(subq.subqType) &&
  435. !MATCHING_QUESTION.includes(questionType)
  436. ) {
  437. if (!subq.quesOptions.length) {
  438. qErrInfo.push(`没有选项`);
  439. }
  440. if (
  441. subq.quesOptions.some((option) =>
  442. isAnEmptyRichText(option.optionBody)
  443. )
  444. ) {
  445. qErrInfo.push(`有选择内容为空`);
  446. }
  447. }
  448. if (qErrInfo.length) {
  449. errInfos.push(`${subqTitle}${qErrInfo.join("、")}`);
  450. qErrInfo = [];
  451. }
  452. });
  453. });
  454. });
  455. if (errInfos.length) {
  456. this.$message({
  457. showClose: true,
  458. message: errInfos.join("。"),
  459. type: "error",
  460. duration: 0,
  461. });
  462. return;
  463. }
  464. if (!this.data.importData.useOriginalPaper) return true;
  465. let detailNumbers = paperData.map((detail) => detail.number);
  466. // 大题号重复性校验
  467. let repeatDetaiNumbers = [];
  468. let detailNums = [];
  469. for (let i = 0; i < detailNumbers.length; i++) {
  470. const num = detailNumbers[i];
  471. if (detailNums.includes(num)) {
  472. if (!repeatDetaiNumbers.includes(num)) repeatDetaiNumbers.push(num);
  473. } else {
  474. detailNums.push(num);
  475. }
  476. }
  477. if (repeatDetaiNumbers.length) {
  478. this.$message({
  479. showClose: true,
  480. message: `大题号${repeatDetaiNumbers.join("、")}重复`,
  481. type: "error",
  482. duration: 0,
  483. });
  484. return;
  485. }
  486. // 大题号连续性校验
  487. for (let i = 0; i < detailNumbers.length; i++) {
  488. if (detailNumbers[i] - 1 !== i) {
  489. this.$message({
  490. showClose: true,
  491. message: "大题号不连续",
  492. type: "error",
  493. duration: 0,
  494. });
  495. return;
  496. }
  497. }
  498. // 答案、分数校验
  499. let totalScore = calcSum(paperData.map((d) => d.totalScore));
  500. let errQuestions = [];
  501. paperData.forEach((detail) => {
  502. detail.questionInfo.forEach((question) => {
  503. if (question.subQuestions && question.subQuestions.length) {
  504. let subIndexs = [];
  505. question.subQuestions.forEach((subq, sind) => {
  506. if (!subq.score)
  507. subIndexs.push(question.number + "-" + (sind + 1));
  508. });
  509. if (subIndexs.length)
  510. errQuestions.push(
  511. `第${detail.number}大题第${subIndexs.join()}小题`
  512. );
  513. } else {
  514. if (!question.score) {
  515. errQuestions.push(
  516. `第${detail.number}大题第${question.number}小题`
  517. );
  518. }
  519. }
  520. });
  521. });
  522. if (errQuestions.length) {
  523. this.$message({
  524. showClose: true,
  525. message: `请设置如下试题的分值:${errQuestions.join("、")}。`,
  526. type: "error",
  527. duration: 0,
  528. });
  529. return;
  530. }
  531. if (
  532. this.data.importData.checkTotalScore &&
  533. totalScore !== this.data.importData.totalScore
  534. ) {
  535. this.$message({
  536. showClose: true,
  537. message: `试卷总分与导入设置的总分不一致!`,
  538. type: "error",
  539. duration: 0,
  540. });
  541. return;
  542. }
  543. return true;
  544. },
  545. async confirm() {
  546. const confirm = await this.$confirm("确认加入题库吗?", "提示", {
  547. type: "warning",
  548. }).catch(() => {});
  549. if (confirm !== "confirm") return;
  550. const detailInfo = this.getImportPaperData();
  551. if (!this.checkImportPaperData(detailInfo)) return;
  552. if (this.loading) return;
  553. this.loading = true;
  554. const res = await questionImportPaperSave({
  555. ...this.data.importData,
  556. detailInfo,
  557. }).catch(() => {});
  558. this.loading = false;
  559. if (!res) return;
  560. this.$message.success("提交成功!");
  561. this.$emit("modified");
  562. this.cancel();
  563. },
  564. // 导入答案属性
  565. toImportAnswer() {
  566. const detailInfo = this.getImportPaperData();
  567. this.uploadAnswerData = {
  568. detailInfo: JSON.stringify(detailInfo),
  569. ...this.data.importData,
  570. };
  571. this.$refs.ImportAnswerDialog.open();
  572. },
  573. async answerTemplateDownload() {
  574. const detailInfo = this.getImportPaperData();
  575. const res = await downloadByApi(() => {
  576. return questionImportDownloadTemplate({
  577. detailInfo,
  578. ...this.data.importData,
  579. });
  580. }).catch((e) => {
  581. this.$message.error(e || "下载失败,请重新尝试!");
  582. });
  583. if (!res) return;
  584. this.$message.success("下载成功!");
  585. },
  586. answerUploaded(res) {
  587. const cacheData = this.getCachePaperInfo(
  588. res.data.detailInfo,
  589. questionInfoField
  590. );
  591. this.paperData = this.assignCachePaperData(
  592. this.paperData,
  593. cacheData,
  594. true
  595. );
  596. this.questionKey = randomCode();
  597. },
  598. },
  599. };
  600. </script>