Browse Source

feat: 添加查看试题功能

xieyong 1 week ago
parent
commit
2d89ffd4e2

+ 11 - 0
src/api/questionBank/questionBank.js

@@ -69,3 +69,14 @@ export async function exportWord(params) {
   });
   return res.data;
 }
+
+/**
+ * 分页查询题库试题
+ */
+export async function pageQuestion(params) {
+  const res = await request.get('/ehs/questionBank/question/page', { params });
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}

+ 10 - 0
src/router/routes.js

@@ -42,6 +42,16 @@ export function getMenuRoutes(menus, homePath) {
       path: REDIRECT_PATH + '/:path(.*)',
       component: RedirectLayout,
       meta: { hideFooter: true }
+    },
+    // 题库试题分页查询(在现有系统中以 tab 形式打开)
+    {
+      path: 'safetyExam/questionBank/questionList',
+      component: () =>
+        import('@/views/safetyExam/questionBank/questionList.vue'),
+      meta: {
+        title: '题库试题',
+        hide: true
+      }
     }
   ];
   // 路由铺平处理

+ 23 - 2
src/views/safetyExam/questionBank/index.vue

@@ -53,6 +53,12 @@
                   @click="handleExport(row)"
                   >导出</el-link
                 >
+                <el-link
+                  type="primary"
+                  :underline="false"
+                  @click="handleViewQuestions(row)"
+                  >查看试题</el-link
+                >
                 <el-link
                   type="danger"
                   :underline="false"
@@ -110,7 +116,11 @@
 <script>
   import maintenance from './maintenance.vue';
   import tabMixins from '@/mixins/tableColumnsMixin';
-  import { getList, remove, exportWord } from '@/api/questionBank/questionBank.js';
+  import {
+    getList,
+    remove,
+    exportWord
+  } from '@/api/questionBank/questionBank.js';
   import { download } from '@/utils/file';
   export default {
     components: {
@@ -150,7 +160,7 @@
           {
             columnKey: 'action',
             label: '操作',
-            width: 180,
+            width: 240,
             align: 'center',
             slot: 'action'
           }
@@ -235,6 +245,17 @@
         };
         this.exportDialogVisible = true;
       },
+      // 查看试题(在系统中以 tab 打开分页查询结果)
+      handleViewQuestions(row) {
+        const query = {
+          questionBankId: row.id,
+          questionBankName: row.questionBankName || ''
+        };
+        this.$router.push({
+          path: '/safetyExam/questionBank/questionList',
+          query
+        });
+      },
       // 重置导出表单
       resetExportForm() {
         this.exportForm = {

+ 374 - 0
src/views/safetyExam/questionBank/questionList.vue

@@ -0,0 +1,374 @@
+<template>
+  <div class="ele-body question-list-page">
+    <el-card shadow="never">
+      <!-- 顶部筛选 -->
+      <div class="filter-bar">
+        <el-form :inline="true" :model="filter" size="small" @submit.native.prevent>
+          <el-form-item label="题库名称:">
+            <el-input
+              v-model="filter.questionBankName"
+              placeholder="支持模糊查询"
+              clearable
+              style="width: 200px"
+            />
+          </el-form-item>
+          <el-form-item label="试题内容:">
+            <el-input
+              v-model="filter.questionContent"
+              placeholder="支持模糊查询"
+              clearable
+              style="width: 220px"
+            />
+          </el-form-item>
+          <el-form-item label="试题类型:">
+            <el-select
+              v-model="filter.questionType"
+              placeholder="全部"
+              clearable
+              style="width: 160px"
+            >
+              <el-option label="单选题" value="SINGLE" />
+              <el-option label="多选题" value="MULTIPLE" />
+              <el-option label="判断题" value="JUDGE" />
+              <el-option label="问答题" value="QUESTION_ANSWER" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="el-icon-search" @click="handleSearch"
+              >查询</el-button
+            >
+            <el-button icon="el-icon-refresh" @click="handleReset"
+              >重置</el-button
+            >
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <!-- 顶部信息 -->
+      <div class="page-meta">
+        <span>题库:{{ questionBankName || '-' }}</span>
+        <span>共 {{ total }} 条</span>
+      </div>
+
+      <!-- 试题列表 -->
+      <div v-loading="loading" class="question-list">
+        <el-empty v-if="!loading && list.length === 0" description="暂无试题数据" />
+        <el-card
+          v-for="(item, index) in list"
+          :key="item.id || index"
+          shadow="hover"
+          class="question-item"
+        >
+          <div class="question-header">
+            <span class="question-no">第 {{ item.sortNo || index + 1 }} 题</span>
+            <el-tag :type="typeTagType(item.questionType)" size="mini">
+              {{ typeLabel(item.questionType) }}
+            </el-tag>
+            <span class="question-score">分值:{{ item.questionScore ?? 0 }}</span>
+          </div>
+
+          <div class="question-title">
+            <span class="label">题目:</span>
+            <span class="content">{{ item.questionContent || '-' }}</span>
+            <el-image
+              v-if="item.questionImage"
+              class="question-image"
+              :src="resolveImageUrl(item.questionImage)"
+              :preview-src-list="[resolveImageUrl(item.questionImage)]"
+              fit="cover"
+            />
+          </div>
+
+          <!-- 选项 -->
+          <div
+            v-if="item.optionList && item.optionList.length"
+            class="question-options"
+          >
+            <div class="label">选项:</div>
+            <div class="options">
+              <div
+                v-for="(opt, optIndex) in item.optionList"
+                :key="opt.id"
+                class="option-row"
+              >
+                <span
+                  v-if="item.questionType !== 'QUESTION_ANSWER'"
+                  class="option-key"
+                  >{{ optionPrefix(optIndex) }}.</span
+                >
+                <span class="option-content">{{ opt.optionContent || '-' }}</span>
+                <el-image
+                  v-if="opt.optionImage"
+                  class="option-image"
+                  :src="resolveImageUrl(opt.optionImage)"
+                  :preview-src-list="[resolveImageUrl(opt.optionImage)]"
+                  fit="cover"
+                />
+              </div>
+            </div>
+          </div>
+
+          <!-- 答案 -->
+          <div class="question-answer">
+            <span class="label">答案:</span>
+            <span class="content answer-text">{{
+              formatAnswer(item)
+            }}</span>
+            <span v-if="item.lessScore" class="less-score">
+              少选得分:{{ item.lessScore }}
+            </span>
+          </div>
+
+          <!-- 解析 -->
+          <div v-if="item.questionExplanation" class="question-explanation">
+            <span class="label">解析:</span>
+            <span class="content">{{ item.questionExplanation }}</span>
+          </div>
+        </el-card>
+      </div>
+
+      <!-- 分页 -->
+      <div class="pagination-wrapper">
+        <el-pagination
+          background
+          layout="total, prev, pager, next, jumper"
+          :total="total"
+          :current-page.sync="filter.pageNum"
+          :page-size="filter.size"
+          @current-change="loadList"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+  import { pageQuestion } from '@/api/questionBank/questionBank.js';
+  import { getImageUrl } from '@/utils/file';
+
+  const TYPE_MAP = {
+    SINGLE: '单选题',
+    MULTIPLE: '多选题',
+    JUDGE: '判断题',
+    QUESTION_ANSWER: '问答题'
+  };
+
+  const TYPE_TAG_TYPE = {
+    SINGLE: '',
+    MULTIPLE: 'success',
+    JUDGE: 'warning',
+    QUESTION_ANSWER: 'info'
+  };
+
+  export default {
+    name: 'QuestionBankQuestionList',
+    data() {
+      return {
+        loading: false,
+        list: [],
+        total: 0,
+        questionBankId: '',
+        questionBankName: '',
+        filter: {
+          questionBankId: '',
+          questionBankName: '',
+          questionContent: '',
+          questionType: '',
+          pageNum: 1,
+          size: 10
+        }
+      };
+    },
+    created() {
+      const { questionBankId, questionBankName } = this.$route.query;
+      this.questionBankId = questionBankId || '';
+      this.questionBankName = questionBankName || '';
+      this.filter.questionBankId = this.questionBankId;
+      this.filter.questionBankName = this.questionBankName;
+      this.loadList();
+    },
+    methods: {
+      typeLabel(type) {
+        return TYPE_MAP[type] || type || '-';
+      },
+      typeTagType(type) {
+        return TYPE_TAG_TYPE[type] || '';
+      },
+      formatAnswer(item) {
+        const answer = item?.correctAnswer;
+        if (!answer) return '-';
+        const type = item?.questionType || '';
+        if (type === 'JUDGE') {
+          return /^(true|1|A|正确)$/i.test(String(answer)) ? '正确' : '错误';
+        }
+        if (type === 'MULTIPLE' && typeof answer === 'string') {
+          return answer
+            .split(',')
+            .map((s) => s.trim())
+            .filter(Boolean)
+            .join('、');
+        }
+        return answer;
+      },
+      /* 选项序号:单选/多选/判断返回 A、B、C...,问答题不显示 */
+      optionPrefix(index) {
+        return String.fromCharCode(65 + index);
+      },
+      /* storePath 拼接为可访问的图片地址 */
+      resolveImageUrl(storePath) {
+        if(storePath.startsWith('http')) {
+          return storePath;
+        }
+        return storePath ? getImageUrl(storePath) : '';
+      },
+      async loadList() {
+        if (!this.filter.questionBankId) {
+          this.$message.warning('缺少题库ID参数');
+          return;
+        }
+        this.loading = true;
+        try {
+          const data = await pageQuestion({
+            questionBankId: this.filter.questionBankId,
+            questionBankName: this.filter.questionBankName || undefined,
+            questionContent: this.filter.questionContent || undefined,
+            questionType: this.filter.questionType || undefined,
+            pageNum: this.filter.pageNum,
+            size: this.filter.size
+          });
+          this.list = data.list || [];
+          this.total = data.count || 0;
+          if (data.questionBankName && !this.questionBankName) {
+            this.questionBankName = data.questionBankName;
+          }
+        } catch (error) {
+          this.$message.error(error.message || '查询试题失败');
+          this.list = [];
+          this.total = 0;
+        } finally {
+          this.loading = false;
+        }
+      },
+      handleSearch() {
+        this.filter.pageNum = 1;
+        this.loadList();
+      },
+      handleReset() {
+        this.filter.questionContent = '';
+        this.filter.questionType = '';
+        this.filter.questionBankName = this.questionBankName;
+        this.filter.pageNum = 1;
+        this.loadList();
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .question-list-page {
+    .filter-bar {
+      margin-bottom: 16px;
+      padding-bottom: 16px;
+      border-bottom: 1px solid #ebeef5;
+    }
+    .page-meta {
+      margin-bottom: 12px;
+      color: #606266;
+      font-size: 14px;
+      display: flex;
+      gap: 24px;
+    }
+    .question-list {
+      min-height: 200px;
+    }
+    .question-item {
+      margin-bottom: 16px;
+      ::v-deep .el-card__body {
+        padding: 16px 20px;
+      }
+    }
+    .question-header {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      margin-bottom: 10px;
+      .question-no {
+        font-weight: 600;
+        color: #303133;
+      }
+      .question-score {
+        color: #909399;
+        font-size: 13px;
+      }
+    }
+    .question-title,
+    .question-options,
+    .question-answer,
+    .question-explanation {
+      margin-bottom: 8px;
+      font-size: 14px;
+      line-height: 1.7;
+      color: #303133;
+      .label {
+        color: #909399;
+        margin-right: 6px;
+      }
+      .content {
+        color: #303133;
+      }
+    }
+    .question-options {
+      display: flex;
+      .options {
+        flex: 1;
+        .option-row {
+          padding: 2px 0;
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          .option-key {
+            font-weight: 600;
+            color: #303133;
+            min-width: 22px;
+          }
+        }
+      }
+    }
+    .question-image {
+      width: 60px;
+      height: 60px;
+      margin-left: 8px;
+      border-radius: 4px;
+      border: 1px solid #ebeef5;
+      vertical-align: middle;
+      ::v-deep img {
+        width: 60px;
+        height: 60px;
+      }
+    }
+    .option-image {
+      width: 40px;
+      height: 40px;
+      border-radius: 4px;
+      border: 1px solid #ebeef5;
+      ::v-deep img {
+        width: 40px;
+        height: 40px;
+      }
+    }
+    .answer-text {
+      color: #67c23a;
+      font-weight: 600;
+    }
+    .less-score {
+      margin-left: 12px;
+      color: #e6a23c;
+      font-size: 13px;
+    }
+    .pagination-wrapper {
+      margin-top: 16px;
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+</style>