QuestionImportEdit.vue 33 KB

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