QuestionImportEdit.vue 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374
  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">
  16. <div class="box-justify">
  17. <div>
  18. <h2>文件上传</h2>
  19. </div>
  20. <div style="display: flex; align-items: center">
  21. <el-button
  22. style="margin-left: 10px"
  23. size="mini"
  24. type="danger"
  25. @click="cancel"
  26. >返回</el-button
  27. >
  28. </div>
  29. </div>
  30. <div class="head-form">
  31. <el-form ref="modalFormComp" :model="modalForm" :rules="rules" inline>
  32. <el-form-item prop="courseId" label="课程名称:">
  33. <course-select
  34. v-model="modalForm.courseId"
  35. @change="courseChange"
  36. >
  37. </course-select>
  38. </el-form-item>
  39. <el-form-item>
  40. <el-checkbox v-model="modalForm.useOriginalPaper"
  41. >是否以导入试题同步生成试卷</el-checkbox
  42. >
  43. </el-form-item>
  44. <template v-if="modalForm.useOriginalPaper">
  45. <el-form-item>
  46. <el-input
  47. v-model="modalForm.name"
  48. placeholder="请输入试卷名称"
  49. ></el-input>
  50. </el-form-item>
  51. <el-form-item>
  52. <el-checkbox v-model="modalForm.checkTotalScore"
  53. >是否校验导入试卷总分</el-checkbox
  54. >
  55. </el-form-item>
  56. <el-form-item v-if="modalForm.checkTotalScore">
  57. <el-input-number
  58. v-model="modalForm.totalScore"
  59. style="width: 80px"
  60. :min="1"
  61. :max="1000"
  62. :step="1"
  63. step-strictly
  64. :controls="false"
  65. ></el-input-number>
  66. </el-form-item>
  67. </template>
  68. </el-form>
  69. </div>
  70. </div>
  71. <div class="qe-body">
  72. <div class="qe-part qe-part-edit">
  73. <div class="qe-part-main">
  74. <div class="qe-part-head">
  75. <div class="qe-part-head-title">
  76. <h3>题目编辑</h3>
  77. <div>
  78. <el-button
  79. size="small"
  80. type="primary"
  81. plain
  82. icon="icon icon-edit-warning"
  83. @click="showDocx"
  84. >录入说明</el-button
  85. >
  86. <el-button
  87. size="small"
  88. type="primary"
  89. plain
  90. icon="icon icon-export-prop"
  91. class="margin-right-10"
  92. @click="getWordTemplate"
  93. >模板下载</el-button
  94. >
  95. <upload-button
  96. btn-content="上传文件"
  97. btn-icon="icon icon-import"
  98. :disabled="loading || !modalForm.courseId"
  99. :upload-data="uploadData"
  100. :upload-url="uploadUrl"
  101. :format="importFileTypes"
  102. @valid-error="validError"
  103. @upload-success="uploaded"
  104. :auto-upload="false"
  105. ></upload-button>
  106. </div>
  107. </div>
  108. <div class="qe-part-head-desc">
  109. <i class="icon icon-tips"></i>
  110. 提示:若识别有误,可点击左侧题目按格式进行修改后重新识别
  111. </div>
  112. </div>
  113. <div class="qe-part-body">
  114. <div id="qe-part-richtext-list">
  115. <div
  116. v-for="(richJsonItem, rindex) in paperRichJsonGroup"
  117. :key="rindex"
  118. :class="[
  119. 'qe-part-richtext',
  120. { 'is-error': richJsonItem.exceptions.length },
  121. ]"
  122. >
  123. <v-editor
  124. ref="RichTextEditor"
  125. :value="richJsonItem"
  126. :show-menu="false"
  127. custom-emit-input
  128. :custom-render-action="renderRichText"
  129. :custom-tojson-action="richTextToJSON"
  130. @focus="() => richTextFocus(richJsonItem)"
  131. ></v-editor>
  132. <div
  133. v-if="richJsonItem.exceptions.length"
  134. class="qe-part-richtext-error"
  135. >
  136. <p
  137. class="tips-info tips-error"
  138. v-for="(exception, index) in richJsonItem.exceptions"
  139. :key="index"
  140. @click="highLightErrorText(exception)"
  141. :class="{ hide: inHasIgnore(exception.indexs) }"
  142. >
  143. {{ exception.message }}
  144. </p>
  145. </div>
  146. </div>
  147. </div>
  148. </div>
  149. </div>
  150. </div>
  151. <div class="qe-part qe-part-view">
  152. <div class="qe-part-main">
  153. <div class="qe-part-head">
  154. <div class="qe-part-head-title">
  155. <h3>题目阅览</h3>
  156. <div>
  157. <el-button
  158. size="small"
  159. type="primary"
  160. plain
  161. icon="icon icon-export-answer"
  162. @click="toImportAnswer"
  163. >导入答案属性</el-button
  164. >
  165. <el-button
  166. size="small"
  167. type="primary"
  168. icon="icon icon-save-white"
  169. :loading="loading"
  170. :disabled="!paperData.length || hasErrorTips"
  171. @click="confirm"
  172. >识别无误,加入题库</el-button
  173. >
  174. </div>
  175. </div>
  176. <div class="qe-part-head-desc">
  177. <p class="desc-qcout">
  178. <span>共识别 </span>
  179. <span class="color-success">
  180. {{ questionStatData.successCount }}
  181. </span>
  182. <span> 题,其中识别有误 </span>
  183. <span class="color-danger">
  184. {{ questionStatData.errorCount }}
  185. </span>
  186. <span> 题</span>
  187. </p>
  188. <el-checkbox
  189. v-model="onlyErrorQuestion"
  190. @change="onlyErrorQuestionChange"
  191. >仅查看识别有误试题</el-checkbox
  192. >
  193. </div>
  194. <div class="qe-part-head-menu">
  195. <el-tabs
  196. v-model="filterQuestionType"
  197. @tab-click="filterQuestionTypeChange"
  198. >
  199. <el-tab-pane
  200. :label="`全部(${
  201. onlyErrorQuestion
  202. ? questionStatData.errorCount
  203. : questionStatData.total
  204. })`"
  205. name="all"
  206. ></el-tab-pane>
  207. <el-tab-pane
  208. v-for="item in questionStatData.qtypes"
  209. :key="item.questionType"
  210. :label="`${item.questionTypeName}(${item.questionCount})`"
  211. :name="item.questionType"
  212. ></el-tab-pane>
  213. </el-tabs>
  214. </div>
  215. </div>
  216. <div id="qe-part-paper" class="qe-part-body">
  217. <question-import-paper-edit
  218. v-if="filterPaperData.length"
  219. ref="QuestionImportPaperEdit"
  220. :key="questionKey"
  221. :paper="filterPaperData"
  222. :course-id="data.importData.courseId"
  223. ></question-import-paper-edit>
  224. </div>
  225. </div>
  226. </div>
  227. <div class="qe-middle">
  228. <div class="qe-middle-arrow"></div>
  229. <el-button
  230. size="small"
  231. type="primary"
  232. :loading="loading"
  233. @click="toParse"
  234. >识别</el-button
  235. >
  236. </div>
  237. </div>
  238. </el-dialog>
  239. <!-- 录入说明 -->
  240. <el-dialog
  241. title="录入说明"
  242. :visible.sync="showIframeDialog"
  243. append-to-body
  244. width="900px"
  245. top="30px"
  246. >
  247. <div
  248. style="width: 100%; height: calc(100vh - 250px); overflow: auto"
  249. id="doc-box"
  250. ></div>
  251. <span slot="footer" class="dialog-footer">
  252. <el-button type="primary" @click="showIframeDialog = false"
  253. >关闭</el-button
  254. >
  255. </span>
  256. </el-dialog>
  257. <!-- 上传答案文件 -->
  258. <import-file-dialog
  259. ref="ImportAnswerDialog"
  260. dialog-title="导入答案"
  261. :template-download-handle="answerTemplateDownload"
  262. :upload-url="uploadAnswerUrl"
  263. :upload-data="uploadAnswerData"
  264. add-file-param="dataFile"
  265. @uploaded="answerUploaded"
  266. ></import-file-dialog>
  267. </div>
  268. </template>
  269. <script>
  270. // import paperRichTextJson from "../datas/paperRichText.json";
  271. // import paperParseData from "../datas/paperParseData.json";
  272. import { calcSum, deepCopy, objTypeOf, randomCode } from "@/plugins/utils";
  273. import QuestionImportPaperEdit from "./QuestionImportPaperEdit.vue";
  274. import UploadButton from "@/components/UploadButton.vue";
  275. import { getRichTextAnswerPointCount, isAnEmptyRichText } from "@/utils/utils";
  276. import {
  277. questionImportPaperSave,
  278. questionImportParseRichText,
  279. questionImportDownloadTemplate,
  280. propertyNameQueryApi,
  281. questionWordImportTemplate,
  282. } from "../api";
  283. import ImportFileDialog from "@/components/ImportFileDialog.vue";
  284. import { QUESTION_API, QUESTION_TYPES } from "@/constants/constants";
  285. import { downloadByApi } from "@/plugins/download";
  286. import { richTextToJSON, renderRichText } from "./import-edit/richText";
  287. import scrollMixins from "./import-edit/scrollMixins";
  288. import timeMixin from "@/mixins/timeMixin";
  289. import { renderAsync } from "docx-preview";
  290. const questionInfoField = [
  291. "courseId",
  292. "difficulty",
  293. "quesProperties",
  294. "score",
  295. "publicity",
  296. "control",
  297. "answerAnalysis",
  298. "quesAnswer",
  299. ];
  300. export default {
  301. name: "QuestionImportEdit",
  302. components: { QuestionImportPaperEdit, ImportFileDialog, UploadButton },
  303. mixins: [scrollMixins, timeMixin],
  304. props: {
  305. data: {
  306. type: Object,
  307. default() {
  308. return {
  309. richText: { sections: [] },
  310. detailInfo: [],
  311. importData: {
  312. courseId: "",
  313. courseName: "",
  314. name: "",
  315. checkTotalScore: false,
  316. useOriginalPaper: false,
  317. totalScore: 0,
  318. },
  319. };
  320. },
  321. },
  322. },
  323. computed: {
  324. hasErrorTips() {
  325. // return (this.paperRichJsonGroup || []).some((item) => {
  326. // return item?.exceptions?.length;
  327. // });
  328. let detailInfo = this.paperData;
  329. if (!Array.isArray(detailInfo)) {
  330. return false;
  331. }
  332. return detailInfo.some((detail) => {
  333. return detail?.questions?.some((item) => {
  334. let questionExceptions = item.questionExceptions;
  335. let arr = item.ignoreOptionRepeat
  336. ? questionExceptions.filter(
  337. (v) => !v.cause.includes("选项内容相同")
  338. )
  339. : questionExceptions;
  340. if (!item.subQuestions?.length) {
  341. return !!arr.length;
  342. } else {
  343. return item.subQuestions.some((sub) => {
  344. let { questionExceptions = [] } = sub;
  345. let ar = sub.ignoreOptionRepeat
  346. ? questionExceptions.filter(
  347. (v) => !v.cause.includes("选项内容相同")
  348. )
  349. : questionExceptions;
  350. return !!ar.length;
  351. });
  352. }
  353. });
  354. });
  355. },
  356. },
  357. data() {
  358. return {
  359. ignoreRepeatExceptionIndexArr: [],
  360. modalIsShow: false,
  361. loading: false,
  362. questionKey: "",
  363. filterPaperData: [],
  364. paperData: [],
  365. paperRichJson: { sections: [] },
  366. paperRichJsonGroup: [],
  367. richTextToJSON,
  368. renderRichText,
  369. lastPaperScrollTop: 0,
  370. lastRichTextScrollTop: 0,
  371. richTextIndexList: [],
  372. scrollType: "",
  373. curException: null,
  374. // upload answer
  375. uploadAnswerUrl: `${QUESTION_API}/word/parse/import`,
  376. uploadAnswerData: {},
  377. // word upload
  378. importFileTypes: ["docx", "doc"],
  379. uploadData: {},
  380. uploadUrl: `${QUESTION_API}/word/parse/struct`,
  381. showIframeDialog: false,
  382. // question types
  383. onlyErrorQuestion: false,
  384. questionStatData: {
  385. total: 0,
  386. successCount: 0,
  387. errorCount: 0,
  388. qtypes: [],
  389. },
  390. filterQuestionType: "all",
  391. modalForm: this.getInitForm(),
  392. rules: {
  393. courseId: [
  394. {
  395. required: true,
  396. message: "请选择课程",
  397. trigger: "change",
  398. },
  399. ],
  400. name: [
  401. {
  402. required: true,
  403. message: "请输入试卷名称",
  404. trigger: "change",
  405. },
  406. ],
  407. totalScore: [
  408. {
  409. required: true,
  410. message: "请输入试卷总分",
  411. trigger: "change",
  412. },
  413. ],
  414. },
  415. };
  416. },
  417. created() {
  418. this.$bus.on("markIgnoreRepeatQuestion", this.markIgnoreRepeat);
  419. },
  420. watch: {
  421. paperData: {
  422. handler() {
  423. this.parseQuestionStatData();
  424. this.filterQuestionTypeChange({ name: this.filterQuestionType });
  425. this.questionKey = randomCode();
  426. },
  427. },
  428. },
  429. methods: {
  430. getInitForm() {
  431. return {
  432. courseId: null,
  433. courseName: null,
  434. name: "",
  435. checkTotalScore: false,
  436. useOriginalPaper: false,
  437. totalScore: 0,
  438. toOtherCourse: false,
  439. };
  440. },
  441. courseChange(val) {
  442. this.modalForm.courseName = val ? val.name : "";
  443. this.getCourseProperty();
  444. this.uploadData = { courseId: this.modalForm.courseId };
  445. },
  446. parseQuestionStatData() {
  447. const total = calcSum(
  448. this.paperData.map((item) => item.questions.length)
  449. );
  450. const successCount = calcSum(
  451. this.paperData.map(
  452. (item) =>
  453. item.questions.filter((v) => !v.questionExceptions.length).length
  454. )
  455. );
  456. const errorCount = calcSum(
  457. this.paperData.map(
  458. (item) =>
  459. item.questions.filter((v) => v.questionExceptions.length).length
  460. )
  461. );
  462. const questionTypeStat = {};
  463. this.paperData.forEach((detail) => {
  464. let questions = detail.questions;
  465. if (this.onlyErrorQuestion) {
  466. questions = questions.filter((v) => v.questionExceptions.length);
  467. }
  468. questions.forEach((question) => {
  469. const { questionType } = question;
  470. if (questionTypeStat[questionType]) {
  471. questionTypeStat[questionType]++;
  472. } else {
  473. questionTypeStat[questionType] = 1;
  474. }
  475. });
  476. });
  477. const qtMap = {};
  478. QUESTION_TYPES.forEach((item) => {
  479. qtMap[item.code] = item.name;
  480. });
  481. const qtypes = Object.keys(questionTypeStat).map((key) => ({
  482. questionType: key,
  483. questionTypeName: qtMap[key],
  484. questionCount: questionTypeStat[key],
  485. }));
  486. this.questionStatData = {
  487. total,
  488. successCount,
  489. errorCount,
  490. qtypes,
  491. };
  492. },
  493. onlyErrorQuestionChange() {
  494. this.parseQuestionStatData();
  495. this.filterQuestionTypeChange({ name: this.filterQuestionType });
  496. this.questionKey = randomCode();
  497. },
  498. inHasIgnore(indexes) {
  499. return this.ignoreRepeatExceptionIndexArr.includes(indexes.toString());
  500. },
  501. markIgnoreRepeat(exceptionIndex, bool) {
  502. this.paperData.forEach((detail) => {
  503. detail.questions.forEach((question) => {
  504. if (
  505. question.questionExceptions?.length &&
  506. question.questionExceptions.find(
  507. (v) => v.exceptionIndex.toString() == exceptionIndex
  508. )
  509. ) {
  510. question.ignoreOptionRepeat = bool;
  511. }
  512. if (question.subQuestions?.length) {
  513. question.subQuestions.forEach((sub) => {
  514. if (
  515. sub.questionExceptions?.length &&
  516. sub.questionExceptions.find(
  517. (v) => v.exceptionIndex.toString() == exceptionIndex
  518. )
  519. ) {
  520. sub.ignoreOptionRepeat = bool;
  521. }
  522. });
  523. }
  524. });
  525. });
  526. this.paperData = JSON.parse(JSON.stringify(this.paperData));
  527. this.paperRichJsonGroup = this.getRichTextGroup();
  528. this.ignoreRepeatExceptionIndexArr.push(exceptionIndex);
  529. },
  530. urlToBlob(url, callback) {
  531. let xhr = new XMLHttpRequest();
  532. xhr.open("GET", url, true);
  533. xhr.responseType = "blob";
  534. xhr.onload = function () {
  535. if (xhr.status == 200) {
  536. callback(xhr.response);
  537. }
  538. };
  539. xhr.send();
  540. },
  541. showDocx() {
  542. this.showIframeDialog = true;
  543. this.$nextTick(() => {
  544. this.urlToBlob("/admin/inputDesc.docx", (data) => {
  545. let box = document.getElementById("doc-box");
  546. renderAsync(data, box);
  547. });
  548. });
  549. },
  550. async getWordTemplate() {
  551. const res = await downloadByApi(() => {
  552. return questionWordImportTemplate();
  553. }).catch((e) => {
  554. this.$message.error(e || "下载失败,请重新尝试!");
  555. });
  556. if (!res) return;
  557. this.$message.success("下载成功!");
  558. },
  559. async visibleChange() {
  560. this.ignoreRepeatExceptionIndexArr = [];
  561. this.getCourseProperty();
  562. if (this.data) this.resetData(this.data);
  563. this.$nextTick(() => {
  564. this.registScrollEvent();
  565. });
  566. },
  567. resetData({ richText, detailInfo }) {
  568. this.paperData = deepCopy(detailInfo);
  569. this.paperRichJson = this.buildRichText(deepCopy(richText));
  570. this.transformDataInfo();
  571. this.paperRichJsonGroup = this.getRichTextGroup();
  572. this.uploadData = { courseId: this.modalForm.courseId };
  573. this.questionKey = randomCode();
  574. this.$nextTick(() => {
  575. this.getRichTextIndexList();
  576. });
  577. },
  578. filterQuestionTypeChange(val) {
  579. this.filterQuestionType = val.name;
  580. const onlyErrorValidater = (question) => {
  581. if (this.onlyErrorQuestion) {
  582. return Boolean(question.questionExceptions.length);
  583. }
  584. return true;
  585. };
  586. if (this.filterQuestionType === "all") {
  587. this.filterPaperData = !this.onlyErrorQuestion
  588. ? this.paperData
  589. : this.paperData
  590. .map((detail) => {
  591. return {
  592. ...detail,
  593. questions: detail.questions.filter((question) =>
  594. onlyErrorValidater(question)
  595. ),
  596. };
  597. })
  598. .filter((detail) => detail.questions.length);
  599. return;
  600. }
  601. this.filterPaperData = this.paperData
  602. .map((detail) => {
  603. return {
  604. ...detail,
  605. questions: detail.questions.filter(
  606. (question) =>
  607. question.questionType === this.filterQuestionType &&
  608. onlyErrorValidater(question)
  609. ),
  610. };
  611. })
  612. .filter((detail) => detail.questions.length);
  613. },
  614. getRichTextIndexList() {
  615. const richTextListDom = document.getElementById("qe-part-richtext-list");
  616. const elPos = richTextListDom.getBoundingClientRect();
  617. let richTextIndexList = [];
  618. const richTextBodyDoms =
  619. richTextListDom.querySelectorAll(".v-editor-body");
  620. richTextBodyDoms.forEach((richTextBodyDom) => {
  621. richTextBodyDom.childNodes.forEach((sectionNode) => {
  622. const id = sectionNode.getAttribute("id");
  623. if (!id) return;
  624. if (
  625. sectionNode.className &&
  626. sectionNode.className.includes("section-error")
  627. )
  628. return;
  629. const index = id.replace("section-", "") * 1;
  630. const sectionPos = sectionNode.getBoundingClientRect();
  631. richTextIndexList.push([index, sectionPos.y - elPos.y]);
  632. });
  633. });
  634. this.richTextIndexList = richTextIndexList;
  635. },
  636. async getCourseProperty() {
  637. if (!this.modalForm.courseId) return;
  638. const res = await propertyNameQueryApi(this.modalForm.courseId, "");
  639. const optionList = res.data || [];
  640. window.sessionStorage.setItem(
  641. "coursePropertys",
  642. JSON.stringify({ optionList, courseId: this.modalForm.courseId })
  643. );
  644. },
  645. buildRichText(richText) {
  646. let nsections = [];
  647. richText.sections.forEach((section) => {
  648. nsections.push({
  649. ...section,
  650. attributes: { id: `section-${section.remark.index}` },
  651. });
  652. });
  653. return { sections: nsections };
  654. },
  655. transformDataInfo() {
  656. this.transformRichImg(this.paperRichJson);
  657. this.paperData.forEach((detail) => {
  658. detail.questions.forEach((question) => {
  659. this.transformQuestion(question);
  660. if (question.subQuestions && question.subQuestions.length) {
  661. question.subQuestions.forEach((subq) => {
  662. this.transformQuestion(subq);
  663. });
  664. }
  665. });
  666. });
  667. },
  668. transformQuestion(question) {
  669. this.transformRichImg(question.body);
  670. this.transformRichImg(question.answerRichTexts);
  671. if (question.options && question.options.length) {
  672. question.options.forEach((item) => {
  673. this.transformRichImg(item.body);
  674. });
  675. }
  676. question.quesAnswer = this.transformQuestionAnser(question.quesAnswer);
  677. },
  678. transformRichImg(richText) {
  679. if (isAnEmptyRichText(richText)) return;
  680. const rate = 96 / 600;
  681. richText.sections.forEach((section) => {
  682. section.blocks.forEach((block) => {
  683. if (block.type !== "image" || !block.param) return;
  684. block.param.width = block.param.width * rate;
  685. block.param.height = block.param.height * rate;
  686. });
  687. });
  688. },
  689. transformQuestionAnser(quesAnswer) {
  690. let qAnswer = null;
  691. try {
  692. qAnswer = quesAnswer ? JSON.parse(quesAnswer) : null;
  693. } catch (error) {
  694. console.log(error);
  695. }
  696. if (!qAnswer || objTypeOf(qAnswer) !== "array") return quesAnswer;
  697. qAnswer.forEach((item) => {
  698. this.transformRichImg(item);
  699. });
  700. return JSON.stringify(qAnswer);
  701. },
  702. getRichTextGroup() {
  703. let groupSetList = [];
  704. this.paperData.forEach((detail) => {
  705. groupSetList.push({
  706. id: randomCode(),
  707. indexs: detail.detailIndex,
  708. });
  709. detail.questions.forEach((question) => {
  710. groupSetList.push({
  711. id: randomCode(),
  712. indexs: question.integralIndex,
  713. });
  714. if (question.subQuestions && question.subQuestions.length) {
  715. question.subQuestions.forEach((subq) => {
  716. groupSetList.push({
  717. id: randomCode(),
  718. indexs: subq.integralIndex,
  719. });
  720. });
  721. }
  722. });
  723. });
  724. let groups = [];
  725. let curGroupId = 0;
  726. let curGroup = [];
  727. const findGroupId = (ind) => {
  728. let data = groupSetList.find((item) => item.indexs.includes(ind));
  729. return data ? data.id : null;
  730. };
  731. this.paperRichJson.sections.forEach((section) => {
  732. const sectionGroupId = findGroupId(section.remark.index);
  733. if (sectionGroupId !== curGroupId) {
  734. if (curGroup.length) {
  735. groups.push({ sections: curGroup, exceptions: [] });
  736. curGroup = [];
  737. }
  738. }
  739. curGroupId = sectionGroupId;
  740. curGroup.push(section);
  741. });
  742. if (curGroup.length) {
  743. groups.push({ sections: curGroup, exceptions: [] });
  744. curGroup = [];
  745. }
  746. const getExceptions = (sections) => {
  747. let cause = {};
  748. sections.forEach((section) => {
  749. if (section.remark.status) return;
  750. if (!cause[section.remark.cause]) {
  751. cause[section.remark.cause] = [];
  752. }
  753. cause[section.remark.cause].push(section.remark.index);
  754. });
  755. const causeList = Object.keys(cause).map((key) => {
  756. return {
  757. message: key,
  758. indexs: cause[key],
  759. };
  760. });
  761. causeList.sort((a, b) => a.indexs[0] - b.indexs[0]);
  762. return causeList;
  763. };
  764. groups.forEach((group) => {
  765. group.indexs = group.sections.map((item) => item.remark.index);
  766. group.exceptions = getExceptions(group.sections);
  767. });
  768. return groups;
  769. },
  770. highLightErrorText(exception) {
  771. if (this.curException) {
  772. this.curException.indexs.forEach((ind) => {
  773. const sectionDoms = document.querySelectorAll(`#section-${ind}`);
  774. if (!sectionDoms.length) return;
  775. sectionDoms.forEach((sectionDom) => {
  776. sectionDom.style = null;
  777. });
  778. });
  779. }
  780. this.clearSetTs();
  781. this.curException = exception;
  782. let firstSectionDom = null;
  783. exception.indexs.forEach((ind) => {
  784. const sectionDom = document.getElementById(`section-${ind}`);
  785. if (!sectionDom) return;
  786. if (!firstSectionDom) firstSectionDom = sectionDom;
  787. sectionDom.style.background = "#fef0f0";
  788. });
  789. if (!firstSectionDom) return;
  790. const richTextListDom = document.getElementById("qe-part-richtext-list");
  791. const elPos = richTextListDom.getBoundingClientRect();
  792. const sectionOffsetTop =
  793. firstSectionDom.getBoundingClientRect().y - elPos.y - 100;
  794. if (sectionOffsetTop < richTextListDom.parentNode.scrollTop) {
  795. richTextListDom.parentNode.scrollTop = sectionOffsetTop;
  796. this.scrollType = "rich-text";
  797. setTimeout(() => {
  798. this.scrollType = "";
  799. }, 100);
  800. }
  801. this.addSetTime(() => {
  802. this.curException.indexs.forEach((ind) => {
  803. const sectionDoms = document.querySelectorAll(`#section-${ind}`);
  804. if (!sectionDoms.length) return;
  805. sectionDoms.forEach((sectionDom) => {
  806. sectionDom.style = null;
  807. });
  808. });
  809. this.curException = null;
  810. }, 5000);
  811. },
  812. initData() {
  813. this.filterPaperData = [];
  814. this.filterQuestionType = "all";
  815. this.paperData = [];
  816. this.paperRichJson = { sections: [] };
  817. window.sessionStorage.removeItem("coursePropertys");
  818. this.$message.closeAll();
  819. this.removeScrollEvent();
  820. },
  821. cancel() {
  822. this.modalIsShow = false;
  823. },
  824. open() {
  825. this.modalIsShow = true;
  826. },
  827. getRichTextJsons() {
  828. let sections = [];
  829. this.$refs.RichTextEditor.forEach((item) => {
  830. const itemRichJson = item.emitJsonAction();
  831. sections.push(...itemRichJson.sections);
  832. });
  833. return { sections };
  834. },
  835. async toParse() {
  836. if (isAnEmptyRichText(this.paperRichJson)) {
  837. this.$message.error("请输入试卷内容!");
  838. return;
  839. }
  840. if (this.loading) return;
  841. this.loading = true;
  842. let richText = this.getRichTextJsons();
  843. const res = await questionImportParseRichText({
  844. richText,
  845. courseId: this.modalForm.courseId,
  846. }).catch(() => {});
  847. this.loading = false;
  848. if (!res) return;
  849. const cacheData = this.getCachePaperInfo(
  850. this.getImportPaperData(),
  851. questionInfoField
  852. );
  853. // console.log(cacheData);
  854. this.paperData = this.assignCachePaperData(
  855. res.data.detailInfo,
  856. cacheData
  857. );
  858. this.paperRichJson = this.buildRichText(deepCopy(res.data.richText));
  859. this.paperRichJsonGroup = this.getRichTextGroup();
  860. this.questionKey = randomCode();
  861. this.$nextTick(() => {
  862. this.getRichTextIndexList();
  863. });
  864. },
  865. getCachePaperInfo(paperData, cacheFields = []) {
  866. let cachePaperInfo = {};
  867. paperData.forEach((detail, dIndex) => {
  868. detail.questionInfo.forEach((question, qIndex) => {
  869. let info = {};
  870. let k = `${dIndex + 1}_${qIndex + 1}`;
  871. if (cacheFields.length) {
  872. cacheFields.forEach((field) => {
  873. info[field] = question[field];
  874. });
  875. } else {
  876. info = { ...question };
  877. }
  878. cachePaperInfo[k] = info;
  879. if (question.subQuestions && question.subQuestions.length) {
  880. question.subQuestions.forEach((subq, subqIndex) => {
  881. let info = {};
  882. let k = `${dIndex + 1}_${qIndex + 1}_${subqIndex + 1}`;
  883. if (cacheFields.length) {
  884. cacheFields.forEach((field) => {
  885. info[field] = subq[field];
  886. });
  887. } else {
  888. info = { ...subq };
  889. }
  890. cachePaperInfo[k] = info;
  891. });
  892. }
  893. });
  894. });
  895. // console.log(cachePaperInfo);
  896. return cachePaperInfo;
  897. },
  898. assignCachePaperData(paperData, cacheData, mergeReverse = false) {
  899. return paperData.map((detail, dIndex) => {
  900. detail.questions = detail.questions.map((question, qIndex) => {
  901. let k = `${dIndex + 1}_${qIndex + 1}`;
  902. let nq = this.mergeObjData(
  903. question,
  904. cacheData[k] || {},
  905. mergeReverse
  906. );
  907. if (question.subQuestions && question.subQuestions.length) {
  908. nq.subQuestions = question.subQuestions.map((subq, subqIndex) => {
  909. let k = `${dIndex + 1}_${qIndex + 1}_${subqIndex + 1}`;
  910. return this.mergeObjData(subq, cacheData[k] || {}, mergeReverse);
  911. });
  912. }
  913. return nq;
  914. });
  915. return detail;
  916. });
  917. },
  918. isNull(val) {
  919. if (val) {
  920. if (val === "[]") return true;
  921. if (objTypeOf(val) === "array" && !val.length) return true;
  922. }
  923. return val === null || val === "" || val === undefined;
  924. },
  925. mergeObjData(targetObj, cacheObj, mergeReverse) {
  926. let data = { ...targetObj };
  927. Object.keys(cacheObj).forEach((k) => {
  928. if (mergeReverse) {
  929. data[k] = this.isNull(cacheObj[k]) ? targetObj[k] : cacheObj[k];
  930. } else {
  931. data[k] = this.isNull(targetObj[k]) ? cacheObj[k] : targetObj[k];
  932. }
  933. });
  934. return data;
  935. },
  936. getImportPaperData() {
  937. if (!this.$refs.QuestionImportPaperEdit) return [];
  938. let paperData = deepCopy(this.$refs.QuestionImportPaperEdit.getData());
  939. const transformFieldMap = { body: "quesBody", options: "quesOptions" };
  940. const fields = Object.keys(transformFieldMap);
  941. const course = {
  942. id: this.modalForm.courseId,
  943. name: this.modalForm.courseName,
  944. };
  945. const transformQuestion = (question) => {
  946. question.id = null;
  947. question.course = course;
  948. fields.forEach((field) => {
  949. question[transformFieldMap[field]] = question[field];
  950. delete question[field];
  951. });
  952. if (question.quesOptions && question.quesOptions.length) {
  953. question.quesOptions = question.quesOptions.map((option) => {
  954. option.optionBody = option.body;
  955. delete option.body;
  956. return option;
  957. });
  958. }
  959. return question;
  960. };
  961. const detailInfo = paperData.map((detail) => {
  962. const questionInfo = detail.questions.map((question) => {
  963. transformQuestion(question);
  964. if (question.subQuestions && question.subQuestions.length) {
  965. question.subQuestions = question.subQuestions.map((subq) =>
  966. transformQuestion(subq)
  967. );
  968. question.score = calcSum(
  969. question.subQuestions.map((q) => q.score || 0)
  970. );
  971. }
  972. return question;
  973. });
  974. return {
  975. name: detail.name,
  976. number: detail.number,
  977. questionCount: questionInfo.length,
  978. questionInfo,
  979. questionScore: detail.questionScore,
  980. totalScore: calcSum(questionInfo.map((q) => q.score || 0)),
  981. };
  982. });
  983. // console.log(detailInfo);
  984. return detailInfo;
  985. },
  986. checkImportPaperData(paperData) {
  987. this.$message.closeAll();
  988. // 题目内容校验
  989. const MATCHING_QUESTION = ["PARAGRAPH_MATCHING", "BANKED_CLOZE"];
  990. const SELECT_QUESTION = [
  991. "SINGLE_ANSWER_QUESTION",
  992. "MULTIPLE_ANSWER_QUESTION",
  993. ...MATCHING_QUESTION,
  994. ];
  995. const NESTED_QUESTION = [
  996. ...MATCHING_QUESTION,
  997. "READING_COMPREHENSION",
  998. "CLOZE",
  999. "LISTENING_QUESTION",
  1000. ];
  1001. const ALLOW_EMPTY_BODY_QUESTION = [
  1002. "LISTENING_QUESTION",
  1003. ...MATCHING_QUESTION,
  1004. ];
  1005. let errInfos = [];
  1006. paperData.forEach((detail) => {
  1007. detail.questionInfo.forEach((question) => {
  1008. const { questionType, quesBody } = question;
  1009. const questionTitle = `第${detail.number}大题第${question.number}小题`;
  1010. let qErrInfo = [];
  1011. // 题干
  1012. if (
  1013. !ALLOW_EMPTY_BODY_QUESTION.includes(questionType) &&
  1014. (!quesBody || isAnEmptyRichText(quesBody))
  1015. ) {
  1016. qErrInfo.push(`没有题干`);
  1017. }
  1018. // 选项
  1019. if (SELECT_QUESTION.includes(questionType)) {
  1020. if (question.quesOptions.length < 2) {
  1021. qErrInfo.push(`选项少于2个`);
  1022. }
  1023. if (
  1024. question.quesOptions.some((option) =>
  1025. isAnEmptyRichText(option.optionBody)
  1026. )
  1027. ) {
  1028. qErrInfo.push(`有选择内容为空`);
  1029. }
  1030. }
  1031. // 小题数
  1032. if (
  1033. NESTED_QUESTION.includes(questionType) &&
  1034. !question.subQuestions.length
  1035. ) {
  1036. qErrInfo.push(`没有小题`);
  1037. }
  1038. if (qErrInfo.length) {
  1039. errInfos.push(`${questionTitle}${qErrInfo.join("、")}`);
  1040. qErrInfo = [];
  1041. }
  1042. // 完形填空
  1043. if (questionType === "CLOZE") {
  1044. const answerPointCount = getRichTextAnswerPointCount(quesBody);
  1045. if (answerPointCount !== question.subQuestions.length) {
  1046. errInfos.push(
  1047. `第${detail.number}大题题干答题点数量与选项数不匹配`
  1048. );
  1049. }
  1050. if (
  1051. question.subQuestions.some(
  1052. (q) => q.questionType !== "SINGLE_ANSWER_QUESTION"
  1053. )
  1054. ) {
  1055. errInfos.push(`第${detail.number}大题存在不是单选题的子题`);
  1056. }
  1057. }
  1058. // 听力题
  1059. if (questionType === "LISTENING_QUESTION") {
  1060. if (
  1061. question.subQuestions.some(
  1062. (q) => q.questionType !== "SINGLE_ANSWER_QUESTION"
  1063. )
  1064. ) {
  1065. errInfos.push(`第${detail.number}大题存在不是单选题的子题`);
  1066. }
  1067. }
  1068. // 选词填空、段落匹配,单用模式时校验输入答案是否重复
  1069. if (
  1070. MATCHING_QUESTION.includes(questionType) &&
  1071. question.quesParam.matchingMode === 1
  1072. ) {
  1073. // 选词填空
  1074. if (questionType === "BANKED_CLOZE") {
  1075. const answerPointCount = getRichTextAnswerPointCount(quesBody);
  1076. if (answerPointCount > question.quesOptions.length) {
  1077. errInfos.push(`第${detail.number}大题题干答题点数量超过选项数`);
  1078. }
  1079. } else {
  1080. if (question.subQuestions.length > question.quesOptions.length) {
  1081. errInfos.push(`第${detail.number}大题小题数量超过选项数`);
  1082. }
  1083. }
  1084. let selectedAnswer = [],
  1085. errorQuestionIndexs = [];
  1086. question.subQuestions.forEach((subq, sindex) => {
  1087. if (selectedAnswer.includes(subq.quesAnswer)) {
  1088. errorQuestionIndexs.push(`${question.number}-${sindex + 1}`);
  1089. } else {
  1090. if (subq.quesAnswer !== "[]")
  1091. selectedAnswer.push(subq.quesAnswer);
  1092. }
  1093. });
  1094. if (errorQuestionIndexs.length) {
  1095. errInfos.push(
  1096. `第${
  1097. detail.number
  1098. }大题${errorQuestionIndexs.join()}小题答案重复`
  1099. );
  1100. }
  1101. }
  1102. if (!NESTED_QUESTION.includes(questionType)) return;
  1103. // 套题小题校验
  1104. question.subQuestions.forEach((subq, sindex) => {
  1105. const subqTitle = `第${detail.number}大题第${question.number}-${
  1106. sindex + 1
  1107. }小题`;
  1108. if (
  1109. questionType === "READING_COMPREHENSION" &&
  1110. (!subq.quesBody || isAnEmptyRichText(subq.quesBody))
  1111. ) {
  1112. qErrInfo.push(`没有题干`);
  1113. }
  1114. if (
  1115. SELECT_QUESTION.includes(subq.questionType) &&
  1116. !MATCHING_QUESTION.includes(questionType)
  1117. ) {
  1118. if (subq.quesOptions.length < 2) {
  1119. qErrInfo.push(`选项少于2个`);
  1120. }
  1121. if (
  1122. subq.quesOptions.some((option) =>
  1123. isAnEmptyRichText(option.optionBody)
  1124. )
  1125. ) {
  1126. qErrInfo.push(`有选择内容为空`);
  1127. }
  1128. }
  1129. if (qErrInfo.length) {
  1130. errInfos.push(`${subqTitle}${qErrInfo.join("、")}`);
  1131. qErrInfo = [];
  1132. }
  1133. });
  1134. });
  1135. });
  1136. if (errInfos.length) {
  1137. this.$message({
  1138. showClose: true,
  1139. message: errInfos.join("。"),
  1140. type: "error",
  1141. duration: 0,
  1142. });
  1143. return;
  1144. }
  1145. if (!this.modalForm.useOriginalPaper) return true;
  1146. let detailNumbers = paperData.map((detail) => detail.number);
  1147. // 大题号重复性校验
  1148. let repeatDetaiNumbers = [];
  1149. let detailNums = [];
  1150. for (let i = 0; i < detailNumbers.length; i++) {
  1151. const num = detailNumbers[i];
  1152. if (detailNums.includes(num)) {
  1153. if (!repeatDetaiNumbers.includes(num)) repeatDetaiNumbers.push(num);
  1154. } else {
  1155. detailNums.push(num);
  1156. }
  1157. }
  1158. if (repeatDetaiNumbers.length) {
  1159. this.$message({
  1160. showClose: true,
  1161. message: `大题号${repeatDetaiNumbers.join("、")}重复`,
  1162. type: "error",
  1163. duration: 0,
  1164. });
  1165. return;
  1166. }
  1167. // 大题号连续性校验
  1168. for (let i = 0; i < detailNumbers.length; i++) {
  1169. if (detailNumbers[i] - 1 !== i) {
  1170. this.$message({
  1171. showClose: true,
  1172. message: "大题号不连续",
  1173. type: "error",
  1174. duration: 0,
  1175. });
  1176. return;
  1177. }
  1178. }
  1179. // 答案、分数校验
  1180. let totalScore = calcSum(paperData.map((d) => d.totalScore));
  1181. let errQuestions = [];
  1182. paperData.forEach((detail) => {
  1183. detail.questionInfo.forEach((question) => {
  1184. if (question.subQuestions && question.subQuestions.length) {
  1185. let subIndexs = [];
  1186. question.subQuestions.forEach((subq, sind) => {
  1187. if (!subq.score)
  1188. subIndexs.push(question.number + "-" + (sind + 1));
  1189. });
  1190. if (subIndexs.length)
  1191. errQuestions.push(
  1192. `第${detail.number}大题第${subIndexs.join()}小题`
  1193. );
  1194. } else {
  1195. if (!question.score) {
  1196. errQuestions.push(
  1197. `第${detail.number}大题第${question.number}小题`
  1198. );
  1199. }
  1200. }
  1201. });
  1202. });
  1203. if (errQuestions.length) {
  1204. this.$message({
  1205. showClose: true,
  1206. message: `请设置如下试题的分值:${errQuestions.join("、")}。`,
  1207. type: "error",
  1208. duration: 0,
  1209. });
  1210. return;
  1211. }
  1212. if (
  1213. this.modalForm.checkTotalScore &&
  1214. totalScore !== this.modalForm.totalScore
  1215. ) {
  1216. this.$message({
  1217. showClose: true,
  1218. message: `试卷总分与导入设置的总分不一致!`,
  1219. type: "error",
  1220. duration: 0,
  1221. });
  1222. return;
  1223. }
  1224. return true;
  1225. },
  1226. async confirm() {
  1227. const valid = await this.$refs.modalFormComp.validate().catch(() => {});
  1228. if (!valid) return;
  1229. const confirm = await this.$confirm("确认加入题库吗?", "提示", {
  1230. type: "warning",
  1231. }).catch(() => {});
  1232. if (confirm !== "confirm") return;
  1233. const detailInfo = this.getImportPaperData();
  1234. if (!this.checkImportPaperData(detailInfo)) return;
  1235. if (this.loading) return;
  1236. this.loading = true;
  1237. console.log("detailInfo", detailInfo);
  1238. const res = await questionImportPaperSave({
  1239. ...this.modalForm,
  1240. detailInfo,
  1241. }).catch(() => {});
  1242. this.loading = false;
  1243. if (!res) return;
  1244. if (res.data) {
  1245. this.$confirm(
  1246. "系统正在计算试题情况,请在后续检查试题查重与试题审核数据。",
  1247. "系统通知",
  1248. {
  1249. type: "warning",
  1250. }
  1251. );
  1252. }
  1253. this.$message.success("提交成功!");
  1254. this.$emit("modified");
  1255. this.cancel();
  1256. },
  1257. // 导入答案属性
  1258. toImportAnswer() {
  1259. const detailInfo = this.getImportPaperData();
  1260. this.uploadAnswerData = {
  1261. detailInfo: JSON.stringify(detailInfo),
  1262. ...this.modalForm,
  1263. };
  1264. this.$refs.ImportAnswerDialog.open();
  1265. },
  1266. async answerTemplateDownload() {
  1267. const detailInfo = this.getImportPaperData();
  1268. const res = await downloadByApi(() => {
  1269. return questionImportDownloadTemplate({
  1270. detailInfo,
  1271. ...this.modalForm,
  1272. });
  1273. }).catch((e) => {
  1274. this.$message.error(e || "下载失败,请重新尝试!");
  1275. });
  1276. if (!res) return;
  1277. this.$message.success("下载成功!");
  1278. },
  1279. answerUploaded(res) {
  1280. const cacheData = this.getCachePaperInfo(
  1281. res.data.detailInfo,
  1282. questionInfoField
  1283. );
  1284. this.paperData = this.assignCachePaperData(
  1285. this.paperData,
  1286. cacheData,
  1287. true
  1288. );
  1289. this.questionKey = randomCode();
  1290. },
  1291. // word upload
  1292. uploaded(res) {
  1293. this.$message.success("上传成功!");
  1294. this.resetData({
  1295. richText: res.data.richText,
  1296. detailInfo: res.data.detailInfo,
  1297. });
  1298. },
  1299. validError(error) {
  1300. console.log("err", error);
  1301. this.$message.error(error.message);
  1302. },
  1303. // rich test editor
  1304. richTextFocus(richTextGroup) {
  1305. this.$refs.QuestionImportPaperEdit.scrollToContentByIndex(
  1306. richTextGroup.indexs
  1307. );
  1308. this.scrollType = "rich-text";
  1309. setTimeout(() => {
  1310. this.scrollType = "";
  1311. }, 100);
  1312. },
  1313. },
  1314. };
  1315. </script>