QuestionImportEdit.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992
  1. <template>
  2. <div class="question-import-edit">
  3. <el-dialog
  4. custom-class="question-import-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 style="display: flex; align-items: center">
  20. <upload-button
  21. btn-content="重新上传文件"
  22. btn-icon="icon icon-import"
  23. :disabled="loading"
  24. :upload-data="uploadData"
  25. :upload-url="uploadUrl"
  26. :format="importFileTypes"
  27. @valid-error="validError"
  28. @upload-success="uploaded"
  29. :auto-upload="false"
  30. ></upload-button>
  31. <el-button
  32. style="margin-left: 10px"
  33. size="small"
  34. type="danger"
  35. icon="icon icon-back-white"
  36. @click="cancel"
  37. >返回</el-button
  38. >
  39. </div>
  40. </div>
  41. <div class="qe-body">
  42. <div class="qe-part qe-part-edit">
  43. <div class="qe-part-main">
  44. <div class="qe-part-head">
  45. <h3>题目编辑</h3>
  46. <div>
  47. <i class="icon icon-tips"></i>
  48. 提示:若识别有误,可点击左侧题目按格式进行修改后重新识别
  49. </div>
  50. </div>
  51. <div class="qe-part-body">
  52. <div id="qe-part-richtext-list">
  53. <div
  54. class="qe-part-richtext"
  55. v-for="(richJsonItem, rindex) in paperRichJsonGroup"
  56. :key="rindex"
  57. >
  58. <v-editor
  59. ref="RichTextEditor"
  60. :value="richJsonItem"
  61. :show-menu="false"
  62. custom-emit-input
  63. :custom-render-action="renderRichText"
  64. :custom-tojson-action="richTextToJSON"
  65. @focus="() => richTextFocus(richJsonItem)"
  66. ></v-editor>
  67. <div
  68. v-if="richJsonItem.exceptions.length"
  69. class="qe-part-richtext-error"
  70. >
  71. <p
  72. class="tips-info tips-error"
  73. v-for="(exception, index) in richJsonItem.exceptions"
  74. :key="index"
  75. @click="highLightErrorText(exception)"
  76. >
  77. {{ exception.message }}
  78. </p>
  79. </div>
  80. </div>
  81. </div>
  82. </div>
  83. </div>
  84. </div>
  85. <div class="qe-part qe-part-view">
  86. <div class="qe-part-main">
  87. <div class="qe-part-head">
  88. <h3>题目阅览</h3>
  89. <div>
  90. <el-button
  91. size="small"
  92. type="primary"
  93. plain
  94. icon="icon icon-export-answer"
  95. @click="toImportAnswer"
  96. >导入答案属性</el-button
  97. >
  98. <el-button
  99. size="small"
  100. type="primary"
  101. icon="icon icon-save-white"
  102. :loading="loading"
  103. :disabled="!paperData.length"
  104. @click="confirm"
  105. >识别无误,加入题库</el-button
  106. >
  107. </div>
  108. </div>
  109. <div id="qe-part-paper" class="qe-part-body">
  110. <question-import-paper-edit
  111. v-if="paperData.length"
  112. ref="QuestionImportPaperEdit"
  113. :key="questionKey"
  114. :paper="paperData"
  115. :course-id="data.importData.courseId"
  116. ></question-import-paper-edit>
  117. </div>
  118. </div>
  119. </div>
  120. <div class="qe-middle">
  121. <div class="qe-middle-arrow"></div>
  122. <el-button
  123. size="small"
  124. type="primary"
  125. :loading="loading"
  126. @click="toParse"
  127. >识别</el-button
  128. >
  129. </div>
  130. </div>
  131. </el-dialog>
  132. <!-- 上传答案文件 -->
  133. <import-file-dialog
  134. ref="ImportAnswerDialog"
  135. dialog-title="导入答案"
  136. :template-download-handle="answerTemplateDownload"
  137. :upload-url="uploadAnswerUrl"
  138. :upload-data="uploadAnswerData"
  139. add-file-param="dataFile"
  140. @uploaded="answerUploaded"
  141. ></import-file-dialog>
  142. </div>
  143. </template>
  144. <script>
  145. // import paperRichTextJson from "../datas/paperRichText.json";
  146. // import paperParseData from "../datas/paperParseData.json";
  147. import { calcSum, deepCopy, objTypeOf, randomCode } from "@/plugins/utils";
  148. import QuestionImportPaperEdit from "./QuestionImportPaperEdit.vue";
  149. import UploadButton from "@/components/UploadButton.vue";
  150. import { getRichTextAnswerPointCount, isAnEmptyRichText } from "@/utils/utils";
  151. import {
  152. questionImportPaperSave,
  153. questionImportParseRichText,
  154. questionImportDownloadTemplate,
  155. } from "../api";
  156. import ImportFileDialog from "@/components/ImportFileDialog.vue";
  157. import { QUESTION_API } from "@/constants/constants";
  158. import { propertyNameQueryApi } from "@/modules/question/api";
  159. import { downloadByApi } from "@/plugins/download";
  160. import { richTextToJSON, renderRichText } from "./import-edit/richText";
  161. import scrollMixins from "./import-edit/scrollMixins";
  162. import timeMixin from "@/mixins/timeMixin";
  163. const questionInfoField = [
  164. "courseId",
  165. "difficulty",
  166. "quesProperties",
  167. "score",
  168. "publicity",
  169. "control",
  170. "answerAnalysis",
  171. "quesAnswer",
  172. ];
  173. export default {
  174. name: "QuestionImportEdit",
  175. components: { QuestionImportPaperEdit, ImportFileDialog, UploadButton },
  176. mixins: [scrollMixins, timeMixin],
  177. props: {
  178. data: {
  179. type: Object,
  180. default() {
  181. return {
  182. richText: { sections: [] },
  183. detailInfo: [],
  184. importData: {
  185. courseId: "",
  186. courseName: "",
  187. name: "",
  188. checkTotalScore: false,
  189. useOriginalPaper: false,
  190. totalScore: 0,
  191. },
  192. };
  193. },
  194. },
  195. },
  196. data() {
  197. return {
  198. modalIsShow: false,
  199. loading: false,
  200. questionKey: "",
  201. paperData: [],
  202. paperRichJson: { sections: [] },
  203. paperRichJsonGroup: [],
  204. richTextToJSON,
  205. renderRichText,
  206. lastPaperScrollTop: 0,
  207. lastRichTextScrollTop: 0,
  208. richTextIndexList: [],
  209. scrollType: "",
  210. curException: null,
  211. // upload answer
  212. uploadAnswerUrl: `${QUESTION_API}/word/parse/import`,
  213. uploadAnswerData: {},
  214. // word upload
  215. importFileTypes: ["docx", "doc"],
  216. uploadData: {},
  217. uploadUrl: `${QUESTION_API}/word/parse/struct`,
  218. };
  219. },
  220. methods: {
  221. async visibleChange() {
  222. await this.getCourseProperty();
  223. // this.resetData({
  224. // richText: paperRichTextJson,
  225. // detailInfo: paperParseData,
  226. // });
  227. this.resetData(this.data);
  228. this.$nextTick(() => {
  229. this.registScrollEvent();
  230. });
  231. },
  232. resetData({ richText, detailInfo }) {
  233. this.paperData = deepCopy(detailInfo);
  234. this.paperRichJson = this.buildRichText(deepCopy(richText));
  235. this.transformDataInfo();
  236. this.paperRichJsonGroup = this.getRichTextGroup();
  237. this.uploadData = { courseId: this.data.importData.courseId };
  238. this.questionKey = randomCode();
  239. this.$nextTick(() => {
  240. this.getRichTextIndexList();
  241. });
  242. },
  243. getRichTextIndexList() {
  244. const richTextListDom = document.getElementById("qe-part-richtext-list");
  245. const elPos = richTextListDom.getBoundingClientRect();
  246. let richTextIndexList = [];
  247. const richTextBodyDoms =
  248. richTextListDom.querySelectorAll(".v-editor-body");
  249. richTextBodyDoms.forEach((richTextBodyDom) => {
  250. richTextBodyDom.childNodes.forEach((sectionNode) => {
  251. const id = sectionNode.getAttribute("id");
  252. if (!id) return;
  253. if (
  254. sectionNode.className &&
  255. sectionNode.className.includes("section-error")
  256. )
  257. return;
  258. const index = id.replace("section-", "") * 1;
  259. const sectionPos = sectionNode.getBoundingClientRect();
  260. richTextIndexList.push([index, sectionPos.y - elPos.y]);
  261. });
  262. });
  263. this.richTextIndexList = richTextIndexList;
  264. },
  265. async getCourseProperty() {
  266. const res = await propertyNameQueryApi(this.data.importData.courseId, "");
  267. const optionList = res.data || [];
  268. window.sessionStorage.setItem(
  269. "coursePropertys",
  270. JSON.stringify({ optionList, courseId: this.data.importData.courseId })
  271. );
  272. },
  273. buildRichText(richText) {
  274. let nsections = [];
  275. richText.sections.forEach((section) => {
  276. nsections.push({
  277. ...section,
  278. attributes: { id: `section-${section.remark.index}` },
  279. });
  280. });
  281. return { sections: nsections };
  282. },
  283. transformDataInfo() {
  284. this.transformRichImg(this.paperRichJson);
  285. this.paperData.forEach((detail) => {
  286. detail.questions.forEach((question) => {
  287. this.transformQuestion(question);
  288. if (question.subQuestions && question.subQuestions.length) {
  289. question.subQuestions.forEach((subq) => {
  290. this.transformQuestion(subq);
  291. });
  292. }
  293. });
  294. });
  295. },
  296. transformQuestion(question) {
  297. this.transformRichImg(question.body);
  298. this.transformRichImg(question.answerRichTexts);
  299. if (question.options && question.options.length) {
  300. question.options.forEach((item) => {
  301. this.transformRichImg(item.body);
  302. });
  303. }
  304. question.quesAnswer = this.transformQuestionAnser(question.quesAnswer);
  305. },
  306. transformRichImg(richText) {
  307. if (isAnEmptyRichText(richText)) return;
  308. const rate = 96 / 300;
  309. richText.sections.forEach((section) => {
  310. section.blocks.forEach((block) => {
  311. if (block.type !== "image" || !block.param) return;
  312. block.param.width = block.param.width * rate;
  313. block.param.height = block.param.height * rate;
  314. });
  315. });
  316. },
  317. transformQuestionAnser(quesAnswer) {
  318. let qAnswer = null;
  319. try {
  320. qAnswer = quesAnswer ? JSON.parse(quesAnswer) : null;
  321. } catch (error) {
  322. console.log(error);
  323. }
  324. if (!qAnswer || objTypeOf(qAnswer) !== "array") return quesAnswer;
  325. qAnswer.forEach((item) => {
  326. this.transformRichImg(item);
  327. });
  328. return JSON.stringify(qAnswer);
  329. },
  330. getRichTextGroup() {
  331. let groupSetList = [];
  332. this.paperData.forEach((detail) => {
  333. groupSetList.push({
  334. id: randomCode(),
  335. indexs: detail.detailIndex,
  336. });
  337. detail.questions.forEach((question) => {
  338. groupSetList.push({
  339. id: randomCode(),
  340. indexs: question.integralIndex,
  341. });
  342. if (question.subQuestions && question.subQuestions.length) {
  343. question.subQuestions.forEach((subq) => {
  344. groupSetList.push({
  345. id: randomCode(),
  346. indexs: subq.integralIndex,
  347. });
  348. });
  349. }
  350. });
  351. });
  352. let groups = [];
  353. let curGroupId = 0;
  354. let curGroup = [];
  355. const findGroupId = (ind) => {
  356. let data = groupSetList.find((item) => item.indexs.includes(ind));
  357. return data ? data.id : null;
  358. };
  359. this.paperRichJson.sections.forEach((section) => {
  360. const sectionGroupId = findGroupId(section.remark.index);
  361. if (sectionGroupId !== curGroupId) {
  362. if (curGroup.length) {
  363. groups.push({ sections: curGroup, exceptions: [] });
  364. curGroup = [];
  365. }
  366. }
  367. curGroupId = sectionGroupId;
  368. curGroup.push(section);
  369. });
  370. if (curGroup.length) {
  371. groups.push({ sections: curGroup, exceptions: [] });
  372. curGroup = [];
  373. }
  374. const getExceptions = (sections) => {
  375. let cause = {};
  376. sections.forEach((section) => {
  377. if (section.remark.status) return;
  378. if (!cause[section.remark.cause]) {
  379. cause[section.remark.cause] = [];
  380. }
  381. cause[section.remark.cause].push(section.remark.index);
  382. });
  383. const causeList = Object.keys(cause).map((key) => {
  384. return {
  385. message: key,
  386. indexs: cause[key],
  387. };
  388. });
  389. causeList.sort((a, b) => a.indexs[0] - b.indexs[0]);
  390. return causeList;
  391. };
  392. groups.forEach((group) => {
  393. group.indexs = group.sections.map((item) => item.remark.index);
  394. group.exceptions = getExceptions(group.sections);
  395. });
  396. return groups;
  397. },
  398. highLightErrorText(exception) {
  399. if (this.curException) {
  400. this.curException.indexs.forEach((ind) => {
  401. const sectionDoms = document.querySelectorAll(`#section-${ind}`);
  402. if (!sectionDoms.length) return;
  403. sectionDoms.forEach((sectionDom) => {
  404. sectionDom.style = null;
  405. });
  406. });
  407. }
  408. this.clearSetTs();
  409. this.curException = exception;
  410. let firstSectionDom = null;
  411. exception.indexs.forEach((ind) => {
  412. const sectionDom = document.getElementById(`section-${ind}`);
  413. if (!sectionDom) return;
  414. if (!firstSectionDom) firstSectionDom = sectionDom;
  415. sectionDom.style.background = "#fef0f0";
  416. });
  417. if (!firstSectionDom) return;
  418. const richTextListDom = document.getElementById("qe-part-richtext-list");
  419. const elPos = richTextListDom.getBoundingClientRect();
  420. const sectionOffsetTop =
  421. firstSectionDom.getBoundingClientRect().y - elPos.y - 100;
  422. if (sectionOffsetTop < richTextListDom.parentNode.scrollTop) {
  423. richTextListDom.parentNode.scrollTop = sectionOffsetTop;
  424. this.scrollType = "rich-text";
  425. setTimeout(() => {
  426. this.scrollType = "";
  427. }, 100);
  428. }
  429. this.addSetTime(() => {
  430. this.curException.indexs.forEach((ind) => {
  431. const sectionDoms = document.querySelectorAll(`#section-${ind}`);
  432. if (!sectionDoms.length) return;
  433. sectionDoms.forEach((sectionDom) => {
  434. sectionDom.style = null;
  435. });
  436. });
  437. this.curException = null;
  438. }, 5000);
  439. },
  440. initData() {
  441. this.paperData = [];
  442. this.paperRichJson = { sections: [] };
  443. window.sessionStorage.removeItem("coursePropertys");
  444. this.$message.closeAll();
  445. this.removeScrollEvent();
  446. },
  447. cancel() {
  448. this.modalIsShow = false;
  449. },
  450. open() {
  451. this.modalIsShow = true;
  452. },
  453. getRichTextJsons() {
  454. let sections = [];
  455. this.$refs.RichTextEditor.forEach((item) => {
  456. const itemRichJson = item.emitJsonAction();
  457. sections.push(...itemRichJson.sections);
  458. });
  459. return { sections };
  460. },
  461. async toParse() {
  462. if (isAnEmptyRichText(this.paperRichJson)) {
  463. this.$message.error("请输入试卷内容!");
  464. return;
  465. }
  466. if (this.loading) return;
  467. this.loading = true;
  468. let richText = this.getRichTextJsons();
  469. const res = await questionImportParseRichText({
  470. richText,
  471. courseId: this.data.importData.courseId,
  472. }).catch(() => {});
  473. this.loading = false;
  474. if (!res) return;
  475. const cacheData = this.getCachePaperInfo(
  476. this.getImportPaperData(),
  477. questionInfoField
  478. );
  479. // console.log(cacheData);
  480. this.paperData = this.assignCachePaperData(
  481. res.data.detailInfo,
  482. cacheData
  483. );
  484. this.paperRichJson = this.buildRichText(deepCopy(res.data.richText));
  485. this.paperRichJsonGroup = this.getRichTextGroup();
  486. this.questionKey = randomCode();
  487. this.$nextTick(() => {
  488. this.getRichTextIndexList();
  489. });
  490. },
  491. getCachePaperInfo(paperData, cacheFields = []) {
  492. let cachePaperInfo = {};
  493. paperData.forEach((detail, dIndex) => {
  494. detail.questionInfo.forEach((question, qIndex) => {
  495. let info = {};
  496. let k = `${dIndex + 1}_${qIndex + 1}`;
  497. if (cacheFields.length) {
  498. cacheFields.forEach((field) => {
  499. info[field] = question[field];
  500. });
  501. } else {
  502. info = { ...question };
  503. }
  504. cachePaperInfo[k] = info;
  505. if (question.subQuestions && question.subQuestions.length) {
  506. question.subQuestions.forEach((subq, subqIndex) => {
  507. let info = {};
  508. let k = `${dIndex + 1}_${qIndex + 1}_${subqIndex + 1}`;
  509. if (cacheFields.length) {
  510. cacheFields.forEach((field) => {
  511. info[field] = subq[field];
  512. });
  513. } else {
  514. info = { ...subq };
  515. }
  516. cachePaperInfo[k] = info;
  517. });
  518. }
  519. });
  520. });
  521. // console.log(cachePaperInfo);
  522. return cachePaperInfo;
  523. },
  524. assignCachePaperData(paperData, cacheData, mergeReverse = false) {
  525. return paperData.map((detail, dIndex) => {
  526. detail.questions = detail.questions.map((question, qIndex) => {
  527. let k = `${dIndex + 1}_${qIndex + 1}`;
  528. let nq = this.mergeObjData(
  529. question,
  530. cacheData[k] || {},
  531. mergeReverse
  532. );
  533. if (question.subQuestions && question.subQuestions.length) {
  534. nq.subQuestions = question.subQuestions.map((subq, subqIndex) => {
  535. let k = `${dIndex + 1}_${qIndex + 1}_${subqIndex + 1}`;
  536. return this.mergeObjData(subq, cacheData[k] || {}, mergeReverse);
  537. });
  538. }
  539. return nq;
  540. });
  541. return detail;
  542. });
  543. },
  544. isNull(val) {
  545. if (val) {
  546. if (val === "[]") return true;
  547. if (objTypeOf(val) === "array" && !val.length) return true;
  548. }
  549. return val === null || val === "" || val === undefined;
  550. },
  551. mergeObjData(targetObj, cacheObj, mergeReverse) {
  552. let data = { ...targetObj };
  553. Object.keys(cacheObj).forEach((k) => {
  554. if (mergeReverse) {
  555. data[k] = this.isNull(cacheObj[k]) ? targetObj[k] : cacheObj[k];
  556. } else {
  557. data[k] = this.isNull(targetObj[k]) ? cacheObj[k] : targetObj[k];
  558. }
  559. });
  560. return data;
  561. },
  562. getImportPaperData() {
  563. if (!this.$refs.QuestionImportPaperEdit) return [];
  564. let paperData = deepCopy(this.$refs.QuestionImportPaperEdit.getData());
  565. const transformFieldMap = { body: "quesBody", options: "quesOptions" };
  566. const fields = Object.keys(transformFieldMap);
  567. const course = {
  568. id: this.data.importData.courseId,
  569. name: this.data.importData.courseName,
  570. };
  571. const transformQuestion = (question) => {
  572. question.id = null;
  573. question.course = course;
  574. fields.forEach((field) => {
  575. question[transformFieldMap[field]] = question[field];
  576. delete question[field];
  577. });
  578. if (question.quesOptions && question.quesOptions.length) {
  579. question.quesOptions = question.quesOptions.map((option) => {
  580. option.optionBody = option.body;
  581. delete option.body;
  582. return option;
  583. });
  584. }
  585. return question;
  586. };
  587. const detailInfo = paperData.map((detail) => {
  588. const questionInfo = detail.questions.map((question) => {
  589. transformQuestion(question);
  590. if (question.subQuestions && question.subQuestions.length) {
  591. question.subQuestions = question.subQuestions.map((subq) =>
  592. transformQuestion(subq)
  593. );
  594. question.score = calcSum(
  595. question.subQuestions.map((q) => q.score || 0)
  596. );
  597. }
  598. return question;
  599. });
  600. return {
  601. name: detail.name,
  602. number: detail.number,
  603. questionCount: questionInfo.length,
  604. questionInfo,
  605. questionScore: detail.questionScore,
  606. totalScore: calcSum(questionInfo.map((q) => q.score || 0)),
  607. };
  608. });
  609. // console.log(detailInfo);
  610. return detailInfo;
  611. },
  612. checkImportPaperData(paperData) {
  613. this.$message.closeAll();
  614. // 题目内容校验
  615. const MATCHING_QUESTION = ["PARAGRAPH_MATCHING", "BANKED_CLOZE"];
  616. const SELECT_QUESTION = [
  617. "SINGLE_ANSWER_QUESTION",
  618. "MULTIPLE_ANSWER_QUESTION",
  619. ...MATCHING_QUESTION,
  620. ];
  621. const NESTED_QUESTION = [
  622. ...MATCHING_QUESTION,
  623. "READING_COMPREHENSION",
  624. "CLOZE",
  625. "LISTENING_QUESTION",
  626. ];
  627. const ALLOW_EMPTY_BODY_QUESTION = [
  628. "LISTENING_QUESTION",
  629. ...MATCHING_QUESTION,
  630. ];
  631. let errInfos = [];
  632. paperData.forEach((detail) => {
  633. detail.questionInfo.forEach((question) => {
  634. const { questionType, quesBody } = question;
  635. const questionTitle = `第${detail.number}大题第${question.number}小题`;
  636. let qErrInfo = [];
  637. // 题干
  638. if (
  639. !ALLOW_EMPTY_BODY_QUESTION.includes(questionType) &&
  640. (!quesBody || isAnEmptyRichText(quesBody))
  641. ) {
  642. qErrInfo.push(`没有题干`);
  643. }
  644. // 选项
  645. if (SELECT_QUESTION.includes(questionType)) {
  646. if (question.quesOptions.length < 2) {
  647. qErrInfo.push(`选项少于2个`);
  648. }
  649. if (
  650. question.quesOptions.some((option) =>
  651. isAnEmptyRichText(option.optionBody)
  652. )
  653. ) {
  654. qErrInfo.push(`有选择内容为空`);
  655. }
  656. }
  657. // 小题数
  658. if (
  659. NESTED_QUESTION.includes(questionType) &&
  660. !question.subQuestions.length
  661. ) {
  662. qErrInfo.push(`没有小题`);
  663. }
  664. if (qErrInfo.length) {
  665. errInfos.push(`${questionTitle}${qErrInfo.join("、")}`);
  666. qErrInfo = [];
  667. }
  668. // 完形填空
  669. if (questionType === "CLOZE") {
  670. const answerPointCount = getRichTextAnswerPointCount(quesBody);
  671. if (answerPointCount !== question.subQuestions.length) {
  672. errInfos.push(
  673. `第${detail.number}大题题干答题点数量与选项数不匹配`
  674. );
  675. }
  676. if (
  677. question.subQuestions.some(
  678. (q) => q.questionType !== "SINGLE_ANSWER_QUESTION"
  679. )
  680. ) {
  681. errInfos.push(`第${detail.number}大题存在不是单选题的子题`);
  682. }
  683. }
  684. // 听力题
  685. if (questionType === "LISTENING_QUESTION") {
  686. if (
  687. question.subQuestions.some(
  688. (q) => q.questionType !== "SINGLE_ANSWER_QUESTION"
  689. )
  690. ) {
  691. errInfos.push(`第${detail.number}大题存在不是单选题的子题`);
  692. }
  693. }
  694. // 选词填空、段落匹配,单用模式时校验输入答案是否重复
  695. if (
  696. MATCHING_QUESTION.includes(questionType) &&
  697. question.quesParam.matchingMode === 1
  698. ) {
  699. // 选词填空
  700. if (questionType === "BANKED_CLOZE") {
  701. const answerPointCount = getRichTextAnswerPointCount(quesBody);
  702. if (answerPointCount > question.quesOptions.length) {
  703. errInfos.push(`第${detail.number}大题题干答题点数量超过选项数`);
  704. }
  705. } else {
  706. if (question.subQuestions.length > question.quesOptions.length) {
  707. errInfos.push(`第${detail.number}大题小题数量超过选项数`);
  708. }
  709. }
  710. let selectedAnswer = [],
  711. errorQuestionIndexs = [];
  712. question.subQuestions.forEach((subq, sindex) => {
  713. if (selectedAnswer.includes(subq.quesAnswer)) {
  714. errorQuestionIndexs.push(`${question.number}-${sindex + 1}`);
  715. } else {
  716. if (subq.quesAnswer !== "[]")
  717. selectedAnswer.push(subq.quesAnswer);
  718. }
  719. });
  720. if (errorQuestionIndexs.length) {
  721. errInfos.push(
  722. `第${
  723. detail.number
  724. }大题${errorQuestionIndexs.join()}小题答案重复`
  725. );
  726. }
  727. }
  728. if (!NESTED_QUESTION.includes(questionType)) return;
  729. // 套题小题校验
  730. question.subQuestions.forEach((subq, sindex) => {
  731. const subqTitle = `第${detail.number}大题第${question.number}-${
  732. sindex + 1
  733. }小题`;
  734. if (
  735. questionType === "READING_COMPREHENSION" &&
  736. (!subq.quesBody || isAnEmptyRichText(subq.quesBody))
  737. ) {
  738. qErrInfo.push(`没有题干`);
  739. }
  740. if (
  741. SELECT_QUESTION.includes(subq.questionType) &&
  742. !MATCHING_QUESTION.includes(questionType)
  743. ) {
  744. if (subq.quesOptions.length < 2) {
  745. qErrInfo.push(`选项少于2个`);
  746. }
  747. if (
  748. subq.quesOptions.some((option) =>
  749. isAnEmptyRichText(option.optionBody)
  750. )
  751. ) {
  752. qErrInfo.push(`有选择内容为空`);
  753. }
  754. }
  755. if (qErrInfo.length) {
  756. errInfos.push(`${subqTitle}${qErrInfo.join("、")}`);
  757. qErrInfo = [];
  758. }
  759. });
  760. });
  761. });
  762. if (errInfos.length) {
  763. this.$message({
  764. showClose: true,
  765. message: errInfos.join("。"),
  766. type: "error",
  767. duration: 0,
  768. });
  769. return;
  770. }
  771. if (!this.data.importData.useOriginalPaper) return true;
  772. let detailNumbers = paperData.map((detail) => detail.number);
  773. // 大题号重复性校验
  774. let repeatDetaiNumbers = [];
  775. let detailNums = [];
  776. for (let i = 0; i < detailNumbers.length; i++) {
  777. const num = detailNumbers[i];
  778. if (detailNums.includes(num)) {
  779. if (!repeatDetaiNumbers.includes(num)) repeatDetaiNumbers.push(num);
  780. } else {
  781. detailNums.push(num);
  782. }
  783. }
  784. if (repeatDetaiNumbers.length) {
  785. this.$message({
  786. showClose: true,
  787. message: `大题号${repeatDetaiNumbers.join("、")}重复`,
  788. type: "error",
  789. duration: 0,
  790. });
  791. return;
  792. }
  793. // 大题号连续性校验
  794. for (let i = 0; i < detailNumbers.length; i++) {
  795. if (detailNumbers[i] - 1 !== i) {
  796. this.$message({
  797. showClose: true,
  798. message: "大题号不连续",
  799. type: "error",
  800. duration: 0,
  801. });
  802. return;
  803. }
  804. }
  805. // 答案、分数校验
  806. let totalScore = calcSum(paperData.map((d) => d.totalScore));
  807. let errQuestions = [];
  808. paperData.forEach((detail) => {
  809. detail.questionInfo.forEach((question) => {
  810. if (question.subQuestions && question.subQuestions.length) {
  811. let subIndexs = [];
  812. question.subQuestions.forEach((subq, sind) => {
  813. if (!subq.score)
  814. subIndexs.push(question.number + "-" + (sind + 1));
  815. });
  816. if (subIndexs.length)
  817. errQuestions.push(
  818. `第${detail.number}大题第${subIndexs.join()}小题`
  819. );
  820. } else {
  821. if (!question.score) {
  822. errQuestions.push(
  823. `第${detail.number}大题第${question.number}小题`
  824. );
  825. }
  826. }
  827. });
  828. });
  829. if (errQuestions.length) {
  830. this.$message({
  831. showClose: true,
  832. message: `请设置如下试题的分值:${errQuestions.join("、")}。`,
  833. type: "error",
  834. duration: 0,
  835. });
  836. return;
  837. }
  838. if (
  839. this.data.importData.checkTotalScore &&
  840. totalScore !== this.data.importData.totalScore
  841. ) {
  842. this.$message({
  843. showClose: true,
  844. message: `试卷总分与导入设置的总分不一致!`,
  845. type: "error",
  846. duration: 0,
  847. });
  848. return;
  849. }
  850. return true;
  851. },
  852. async confirm() {
  853. const confirm = await this.$confirm("确认加入题库吗?", "提示", {
  854. type: "warning",
  855. }).catch(() => {});
  856. if (confirm !== "confirm") return;
  857. const detailInfo = this.getImportPaperData();
  858. if (!this.checkImportPaperData(detailInfo)) return;
  859. if (this.loading) return;
  860. this.loading = true;
  861. const res = await questionImportPaperSave({
  862. ...this.data.importData,
  863. detailInfo,
  864. }).catch(() => {});
  865. this.loading = false;
  866. if (!res) return;
  867. if (res.data) {
  868. this.$confirm(
  869. "系统正在计算试题情况,请在后续检查试题查重与试题审核数据。",
  870. "系统通知",
  871. {
  872. type: "warning",
  873. }
  874. );
  875. }
  876. this.$message.success("提交成功!");
  877. this.$emit("modified");
  878. this.cancel();
  879. },
  880. // 导入答案属性
  881. toImportAnswer() {
  882. const detailInfo = this.getImportPaperData();
  883. this.uploadAnswerData = {
  884. detailInfo: JSON.stringify(detailInfo),
  885. ...this.data.importData,
  886. };
  887. this.$refs.ImportAnswerDialog.open();
  888. },
  889. async answerTemplateDownload() {
  890. const detailInfo = this.getImportPaperData();
  891. const res = await downloadByApi(() => {
  892. return questionImportDownloadTemplate({
  893. detailInfo,
  894. ...this.data.importData,
  895. });
  896. }).catch((e) => {
  897. this.$message.error(e || "下载失败,请重新尝试!");
  898. });
  899. if (!res) return;
  900. this.$message.success("下载成功!");
  901. },
  902. answerUploaded(res) {
  903. const cacheData = this.getCachePaperInfo(
  904. res.data.detailInfo,
  905. questionInfoField
  906. );
  907. this.paperData = this.assignCachePaperData(
  908. this.paperData,
  909. cacheData,
  910. true
  911. );
  912. this.questionKey = randomCode();
  913. },
  914. // word upload
  915. uploaded(res) {
  916. this.$message.success("上传成功!");
  917. this.resetData({
  918. richText: res.data.richText,
  919. detailInfo: res.data.detailInfo,
  920. });
  921. },
  922. validError(error) {
  923. console.log("err", error);
  924. this.$message.error(error.message);
  925. },
  926. // rich test editor
  927. richTextFocus(richTextGroup) {
  928. console.log("ggg", richTextGroup);
  929. this.$refs.QuestionImportPaperEdit.scrollToContentByIndex(
  930. richTextGroup.indexs
  931. );
  932. this.scrollType = "rich-text";
  933. setTimeout(() => {
  934. this.scrollType = "";
  935. }, 100);
  936. },
  937. },
  938. };
  939. </script>