Files
aue/docs/superpowers/specs/2026-05-16-question-management-design.md
2026-06-03 13:16:30 +08:00

20 KiB
Raw Permalink Blame History

题目管理系统设计方案

日期: 2026-05-16 基于: 前端优化方案与设计规范.md 模式: 集成式设计(生成→待审核列表→操作)


一、系统架构

1.1 数据流图

[用户操作]
    ↓
[GeneratePanel.vue]
    ├─ 配置区域 (文件选择/题型/难度)
    ├─ 生成按钮 → POST /api/exam/generate
    ↓
[待审核列表区域]
    ├─ 初始化: GET /api/question/pending (自动加载)
    ├─ 搜索/筛选
    ├─ 题目卡片展示
    │   ├─ 审批: PUT /api/question/review/{id}
    │   ├─ 编辑: PUT /api/question/{id}
    │   └─ 发布: PUT /api/question/review/{id} (status=published)
    └─ 批量操作

1.2 状态管理

// useQuestionManagement.js (新增composable)
const state = reactive({
  // 生成相关
  isGenerating: false,
  generatedCount: 0,

  // 列表相关
  pendingQuestions: [],        // 待审核题目列表
  loading: false,
  total: 0,
  currentPage: 1,
  pageSize: 10,

  // 筛选条件
  searchKeyword: '',
  statusFilter: 'all',         // all/pending/approved/rejected/published
  typeFilter: 'all',           // all/single_choice/multiple_choice/...

  // 编辑状态
  editingQuestion: null,       // 当前编辑的题目
  showEditModal: false,

  // 批量操作
  selectedIds: [],             // 选中的题目ID
})

1.3 接口调用规划

功能 方法 接口 触发时机
生成题目 POST /api/exam/generate 点击"开始生成"按钮
获取待审核列表 GET /api/question/pending 页面加载/生成完成/刷新
搜索题目 GET /api/question/pending?keyword=xxx 输入搜索关键词
筛选题目 GET /api/question/pending?status=xxx&type=xxx 选择筛选条件
审批通过 PUT /api/question/review/{id}?status=approved 点击"通过"按钮
驳回题目 PUT /api/question/review/{id}?status=rejected&comment=xxx 点击"驳回"+填写原因
编辑题目 PUT /api/question/{id} 点击"编辑"→修改→保存
发布题目 PUT /api/question/review/{id}?status=published 点击"发布"按钮
批量审批 PUT /api/question/batch-review 选择多条→批量操作
删除题目 DELETE /api/question/{id} 点击"删除"

二、UI组件设计遵循设计规范

2.1 组件结构树

GeneratePanel.vue (主容器)
├── PageHeader.vue (标题栏)
├── StatsRow.vue (统计信息)
├── ContentCard: "智能出题配置"
│   ├── FileSelector (文件选择器)
│   ├── DifficultySelect (难度选择)
│   ├── TypeConfigGrid (题型数量配置)
│   └── GenerateButton (生成按钮)
├── ContentCard: "待审核题目管理"
│   ├── Toolbar (工具栏)
│   │   ├── SearchInput (搜索框)
│   │   ├── StatusFilter (状态筛选)
│   │   ├── TypeFilter (题型筛选)
│   │   └── BatchActions (批量操作按钮组)
│   ├── QuestionList (题目列表)
│   │   └── QuestionCard (题目卡片) *N
│   │       ├── QuestionHeader (题号+类型+状态)
│   │       ├── QuestionContent (题干内容)
│   │       ├── QuestionOptions (选项展示)
│   │       └── ActionButtons (操作按钮组)
│   └── Pagination (分页器)
└── EditModal.vue (编辑弹窗) [可选]

2.2 题目卡片设计QuestionCard

视觉规范

/* 白色体系 + 设计规范 */
.question-card {
  background: #FFFFFF;
  border: 1px solid #E9ECEF;           /* --border-light */
  border-radius: 8px;                   /* --radius-lg */
  box-shadow: 0 1px 2px rgba(0,0,0,0.04); /* --shadow-sm */
  padding: 16px;                        /* --space-lg */
  margin-bottom: 12px;                  /* --space-md */
  transition: all 0.15s ease;           /* --transition-fast */
}

.question-card:hover {
  border-color: #DEE2E6;                /* --border-default */
  box-shadow: 0 2px 8px rgba(0,0,0,0.06); /* --shadow-md */
}

卡片内容布局

┌─────────────────────────────────────────────────────┐
│ [☑] 第1题  [单选题]  [待审核]  中等难度              │ ← Header
├─────────────────────────────────────────────────────┤
│ 在按价位段自选投放工作指引中,用于评估样本零售客户     │ ← Content
│ 库存水平与销售速度匹配程度的核心公式是?              │
│                                                     │
│ A. 客户实际订货数量/订货客户订单提报需求数量*100%     │ ← Options
│ B. 样本零售客户期末库存/月销量                       │
│ C. 实际零售价格/零售指导价格*100%                    │
│ D. 特定聚类内市场状态结果按销量加权平均               │
│                                                     │
│ [✓ 通过] [✗ 驳回] [✎ 编辑] [📤 发布] [🗑️ 删除]    │ ← Actions
└─────────────────────────────────────────────────────┘

状态徽章设计

/* 状态标识 - 仅使用功能色 */
.status-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 4px;                   /* --radius-sm */
  font-size: 12px;                      /* --font-sm */
  font-weight: 500;                     /* --weight-medium */
}

.status-pending {
  background: #FFF9DB;                 /* --bg-warning */
  color: #E67700;                      /* --color-warning */
  border: 1px solid #FFE066;
}

.status-approved {
  background: #EBFBEE;                 /* --bg-success */
  color: #2B8A3E;                      /* --color-success */
  border: 1px solid #B2F2BB;
}

.status-rejected {
  background: #FFF5F5;                 /* --bg-error */
  color: #C92A2A;                      /* --color-error */
  border: 1px solid #FFC9C9;
}

.status-published {
  background: #E7F5FF;                 /* --bg-info */
  color: #1C7ED6;                      /* --color-info */
  border: 1px solid #A5D8FF;
}

2.3 操作按钮设计

/* 按钮组 - 遵循设计规范 5.3 */
.action-buttons {
  display: flex;
  gap: 6px;                            /* --space-xs + 2px */
  margin-top: 12px;                    /* --space-md */
  padding-top: 12px;
  border-top: 1px solid #E9ECEF;       /* --border-light */
}

.btn-action {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 12px;                   /* 紧凑尺寸 */
  border-radius: 6px;                  /* --radius-md */
  font-size: 12px;                     /* --font-sm */
  font-weight: 500;
  cursor: pointer;
  transition: all 0.15s ease;
  border: 1px solid transparent;
}

.btn-approve {
  background: #EBFBEE;                 /* 浅绿底 */
  color: #2B8A3E;                      /* 绿色文字 */
  border-color: #B2F2BB;
}
.btn-approve:hover {
  background: #D3F9D8;
}

.btn-reject {
  background: #FFF5F5;                 /* 浅红底 */
  color: #C92A2A;                      /* 红色文字 */
  border-color: #FFC9C9;
}
.btn-reject:hover {
  background: #FFE3E3;
}

.btn-edit {
  background: #FFFFFF;
  color: #495057;                      /* --text-secondary */
  border-color: #DEE2E6;               /* --border-default */
}
.btn-edit:hover {
  background: #F8F9FA;                 /* --bg-container */
  border-color: #ADB5BD;
}

.btn-publish {
  background: #E7F5FF;                 /* 浅蓝底 */
  color: #1C7ED6;                      /* 蓝色文字 */
  border-color: #A5D8FF;
}
.btn-publish:hover {
  background: #D0EBFF;
}

.btn-delete {
  background: #FFFFFF;
  color: #C92A2A;                      /* 红色文字 */
  border-color: #FFC9C9;
}
.btn-delete:hover {
  background: #FFF5F5;
}

2.4 工具栏设计Toolbar

┌─────────────────────────────────────────────────────┐
│ 🔍 搜索题目...          [状态: 全部 ▼] [题型: 全部 ▼] │
│                        [✓ 批量通过] [✗ 批量驳回] [刷新] │
└─────────────────────────────────────────────────────┘
.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  background: #F8F9FA;                 /* --bg-container */
  border: 1px solid #E9ECEF;
  border-radius: 8px;                   /* --radius-lg */
  margin-bottom: 16px;                  /* --space-lg */
}

.search-input {
  flex: 1;
  max-width: 320px;
  height: 36px;                         /* 表单规范 */
  padding: 6px 12px;
  border: 1px solid #DEE2E6;
  border-radius: 6px;
  font-size: 13px;                      /* --font-md */
}

.filter-select {
  height: 36px;
  padding: 6px 12px;
  border: 1px solid #DEE2E6;
  border-radius: 6px;
  font-size: 13px;
  margin-left: 8px;
}

三、交互流程设计

3.1 生成题目流程

1. 用户配置参数(文件/题型/难度)
      ↓
2. 点击"开始生成"按钮
      ↓
3. 显示loading状态 + 进度提示
      ↓
4. 调用 POST /api/exam/generate
      ↓
5. 轮询检查生成状态(已有逻辑)
      ↓
6. 生成完成后:
   - 显示成功提示:"成功生成 X 道题目"
   - 自动调用 GET /api/question/pending 刷新列表
   - 滚动到待审核列表区域
      ↓
7. 展示待审核题目列表

3.2 审批流程

单个审批:
1. 用户点击"通过"/"驳回"按钮
      ↓
2. 弹出确认对话框(驳回时需要填写原因)
      ↓
3. 调用 PUT /api/question/review/{id}?status=approved/rejected
      ↓
4. 更新本地状态(乐观更新)
      ↓
5. 刷新列表数据

批量审批:
1. 用户勾选多个题目复选框
      ↓
2. 点击"批量通过"/"批量驳回"
      ↓
3. 弹出确认对话框
      ↓
4. 循环调用单个接口 或 调用批量接口
      ↓
5. 刷新列表

3.3 编辑流程

方式1内联编辑推荐简单字段
1. 点击"编辑"按钮
      ↓
2. 题目卡片变为编辑模式
      ↓
3. 直接修改题干/选项
      ↓
4. 点击"保存"/"取消"

方式2弹窗编辑推荐复杂编辑
1. 点击"编辑"按钮
      ↓
2. 打开 EditModal 弹窗
      ↓
3. 完整的表单编辑界面
      ↓
4. 点击"保存"/"取消"

四、响应式与滚动策略

4.1 布局原则(解决之前的滚动问题)

/* 主容器 - 允许自然流动,不限制高度 */
.generate-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;                        /* 允许收缩 */
  /* 移除 height: 100% 和 overflow: hidden */
}

/* 配置面板 - 固定不滚动 */
.config-panel {
  flex-shrink: 0;                       /* 不压缩 */
  margin-bottom: 16px;
}

/* 结果面板 - 内容可撑开 */
.result-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  /* 不设置 max-height 或 overflow */
}

/* 题目列表 - 自然流动 */
.question-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* 不设置 overflow-y: auto */
}

效果

  • 配置区域固定在顶部
  • 题目列表随内容自然增长
  • 当内容超出视口时,整个页面可滚动
  • 无局部滚动条

4.2 分页策略

当题目数量较多时(>20道使用分页

const pagination = reactive({
  current: 1,
  pageSize: 10,                          // 每页10道
  total: 0,
  showSizeChanger: true,
  showQuickJumper: true,
  pageSizeOptions: ['10', '20', '50'],
})

// 分页变化时重新请求
const handlePageChange = async (page, size) => {
  pagination.current = page
  pagination.pageSize = size
  await fetchPendingQuestions()
}

五、错误处理与边界情况

5.1 错误处理

场景 处理方式
生成失败 显示错误消息,保留配置不变
网络超时 提供重试按钮
审批接口失败 回滚到之前状态,显示错误
并发冲突 提示"数据已被其他人修改",刷新列表
权限不足 显示"无权限操作",隐藏操作按钮

5.2 空状态处理

<!-- 无待审核题目 -->
<div class="empty-state">
  <div class="empty-icon">📋</div>
  <p class="empty-title">暂无需审核的题目</p>
  <p class="empty-hint">
    您可以:<br/>
    • 使用上方配置生成新题目<br/>
    • 所有题目已审核完毕 ✨
  </p>
</div>

5.3 加载状态

<!-- 加载中 -->
<div class="loading-state">
  <a-spin size="large" />
  <p>正在加载待审核题目...</p>
</div>

<!-- 生成中 -->
<div class="generating-state">
  <a-spin tip="AI正在生成题目请耐心等待..." />
  <progress :percent="generateProgress" />
</div>

六、实施计划

Phase 1: 核心功能优先级P0

目标:实现基本的生成+展示+审批流程

  1. 修改 useGenerateState.js

    • 修复接口调用:生成后调用 /question/pending
    • 添加 question_count 参数
    • 保留现有的轮询逻辑
  2. 创建 useQuestionManagement.js

    • 实现 fetchPendingQuestions()
    • 实现 approveQuestion(id)
    • 实现 rejectQuestion(id, comment)
    • 实现搜索/筛选逻辑
  3. 重构 GeneratePanel.vue

    • 添加工具栏(搜索/筛选)
    • 重构题目列表为卡片式
    • 添加操作按钮组
    • 实现分页功能

Phase 2: 编辑功能优先级P1

  1. 创建 EditModal.vue

    • 题干编辑器
    • 选项编辑器(动态增删)
    • 答案设置
    • 表单验证
  2. 集成编辑功能

    • 调用 PUT /question/{id}
    • 保存后刷新列表

Phase 3: 批量操作优先级P2

  1. 实现批量选择
    • 复选框
    • 全选/反选
    • 批量审批接口

Phase 4: 体验优化优先级P3

  1. 性能优化

    • 虚拟滚动(大量题目时)
    • 防抖搜索
    • 缓存策略
  2. 视觉优化

    • 动画过渡
    • 拖拽排序
    • 快捷键支持

七、技术要点

7.1 关键代码示例

获取待审核题目

// useQuestionManagement.js
const fetchPendingQuestions = async () => {
  loading.value = true
  
  try {
    const params = {
      page: pagination.current,
      pageSize: pagination.pageSize,
    }
    
    if (searchKeyword.value) {
      params.keyword = searchKeyword.value
    }
    
    if (statusFilter.value !== 'all') {
      params.status = statusFilter.value
    }
    
    if (typeFilter.value !== 'all') {
      params.type = typeFilter.value
    }
    
    const res = await questionAPI.getPendingQuestions(params)
    
    pendingQuestions.value = transformQuestionData(res.data?.records || [])
    pagination.total = res.data?.total || 0
    
    console.log(`✅ 获取待审核题目: ${pendingQuestions.value.length} 条`)
  } catch (error) {
    console.error('❌ 获取待审核题目失败:', error)
    message.error('加载题目列表失败')
  } finally {
    loading.value = false
  }
}

审批操作

const approveQuestion = async (questionId) => {
  try {
    await questionAPI.reviewQuestion(questionId, 'approved')
    
    message.success('审批通过 ✓')
    
    // 乐观更新立即更新UI
    const index = pendingQuestions.value.findIndex(q => q.id === questionId)
    if (index !== -1) {
      pendingQuestions.value[index].status = 'approved'
    }
    
    // 可选:延迟刷新确保数据一致性
    setTimeout(() => fetchPendingQuestions(), 500)
    
  } catch (error) {
    console.error('❌ 审批失败:', error)
    message.error('审批操作失败,请重试')
    
    // 回滚:重新获取数据
    await fetchPendingQuestions()
  }
}

const rejectQuestion = async (questionId, comment) => {
  try {
    await questionAPI.reviewQuestion(questionId, 'rejected', comment)
    
    message.success('已驳回')
    
    // 更新本地状态
    const index = pendingQuestions.value.findIndex(q => q.id === questionId)
    if (index !== -1) {
      pendingQuestions.value[index].status = 'rejected'
    }
    
    setTimeout(() => fetchPendingQuestions(), 500)
    
  } catch (error) {
    message.error('驳回操作失败')
    await fetchPendingQuestions()
  }
}

7.2 数据转换函数(复用)

// 与 useGenerateState.js 保持一致
const transformQuestionData = (rawQuestions) => {
  return rawQuestions.map((q, qIdx) => {
    let contentObj = {}
    
    try {
      if (typeof q.content === 'string' && q.content.startsWith('{')) {
        contentObj = JSON.parse(q.content || '{}')
      } else if (typeof q.content === 'object') {
        contentObj = q.content
      }
    } catch (e) {
      console.warn(`题目${qIdx} content解析失败:`, e.message)
    }

    return {
      id: q.id,
      questionId: q.questionId,
      type: String(q.questionType || q.type || '').toLowerCase(),
      typeLabel: formatQuestionType(q.questionType),
      difficulty: q.difficulty || 2,
      score: Number(q.score) || 5,
      stem: contentObj.stem || q.stem || (typeof q.content === 'string' ? q.content : ''),
      options: formatOptions(contentObj.options || q.options || []),
      answer: q.answer || contentObj.answer || '',
      status: q.status || 'pending',
      reviewerComment: q.reviewerComment || '',
      createdAt: q.createdAt,
      updatedAt: q.updatedAt,
    }
  })
}

八、测试场景

8.1 功能测试

  • 生成5道题目验证列表显示正确
  • 搜索题目,验证过滤结果准确
  • 按状态筛选(待审核/已通过/已驳回/已发布)
  • 单个审批通过/驳回
  • 批量审批多道题目
  • 编辑题目并保存
  • 发布已通过的题目
  • 删除题目(带确认)
  • 分页切换第1页/第2页/每页20条

8.2 边界测试

  • 生成了0道题目空状态显示
  • 生成了100道题目性能测试
  • 同时点击多次生成(防重复提交)
  • 网络断开时的错误处理
  • 权限不足时的UI反馈

8.3 UI/UX测试

  • 页面滚动流畅(无局部滚动条)
  • 按钮hover效果正常
  • 加载动画显示正确
  • 响应式布局适配不同屏幕
  • 空状态提示友好

九、成功标准

9.1 功能完整性

用户可以完整执行以下流程:

  1. 选择文件 → 配置参数 → 生成题目
  2. 查看待审核题目列表
  3. 对每道题目进行审批/编辑/发布
  4. 搜索、筛选、分页浏览

9.2 用户体验

符合设计规范:

  • 白色体系配色
  • 紧凑但清晰的布局
  • 一致的组件样式
  • 流畅的交互动画
  • 友好的错误提示

9.3 技术质量

代码质量:

  • 无语法错误
  • 接口调用正确
  • 状态管理清晰
  • 错误处理完善
  • 性能可接受

十、后续扩展方向

  1. 题目版本管理:记录每次编辑的历史版本
  2. 批量导入导出支持Excel格式导入/导出题目
  3. AI辅助审核AI自动预审人工复核
  4. 统计分析:题目通过率、平均审核时间等
  5. 权限细化:角色权限(管理员/审核员/出题人)
  6. 移动端适配:响应式布局优化手机端体验

文档版本: v1.0 最后更新: 2026-05-16 状态: 待用户确认