|
@@ -0,0 +1,970 @@
|
|
|
+<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" class="box-justify">
|
|
|
+ <div>
|
|
|
+ <h2>文件上传</h2>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <upload-button
|
|
|
+ btn-content="重新上传文件"
|
|
|
+ btn-icon="icon icon-import"
|
|
|
+ :disabled="loading"
|
|
|
+ :upload-data="uploadData"
|
|
|
+ :upload-url="uploadUrl"
|
|
|
+ :format="importFileTypes"
|
|
|
+ @valid-error="uploadError"
|
|
|
+ @upload-error="uploadError"
|
|
|
+ @upload-success="uploaded"
|
|
|
+ ></upload-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="danger"
|
|
|
+ icon="icon icon-back-white"
|
|
|
+ @click="cancel"
|
|
|
+ >返回</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="qe-body">
|
|
|
+ <div class="qe-part qe-part-edit">
|
|
|
+ <div class="qe-part-main">
|
|
|
+ <div class="qe-part-head">
|
|
|
+ <h3>题目编辑</h3>
|
|
|
+ <div>
|
|
|
+ <i class="icon icon-tips"></i>
|
|
|
+ 提示:若识别有误,可点击左侧题目按格式进行修改后重新识别
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="qe-part-body">
|
|
|
+ <v-editor
|
|
|
+ ref="RichTextEditor"
|
|
|
+ v-model="paperRichJson"
|
|
|
+ :enable-formula="false"
|
|
|
+ :enable-audio="false"
|
|
|
+ custom-emit-input
|
|
|
+ :custom-render-action="renderRichText"
|
|
|
+ :custom-tojson-action="richTextToJSON"
|
|
|
+ ></v-editor>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="qe-part qe-part-view">
|
|
|
+ <div class="qe-part-main">
|
|
|
+ <div class="qe-part-head">
|
|
|
+ <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"
|
|
|
+ @click="confirm"
|
|
|
+ >识别无误,加入题库</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div id="qe-part-paper" class="qe-part-body">
|
|
|
+ <question-import-paper-edit
|
|
|
+ v-if="paperData.length"
|
|
|
+ ref="QuestionImportPaperEdit"
|
|
|
+ :key="questionKey"
|
|
|
+ :paper="paperData"
|
|
|
+ :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>
|
|
|
+
|
|
|
+ <!-- 上传答案文件 -->
|
|
|
+ <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 { isAnEmptyRichText } from "@/utils/utils";
|
|
|
+import {
|
|
|
+ questionImportPaperSave,
|
|
|
+ questionImportParseRichText,
|
|
|
+ questionImportDownloadTemplate,
|
|
|
+} from "../api";
|
|
|
+import ImportFileDialog from "@/components/ImportFileDialog.vue";
|
|
|
+import { QUESTION_API } from "@/constants/constants";
|
|
|
+import { propertyNameQueryApi } from "@/modules/question/api";
|
|
|
+import { downloadByApi } from "@/plugins/download";
|
|
|
+import { richTextToJSON, renderRichText } from "./import-edit/richText";
|
|
|
+
|
|
|
+const questionInfoField = [
|
|
|
+ "courseId",
|
|
|
+ "difficulty",
|
|
|
+ "quesProperties",
|
|
|
+ "score",
|
|
|
+ "publicity",
|
|
|
+ "control",
|
|
|
+ "answerAnalysis",
|
|
|
+ "quesAnswer",
|
|
|
+];
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "QuestionExportEdit",
|
|
|
+ components: { QuestionImportPaperEdit, ImportFileDialog, UploadButton },
|
|
|
+ props: {
|
|
|
+ data: {
|
|
|
+ type: Object,
|
|
|
+ default() {
|
|
|
+ return {
|
|
|
+ richText: { sections: [] },
|
|
|
+ detailInfo: [],
|
|
|
+ importData: {
|
|
|
+ courseId: "",
|
|
|
+ courseName: "",
|
|
|
+ name: "",
|
|
|
+ checkTotalScore: false,
|
|
|
+ useOriginalPaper: false,
|
|
|
+ totalScore: 0,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ modalIsShow: false,
|
|
|
+ loading: false,
|
|
|
+ questionKey: "",
|
|
|
+ paperData: [],
|
|
|
+ paperRichJson: { sections: [] },
|
|
|
+ richTextToJSON,
|
|
|
+ renderRichText,
|
|
|
+ lastPaperScrollTop: 0,
|
|
|
+ lastRichTextScrollTop: 0,
|
|
|
+ richTextIndexList: [],
|
|
|
+ scrollType: "",
|
|
|
+ // upload answer
|
|
|
+ uploadAnswerUrl: `${QUESTION_API}/word/parse/import`,
|
|
|
+ uploadAnswerData: {},
|
|
|
+ // word upload
|
|
|
+ uploadData: {},
|
|
|
+ importFileTypes: ["docx", "doc"],
|
|
|
+ uploadUrl: `${QUESTION_API}/word/parse/struct`,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async visibleChange() {
|
|
|
+ await this.getCourseProperty();
|
|
|
+
|
|
|
+ // this.resetData({
|
|
|
+ // richText: paperRichTextJson,
|
|
|
+ // detailInfo: paperParseData,
|
|
|
+ // });
|
|
|
+ this.resetData(this.data);
|
|
|
+
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.registScrollEvent();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ resetData({ richText, detailInfo }) {
|
|
|
+ this.paperData = deepCopy(detailInfo);
|
|
|
+ this.paperRichJson = this.transformRichText(deepCopy(richText));
|
|
|
+ this.uploadData = { courseId: this.data.importData.courseId };
|
|
|
+ this.transformDataInfo();
|
|
|
+ this.questionKey = randomCode();
|
|
|
+
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.getRichTextIndexList();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ getRichTextIndexList() {
|
|
|
+ const richTextBodyDom =
|
|
|
+ this.$refs.RichTextEditor.$el.querySelector(".v-editor-body");
|
|
|
+ let richTextIndexList = [];
|
|
|
+ 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;
|
|
|
+ richTextIndexList.push([index, sectionNode.offsetTop]);
|
|
|
+ });
|
|
|
+ this.richTextIndexList = richTextIndexList;
|
|
|
+ },
|
|
|
+ transformRichText(richText) {
|
|
|
+ let nsections = [];
|
|
|
+ richText.sections.forEach((section) => {
|
|
|
+ nsections.push({
|
|
|
+ blocks: section.blocks,
|
|
|
+ attributes: { id: `section-${section.remark.index}` },
|
|
|
+ });
|
|
|
+ if (section.remark && !section.remark.status) {
|
|
|
+ nsections.push({
|
|
|
+ blocks: [
|
|
|
+ {
|
|
|
+ type: "text",
|
|
|
+ value: section.remark.cause,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ attributes: {
|
|
|
+ id: `section-error-${section.remark.index}`,
|
|
|
+ class: "section-error",
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return { sections: nsections };
|
|
|
+ },
|
|
|
+ async getCourseProperty() {
|
|
|
+ const res = await propertyNameQueryApi(this.data.importData.courseId, "");
|
|
|
+ const optionList = res.data || [];
|
|
|
+ window.sessionStorage.setItem(
|
|
|
+ "coursePropertys",
|
|
|
+ JSON.stringify({ optionList, courseId: this.data.importData.courseId })
|
|
|
+ );
|
|
|
+ },
|
|
|
+ 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 / 300;
|
|
|
+ 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);
|
|
|
+ },
|
|
|
+ initData() {
|
|
|
+ this.paperData = [];
|
|
|
+ this.paperRichJson = { sections: [] };
|
|
|
+ window.sessionStorage.removeItem("coursePropertys");
|
|
|
+ this.$message.closeAll();
|
|
|
+ this.removeScrollEvent();
|
|
|
+ },
|
|
|
+ cancel() {
|
|
|
+ this.modalIsShow = false;
|
|
|
+ },
|
|
|
+ open() {
|
|
|
+ this.modalIsShow = true;
|
|
|
+ },
|
|
|
+ async toParse() {
|
|
|
+ if (isAnEmptyRichText(this.paperRichJson)) {
|
|
|
+ this.$message.error("请输入试卷内容!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.loading) return;
|
|
|
+ this.loading = true;
|
|
|
+
|
|
|
+ let richText = this.$refs.RichTextEditor.emitJsonAction();
|
|
|
+ richText.sections = richText.sections.filter(
|
|
|
+ (item) =>
|
|
|
+ !item.attributes || item.attributes["class"] !== "section-error"
|
|
|
+ );
|
|
|
+
|
|
|
+ const res = await questionImportParseRichText({
|
|
|
+ richText,
|
|
|
+ courseId: this.data.importData.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.transformRichText(deepCopy(res.data.richText));
|
|
|
+ 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.data.importData.courseId,
|
|
|
+ name: this.data.importData.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) {
|
|
|
+ qErrInfo.push(`没有选项`);
|
|
|
+ }
|
|
|
+ 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 (
|
|
|
+ MATCHING_QUESTION.includes(questionType) &&
|
|
|
+ question.quesParam.matchingMode === 1
|
|
|
+ ) {
|
|
|
+ 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.subqType) &&
|
|
|
+ !MATCHING_QUESTION.includes(questionType)
|
|
|
+ ) {
|
|
|
+ if (!subq.quesOptions.length) {
|
|
|
+ qErrInfo.push(`没有选项`);
|
|
|
+ }
|
|
|
+ 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.data.importData.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.data.importData.checkTotalScore &&
|
|
|
+ totalScore !== this.data.importData.totalScore
|
|
|
+ ) {
|
|
|
+ this.$message({
|
|
|
+ showClose: true,
|
|
|
+ message: `试卷总分与导入设置的总分不一致!`,
|
|
|
+ type: "error",
|
|
|
+ duration: 0,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+ async confirm() {
|
|
|
+ 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;
|
|
|
+
|
|
|
+ const res = await questionImportPaperSave({
|
|
|
+ ...this.data.importData,
|
|
|
+ detailInfo,
|
|
|
+ }).catch(() => {});
|
|
|
+
|
|
|
+ this.loading = false;
|
|
|
+ if (!res) return;
|
|
|
+
|
|
|
+ this.$message.success("提交成功!");
|
|
|
+ this.$emit("modified");
|
|
|
+ this.cancel();
|
|
|
+ },
|
|
|
+ // 导入答案属性
|
|
|
+ toImportAnswer() {
|
|
|
+ const detailInfo = this.getImportPaperData();
|
|
|
+ this.uploadAnswerData = {
|
|
|
+ detailInfo: JSON.stringify(detailInfo),
|
|
|
+ ...this.data.importData,
|
|
|
+ };
|
|
|
+ this.$refs.ImportAnswerDialog.open();
|
|
|
+ },
|
|
|
+ async answerTemplateDownload() {
|
|
|
+ const detailInfo = this.getImportPaperData();
|
|
|
+
|
|
|
+ const res = await downloadByApi(() => {
|
|
|
+ return questionImportDownloadTemplate({
|
|
|
+ detailInfo,
|
|
|
+ ...this.data.importData,
|
|
|
+ });
|
|
|
+ }).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.paperRichJson = deepCopy(res.data.richText);
|
|
|
+ this.paperData = deepCopy(res.data.detailInfo);
|
|
|
+ this.transformDataInfo();
|
|
|
+ this.questionKey = randomCode();
|
|
|
+ },
|
|
|
+ uploadError(error) {
|
|
|
+ this.$message.error(error.message);
|
|
|
+ },
|
|
|
+ // scroll
|
|
|
+ registScrollEvent() {
|
|
|
+ document
|
|
|
+ .getElementById("qe-part-paper")
|
|
|
+ .addEventListener("scroll", this.paperScrollEvent);
|
|
|
+ this.$refs.RichTextEditor.$el
|
|
|
+ .querySelector(".v-editor-container")
|
|
|
+ .addEventListener("scroll", this.richTextScrollEvent);
|
|
|
+ },
|
|
|
+ removeScrollEvent() {
|
|
|
+ document
|
|
|
+ .getElementById("qe-part-paper")
|
|
|
+ .removeEventListener("scroll", this.paperScrollEvent);
|
|
|
+ this.$refs.RichTextEditor.$el
|
|
|
+ .querySelector(".v-editor-container")
|
|
|
+ .removeEventListener("scroll", this.richTextScrollEvent);
|
|
|
+ },
|
|
|
+ paperScrollEvent(e) {
|
|
|
+ // e.preventDefault();
|
|
|
+ // e.stopPropagation();
|
|
|
+ if (this.scrollType === "rich-text") {
|
|
|
+ this.lastPaperScrollTop =
|
|
|
+ document.getElementById("qe-part-paper").scrollTop;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.scrollType = "paper";
|
|
|
+ setTimeout(() => {
|
|
|
+ this.scrollType = "";
|
|
|
+ }, 100);
|
|
|
+ const questionContIndexList =
|
|
|
+ this.$refs.QuestionImportPaperEdit.questionContIndexList;
|
|
|
+
|
|
|
+ const scrollTop = e.target.scrollTop;
|
|
|
+ const isScrollDown = scrollTop > this.lastPaperScrollTop;
|
|
|
+ this.lastPaperScrollTop = scrollTop;
|
|
|
+ const targeContIndex = questionContIndexList.findIndex(
|
|
|
+ (item) => scrollTop < item[3]
|
|
|
+ );
|
|
|
+ let targeContPercent = 0;
|
|
|
+ let targeCont = null;
|
|
|
+ let nextTargetCont = null;
|
|
|
+ if (targeContIndex !== -1) {
|
|
|
+ targeCont = questionContIndexList[targeContIndex - 1];
|
|
|
+ nextTargetCont = questionContIndexList[targeContIndex];
|
|
|
+ targeContPercent =
|
|
|
+ (scrollTop - targeCont[3]) / (nextTargetCont[3] - targeCont[3]);
|
|
|
+ } else {
|
|
|
+ targeCont = questionContIndexList.slice(-1)[0];
|
|
|
+ const textHeight = this.$refs.QuestionImportPaperEdit.$el.offsetHeight;
|
|
|
+ targeContPercent =
|
|
|
+ (scrollTop - targeCont[3]) / (textHeight - targeCont[3]);
|
|
|
+ }
|
|
|
+
|
|
|
+ const richTextSectionDom = document.getElementById(
|
|
|
+ `section-${targeCont[2][0]}`
|
|
|
+ );
|
|
|
+ if (!richTextSectionDom) return;
|
|
|
+
|
|
|
+ const richTextContainerDom = this.$refs.RichTextEditor.$el.querySelector(
|
|
|
+ ".v-editor-container"
|
|
|
+ );
|
|
|
+ const richTextMainDom =
|
|
|
+ this.$refs.RichTextEditor.$el.querySelector(".v-editor-main");
|
|
|
+ const sectionOffsetTop = richTextSectionDom.offsetTop;
|
|
|
+ let nextSectionOffsetTop = richTextMainDom.offsetHeight;
|
|
|
+
|
|
|
+ if (nextTargetCont) {
|
|
|
+ const nextRichTextSectionDom = document.getElementById(
|
|
|
+ `section-${nextTargetCont[2][0]}`
|
|
|
+ );
|
|
|
+ if (nextRichTextSectionDom) {
|
|
|
+ nextSectionOffsetTop = nextRichTextSectionDom.offsetTop;
|
|
|
+ } else {
|
|
|
+ nextSectionOffsetTop =
|
|
|
+ richTextSectionDom.offsetTop + richTextSectionDom.offsetHeight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const textScrollTop =
|
|
|
+ sectionOffsetTop +
|
|
|
+ targeContPercent * (nextSectionOffsetTop - sectionOffsetTop);
|
|
|
+ // console.log(
|
|
|
+ // targeCont[2],
|
|
|
+ // textScrollTop,
|
|
|
+ // targeContPercent,
|
|
|
+ // nextSectionOffsetTop,
|
|
|
+ // sectionOffsetTop
|
|
|
+ // );
|
|
|
+ richTextContainerDom.scrollTop = isScrollDown
|
|
|
+ ? Math.max(textScrollTop, richTextContainerDom.scrollTop)
|
|
|
+ : Math.min(textScrollTop, richTextContainerDom.scrollTop);
|
|
|
+ },
|
|
|
+ richTextScrollEvent(e) {
|
|
|
+ if (this.scrollType === "paper") {
|
|
|
+ this.lastRichTextScrollTop =
|
|
|
+ this.$refs.RichTextEditor.$el.querySelector(
|
|
|
+ ".v-editor-container"
|
|
|
+ ).scrollTop;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.scrollType = "rich-text";
|
|
|
+ setTimeout(() => {
|
|
|
+ this.scrollType = "";
|
|
|
+ }, 100);
|
|
|
+
|
|
|
+ const isScrollDown = e.target.scrollTop > this.lastRichTextScrollTop;
|
|
|
+ // console.log(isScrollDown, e.target.scrollTop, this.lastRichTextScrollTop);
|
|
|
+ this.lastRichTextScrollTop = e.target.scrollTop;
|
|
|
+ const offsetH = isScrollDown ? 150 : 0;
|
|
|
+ const scrollTop = e.target.scrollTop + offsetH;
|
|
|
+
|
|
|
+ const richTextMainDom =
|
|
|
+ this.$refs.RichTextEditor.$el.querySelector(".v-editor-main");
|
|
|
+ const questionContIndexList =
|
|
|
+ this.$refs.QuestionImportPaperEdit.questionContIndexList;
|
|
|
+
|
|
|
+ const findQuestionItemDom = (sectionIndex) => {
|
|
|
+ const questionCont = questionContIndexList.find((item) =>
|
|
|
+ item[2].includes(sectionIndex)
|
|
|
+ );
|
|
|
+ if (!questionCont) return;
|
|
|
+ const [id, type] = questionCont;
|
|
|
+ let itemDom = document.getElementById(id);
|
|
|
+ if (type === "body") {
|
|
|
+ itemDom = itemDom.querySelector(".ep-question-title");
|
|
|
+ } else if (type === "option") {
|
|
|
+ itemDom = itemDom.querySelector(".ep-question-body");
|
|
|
+ } else if (type === "answer") {
|
|
|
+ itemDom =
|
|
|
+ itemDom.querySelector(".question-info-view") ||
|
|
|
+ itemDom.querySelector(".ep-question-props");
|
|
|
+ }
|
|
|
+ return itemDom;
|
|
|
+ };
|
|
|
+
|
|
|
+ const targeContIndex = this.richTextIndexList.findIndex(
|
|
|
+ (item) => scrollTop < item[1]
|
|
|
+ );
|
|
|
+ if (!targeContIndex) return;
|
|
|
+ let targeContPercent = 0;
|
|
|
+ let targeCont = null;
|
|
|
+ let nextTargetCont = null;
|
|
|
+ if (targeContIndex !== -1) {
|
|
|
+ targeCont = this.richTextIndexList[targeContIndex - 1];
|
|
|
+ nextTargetCont = this.richTextIndexList[targeContIndex];
|
|
|
+ targeContPercent =
|
|
|
+ (scrollTop - targeCont[1]) / (nextTargetCont[1] - targeCont[1]);
|
|
|
+ } else {
|
|
|
+ targeCont = this.richTextIndexList.slice(-1)[0];
|
|
|
+ const textHeight = richTextMainDom.offsetHeight;
|
|
|
+ targeContPercent =
|
|
|
+ (scrollTop - targeCont[1]) / (textHeight - targeCont[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ const questionContDom = findQuestionItemDom(targeCont[0]);
|
|
|
+ if (!questionContDom) return;
|
|
|
+ const questionListDom = this.$refs.QuestionImportPaperEdit.$el;
|
|
|
+ const elPos = questionListDom.getBoundingClientRect();
|
|
|
+ const questionPos = questionContDom.getBoundingClientRect();
|
|
|
+ const questionContOffsetTop = questionPos.y - elPos.y;
|
|
|
+ let nextQuestionContOffsetTop = questionListDom.offsetHeight;
|
|
|
+
|
|
|
+ if (nextTargetCont) {
|
|
|
+ const nextQuestionContDom = findQuestionItemDom(nextTargetCont[0]);
|
|
|
+ if (nextQuestionContDom) {
|
|
|
+ const nextQuestionPos = nextQuestionContDom.getBoundingClientRect();
|
|
|
+ nextQuestionContOffsetTop = nextQuestionPos.y - elPos.y;
|
|
|
+ } else {
|
|
|
+ nextQuestionContOffsetTop =
|
|
|
+ questionContOffsetTop + questionContDom.offsetHeight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const questionScrollTop =
|
|
|
+ questionContOffsetTop +
|
|
|
+ targeContPercent * (nextQuestionContOffsetTop - questionContOffsetTop);
|
|
|
+ const questionContainerDom = document.getElementById("qe-part-paper");
|
|
|
+ questionContainerDom.scrollTop = isScrollDown
|
|
|
+ ? Math.max(questionScrollTop, questionContainerDom.scrollTop)
|
|
|
+ : Math.min(questionScrollTop, questionContainerDom.scrollTop);
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|