QuestionImportEdit.vue 30 KB

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