Browse Source

段落匹配/选词填空

zhangjie 2 years ago
parent
commit
6b785a1aaa

+ 1 - 1
src/assets/styles/pages.scss

@@ -884,7 +884,7 @@
     padding-right: 10px;
     padding-right: 10px;
   }
   }
   .el-collapse {
   .el-collapse {
-    margin: 10px 0;
+    margin: 30px 0 10px;
     border: none;
     border: none;
   }
   }
   .el-collapse-item {
   .el-collapse-item {

+ 1 - 1
src/modules/question/components/QuestionEditDialog.vue

@@ -94,7 +94,7 @@ export default {
     return {
     return {
       modalIsShow: false,
       modalIsShow: false,
       QUESTION_TYPES,
       QUESTION_TYPES,
-      curQuestionType: "READING_COMPREHENSION",
+      curQuestionType: "PARAGRAPH_MATCHING",
       // curQuestionType: "SINGLE_ANSWER_QUESTION",
       // curQuestionType: "SINGLE_ANSWER_QUESTION",
       questionModel: {},
       questionModel: {},
       questionKey: "",
       questionKey: "",

+ 255 - 3
src/modules/question/components/edit/BankedClozeQuestion.vue

@@ -1,13 +1,265 @@
 <template>
 <template>
-  <div class="banked-cloze-question">BankedClozeQuestion</div>
+  <div class="banked-cloze-question nested-question">
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-form-item prop="quesBody" label="大题题干">
+        <v-editor
+          v-model="modalForm.quesBody"
+          :enable-answer-point="IS_BANKED_CLOZE"
+          @answer-point-changed="answerPointsChange"
+          @change="quesBodyChange"
+        ></v-editor>
+      </el-form-item>
+      <el-form-item
+        v-for="(option, oindex) in modalForm.quesOptions"
+        :key="option.nmuber"
+        :prop="`quesOptions.${oindex}.body`"
+        :rules="optionRule"
+      >
+        <div class="question-edit-option">
+          <div class="option-check">
+            {{ (option.number - 1) | optionOrderWordFilter }}
+          </div>
+          <div class="option-body">
+            <v-editor
+              v-model="option.body"
+              @change="() => optionBodyChange(oindex)"
+            ></v-editor>
+          </div>
+          <div class="option-delete">
+            <el-button
+              size="mini"
+              circle
+              type="primary"
+              icon="el-icon-plus"
+              title="新增"
+              @click.prevent="addQuesOption(oindex)"
+            ></el-button>
+            <el-button
+              size="mini"
+              circle
+              type="danger"
+              icon="el-icon-delete"
+              title="删除"
+              :disabled="deleleOptionDisabled"
+              @click.prevent="removeQuesOption(oindex)"
+            ></el-button>
+          </div>
+        </div>
+      </el-form-item>
+    </el-form>
+
+    <el-collapse v-model="activeNames">
+      <el-collapse-item
+        v-for="(subq, index) in modalForm.subQuestions"
+        :key="subq.questionKey"
+        :name="subq.questionKey"
+      >
+        <div slot="title" class="subq-header">
+          <h3>第{{ index + 1 }}小题</h3>
+          <div>
+            <el-button
+              v-if="!IS_BANKED_CLOZE"
+              size="mini"
+              type="text"
+              icon="el-icon-delete"
+              title="删除"
+              @click.stop="removeSubQuestion(index)"
+            ></el-button>
+          </div>
+        </div>
+
+        <match-question
+          ref="MatchQuestion"
+          :question="subq"
+          :parent-question="modalForm"
+        ></match-question>
+      </el-collapse-item>
+    </el-collapse>
+
+    <el-button
+      v-if="!IS_BANKED_CLOZE"
+      type="primary"
+      size="small"
+      icon="el-icon-circle-plus-outline"
+      @click="addSubQuestion()"
+      >新增小题</el-button
+    >
+  </div>
 </template>
 </template>
 
 
 <script>
 <script>
+import { isAnEmptyRichText } from "@/utils/utils";
+import { randomCode } from "@/plugins/utils";
+import {
+  getInitQuestionModel,
+  getMatchQuestionModel,
+} from "../model/questionModel";
+import MatchQuestion from "./MatchQuestion.vue";
+
 export default {
 export default {
   name: "BankedClozeQuestion",
   name: "BankedClozeQuestion",
+  components: { MatchQuestion },
+  props: {
+    question: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
   data() {
   data() {
-    return {};
+    return {
+      modalForm: {},
+      activeNames: [],
+      rules: {
+        quesBody: [
+          {
+            validator: (rule, value, callback) => {
+              if (!this.IS_BANKED_CLOZE) return callback();
+
+              if (!value || isAnEmptyRichText(value)) {
+                return callback(new Error(`请输入题干`));
+              }
+              callback();
+            },
+            trigger: "change",
+          },
+        ],
+      },
+      optionRule: {
+        validator: (rule, value, callback) => {
+          if (!value || isAnEmptyRichText(value)) {
+            return callback(new Error(`请输入选项内容`));
+          }
+          callback();
+        },
+        trigger: "change",
+      },
+    };
+  },
+  computed: {
+    IS_BANKED_CLOZE() {
+      return this.modalForm.questionType === "BANKED_CLOZE";
+    },
+    deleleOptionDisabled() {
+      return this.modalForm.quesOptions.length === 1;
+    },
+  },
+  created() {
+    this.initData();
+  },
+  methods: {
+    initData() {
+      this.modalForm = this.$objAssign(
+        getInitQuestionModel("PARAGRAPH_MATCHING"),
+        this.question
+      );
+    },
+    optionBodyChange(oindex) {
+      this.$refs.modalFormComp.validateField(
+        `quesOptions.${oindex}.body`,
+        () => {}
+      );
+    },
+    addQuesOption(index) {
+      if (this.modalForm.quesOptions.length >= 20) {
+        this.$message.error("选项最多20个");
+        return;
+      }
+      this.modalForm.quesOptions.splice(index + 1, 0, {
+        number: 0,
+        body: { sections: [] },
+      });
+      this.resetNumberAndSaveOptions();
+    },
+    removeQuesOption(index) {
+      if (this.deleleOptionDisabled) return;
+      this.modalForm.quesOptions.splice(index, 1);
+      this.resetNumberAndSaveOptions();
+
+      this.$nextTick(() => {
+        for (let i = 0; i < this.$refs.MatchQuestion.length; i++) {
+          this.$refs.MatchQuestion[i].optionChange();
+        }
+      });
+    },
+    resetNumberAndSaveOptions() {
+      this.modalForm.quesOptions.forEach((option, index) => {
+        option.number = index + 1;
+      });
+    },
+    addSubQuestion(index = null) {
+      let newQuestion = getMatchQuestionModel();
+      newQuestion.questionKey = randomCode();
+      const nindex =
+        index === null ? this.modalForm.subQuestions.length : index + 1;
+      this.modalForm.subQuestions.splice(nindex, 0, newQuestion);
+      if (!this.activeNames.includes(newQuestion.questionKey))
+        this.activeNames.push(newQuestion.questionKey);
+    },
+    async removeSubQuestion(index) {
+      if (this.IS_BANKED_CLOZE) return;
+
+      const res = await this.$confirm("确定要删除该小题吗?", "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (res !== "confirm") return;
+
+      this.modalForm.subQuestions.splice(index, 1);
+      const delQuestionKey =
+        this.modalForm.subQuestions[index].newQuestion.questionKey;
+      this.activeNames = this.activeNames.filter((k) => k !== delQuestionKey);
+    },
+    answerPointsChange(answerPointsCount) {
+      let curSubQuestionCount = this.modalForm.subQuestions.length;
+      if (curSubQuestionCount === answerPointsCount) return;
+
+      if (curSubQuestionCount > answerPointsCount) {
+        this.modalForm.subQuestions = this.modalForm.subQuestions.slice(
+          0,
+          answerPointsCount
+        );
+      } else {
+        for (let i = curSubQuestionCount; i < answerPointsCount; i++) {
+          this.addSubQuestion();
+        }
+      }
+    },
+    quesBodyChange() {
+      this.$refs.modalFormComp.validateField(`quesBody`, () => {});
+    },
+    async validate() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      let errorQuestionIndexs = [];
+      for (let i = 0; i < this.$refs.MatchQuestion.length; i++) {
+        const compInst = this.$refs.MatchQuestion[i];
+        const res = await compInst.validate().catch(() => {});
+        if (!res) {
+          errorQuestionIndexs.push(i + 1);
+        }
+      }
+      if (errorQuestionIndexs.length) {
+        this.$message.error(
+          `请完成第${errorQuestionIndexs.join()}小题的编辑!`
+        );
+        return Promise.reject();
+      }
+
+      return true;
+    },
+    getData() {
+      this.modalForm.subQuestions = this.$refs.MatchQuestion.map((compInst) =>
+        compInst.getData()
+      );
+      return this.modalForm;
+    },
   },
   },
-  methods: {},
 };
 };
 </script>
 </script>

+ 131 - 0
src/modules/question/components/edit/MatchQuestion.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="match-question">
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-form-item v-if="IS_PARAGRAPH_MATCHING" prop="quesBody" label="题干">
+        <v-editor
+          v-model="modalForm.quesBody"
+          @change="quesBodyChange"
+        ></v-editor>
+      </el-form-item>
+      <el-form-item prop="quesAnswer" label="答案">
+        <el-radio-group v-model="quesAnswer" @change="answerChange">
+          <el-radio-button
+            v-for="option in parentQuestion.quesOptions"
+            :key="option.number"
+            :label="option.number"
+          >
+            {{ (option.number - 1) | optionOrderWordFilter }}
+          </el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <question-info-edit
+      ref="QuestionInfoEdit"
+      :question="modalForm"
+      @change="questionInfoChange"
+    ></question-info-edit>
+  </div>
+</template>
+
+<script>
+import { isAnEmptyRichText } from "@/utils/utils";
+import QuestionInfoEdit from "../QuestionInfoEdit.vue";
+import { getMatchQuestionModel } from "../model/questionModel";
+
+export default {
+  name: "MatchQuestion",
+  components: { QuestionInfoEdit },
+  props: {
+    question: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    parentQuestion: {
+      type: Object,
+      default() {
+        return {
+          quesOptions: [],
+          questionType: "",
+        };
+      },
+    },
+  },
+  data() {
+    return {
+      modalForm: {},
+      quesAnswer: null,
+      rules: {
+        quesBody: [
+          {
+            validator: (rule, value, callback) => {
+              if (!value || isAnEmptyRichText(value)) {
+                return callback(new Error(`请输入题干`));
+              }
+              callback();
+            },
+            trigger: "change",
+          },
+        ],
+        quesAnswer: [
+          {
+            validator: (rule, value, callback) => {
+              if (!value || !value.length) {
+                return callback(new Error(`请设置答案`));
+              }
+              callback();
+            },
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  computed: {
+    IS_PARAGRAPH_MATCHING() {
+      return this.parentQuestion.questionType === "PARAGRAPH_MATCHING";
+    },
+  },
+  created() {
+    this.initData();
+  },
+  methods: {
+    initData() {
+      this.modalForm = this.$objAssign(getMatchQuestionModel(), this.question);
+      this.quesAnswer = this.question.quesAnswer
+        ? this.question.quesAnswer[0]
+        : null;
+    },
+    quesBodyChange() {
+      this.$refs.modalFormComp.validateField(`quesBody`, () => {});
+    },
+    answerChange() {
+      this.modalForm.quesAnswer =
+        this.quesAnswer || this.quesAnswer === 0 ? [this.quesAnswer] : [];
+      this.$refs.modalFormComp.validateField(`quesAnswer`, () => {});
+    },
+    optionChange() {
+      const optionCount = this.parentQuestion.quesOptions.length;
+      if (!this.quesAnswer || this.quesAnswer <= optionCount) return;
+
+      this.quesAnswer = null;
+      this.answerChange();
+    },
+    questionInfoChange(questionInfo) {
+      this.modalForm = Object.assign({}, this.modalForm, questionInfo);
+    },
+    validate() {
+      return this.$refs.modalFormComp.validate();
+    },
+    getData() {
+      return this.modalForm;
+    },
+  },
+};
+</script>

+ 19 - 5
src/modules/question/components/edit/NestedQuestion.vue

@@ -22,7 +22,7 @@
     <el-collapse v-model="activeNames">
     <el-collapse v-model="activeNames">
       <el-collapse-item
       <el-collapse-item
         v-for="(subq, index) in modalForm.subQuestions"
         v-for="(subq, index) in modalForm.subQuestions"
-        :key="index"
+        :key="subq.questionKey"
         :name="index"
         :name="index"
       >
       >
         <div slot="title" class="subq-header">
         <div slot="title" class="subq-header">
@@ -43,7 +43,6 @@
 
 
         <component
         <component
           :is="getStructTypeComp(subq.questionType)"
           :is="getStructTypeComp(subq.questionType)"
-          :key="subq.questionKey"
           ref="QuestionEditDetail"
           ref="QuestionEditDetail"
           :question="subq"
           :question="subq"
         ></component>
         ></component>
@@ -51,7 +50,7 @@
     </el-collapse>
     </el-collapse>
 
 
     <el-popover
     <el-popover
-      v-if="!IS_CLOZE"
+      v-if="!IS_CLOZE && !IS_LISTENING_QUESTION"
       popper-class="nested-subq-popover"
       popper-class="nested-subq-popover"
       trigger="hover"
       trigger="hover"
     >
     >
@@ -74,6 +73,15 @@
         >新增小题</el-button
         >新增小题</el-button
       >
       >
     </el-popover>
     </el-popover>
+    <!-- 听力题只能添加单选题 -->
+    <el-button
+      v-if="IS_LISTENING_QUESTION"
+      type="primary"
+      size="small"
+      icon="el-icon-circle-plus-outline"
+      @click="addSubQuestion('SINGLE_ANSWER_QUESTION')"
+      >新增小题</el-button
+    >
   </div>
   </div>
 </template>
 </template>
 
 
@@ -135,6 +143,9 @@ export default {
     IS_CLOZE() {
     IS_CLOZE() {
       return this.modalForm.questionType === "CLOZE";
       return this.modalForm.questionType === "CLOZE";
     },
     },
+    IS_LISTENING_QUESTION() {
+      return this.modalForm.questionType === "LISTENING_QUESTION";
+    },
   },
   },
   created() {
   created() {
     this.initData();
     this.initData();
@@ -155,8 +166,8 @@ export default {
       const nindex =
       const nindex =
         index === null ? this.modalForm.subQuestions.length : index + 1;
         index === null ? this.modalForm.subQuestions.length : index + 1;
       this.modalForm.subQuestions.splice(nindex, 0, newQuestion);
       this.modalForm.subQuestions.splice(nindex, 0, newQuestion);
-      if (!this.activeNames.includes(nindex) && !this.IS_CLOZE)
-        this.activeNames.push(nindex);
+      if (!this.activeNames.includes(newQuestion.questionKey) && !this.IS_CLOZE)
+        this.activeNames.push(newQuestion.questionKey);
     },
     },
     async removeSubQuestion(index) {
     async removeSubQuestion(index) {
       if (this.deleleDisabled) return;
       if (this.deleleDisabled) return;
@@ -167,6 +178,9 @@ export default {
       if (res !== "confirm") return;
       if (res !== "confirm") return;
 
 
       this.modalForm.subQuestions.splice(index, 1);
       this.modalForm.subQuestions.splice(index, 1);
+      const delQuestionKey =
+        this.modalForm.subQuestions[index].newQuestion.questionKey;
+      this.activeNames = this.activeNames.filter((k) => k !== delQuestionKey);
     },
     },
     answerPointsChange(answerPointsCount) {
     answerPointsChange(answerPointsCount) {
       let curSubQuestionCount = this.modalForm.subQuestions.length;
       let curSubQuestionCount = this.modalForm.subQuestions.length;

+ 12 - 7
src/modules/question/components/edit/SelectQuestion.vue

@@ -58,7 +58,7 @@
               type="danger"
               type="danger"
               icon="el-icon-delete"
               icon="el-icon-delete"
               title="删除"
               title="删除"
-              :disabled="deleleDisabled"
+              :disabled="deleleOptionDisabled"
               @click.prevent="removeQuesOption(oindex)"
               @click.prevent="removeQuesOption(oindex)"
             ></el-button>
             ></el-button>
           </div>
           </div>
@@ -171,7 +171,7 @@ export default {
         .map((value) => String.fromCharCode(64 + value))
         .map((value) => String.fromCharCode(64 + value))
         .join("");
         .join("");
     },
     },
-    deleleDisabled() {
+    deleleOptionDisabled() {
       return this.modalForm.quesOptions.length === 1;
       return this.modalForm.quesOptions.length === 1;
     },
     },
   },
   },
@@ -184,10 +184,14 @@ export default {
         getInitQuestionModel("SINGLE_ANSWER_QUESTION"),
         getInitQuestionModel("SINGLE_ANSWER_QUESTION"),
         this.question
         this.question
       );
       );
-      if (!this.IS_SIMPLE) {
-        const quesAnswer = this.question.quesAnswer || [];
+      if (this.IS_SIMPLE) {
+        this.quesAnswer = this.question.quesAnswer
+          ? this.question.quesAnswer[0]
+          : null;
+      } else {
+        this.quesAnswer = this.question.quesAnswer || [];
         this.modalForm.quesOptions.forEach((item) => {
         this.modalForm.quesOptions.forEach((item) => {
-          item.isCorrect = quesAnswer.includes(item.number);
+          item.isCorrect = this.quesAnswer.includes(item.number);
         });
         });
       }
       }
     },
     },
@@ -203,7 +207,7 @@ export default {
       this.resetNumberAndSaveOptions();
       this.resetNumberAndSaveOptions();
     },
     },
     removeQuesOption(index) {
     removeQuesOption(index) {
-      if (this.deleleDisabled) return;
+      if (this.deleleOptionDisabled) return;
       this.modalForm.quesOptions.splice(index, 1);
       this.modalForm.quesOptions.splice(index, 1);
       this.resetNumberAndSaveOptions();
       this.resetNumberAndSaveOptions();
     },
     },
@@ -228,7 +232,8 @@ export default {
     },
     },
     answerChange() {
     answerChange() {
       if (this.IS_SIMPLE) {
       if (this.IS_SIMPLE) {
-        this.modalForm.quesAnswer = this.quesAnswer ? [this.quesAnswer] : [];
+        this.modalForm.quesAnswer =
+          this.quesAnswer || this.quesAnswer === 0 ? [this.quesAnswer] : [];
       } else {
       } else {
         this.quesAnswer = this.modalForm.quesOptions
         this.quesAnswer = this.modalForm.quesOptions
           .filter((item) => item.isCorrect)
           .filter((item) => item.isCorrect)

+ 21 - 12
src/modules/question/components/model/questionModel.js

@@ -67,30 +67,29 @@ export const readingComprehensionQuestion = {
 // 段落匹配/选词填空
 // 段落匹配/选词填空
 export const bankedClozeQuestion = {
 export const bankedClozeQuestion = {
   questionType: "BANKED_CLOZE",
   questionType: "BANKED_CLOZE",
-  quesBody: {
-    sections: [],
-  },
-  comment: { sections: [] },
+  quesBody: null,
+  comment: null,
   subQuestions: [
   subQuestions: [
-    // select_question
+    // {
+    //   number: 1,
+    //   quesAnswer: null,
+    //   difficulty: "易",
+    //   quesProperties: [],
+    // },
   ],
   ],
   quesOptions: [
   quesOptions: [
     {
     {
       number: 1,
       number: 1,
-      body: {
-        sections: [{ blocks: [{ type: "text", value: "黑格尔" }] }],
-      },
+      body: null,
     },
     },
     {
     {
       number: 2,
       number: 2,
-      body: {
-        sections: [{ blocks: [{ type: "text", value: "苏格拉底" }] }],
-      },
+      body: null,
     },
     },
   ],
   ],
   param: { matchingMode: 1, matchingType: 1 },
   param: { matchingMode: 1, matchingType: 1 },
   // 段落匹配: "param": { "matchingMode": 2, "matchingType": 2 },
   // 段落匹配: "param": { "matchingMode": 2, "matchingType": 2 },
-  quesAnswer: [{ number: 1, answer: [8] }],
+  // quesAnswer: [{ number: 1, answer: [8] }],
 };
 };
 
 
 const models = {
 const models = {
@@ -110,6 +109,7 @@ const models = {
   }),
   }),
   PARAGRAPH_MATCHING: Object.assign({}, bankedClozeQuestion, {
   PARAGRAPH_MATCHING: Object.assign({}, bankedClozeQuestion, {
     questionType: "PARAGRAPH_MATCHING",
     questionType: "PARAGRAPH_MATCHING",
+    param: { matchingMode: 2, matchingType: 2 },
   }),
   }),
   BANKED_CLOZE: bankedClozeQuestion,
   BANKED_CLOZE: bankedClozeQuestion,
 };
 };
@@ -124,6 +124,15 @@ export const getInitQuestionModel = (qtype) => {
   };
   };
 };
 };
 
 
+export const getMatchQuestionModel = () => {
+  return {
+    number: 1,
+    quesAnswer: null,
+    difficulty: "易",
+    quesProperties: [],
+  };
+};
+
 export const STRUCT_TYPE_COMP_DICT = {
 export const STRUCT_TYPE_COMP_DICT = {
   SINGLE_ANSWER_QUESTION: "select-question",
   SINGLE_ANSWER_QUESTION: "select-question",
   MULTIPLE_ANSWER_QUESTION: "select-question",
   MULTIPLE_ANSWER_QUESTION: "select-question",