zhangjie 1 éve
szülő
commit
1902f3860c

+ 34 - 0
src/modules/question/api.js

@@ -303,3 +303,37 @@ export function questionImportDownloadTemplate(datas) {
     responseType: "blob",
   });
 }
+
+// gpt-question
+export function buildGptQuestionApi(datas) {
+  // auditResult,auditRemark,questionIds
+  return $httpWithMsg.post(
+    `${QUESTION_API}/question/gpt/build`,
+    {},
+    {
+      params: datas,
+    }
+  );
+}
+export function gptQuestionListApi(data = {}) {
+  return $httpWithMsg.post(
+    `${QUESTION_API}/question/gpt/list`,
+    {},
+    { params: data }
+  );
+}
+export function saveGptQuestionApi(datas) {
+  // auditResult,auditRemark,questionIds
+  return $httpWithMsg.post(`${QUESTION_API}/question/gpt/save`, datas);
+}
+export function updateGptQuestionApi(datas) {
+  // auditResult,auditRemark,questionIds
+  return $httpWithMsg.post(`${QUESTION_API}/question/gpt/update`, datas);
+}
+export function deleteGptQuestionApi(ids) {
+  return $httpWithMsg.post(
+    `${QUESTION_API}/question/gpt/delete`,
+    {},
+    { params: { ids: ids.join() } }
+  );
+}

+ 374 - 0
src/modules/question/components/GptQuestionDialog.vue

@@ -0,0 +1,374 @@
+<template>
+  <div>
+    <el-dialog
+      custom-class="gpt-question-dialog"
+      :visible.sync="modalIsShow"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      append-to-body
+      fullscreen
+      @opened="dialogOpened"
+    >
+      <div slot="title">
+        <div>
+          <h2>智能出题</h2>
+          <span>课程代码:{{ formModel.courseCode }}</span>
+          <span>课程名称:{{ formModel.courseName }}</span>
+        </div>
+      </div>
+
+      <div class="part-box">
+        <h2 class="part-box-title">生成试题</h2>
+
+        <el-form :model="formModel" label-width="100px">
+          <el-form-item label="题型">
+            <el-button
+              v-for="item in BASE_QUESTION_TYPES"
+              :key="item.code"
+              :type="
+                formModel.questionType === item.code ? 'primary' : 'default'
+              "
+              size="small"
+              @click="switchQuestionType(item.code)"
+              >{{ item.name }}</el-button
+            >
+          </el-form-item>
+          <el-form-item label="出题数量">
+            <el-input-number
+              v-model="formModel.questionCount"
+              placeholder="出题数量"
+              style="width: 150px"
+              :min="1"
+              :max="100"
+              :step="1"
+              step-strictly
+              :controls="false"
+            ></el-input-number>
+          </el-form-item>
+          <el-form-item v-if="IS_SELECTION_QUESTION" label="选项个数">
+            <el-input-number
+              v-model="formModel.optionCount"
+              placeholder="选项个数"
+              style="width: 150px"
+              :min="1"
+              :max="26"
+              :step="1"
+              step-strictly
+              :controls="false"
+            ></el-input-number>
+          </el-form-item>
+          <el-form-item v-if="IS_BOOLEAN_QUESTION" label="填空个数">
+            <el-input-number
+              v-model="formModel.fillCount"
+              placeholder="填空个数"
+              style="width: 150px"
+              :min="1"
+              :max="100"
+              :step="1"
+              step-strictly
+              :controls="false"
+            ></el-input-number>
+          </el-form-item>
+          <el-form-item label="知识点">
+            <property-tree-select
+              v-model="formModel.questionProperty"
+              :course-id="formModel.courseId"
+              @change="propertyChange"
+            ></property-tree-select>
+            <br />
+            <el-input
+              v-model="formModel.supplement"
+              placeholder="请录入知识点补充说明"
+              type="textarea"
+              clearable
+            ></el-input>
+          </el-form-item>
+
+          <el-form-item>
+            <el-button type="danger" @click="toProduct">生成试题</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <div class="part-box">
+        <div class="box-justify">
+          <h2 class="part-box-title">检查试题</h2>
+          <div>
+            <el-button
+              type="danger"
+              :disabled="loading"
+              @click="toBatchDeleteQuestion"
+              >批量删除</el-button
+            >
+            <el-button
+              type="primary"
+              :disabled="loading"
+              @click="toSaveQuestion"
+              >加入题库</el-button
+            >
+          </div>
+        </div>
+
+        <el-table
+          v-loading="loading"
+          element-loading-text="加载中"
+          :data="questionList"
+          @selection-change="tableSelectChange"
+        >
+          <el-table-column
+            type="selection"
+            width="50"
+            align="center"
+          ></el-table-column>
+          <el-table-column label="试题" min-width="200">
+            <div slot-scope="scope">
+              <rich-text
+                class="row-question-body"
+                title="点击查看试题"
+                :text-json="scope.row.quesBody"
+              ></rich-text>
+            </div>
+          </el-table-column>
+          <el-table-column label="课程" width="120">
+            <template slot-scope="scope">
+              <span>{{ scope.row.course.name }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="题型" prop="sourceDetailName" width="100">
+          </el-table-column>
+          <el-table-column label="操作" width="180" fixed="right">
+            <template slot-scope="scope">
+              <div class="operate_left">
+                <el-button
+                  size="mini"
+                  type="primary"
+                  plain
+                  @click="toEditQuestion(scope.row)"
+                  >查看</el-button
+                >
+                <el-button
+                  size="mini"
+                  type="danger"
+                  plain
+                  @click="toDeleteQuestion(scope.row)"
+                  >删除</el-button
+                >
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <div class="part-page">
+          <el-pagination
+            :current-page="currentPage"
+            :page-size="pageSize"
+            :page-sizes="[10, 20, 50, 100, 200, 300]"
+            layout="total, sizes, prev, pager, next, jumper"
+            :total="total"
+            @current-change="toPage"
+            @size-change="handleSizeChange"
+          >
+          </el-pagination>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- GptQuestionEditDialog -->
+    <gpt-question-edit-dialog
+      ref="GptQuestionEditDialog"
+      :option-info="{ ...searchFormModel, propertyInfos }"
+      :question="curQuestion"
+      @modified="getList"
+    ></gpt-question-edit-dialog>
+  </div>
+</template>
+
+<script>
+import { BASE_QUESTION_TYPES } from "@/constants/constants";
+import PropertyTreeSelect from "./PropertyTreeSelect.vue";
+import GptQuestionEditDialog from "./GptQuestionEditDialog.vue";
+import {
+  buildGptQuestionApi,
+  gptQuestionListApi,
+  saveGptQuestionApi,
+  deleteGptQuestionApi,
+} from "../api";
+
+const initFormModel = {
+  courseId: null,
+  courseCode: "",
+  courseName: "",
+  questionType: "SINGLE_ANSWER_QUESTION",
+  questionCount: null,
+  optionCount: null,
+  fillCount: null,
+  questionProperty: [],
+  supplement: "",
+};
+
+export default {
+  name: "GptQuestionDialog",
+  components: { PropertyTreeSelect, GptQuestionEditDialog },
+  data() {
+    return {
+      taskId: null,
+      formModel: {
+        ...initFormModel,
+      },
+      searchFormModel: {},
+      BASE_QUESTION_TYPES,
+      modalIsShow: false,
+      loading: false,
+      questionList: [],
+      selectedQuestionIds: [],
+      propertyInfos: [],
+      currentPage: 1,
+      pageSize: 10,
+      total: 0,
+      curQuestion: {},
+    };
+  },
+  computed: {
+    IS_SELECTION_QUESTION() {
+      return ["SINGLE_ANSWER_QUESTION", "MULTIPLE_ANSWER_QUESTION"].includes(
+        this.formModel.questionType
+      );
+    },
+    IS_BOOLEAN_QUESTION() {
+      return this.formModel.questionType === "BOOL_ANSWER_QUESTION";
+    },
+    IS_FILL_QUESTION() {
+      return this.formModel.questionType === "FILL_BLANK_QUESTION";
+    },
+  },
+  methods: {
+    dialogOpened() {},
+    async toProduct() {
+      this.loading = true;
+
+      const res = await buildGptQuestionApi({ ...this.formModel }).catch(
+        () => {}
+      );
+      this.loading = false;
+      if (!res) return;
+
+      this.searchFormModel = { ...this.formModel };
+      this.taskId = res.data;
+      this.toPage(1);
+    },
+    toPage(page) {
+      this.currentPage = page;
+      this.getList();
+    },
+    async getList() {
+      // this.selectedQuestionIds = [];
+      // this.questionList = this.questions.slice(
+      //   (this.currentPage - 1) * this.pageSize,
+      //   this.currentPage * this.pageSize
+      // );
+      let data = {
+        taskId: this.taskId,
+        pageNumber: this.currentPage,
+        pageSize: this.pageSize,
+      };
+      const res = await gptQuestionListApi(data).catch(() => {});
+      this.loading = false;
+      if (!res) return;
+      this.questionList = res.data.content;
+      this.total = res.data.totalElements;
+    },
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.toPage(1);
+    },
+    tableSelectChange(selections) {
+      this.selectedQuestionIds = selections.map((item) => item.id);
+    },
+    switchQuestionType(questionType) {
+      this.questionModel.questionType = questionType;
+      this.$nextTick(() => {
+        this.questionTypeChange();
+      });
+    },
+    questionTypeChange() {
+      if (this.IS_FILL_QUESTION) {
+        this.questionModel.optionCount = null;
+      } else if (this.IS_SELECTION_QUESTION) {
+        this.questionModel.fillCount = null;
+      } else {
+        this.questionModel.optionCount = null;
+        this.questionModel.fillCount = null;
+      }
+    },
+    propertyChange(propertyInfos) {
+      this.propertyInfos = propertyInfos;
+    },
+    toEditQuestion(row) {
+      this.curQuestion = row;
+      this.$refs.GptQuestionEditDialog.open();
+    },
+    async toDeleteQuestion(row) {
+      const confirm = await this.$confirm("确认删除选中试题吗?", "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      this.deleteQuestion([row.id]);
+    },
+    async toBatchDeleteQuestion() {
+      if (!this.selectedQuestionIds.length) {
+        this.$message.error("请选择需要删除的试题!");
+        return;
+      }
+      const confirm = await this.$confirm("确认删除选中试题吗?", "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      this.deleteQuestion(this.selectedQuestionIds);
+    },
+    async deleteQuestion(ids) {
+      // this.questions = this.questions.filter((q) => !ids.includes(q.id));
+      // this.total = this.questions.length;
+      // const maxPage = Math.ceil(this.total / this.pageSize);
+      // this.currentPage = Math.min(maxPage, this.currentPage);
+      // this.toPage(this.currentPage);
+
+      this.loading = true;
+      const res = await deleteGptQuestionApi(ids.join()).catch(() => {});
+      this.loading = false;
+      if (!res) return;
+
+      this.$notify({
+        message: "删除成功",
+        type: "success",
+      });
+      this.getList();
+    },
+    async toSaveQuestion() {
+      if (this.loading) return;
+
+      if (!this.selectedQuestionIds.length) {
+        this.$message.error("请选择需要删除的试题!");
+        return;
+      }
+
+      const confirm = await this.$confirm("确认保存所选择的试题吗?", "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") {
+        return;
+      }
+
+      this.loading = true;
+      const datas = {
+        questions: this.selectedQuestionIds,
+      };
+      const res = await saveGptQuestionApi(datas).catch(() => {});
+      this.loading = false;
+      if (!res) return;
+      this.$message.success("保存成功!");
+      this.deleteQuestion(this.selectedQuestionIds);
+    },
+  },
+};
+</script>

+ 218 - 0
src/modules/question/components/GptQuestionEditDialog.vue

@@ -0,0 +1,218 @@
+<template>
+  <el-dialog
+    custom-class="question-edit-dialog"
+    :visible.sync="modalIsShow"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    fullscreen
+    :show-close="false"
+    @open="visibleChange"
+  >
+    <div slot="title">
+      <h2>{{ isEdit ? "编辑试题" : "创建试题" }}</h2>
+      <span>课程代码:{{ formModel.courseCode }}</span>
+      <span>课程名称:{{ formModel.courseName }}</span>
+    </div>
+    <div class="part-box question-edit">
+      <el-form label-width="100px">
+        <el-form-item v-if="isEdit" label="题型">
+          <el-button type="primary" size="small">
+            {{ formModel.questionType | questionTypeFilter }}
+          </el-button>
+        </el-form-item>
+        <el-form-item v-if="IS_SELECTION_QUESTION" label="选项个数">
+          {{ formModel.optionCount }}
+        </el-form-item>
+        <el-form-item v-if="IS_BOOLEAN_QUESTION" label="填空个数">
+          {{ formModel.fillCount }}
+        </el-form-item>
+        <el-form-item label="知识点">
+          <el-tag
+            v-for="content in formModel.quesProperties"
+            :key="content.key"
+            closable
+            effect="dark"
+            type="primary"
+            style="margin-right: 5px; margin-bottom: 5px"
+          >
+            {{ content.courseProperty && content.courseProperty.name }}
+            <span style="margin: 0 3px">/</span>
+            {{ content.firstProperty && content.firstProperty.name }}
+            <span
+              v-if="content.secondProperty && content.secondProperty.name"
+              style="margin: 0 3px"
+              >/</span
+            >
+            {{ content.secondProperty && content.secondProperty.name }}
+          </el-tag>
+          <br />
+          <el-input
+            v-model="formModel.supplement"
+            placeholder="请录入知识点补充说明"
+            type="textarea"
+            clearable
+          ></el-input>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="danger" :disabled="loading" @click="toProduct"
+            >生成试题</el-button
+          >
+        </el-form-item>
+      </el-form>
+      <!-- question-body -->
+      <component
+        :is="structTypeComp"
+        :key="questionKey"
+        ref="QuestionEditDetail"
+        :question="questionModel"
+        :can-edit-question-info="false"
+      ></component>
+    </div>
+    <div slot="footer" class="text-center">
+      <el-button type="primary" :disabled="loading" @click="confirm"
+        >确定</el-button
+      >
+      <el-button @click="cancel"> 取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { STRUCT_TYPE_COMP_DICT } from "./edit/questionModel";
+import BooleanQuestion from "./edit/BooleanQuestion.vue";
+import FillBlankQuestion from "./edit/FillBlankQuestion.vue";
+import SelectQuestion from "./edit/SelectQuestion.vue";
+import TextAnswerQuestion from "./edit/TextAnswerQuestion.vue";
+import { randomCode } from "@/plugins/utils";
+import { buildGptQuestionApi, updateGptQuestionApi } from "../api";
+
+export default {
+  name: "GptQuestionEditDialog",
+  components: {
+    FillBlankQuestion,
+    SelectQuestion,
+    TextAnswerQuestion,
+    BooleanQuestion,
+  },
+  props: {
+    question: {
+      type: Object,
+      default() {
+        return { id: "" };
+      },
+    },
+    optionInfo: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      questionModel: {
+        questionType: "SINGLE_ANSWER_QUESTION",
+      },
+      formModel: {},
+      seletedPropertyInfos: [],
+      questionKey: "",
+      editMode: "question",
+      loading: false,
+    };
+  },
+  computed: {
+    isEdit() {
+      return !!this.question.id;
+    },
+    title() {
+      return this.isEdit ? "编辑试题" : "创建试题";
+    },
+    structTypeComp() {
+      return STRUCT_TYPE_COMP_DICT[this.questionModel.questionType];
+    },
+    IS_BOOLEAN_QUESTION() {
+      return this.questionModel.questionType === "BOOL_ANSWER_QUESTION";
+    },
+    IS_FILL_QUESTION() {
+      return this.questionModel.questionType === "FILL_BLANK_QUESTION";
+    },
+  },
+  methods: {
+    visibleChange() {
+      this.formModel = { ...this.optionInfo };
+      delete this.formModel.propertyInfos;
+      let seletedPropertyInfos = [];
+      this.optionInfo.propertyInfos.forEach((item) => {
+        let nitem = {};
+        nitem.key = item.map((elem) => elem.id).join("_");
+        nitem.courseProperty = item[0];
+        nitem.firstProperty = item[1];
+        if (item[2]) nitem.secondProperty = item[2];
+        seletedPropertyInfos.push(nitem);
+      });
+      this.seletedPropertyInfos = seletedPropertyInfos;
+
+      this.initData(this.question);
+    },
+    initData(question) {
+      const courseInfo = {
+        courseId: question.course.id,
+        courseCode: question.course.code,
+        courseName: question.course.name,
+      };
+      let questionModel = {
+        ...question,
+        ...courseInfo,
+        editMode: this.editMode,
+      };
+
+      if (questionModel.subQuestions && questionModel.subQuestions.length) {
+        questionModel.subQuestions = questionModel.subQuestions.map((q) => {
+          let nq = { ...q, ...courseInfo, editMode: this.editMode };
+          return nq;
+        });
+      }
+      this.questionModel = questionModel;
+
+      this.questionKey = randomCode();
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async toProduct() {
+      this.loading = true;
+
+      const res = await buildGptQuestionApi({ ...this.formModel }).catch(
+        () => {}
+      );
+      this.loading = false;
+      if (!res) return;
+      this.initData(res.data);
+    },
+    async confirm() {
+      const valid = await this.$refs.QuestionEditDetail.validate().catch(
+        () => {}
+      );
+      if (!valid) return;
+
+      if (this.loading) return;
+      this.loading = true;
+
+      let questionModel = this.$refs.QuestionEditDetail.getData();
+      const res = await updateGptQuestionApi(questionModel).catch(() => {});
+      this.loading = false;
+      if (!res) return;
+
+      this.$message.success(this.title + "成功");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 23 - 2
src/modules/question/components/PropertyTreeSelect.vue

@@ -194,14 +194,35 @@ export default {
       this.handleClose();
     },
     checkChange() {
-      const nodes = this.$refs.PropertyTree.getCheckedNodes(true);
+      const nodes = this.$refs.PropertyTree.getCheckedNodes();
       this.selectedPropName = nodes.map((item) => item.name).join(";");
       this.emitChange();
     },
+    getCheckedPropertyInfos() {
+      let propertyInfos = [];
+      const getProperty = (propertyList, parents) => {
+        propertyList.forEach((item) => {
+          let nitem = Object.assign({}, item, { propertyList: null });
+          let nparents = [...parents, nitem];
+          if (this.selectedPropIds.includes(item.id)) {
+            propertyInfos.push(nparents);
+          }
+
+          if (item.propertyList && item.propertyList.length) {
+            getProperty(item.propertyList, nparents);
+          }
+        });
+      };
+
+      getProperty(this.proptree, []);
+
+      return propertyInfos;
+    },
     emitChange() {
       this.selectedPropIds = this.$refs.PropertyTree.getCheckedKeys();
+      const propertyInfos = this.getCheckedPropertyInfos();
       this.$emit("input", this.selectedPropIds);
-      this.$emit("change", this.selectedPropIds);
+      this.$emit("change", propertyInfos);
     },
   },
 };

+ 5 - 0
src/modules/question/components/edit/BooleanQuestion.vue

@@ -31,6 +31,7 @@
       </el-form-item>
     </el-form>
     <question-info-edit
+      v-if="canEditQuestionInfo"
       ref="QuestionInfoEdit"
       :question="modalForm"
       @change="questionInfoChange"
@@ -53,6 +54,10 @@ export default {
         return {};
       },
     },
+    canEditQuestionInfo: {
+      type: Boolean,
+      default: true,
+    },
   },
   data() {
     return {

+ 4 - 0
src/modules/question/components/edit/FillBlankQuestion.vue

@@ -62,6 +62,10 @@ export default {
         return {};
       },
     },
+    canEditQuestionInfo: {
+      type: Boolean,
+      default: true,
+    },
   },
   data() {
     return {

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

@@ -62,6 +62,10 @@ export default {
         };
       },
     },
+    canEditQuestionInfo: {
+      type: Boolean,
+      default: true,
+    },
   },
   data() {
     return {

+ 4 - 0
src/modules/question/components/edit/SelectQuestion.vue

@@ -131,6 +131,10 @@ export default {
       type: Boolean,
       default: true,
     },
+    canEditQuestionInfo: {
+      type: Boolean,
+      default: true,
+    },
   },
   data() {
     return {

+ 4 - 0
src/modules/question/components/edit/TextAnswerQuestion.vue

@@ -49,6 +49,10 @@ export default {
         return {};
       },
     },
+    canEditQuestionInfo: {
+      type: Boolean,
+      default: true,
+    },
   },
   data() {
     return {

+ 17 - 0
src/modules/question/views/QuestionManage.vue

@@ -69,6 +69,13 @@
             @click="toBatchDelete"
             >删除</el-button
           >
+          <el-button
+            type="primary"
+            plain
+            icon="el-icon-tickets"
+            @click="toGPTQuestion"
+            >智能出题</el-button
+          >
           <el-button
             type="primary"
             plain
@@ -451,6 +458,16 @@ export default {
       this.questionImportData = data;
       this.$refs.QuestionImportEdit.open();
     },
+    toGPTQuestion() {
+      if (!this.filter.courseId) {
+        this.$message.error("请先选择课程!");
+        return;
+      }
+      window.sessionStorage.setItem(
+        "courseInfo",
+        JSON.stringify(this.curCourse)
+      );
+    },
   },
 };
 </script>