123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374 |
- <template>
- <div class="question-import-edit">
- <el-dialog
- custom-class="question-import-edit-dialog"
- :visible.sync="modalIsShow"
- :close-on-click-modal="false"
- :close-on-press-escape="false"
- append-to-body
- fullscreen
- destroy-on-close
- :show-close="false"
- @opened="visibleChange"
- @closed="initData"
- >
- <div slot="title">
- <div class="box-justify">
- <div>
- <h2>文件上传</h2>
- </div>
- <div style="display: flex; align-items: center">
- <el-button
- style="margin-left: 10px"
- size="mini"
- type="danger"
- @click="cancel"
- >返回</el-button
- >
- </div>
- </div>
- <div class="head-form">
- <el-form ref="modalFormComp" :model="modalForm" :rules="rules" inline>
- <el-form-item prop="courseId" label="课程名称:">
- <course-select
- v-model="modalForm.courseId"
- @change="courseChange"
- >
- </course-select>
- </el-form-item>
- <el-form-item>
- <el-checkbox v-model="modalForm.useOriginalPaper"
- >是否以导入试题同步生成试卷</el-checkbox
- >
- </el-form-item>
- <template v-if="modalForm.useOriginalPaper">
- <el-form-item>
- <el-input
- v-model="modalForm.name"
- placeholder="请输入试卷名称"
- ></el-input>
- </el-form-item>
- <el-form-item>
- <el-checkbox v-model="modalForm.checkTotalScore"
- >是否校验导入试卷总分</el-checkbox
- >
- </el-form-item>
- <el-form-item v-if="modalForm.checkTotalScore">
- <el-input-number
- v-model="modalForm.totalScore"
- style="width: 80px"
- :min="1"
- :max="1000"
- :step="1"
- step-strictly
- :controls="false"
- ></el-input-number>
- </el-form-item>
- </template>
- </el-form>
- </div>
- </div>
- <div class="qe-body">
- <div class="qe-part qe-part-edit">
- <div class="qe-part-main">
- <div class="qe-part-head">
- <div class="qe-part-head-title">
- <h3>题目编辑</h3>
- <div>
- <el-button
- size="small"
- type="primary"
- plain
- icon="icon icon-edit-warning"
- @click="showDocx"
- >录入说明</el-button
- >
- <el-button
- size="small"
- type="primary"
- plain
- icon="icon icon-export-prop"
- class="margin-right-10"
- @click="getWordTemplate"
- >模板下载</el-button
- >
- <upload-button
- btn-content="上传文件"
- btn-icon="icon icon-import"
- :disabled="loading || !modalForm.courseId"
- :upload-data="uploadData"
- :upload-url="uploadUrl"
- :format="importFileTypes"
- @valid-error="validError"
- @upload-success="uploaded"
- :auto-upload="false"
- ></upload-button>
- </div>
- </div>
- <div class="qe-part-head-desc">
- <i class="icon icon-tips"></i>
- 提示:若识别有误,可点击左侧题目按格式进行修改后重新识别
- </div>
- </div>
- <div class="qe-part-body">
- <div id="qe-part-richtext-list">
- <div
- v-for="(richJsonItem, rindex) in paperRichJsonGroup"
- :key="rindex"
- :class="[
- 'qe-part-richtext',
- { 'is-error': richJsonItem.exceptions.length },
- ]"
- >
- <v-editor
- ref="RichTextEditor"
- :value="richJsonItem"
- :show-menu="false"
- custom-emit-input
- :custom-render-action="renderRichText"
- :custom-tojson-action="richTextToJSON"
- @focus="() => richTextFocus(richJsonItem)"
- ></v-editor>
- <div
- v-if="richJsonItem.exceptions.length"
- class="qe-part-richtext-error"
- >
- <p
- class="tips-info tips-error"
- v-for="(exception, index) in richJsonItem.exceptions"
- :key="index"
- @click="highLightErrorText(exception)"
- :class="{ hide: inHasIgnore(exception.indexs) }"
- >
- {{ exception.message }}
- </p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="qe-part qe-part-view">
- <div class="qe-part-main">
- <div class="qe-part-head">
- <div class="qe-part-head-title">
- <h3>题目阅览</h3>
- <div>
- <el-button
- size="small"
- type="primary"
- plain
- icon="icon icon-export-answer"
- @click="toImportAnswer"
- >导入答案属性</el-button
- >
- <el-button
- size="small"
- type="primary"
- icon="icon icon-save-white"
- :loading="loading"
- :disabled="!paperData.length || hasErrorTips"
- @click="confirm"
- >识别无误,加入题库</el-button
- >
- </div>
- </div>
- <div class="qe-part-head-desc">
- <p class="desc-qcout">
- <span>共识别 </span>
- <span class="color-success">
- {{ questionStatData.successCount }}
- </span>
- <span> 题,其中识别有误 </span>
- <span class="color-danger">
- {{ questionStatData.errorCount }}
- </span>
- <span> 题</span>
- </p>
- <el-checkbox
- v-model="onlyErrorQuestion"
- @change="onlyErrorQuestionChange"
- >仅查看识别有误试题</el-checkbox
- >
- </div>
- <div class="qe-part-head-menu">
- <el-tabs
- v-model="filterQuestionType"
- @tab-click="filterQuestionTypeChange"
- >
- <el-tab-pane
- :label="`全部(${
- onlyErrorQuestion
- ? questionStatData.errorCount
- : questionStatData.total
- })`"
- name="all"
- ></el-tab-pane>
- <el-tab-pane
- v-for="item in questionStatData.qtypes"
- :key="item.questionType"
- :label="`${item.questionTypeName}(${item.questionCount})`"
- :name="item.questionType"
- ></el-tab-pane>
- </el-tabs>
- </div>
- </div>
- <div id="qe-part-paper" class="qe-part-body">
- <question-import-paper-edit
- v-if="filterPaperData.length"
- ref="QuestionImportPaperEdit"
- :key="questionKey"
- :paper="filterPaperData"
- :course-id="data.importData.courseId"
- ></question-import-paper-edit>
- </div>
- </div>
- </div>
- <div class="qe-middle">
- <div class="qe-middle-arrow"></div>
- <el-button
- size="small"
- type="primary"
- :loading="loading"
- @click="toParse"
- >识别</el-button
- >
- </div>
- </div>
- </el-dialog>
- <!-- 录入说明 -->
- <el-dialog
- title="录入说明"
- :visible.sync="showIframeDialog"
- append-to-body
- width="900px"
- top="30px"
- >
- <div
- style="width: 100%; height: calc(100vh - 250px); overflow: auto"
- id="doc-box"
- ></div>
- <span slot="footer" class="dialog-footer">
- <el-button type="primary" @click="showIframeDialog = false"
- >关闭</el-button
- >
- </span>
- </el-dialog>
- <!-- 上传答案文件 -->
- <import-file-dialog
- ref="ImportAnswerDialog"
- dialog-title="导入答案"
- :template-download-handle="answerTemplateDownload"
- :upload-url="uploadAnswerUrl"
- :upload-data="uploadAnswerData"
- add-file-param="dataFile"
- @uploaded="answerUploaded"
- ></import-file-dialog>
- </div>
- </template>
- <script>
- // import paperRichTextJson from "../datas/paperRichText.json";
- // import paperParseData from "../datas/paperParseData.json";
- import { calcSum, deepCopy, objTypeOf, randomCode } from "@/plugins/utils";
- import QuestionImportPaperEdit from "./QuestionImportPaperEdit.vue";
- import UploadButton from "@/components/UploadButton.vue";
- import { getRichTextAnswerPointCount, isAnEmptyRichText } from "@/utils/utils";
- import {
- questionImportPaperSave,
- questionImportParseRichText,
- questionImportDownloadTemplate,
- propertyNameQueryApi,
- questionWordImportTemplate,
- } from "../api";
- import ImportFileDialog from "@/components/ImportFileDialog.vue";
- import { QUESTION_API, QUESTION_TYPES } from "@/constants/constants";
- import { downloadByApi } from "@/plugins/download";
- import { richTextToJSON, renderRichText } from "./import-edit/richText";
- import scrollMixins from "./import-edit/scrollMixins";
- import timeMixin from "@/mixins/timeMixin";
- import { renderAsync } from "docx-preview";
- const questionInfoField = [
- "courseId",
- "difficulty",
- "quesProperties",
- "score",
- "publicity",
- "control",
- "answerAnalysis",
- "quesAnswer",
- ];
- export default {
- name: "QuestionImportEdit",
- components: { QuestionImportPaperEdit, ImportFileDialog, UploadButton },
- mixins: [scrollMixins, timeMixin],
- props: {
- data: {
- type: Object,
- default() {
- return {
- richText: { sections: [] },
- detailInfo: [],
- importData: {
- courseId: "",
- courseName: "",
- name: "",
- checkTotalScore: false,
- useOriginalPaper: false,
- totalScore: 0,
- },
- };
- },
- },
- },
- computed: {
- hasErrorTips() {
- // return (this.paperRichJsonGroup || []).some((item) => {
- // return item?.exceptions?.length;
- // });
- let detailInfo = this.paperData;
- if (!Array.isArray(detailInfo)) {
- return false;
- }
- return detailInfo.some((detail) => {
- return detail?.questions?.some((item) => {
- let questionExceptions = item.questionExceptions;
- let arr = item.ignoreOptionRepeat
- ? questionExceptions.filter(
- (v) => !v.cause.includes("选项内容相同")
- )
- : questionExceptions;
- if (!item.subQuestions?.length) {
- return !!arr.length;
- } else {
- return item.subQuestions.some((sub) => {
- let { questionExceptions = [] } = sub;
- let ar = sub.ignoreOptionRepeat
- ? questionExceptions.filter(
- (v) => !v.cause.includes("选项内容相同")
- )
- : questionExceptions;
- return !!ar.length;
- });
- }
- });
- });
- },
- },
- data() {
- return {
- ignoreRepeatExceptionIndexArr: [],
- modalIsShow: false,
- loading: false,
- questionKey: "",
- filterPaperData: [],
- paperData: [],
- paperRichJson: { sections: [] },
- paperRichJsonGroup: [],
- richTextToJSON,
- renderRichText,
- lastPaperScrollTop: 0,
- lastRichTextScrollTop: 0,
- richTextIndexList: [],
- scrollType: "",
- curException: null,
- // upload answer
- uploadAnswerUrl: `${QUESTION_API}/word/parse/import`,
- uploadAnswerData: {},
- // word upload
- importFileTypes: ["docx", "doc"],
- uploadData: {},
- uploadUrl: `${QUESTION_API}/word/parse/struct`,
- showIframeDialog: false,
- // question types
- onlyErrorQuestion: false,
- questionStatData: {
- total: 0,
- successCount: 0,
- errorCount: 0,
- qtypes: [],
- },
- filterQuestionType: "all",
- modalForm: this.getInitForm(),
- rules: {
- courseId: [
- {
- required: true,
- message: "请选择课程",
- trigger: "change",
- },
- ],
- name: [
- {
- required: true,
- message: "请输入试卷名称",
- trigger: "change",
- },
- ],
- totalScore: [
- {
- required: true,
- message: "请输入试卷总分",
- trigger: "change",
- },
- ],
- },
- };
- },
- created() {
- this.$bus.on("markIgnoreRepeatQuestion", this.markIgnoreRepeat);
- },
- watch: {
- paperData: {
- handler() {
- this.parseQuestionStatData();
- this.filterQuestionTypeChange({ name: this.filterQuestionType });
- this.questionKey = randomCode();
- },
- },
- },
- methods: {
- getInitForm() {
- return {
- courseId: null,
- courseName: null,
- name: "",
- checkTotalScore: false,
- useOriginalPaper: false,
- totalScore: 0,
- toOtherCourse: false,
- };
- },
- courseChange(val) {
- this.modalForm.courseName = val ? val.name : "";
- this.getCourseProperty();
- this.uploadData = { courseId: this.modalForm.courseId };
- },
- parseQuestionStatData() {
- const total = calcSum(
- this.paperData.map((item) => item.questions.length)
- );
- const successCount = calcSum(
- this.paperData.map(
- (item) =>
- item.questions.filter((v) => !v.questionExceptions.length).length
- )
- );
- const errorCount = calcSum(
- this.paperData.map(
- (item) =>
- item.questions.filter((v) => v.questionExceptions.length).length
- )
- );
- const questionTypeStat = {};
- this.paperData.forEach((detail) => {
- let questions = detail.questions;
- if (this.onlyErrorQuestion) {
- questions = questions.filter((v) => v.questionExceptions.length);
- }
- questions.forEach((question) => {
- const { questionType } = question;
- if (questionTypeStat[questionType]) {
- questionTypeStat[questionType]++;
- } else {
- questionTypeStat[questionType] = 1;
- }
- });
- });
- const qtMap = {};
- QUESTION_TYPES.forEach((item) => {
- qtMap[item.code] = item.name;
- });
- const qtypes = Object.keys(questionTypeStat).map((key) => ({
- questionType: key,
- questionTypeName: qtMap[key],
- questionCount: questionTypeStat[key],
- }));
- this.questionStatData = {
- total,
- successCount,
- errorCount,
- qtypes,
- };
- },
- onlyErrorQuestionChange() {
- this.parseQuestionStatData();
- this.filterQuestionTypeChange({ name: this.filterQuestionType });
- this.questionKey = randomCode();
- },
- inHasIgnore(indexes) {
- return this.ignoreRepeatExceptionIndexArr.includes(indexes.toString());
- },
- markIgnoreRepeat(exceptionIndex, bool) {
- this.paperData.forEach((detail) => {
- detail.questions.forEach((question) => {
- if (
- question.questionExceptions?.length &&
- question.questionExceptions.find(
- (v) => v.exceptionIndex.toString() == exceptionIndex
- )
- ) {
- question.ignoreOptionRepeat = bool;
- }
- if (question.subQuestions?.length) {
- question.subQuestions.forEach((sub) => {
- if (
- sub.questionExceptions?.length &&
- sub.questionExceptions.find(
- (v) => v.exceptionIndex.toString() == exceptionIndex
- )
- ) {
- sub.ignoreOptionRepeat = bool;
- }
- });
- }
- });
- });
- this.paperData = JSON.parse(JSON.stringify(this.paperData));
- this.paperRichJsonGroup = this.getRichTextGroup();
- this.ignoreRepeatExceptionIndexArr.push(exceptionIndex);
- },
- urlToBlob(url, callback) {
- let xhr = new XMLHttpRequest();
- xhr.open("GET", url, true);
- xhr.responseType = "blob";
- xhr.onload = function () {
- if (xhr.status == 200) {
- callback(xhr.response);
- }
- };
- xhr.send();
- },
- showDocx() {
- this.showIframeDialog = true;
- this.$nextTick(() => {
- this.urlToBlob("/admin/inputDesc.docx", (data) => {
- let box = document.getElementById("doc-box");
- renderAsync(data, box);
- });
- });
- },
- async getWordTemplate() {
- const res = await downloadByApi(() => {
- return questionWordImportTemplate();
- }).catch((e) => {
- this.$message.error(e || "下载失败,请重新尝试!");
- });
- if (!res) return;
- this.$message.success("下载成功!");
- },
- async visibleChange() {
- this.ignoreRepeatExceptionIndexArr = [];
- this.getCourseProperty();
- if (this.data) this.resetData(this.data);
- this.$nextTick(() => {
- this.registScrollEvent();
- });
- },
- resetData({ richText, detailInfo }) {
- this.paperData = deepCopy(detailInfo);
- this.paperRichJson = this.buildRichText(deepCopy(richText));
- this.transformDataInfo();
- this.paperRichJsonGroup = this.getRichTextGroup();
- this.uploadData = { courseId: this.modalForm.courseId };
- this.questionKey = randomCode();
- this.$nextTick(() => {
- this.getRichTextIndexList();
- });
- },
- filterQuestionTypeChange(val) {
- this.filterQuestionType = val.name;
- const onlyErrorValidater = (question) => {
- if (this.onlyErrorQuestion) {
- return Boolean(question.questionExceptions.length);
- }
- return true;
- };
- if (this.filterQuestionType === "all") {
- this.filterPaperData = !this.onlyErrorQuestion
- ? this.paperData
- : this.paperData
- .map((detail) => {
- return {
- ...detail,
- questions: detail.questions.filter((question) =>
- onlyErrorValidater(question)
- ),
- };
- })
- .filter((detail) => detail.questions.length);
- return;
- }
- this.filterPaperData = this.paperData
- .map((detail) => {
- return {
- ...detail,
- questions: detail.questions.filter(
- (question) =>
- question.questionType === this.filterQuestionType &&
- onlyErrorValidater(question)
- ),
- };
- })
- .filter((detail) => detail.questions.length);
- },
- getRichTextIndexList() {
- const richTextListDom = document.getElementById("qe-part-richtext-list");
- const elPos = richTextListDom.getBoundingClientRect();
- let richTextIndexList = [];
- const richTextBodyDoms =
- richTextListDom.querySelectorAll(".v-editor-body");
- richTextBodyDoms.forEach((richTextBodyDom) => {
- richTextBodyDom.childNodes.forEach((sectionNode) => {
- const id = sectionNode.getAttribute("id");
- if (!id) return;
- if (
- sectionNode.className &&
- sectionNode.className.includes("section-error")
- )
- return;
- const index = id.replace("section-", "") * 1;
- const sectionPos = sectionNode.getBoundingClientRect();
- richTextIndexList.push([index, sectionPos.y - elPos.y]);
- });
- });
- this.richTextIndexList = richTextIndexList;
- },
- async getCourseProperty() {
- if (!this.modalForm.courseId) return;
- const res = await propertyNameQueryApi(this.modalForm.courseId, "");
- const optionList = res.data || [];
- window.sessionStorage.setItem(
- "coursePropertys",
- JSON.stringify({ optionList, courseId: this.modalForm.courseId })
- );
- },
- buildRichText(richText) {
- let nsections = [];
- richText.sections.forEach((section) => {
- nsections.push({
- ...section,
- attributes: { id: `section-${section.remark.index}` },
- });
- });
- return { sections: nsections };
- },
- transformDataInfo() {
- this.transformRichImg(this.paperRichJson);
- this.paperData.forEach((detail) => {
- detail.questions.forEach((question) => {
- this.transformQuestion(question);
- if (question.subQuestions && question.subQuestions.length) {
- question.subQuestions.forEach((subq) => {
- this.transformQuestion(subq);
- });
- }
- });
- });
- },
- transformQuestion(question) {
- this.transformRichImg(question.body);
- this.transformRichImg(question.answerRichTexts);
- if (question.options && question.options.length) {
- question.options.forEach((item) => {
- this.transformRichImg(item.body);
- });
- }
- question.quesAnswer = this.transformQuestionAnser(question.quesAnswer);
- },
- transformRichImg(richText) {
- if (isAnEmptyRichText(richText)) return;
- const rate = 96 / 600;
- richText.sections.forEach((section) => {
- section.blocks.forEach((block) => {
- if (block.type !== "image" || !block.param) return;
- block.param.width = block.param.width * rate;
- block.param.height = block.param.height * rate;
- });
- });
- },
- transformQuestionAnser(quesAnswer) {
- let qAnswer = null;
- try {
- qAnswer = quesAnswer ? JSON.parse(quesAnswer) : null;
- } catch (error) {
- console.log(error);
- }
- if (!qAnswer || objTypeOf(qAnswer) !== "array") return quesAnswer;
- qAnswer.forEach((item) => {
- this.transformRichImg(item);
- });
- return JSON.stringify(qAnswer);
- },
- getRichTextGroup() {
- let groupSetList = [];
- this.paperData.forEach((detail) => {
- groupSetList.push({
- id: randomCode(),
- indexs: detail.detailIndex,
- });
- detail.questions.forEach((question) => {
- groupSetList.push({
- id: randomCode(),
- indexs: question.integralIndex,
- });
- if (question.subQuestions && question.subQuestions.length) {
- question.subQuestions.forEach((subq) => {
- groupSetList.push({
- id: randomCode(),
- indexs: subq.integralIndex,
- });
- });
- }
- });
- });
- let groups = [];
- let curGroupId = 0;
- let curGroup = [];
- const findGroupId = (ind) => {
- let data = groupSetList.find((item) => item.indexs.includes(ind));
- return data ? data.id : null;
- };
- this.paperRichJson.sections.forEach((section) => {
- const sectionGroupId = findGroupId(section.remark.index);
- if (sectionGroupId !== curGroupId) {
- if (curGroup.length) {
- groups.push({ sections: curGroup, exceptions: [] });
- curGroup = [];
- }
- }
- curGroupId = sectionGroupId;
- curGroup.push(section);
- });
- if (curGroup.length) {
- groups.push({ sections: curGroup, exceptions: [] });
- curGroup = [];
- }
- const getExceptions = (sections) => {
- let cause = {};
- sections.forEach((section) => {
- if (section.remark.status) return;
- if (!cause[section.remark.cause]) {
- cause[section.remark.cause] = [];
- }
- cause[section.remark.cause].push(section.remark.index);
- });
- const causeList = Object.keys(cause).map((key) => {
- return {
- message: key,
- indexs: cause[key],
- };
- });
- causeList.sort((a, b) => a.indexs[0] - b.indexs[0]);
- return causeList;
- };
- groups.forEach((group) => {
- group.indexs = group.sections.map((item) => item.remark.index);
- group.exceptions = getExceptions(group.sections);
- });
- return groups;
- },
- highLightErrorText(exception) {
- if (this.curException) {
- this.curException.indexs.forEach((ind) => {
- const sectionDoms = document.querySelectorAll(`#section-${ind}`);
- if (!sectionDoms.length) return;
- sectionDoms.forEach((sectionDom) => {
- sectionDom.style = null;
- });
- });
- }
- this.clearSetTs();
- this.curException = exception;
- let firstSectionDom = null;
- exception.indexs.forEach((ind) => {
- const sectionDom = document.getElementById(`section-${ind}`);
- if (!sectionDom) return;
- if (!firstSectionDom) firstSectionDom = sectionDom;
- sectionDom.style.background = "#fef0f0";
- });
- if (!firstSectionDom) return;
- const richTextListDom = document.getElementById("qe-part-richtext-list");
- const elPos = richTextListDom.getBoundingClientRect();
- const sectionOffsetTop =
- firstSectionDom.getBoundingClientRect().y - elPos.y - 100;
- if (sectionOffsetTop < richTextListDom.parentNode.scrollTop) {
- richTextListDom.parentNode.scrollTop = sectionOffsetTop;
- this.scrollType = "rich-text";
- setTimeout(() => {
- this.scrollType = "";
- }, 100);
- }
- this.addSetTime(() => {
- this.curException.indexs.forEach((ind) => {
- const sectionDoms = document.querySelectorAll(`#section-${ind}`);
- if (!sectionDoms.length) return;
- sectionDoms.forEach((sectionDom) => {
- sectionDom.style = null;
- });
- });
- this.curException = null;
- }, 5000);
- },
- initData() {
- this.filterPaperData = [];
- this.filterQuestionType = "all";
- this.paperData = [];
- this.paperRichJson = { sections: [] };
- window.sessionStorage.removeItem("coursePropertys");
- this.$message.closeAll();
- this.removeScrollEvent();
- },
- cancel() {
- this.modalIsShow = false;
- },
- open() {
- this.modalIsShow = true;
- },
- getRichTextJsons() {
- let sections = [];
- this.$refs.RichTextEditor.forEach((item) => {
- const itemRichJson = item.emitJsonAction();
- sections.push(...itemRichJson.sections);
- });
- return { sections };
- },
- async toParse() {
- if (isAnEmptyRichText(this.paperRichJson)) {
- this.$message.error("请输入试卷内容!");
- return;
- }
- if (this.loading) return;
- this.loading = true;
- let richText = this.getRichTextJsons();
- const res = await questionImportParseRichText({
- richText,
- courseId: this.modalForm.courseId,
- }).catch(() => {});
- this.loading = false;
- if (!res) return;
- const cacheData = this.getCachePaperInfo(
- this.getImportPaperData(),
- questionInfoField
- );
- // console.log(cacheData);
- this.paperData = this.assignCachePaperData(
- res.data.detailInfo,
- cacheData
- );
- this.paperRichJson = this.buildRichText(deepCopy(res.data.richText));
- this.paperRichJsonGroup = this.getRichTextGroup();
- this.questionKey = randomCode();
- this.$nextTick(() => {
- this.getRichTextIndexList();
- });
- },
- getCachePaperInfo(paperData, cacheFields = []) {
- let cachePaperInfo = {};
- paperData.forEach((detail, dIndex) => {
- detail.questionInfo.forEach((question, qIndex) => {
- let info = {};
- let k = `${dIndex + 1}_${qIndex + 1}`;
- if (cacheFields.length) {
- cacheFields.forEach((field) => {
- info[field] = question[field];
- });
- } else {
- info = { ...question };
- }
- cachePaperInfo[k] = info;
- if (question.subQuestions && question.subQuestions.length) {
- question.subQuestions.forEach((subq, subqIndex) => {
- let info = {};
- let k = `${dIndex + 1}_${qIndex + 1}_${subqIndex + 1}`;
- if (cacheFields.length) {
- cacheFields.forEach((field) => {
- info[field] = subq[field];
- });
- } else {
- info = { ...subq };
- }
- cachePaperInfo[k] = info;
- });
- }
- });
- });
- // console.log(cachePaperInfo);
- return cachePaperInfo;
- },
- assignCachePaperData(paperData, cacheData, mergeReverse = false) {
- return paperData.map((detail, dIndex) => {
- detail.questions = detail.questions.map((question, qIndex) => {
- let k = `${dIndex + 1}_${qIndex + 1}`;
- let nq = this.mergeObjData(
- question,
- cacheData[k] || {},
- mergeReverse
- );
- if (question.subQuestions && question.subQuestions.length) {
- nq.subQuestions = question.subQuestions.map((subq, subqIndex) => {
- let k = `${dIndex + 1}_${qIndex + 1}_${subqIndex + 1}`;
- return this.mergeObjData(subq, cacheData[k] || {}, mergeReverse);
- });
- }
- return nq;
- });
- return detail;
- });
- },
- isNull(val) {
- if (val) {
- if (val === "[]") return true;
- if (objTypeOf(val) === "array" && !val.length) return true;
- }
- return val === null || val === "" || val === undefined;
- },
- mergeObjData(targetObj, cacheObj, mergeReverse) {
- let data = { ...targetObj };
- Object.keys(cacheObj).forEach((k) => {
- if (mergeReverse) {
- data[k] = this.isNull(cacheObj[k]) ? targetObj[k] : cacheObj[k];
- } else {
- data[k] = this.isNull(targetObj[k]) ? cacheObj[k] : targetObj[k];
- }
- });
- return data;
- },
- getImportPaperData() {
- if (!this.$refs.QuestionImportPaperEdit) return [];
- let paperData = deepCopy(this.$refs.QuestionImportPaperEdit.getData());
- const transformFieldMap = { body: "quesBody", options: "quesOptions" };
- const fields = Object.keys(transformFieldMap);
- const course = {
- id: this.modalForm.courseId,
- name: this.modalForm.courseName,
- };
- const transformQuestion = (question) => {
- question.id = null;
- question.course = course;
- fields.forEach((field) => {
- question[transformFieldMap[field]] = question[field];
- delete question[field];
- });
- if (question.quesOptions && question.quesOptions.length) {
- question.quesOptions = question.quesOptions.map((option) => {
- option.optionBody = option.body;
- delete option.body;
- return option;
- });
- }
- return question;
- };
- const detailInfo = paperData.map((detail) => {
- const questionInfo = detail.questions.map((question) => {
- transformQuestion(question);
- if (question.subQuestions && question.subQuestions.length) {
- question.subQuestions = question.subQuestions.map((subq) =>
- transformQuestion(subq)
- );
- question.score = calcSum(
- question.subQuestions.map((q) => q.score || 0)
- );
- }
- return question;
- });
- return {
- name: detail.name,
- number: detail.number,
- questionCount: questionInfo.length,
- questionInfo,
- questionScore: detail.questionScore,
- totalScore: calcSum(questionInfo.map((q) => q.score || 0)),
- };
- });
- // console.log(detailInfo);
- return detailInfo;
- },
- checkImportPaperData(paperData) {
- this.$message.closeAll();
- // 题目内容校验
- const MATCHING_QUESTION = ["PARAGRAPH_MATCHING", "BANKED_CLOZE"];
- const SELECT_QUESTION = [
- "SINGLE_ANSWER_QUESTION",
- "MULTIPLE_ANSWER_QUESTION",
- ...MATCHING_QUESTION,
- ];
- const NESTED_QUESTION = [
- ...MATCHING_QUESTION,
- "READING_COMPREHENSION",
- "CLOZE",
- "LISTENING_QUESTION",
- ];
- const ALLOW_EMPTY_BODY_QUESTION = [
- "LISTENING_QUESTION",
- ...MATCHING_QUESTION,
- ];
- let errInfos = [];
- paperData.forEach((detail) => {
- detail.questionInfo.forEach((question) => {
- const { questionType, quesBody } = question;
- const questionTitle = `第${detail.number}大题第${question.number}小题`;
- let qErrInfo = [];
- // 题干
- if (
- !ALLOW_EMPTY_BODY_QUESTION.includes(questionType) &&
- (!quesBody || isAnEmptyRichText(quesBody))
- ) {
- qErrInfo.push(`没有题干`);
- }
- // 选项
- if (SELECT_QUESTION.includes(questionType)) {
- if (question.quesOptions.length < 2) {
- qErrInfo.push(`选项少于2个`);
- }
- if (
- question.quesOptions.some((option) =>
- isAnEmptyRichText(option.optionBody)
- )
- ) {
- qErrInfo.push(`有选择内容为空`);
- }
- }
- // 小题数
- if (
- NESTED_QUESTION.includes(questionType) &&
- !question.subQuestions.length
- ) {
- qErrInfo.push(`没有小题`);
- }
- if (qErrInfo.length) {
- errInfos.push(`${questionTitle}${qErrInfo.join("、")}`);
- qErrInfo = [];
- }
- // 完形填空
- if (questionType === "CLOZE") {
- const answerPointCount = getRichTextAnswerPointCount(quesBody);
- if (answerPointCount !== question.subQuestions.length) {
- errInfos.push(
- `第${detail.number}大题题干答题点数量与选项数不匹配`
- );
- }
- if (
- question.subQuestions.some(
- (q) => q.questionType !== "SINGLE_ANSWER_QUESTION"
- )
- ) {
- errInfos.push(`第${detail.number}大题存在不是单选题的子题`);
- }
- }
- // 听力题
- if (questionType === "LISTENING_QUESTION") {
- if (
- question.subQuestions.some(
- (q) => q.questionType !== "SINGLE_ANSWER_QUESTION"
- )
- ) {
- errInfos.push(`第${detail.number}大题存在不是单选题的子题`);
- }
- }
- // 选词填空、段落匹配,单用模式时校验输入答案是否重复
- if (
- MATCHING_QUESTION.includes(questionType) &&
- question.quesParam.matchingMode === 1
- ) {
- // 选词填空
- if (questionType === "BANKED_CLOZE") {
- const answerPointCount = getRichTextAnswerPointCount(quesBody);
- if (answerPointCount > question.quesOptions.length) {
- errInfos.push(`第${detail.number}大题题干答题点数量超过选项数`);
- }
- } else {
- if (question.subQuestions.length > question.quesOptions.length) {
- errInfos.push(`第${detail.number}大题小题数量超过选项数`);
- }
- }
- let selectedAnswer = [],
- errorQuestionIndexs = [];
- question.subQuestions.forEach((subq, sindex) => {
- if (selectedAnswer.includes(subq.quesAnswer)) {
- errorQuestionIndexs.push(`${question.number}-${sindex + 1}`);
- } else {
- if (subq.quesAnswer !== "[]")
- selectedAnswer.push(subq.quesAnswer);
- }
- });
- if (errorQuestionIndexs.length) {
- errInfos.push(
- `第${
- detail.number
- }大题${errorQuestionIndexs.join()}小题答案重复`
- );
- }
- }
- if (!NESTED_QUESTION.includes(questionType)) return;
- // 套题小题校验
- question.subQuestions.forEach((subq, sindex) => {
- const subqTitle = `第${detail.number}大题第${question.number}-${
- sindex + 1
- }小题`;
- if (
- questionType === "READING_COMPREHENSION" &&
- (!subq.quesBody || isAnEmptyRichText(subq.quesBody))
- ) {
- qErrInfo.push(`没有题干`);
- }
- if (
- SELECT_QUESTION.includes(subq.questionType) &&
- !MATCHING_QUESTION.includes(questionType)
- ) {
- if (subq.quesOptions.length < 2) {
- qErrInfo.push(`选项少于2个`);
- }
- if (
- subq.quesOptions.some((option) =>
- isAnEmptyRichText(option.optionBody)
- )
- ) {
- qErrInfo.push(`有选择内容为空`);
- }
- }
- if (qErrInfo.length) {
- errInfos.push(`${subqTitle}${qErrInfo.join("、")}`);
- qErrInfo = [];
- }
- });
- });
- });
- if (errInfos.length) {
- this.$message({
- showClose: true,
- message: errInfos.join("。"),
- type: "error",
- duration: 0,
- });
- return;
- }
- if (!this.modalForm.useOriginalPaper) return true;
- let detailNumbers = paperData.map((detail) => detail.number);
- // 大题号重复性校验
- let repeatDetaiNumbers = [];
- let detailNums = [];
- for (let i = 0; i < detailNumbers.length; i++) {
- const num = detailNumbers[i];
- if (detailNums.includes(num)) {
- if (!repeatDetaiNumbers.includes(num)) repeatDetaiNumbers.push(num);
- } else {
- detailNums.push(num);
- }
- }
- if (repeatDetaiNumbers.length) {
- this.$message({
- showClose: true,
- message: `大题号${repeatDetaiNumbers.join("、")}重复`,
- type: "error",
- duration: 0,
- });
- return;
- }
- // 大题号连续性校验
- for (let i = 0; i < detailNumbers.length; i++) {
- if (detailNumbers[i] - 1 !== i) {
- this.$message({
- showClose: true,
- message: "大题号不连续",
- type: "error",
- duration: 0,
- });
- return;
- }
- }
- // 答案、分数校验
- let totalScore = calcSum(paperData.map((d) => d.totalScore));
- let errQuestions = [];
- paperData.forEach((detail) => {
- detail.questionInfo.forEach((question) => {
- if (question.subQuestions && question.subQuestions.length) {
- let subIndexs = [];
- question.subQuestions.forEach((subq, sind) => {
- if (!subq.score)
- subIndexs.push(question.number + "-" + (sind + 1));
- });
- if (subIndexs.length)
- errQuestions.push(
- `第${detail.number}大题第${subIndexs.join()}小题`
- );
- } else {
- if (!question.score) {
- errQuestions.push(
- `第${detail.number}大题第${question.number}小题`
- );
- }
- }
- });
- });
- if (errQuestions.length) {
- this.$message({
- showClose: true,
- message: `请设置如下试题的分值:${errQuestions.join("、")}。`,
- type: "error",
- duration: 0,
- });
- return;
- }
- if (
- this.modalForm.checkTotalScore &&
- totalScore !== this.modalForm.totalScore
- ) {
- this.$message({
- showClose: true,
- message: `试卷总分与导入设置的总分不一致!`,
- type: "error",
- duration: 0,
- });
- return;
- }
- return true;
- },
- async confirm() {
- const valid = await this.$refs.modalFormComp.validate().catch(() => {});
- if (!valid) return;
- const confirm = await this.$confirm("确认加入题库吗?", "提示", {
- type: "warning",
- }).catch(() => {});
- if (confirm !== "confirm") return;
- const detailInfo = this.getImportPaperData();
- if (!this.checkImportPaperData(detailInfo)) return;
- if (this.loading) return;
- this.loading = true;
- console.log("detailInfo", detailInfo);
- const res = await questionImportPaperSave({
- ...this.modalForm,
- detailInfo,
- }).catch(() => {});
- this.loading = false;
- if (!res) return;
- if (res.data) {
- this.$confirm(
- "系统正在计算试题情况,请在后续检查试题查重与试题审核数据。",
- "系统通知",
- {
- type: "warning",
- }
- );
- }
- this.$message.success("提交成功!");
- this.$emit("modified");
- this.cancel();
- },
- // 导入答案属性
- toImportAnswer() {
- const detailInfo = this.getImportPaperData();
- this.uploadAnswerData = {
- detailInfo: JSON.stringify(detailInfo),
- ...this.modalForm,
- };
- this.$refs.ImportAnswerDialog.open();
- },
- async answerTemplateDownload() {
- const detailInfo = this.getImportPaperData();
- const res = await downloadByApi(() => {
- return questionImportDownloadTemplate({
- detailInfo,
- ...this.modalForm,
- });
- }).catch((e) => {
- this.$message.error(e || "下载失败,请重新尝试!");
- });
- if (!res) return;
- this.$message.success("下载成功!");
- },
- answerUploaded(res) {
- const cacheData = this.getCachePaperInfo(
- res.data.detailInfo,
- questionInfoField
- );
- this.paperData = this.assignCachePaperData(
- this.paperData,
- cacheData,
- true
- );
- this.questionKey = randomCode();
- },
- // word upload
- uploaded(res) {
- this.$message.success("上传成功!");
- this.resetData({
- richText: res.data.richText,
- detailInfo: res.data.detailInfo,
- });
- },
- validError(error) {
- console.log("err", error);
- this.$message.error(error.message);
- },
- // rich test editor
- richTextFocus(richTextGroup) {
- this.$refs.QuestionImportPaperEdit.scrollToContentByIndex(
- richTextGroup.indexs
- );
- this.scrollType = "rich-text";
- setTimeout(() => {
- this.scrollType = "";
- }, 100);
- },
- },
- };
- </script>
|