Bläddra i källkod

feat: 题库管理页面

wuzh 1 vecka sedan
förälder
incheckning
76f1288d38

+ 9 - 8
src/api/questionBank/questionBank.js

@@ -1,3 +1,5 @@
+import request from '@/utils/request';
+
 /**
  * 列表
  */
@@ -13,7 +15,7 @@ export async function getList(params) {
  * 详情
  */
 export async function getById(id) {
-  const res = await request.get('/ehs/questionBank/getById/${id}');
+  const res = await request.get(`/ehs/questionBank/getById/${id}`);
   if (res.data.code == 0) {
     return res.data.data;
   }
@@ -24,7 +26,7 @@ export async function getById(id) {
 export async function save(params) {
   const res = await request.post(`/ehs/questionBank/save`, params);
   if (res.data.code == 0) {
-    return res.data.message;
+    return res.data.data || res.data.message;
   }
   return Promise.reject(new Error(res.data.message));
 }
@@ -33,28 +35,27 @@ export async function save(params) {
 export async function edit(params) {
   const res = await request.put(`/ehs/questionBank/update`, params);
   if (res.data.code == 0) {
-    return res.data.message;
+    return res.data.data || res.data.message;
   }
   return Promise.reject(new Error(res.data.message));
 }
 
 // 删除
 export async function remove(data) {
-  const res = await request.delete(`/ehs/questionBank/delete`,  { data });
+  const res = await request.delete(`/ehs/questionBank/delete`, { data });
   if (res.data.code == 0) {
     return res.data.message;
   }
   return Promise.reject(new Error(res.data.message));
 }
 
-
 /**
  *  删除单个试题
- * */ 
+ * */
 export async function deleteQuestion(questionId) {
-  const res = await request.delete(`/ehs/questionBank/dquestion/${questionId}`);
+  const res = await request.delete(`/ehs/questionBank/question/${questionId}`);
   if (res.data.code == 0) {
     return res.data.message;
   }
   return Promise.reject(new Error(res.data.message));
-}
+}

+ 27 - 1
src/views/safetyExam/paperManagement/question-form.vue

@@ -66,7 +66,11 @@
 
     <!-- 选项 -->
     <el-form-item
-      v-if="innerValue.type === '1' || innerValue.type === '2' || innerValue.type === '3'"
+      v-if="
+        innerValue.type === '1' ||
+        innerValue.type === '2' ||
+        innerValue.type === '3'
+      "
       label="选项:"
       prop="options"
     >
@@ -172,6 +176,28 @@
           ],
           score: [
             { required: true, message: '请输入试题分数', trigger: 'blur' }
+          ],
+          options: [
+            {
+              validator: (rule, value, callback) => {
+                if (this.innerValue.type === '2') {
+                  if (!this.innerValue.rightAnswers.length) {
+                    callback(new Error('请选择正确答案'));
+                    return;
+                  }
+                } else if (
+                  this.innerValue.type === '1' ||
+                  this.innerValue.type === '3'
+                ) {
+                  if (!this.innerValue.rightAnswer) {
+                    callback(new Error('请选择正确答案'));
+                    return;
+                  }
+                }
+                callback();
+              },
+              trigger: 'change'
+            }
           ]
         }
       };

+ 18 - 18
src/views/safetyExam/questionBank/index.vue

@@ -68,14 +68,13 @@
 <script>
   import maintenance from './maintenance.vue';
   import tabMixins from '@/mixins/tableColumnsMixin';
-  import {getList} from '@/api/questionBank/questionBank.js';
+  import { getList, remove } from '@/api/questionBank/questionBank.js';
   export default {
     components: {
       maintenance
     },
     mixins: [tabMixins],
-    async created(){
-    },
+    async created() {},
     data() {
       return {
         columns: [
@@ -121,14 +120,14 @@
         return [
           {
             label: '题库名称:',
-            value: 'name',
+            value: 'questionBankName',
             type: 'input',
             width: 220,
             placeholder: '请输入题库名称'
           },
           {
             label: '所属部门:',
-            value: 'deptId',
+            value: 'departmentId',
             type: 'deptSelect',
             width: 220,
             placeholder: '请选择所属部门'
@@ -136,20 +135,20 @@
         ];
       }
     },
-    
+
     methods: {
-      async loadPageData(){
+      async loadPageData() {
         let data = await getPage();
         if (res?.code === '0') {
-            this.treeList = res.data;
-            this.currentId = res.data[0].id;
-            this.current = res.data[0];
-            this.$nextTick(() => {
-              this.$refs.treeRef.setCurrentKey(res.data[0].id);
-            });
-            this.$refs.listRef.reload();
-            return this.treeList;
-          }
+          this.treeList = res.data;
+          this.currentId = res.data[0].id;
+          this.current = res.data[0];
+          this.$nextTick(() => {
+            this.$refs.treeRef.setCurrentKey(res.data[0].id);
+          });
+          this.$refs.listRef.reload();
+          return this.treeList;
+        }
       },
       // 新建
       handleAdd() {
@@ -170,8 +169,9 @@
           cancelButtonText: '取消',
           type: 'warning'
         })
-          .then(() => {
-            this.$message.success('删除成功');
+          .then(() => remove([row.id]))
+          .then((message) => {
+            this.$message.success(message || '删除成功');
             this.$refs.table.reload();
           })
           .catch(() => {});

+ 248 - 29
src/views/safetyExam/questionBank/maintenance.vue

@@ -63,16 +63,29 @@
           highlight-current
           :props="{ label: 'label', children: 'children' }"
           @node-click="handleQuestionClick"
-        />
+        >
+          <span class="question-node" slot-scope="{ node, data }">
+            <span>{{ node.label }}</span>
+            <el-tooltip
+              v-if="!isView && data.question"
+              content="删除试题"
+              placement="top"
+            >
+              <el-button
+                type="text"
+                class="question-delete"
+                icon="el-icon-delete"
+                aria-label="删除试题"
+                @click.stop="handleDeleteQuestion(data)"
+              />
+            </el-tooltip>
+          </span>
+        </el-tree>
       </div>
 
       <!-- 右侧试题表单 -->
       <div class="right-panel">
-        <question-form
-          ref="questionFormRef"
-          v-model="form"
-          :is-view="isView"
-        />
+        <question-form ref="questionFormRef" v-model="form" :is-view="isView" />
       </div>
     </div>
 
@@ -93,6 +106,27 @@
 <script>
   import DeptSelect from '@/components/CommomSelect/dept-select.vue';
   import QuestionForm from '../paperManagement/question-form.vue';
+  import {
+    deleteQuestion,
+    edit as updateQuestionBank,
+    getById,
+    getList,
+    save as saveQuestionBank
+  } from '@/api/questionBank/questionBank.js';
+
+  const questionTypeMap = {
+    1: 'SINGLE',
+    2: 'MULTIPLE',
+    3: 'JUDGE',
+    4: 'QUESTION_ANSWER'
+  };
+
+  const questionTypeReverseMap = {
+    SINGLE: '1',
+    MULTIPLE: '2',
+    JUDGE: '3',
+    QUESTION_ANSWER: '4'
+  };
 
   const defBankForm = {
     id: '',
@@ -102,6 +136,7 @@
   };
 
   const defQuestionForm = {
+    id: '',
     sort: '',
     content: '',
     type: '1',
@@ -118,6 +153,8 @@
     analysis: ''
   };
 
+  const createQuestionList = () => [];
+
   export default {
     components: {
       DeptSelect,
@@ -139,11 +176,7 @@
           ]
         },
         form: JSON.parse(JSON.stringify(defQuestionForm)),
-        questionList: [
-          { id: '1', label: '第1题' },
-          { id: '2', label: '第2题' },
-          { id: '3', label: '第3题' }
-        ]
+        questionList: createQuestionList()
       };
     },
     computed: {
@@ -160,27 +193,44 @@
       }
     },
     methods: {
-      open(row, type = 'add') {
+      async open(row, type = 'add') {
         this.visible = true;
         this.dialogType = type;
-        if (row && row.id) {
-          this.bankForm = JSON.parse(
-            JSON.stringify({ ...defBankForm, ...row })
-          );
+        const questionBankId = row && (row.id || row.questionBankId);
+        if (questionBankId) {
+          this.bankForm = {
+            id: questionBankId,
+            name: row.questionBankName || row.name || '',
+            deptId: row.departmentId || row.deptId || '',
+            deptName: row.departmentName || row.deptName || ''
+          };
+          this.loading = true;
+          try {
+            await this.loadDetail(questionBankId, type === 'view');
+          } catch (error) {
+            this.$message.error(error.message || '获取题库详情失败');
+            this.cancel();
+          } finally {
+            this.loading = false;
+          }
         } else {
           this.bankForm = JSON.parse(JSON.stringify(defBankForm));
+          this.questionList = createQuestionList();
+          this.form = JSON.parse(JSON.stringify(defQuestionForm));
+          this.$nextTick(() => {
+            this.$refs.treeRef && this.$refs.treeRef.setCurrentKey('1');
+          });
         }
-        this.form = JSON.parse(JSON.stringify(defQuestionForm));
         this.$nextTick(() => {
           this.$refs.bankForm && this.$refs.bankForm.clearValidate();
           this.$refs.questionFormRef &&
             this.$refs.questionFormRef.clearValidate();
-          this.$refs.treeRef && this.$refs.treeRef.setCurrentKey('1');
         });
       },
       cancel() {
         this.bankForm = JSON.parse(JSON.stringify(defBankForm));
         this.form = JSON.parse(JSON.stringify(defQuestionForm));
+        this.questionList = createQuestionList();
         this.visible = false;
       },
       onDeptChange(deptId, node) {
@@ -191,12 +241,152 @@
         console.log('搜索试题', this.questionSearch);
       },
       handleQuestionClick(data) {
-        // TODO: 切换试题加载对应数据
-        console.log('切换试题', data);
+        if (!data.question) return;
+        this.form = this.mapQuestionToForm(data.question);
+        this.$nextTick(() => {
+          this.$refs.questionFormRef &&
+            this.$refs.questionFormRef.clearValidate();
+        });
+      },
+      async handleDeleteQuestion(data) {
+        try {
+          await this.$confirm('确认删除该试题吗?', '提示', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+          });
+          const message = await deleteQuestion(data.id);
+          await this.loadDetail(this.bankForm.id, false);
+          this.$message.success(message || '删除成功');
+          this.$emit('reload');
+        } catch (error) {
+          if (error !== 'cancel' && error !== 'close') {
+            this.$message.error(error.message || '删除失败');
+          }
+        }
+      },
+      mapQuestionToForm(question) {
+        const type = questionTypeReverseMap[question.questionType] || '1';
+        const options = [...(question.optionList || [])]
+          .sort((a, b) => Number(a.sortNo) - Number(b.sortNo))
+          .map((option, index) => ({
+            key: String.fromCharCode(65 + index),
+            content: option.optionContent || ''
+          }));
+        const correctAnswer = question.correctAnswer || '';
+        return {
+          id: question.id || '',
+          sort: question.sortNo === undefined ? '' : String(question.sortNo),
+          content: question.questionContent || '',
+          type,
+          category:
+            question.professionalType === undefined
+              ? ''
+              : String(question.professionalType),
+          score:
+            question.questionScore === undefined
+              ? ''
+              : String(question.questionScore),
+          options,
+          rightAnswer: type === '2' ? '' : correctAnswer,
+          rightAnswers:
+            type === '2' ? correctAnswer.split(',').filter((item) => item) : [],
+          analysis: question.questionExplanation || ''
+        };
+      },
+      async loadDetail(questionBankId, selectFirstQuestion = true) {
+        const detail = await getById(questionBankId);
+        this.bankForm = {
+          id: detail.id || questionBankId,
+          name: detail.questionBankName || '',
+          deptId: detail.departmentId || '',
+          deptName: detail.departmentName || ''
+        };
+        const questions = [...(detail.questionList || [])].sort(
+          (a, b) => Number(a.sortNo) - Number(b.sortNo)
+        );
+        this.questionList = questions.map((question, index) => ({
+          id: question.id,
+          label: `第${question.sortNo || index + 1}题`,
+          question
+        }));
+        this.form =
+          selectFirstQuestion && questions.length
+            ? this.mapQuestionToForm(questions[0])
+            : JSON.parse(JSON.stringify(defQuestionForm));
+        this.$nextTick(() => {
+          if (selectFirstQuestion && questions.length && this.$refs.treeRef) {
+            this.$refs.treeRef.setCurrentKey(questions[0].id);
+          } else if (this.$refs.treeRef) {
+            this.$refs.treeRef.setCurrentKey(null);
+          }
+        });
       },
       handleReset() {
         this.form = JSON.parse(JSON.stringify(defQuestionForm));
-        this.$refs.questionFormRef && this.$refs.questionFormRef.clearValidate();
+        this.$refs.questionFormRef &&
+          this.$refs.questionFormRef.clearValidate();
+      },
+      toNumber(value) {
+        return value === '' || value === null || value === undefined
+          ? undefined
+          : Number(value);
+      },
+      buildSavePayload() {
+        const isMultiple = this.form.type === '2';
+        return {
+          questionBankId: this.bankForm.id || undefined,
+          questionBankName: this.bankForm.name,
+          departmentId: this.bankForm.deptId,
+          departmentName: this.bankForm.deptName,
+          question: {
+            questionId: this.form.id || undefined,
+            sortNo: this.toNumber(this.form.sort),
+            questionContent: this.form.content,
+            questionType: questionTypeMap[this.form.type],
+            professionalType: this.toNumber(this.form.category),
+            questionScore: this.toNumber(this.form.score),
+            correctAnswer: isMultiple
+              ? this.form.rightAnswers.join(',')
+              : this.form.rightAnswer,
+            questionExplanation: this.form.analysis,
+            optionList: this.form.options.map((item, index) => ({
+              optionContent: item.content,
+              sortNo: index + 1
+            }))
+          }
+        };
+      },
+      async resolveQuestionBankId(saveResult) {
+        if (
+          typeof saveResult === 'number' ||
+          (typeof saveResult === 'string' && /^\d+$/.test(saveResult))
+        ) {
+          return saveResult;
+        }
+        const resultId =
+          typeof saveResult === 'object'
+            ? saveResult.id || saveResult.questionBankId
+            : undefined;
+        if (resultId) {
+          return resultId;
+        }
+
+        const result = await getList({
+          questionBankName: this.bankForm.name,
+          departmentId: this.bankForm.deptId,
+          pageNum: 1,
+          size: 20
+        });
+        const questionBank = (result.list || []).find(
+          (item) =>
+            item.questionBankName === this.bankForm.name &&
+            String(item.departmentId) === String(this.bankForm.deptId)
+        );
+        if (!questionBank) {
+          throw new Error('题库已保存,但未能获取题库ID');
+        }
+        return questionBank.id;
       },
       saveNext() {
         this.$refs.bankForm.validate((bankValid) => {
@@ -204,14 +394,31 @@
           this.$refs.questionFormRef.validate((valid) => {
             if (valid) {
               this.loading = true;
-              setTimeout(() => {
-                this.loading = false;
-                this.$message.success('保存成功');
-                this.form = JSON.parse(JSON.stringify(defQuestionForm));
-                this.$refs.questionFormRef &&
-                  this.$refs.questionFormRef.clearValidate();
-                // TODO: 切换到下一题
-              }, 500);
+              const payload = this.buildSavePayload();
+              const saveRequest = this.bankForm.id
+                ? updateQuestionBank
+                : saveQuestionBank;
+              saveRequest(payload)
+                .then(async (saveResult) => {
+                  if (!this.bankForm.id) {
+                    this.bankForm.id = await this.resolveQuestionBankId(
+                      saveResult
+                    );
+                  }
+                  await this.loadDetail(this.bankForm.id, false);
+                  this.$message.success('保存成功');
+                  this.$emit('reload');
+                  this.$nextTick(() => {
+                    this.$refs.questionFormRef &&
+                      this.$refs.questionFormRef.clearValidate();
+                  });
+                })
+                .catch((error) => {
+                  this.$message.error(error.message || '保存失败');
+                })
+                .finally(() => {
+                  this.loading = false;
+                });
             }
           });
         });
@@ -270,6 +477,18 @@
         border-color: #409eff;
       }
     }
+
+    .question-node {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      flex: 1;
+      padding-right: 8px;
+    }
+
+    .question-delete {
+      color: #f56c6c;
+    }
   }
 
   .right-panel {