Files
aue/docs/superpowers/specs/2026-05-29-paper-compose-redesign-design.md
2026-06-03 13:16:30 +08:00

18 KiB
Raw Blame History

PaperManagementPanel 题目配置组件重设计

日期: 2026-05-29 状态: 待审核 范围: PaperManagementPanel.vue 全面模块化拆分与视觉优化

1. 背景与目标

1.1 现状问题

src/components/exam/PaperManagementPanel.vue 是一个 2235 行的巨型单文件组件,包含 6 个独立功能区全部耦合在一起:

维度 当前数值
Template 567 行
Script ~1065 行
Style ~600 行
功能区数 6+ 个
硬编码颜色值 ~30 处(未使用 design-tokens
console.log ~40 处(调试残留)

1.2 设计目标

  1. 组件拆分: 将单文件拆分为 7 个子组件 + 2 个 composable每个 < 350 行
  2. 代码精简: 移除冗余逻辑、调试日志、硬编码颜色
  3. 视觉优化: 延续现有扁平设计风格,统一使用 design-tokens
  4. 交互改进: 优化组卷配置区控件、预览区展示、弹窗体验
  5. 响应式: 保持移动端/平板/桌面断点适配

2. 组件架构

2.1 文件结构

src/components/exam/
├── PaperManagementPanel.vue          # 主容器 (~80行)
├── ComposePanel.vue                  # 智能组卷面板 - 左右分栏容器 (~50行)
│   ├── ComposeConfigSidebar.vue      # 左侧配置栏 (~200行)
│   └── ComposePreviewPane.vue        # 右侧预览区 (~180行)
├── PaperListTab.vue                  # 草稿/已发布列表 - 复用型 (~150行)
├── PaperDetailModal.vue              # 试卷详情弹窗 (~250行)
├── PublishDialog.vue                 # 发布弹窗 (~300行)
├── EntityPickerModal.vue             # 通用实体选择器弹窗 (~180行)
├── composables/
│   ├── usePaperManagement.js         # 共享状态管理 (~400行)
│   └── useQuestionParser.js          # 题目数据解析工具 (~150行)
└── common/                           # 已有公共组件 (不变更)
    ├── PageHeader.vue
    ├── StatsRow.vue
    └── ContentCard.vue

2.2 组件树与数据流

PaperManagementPanel (主容器)
  │
  ├─ usePaperManagement (composable: 全局状态中心)
  │
  ├─► Tab Navigation (内置: 智能组卷 / 我的草稿 / 已发布试卷)
  │
  ├─► [activeTab='compose'] ComposePanel
  │     ├─► ComposeConfigSidebar
  │     │     props: form, selectedFiles, allFiles, loadingFiles
  │     │     emit: generate, update:form
  │     └─► ComposePreviewPane
  │           props: result, composing, totalScore
  │           emit: save
  │
  ├─► [activeTab='drafts'] PaperListTab(mode='drafts')
  │     props: papers, loading
  │     emit: view, publish, delete
  │
  ├─► [activeTab='published'] PaperListTab(mode='published')
  │     props: papers, loading
  │     emit: view, revoke
  │
  ├─► PaperDetailModal (Teleport to body)
  │     v-model: visible
  │     props: rawData
  │
  ├─► PublishDialog (Teleport to body)
  │     v-model: visible
  │     props: paperId
  │     emit: confirm
  │     └─► EntityPickerModal (Teleport to body)
  │           v-model: visible
  │           props: type, mode, sourceList, selectedIds
  │           emit: confirm
  └─► EntityPickerModal (独立使用场景预留)

2.3 通信原则

  • 单向数据流: 父 → 子通过 props子 → 父通过 emits
  • 状态提升: 所有业务状态集中在 usePaperManagement composable
  • v-model 模式: 弹窗类组件支持 v-model:visible 双向绑定
  • 接口最小化: 每个子组件只暴露必要的 props/emits

3. 各组件详细设计

3.1 PaperManagementPanel.vue — 主容器

职责: Tab 导航 + 子组件编排 + 状态初始化

Template 结构:

<div class="pmp-panel">
  <PageHeader title="智能组卷" description="..." />
  <div class="pmp-tabs"> <!-- 3个Tab按钮 --> </div>

  <ComposePanel v-if="activeTab==='compose'" ... />
  <PaperListTab v-else-if="activeTab==='drafts'" mode="drafts" ... />
  <PaperListTab v-else-if="activeTab==='published'" mode="published" ... />

  <PaperDetailModal v-model="detailVisible" :raw-data="previewRaw" />
  <PublishDialog v-model="publishVisible" :paper-id="pubPaperId" @confirm="confirmPublish" />
</div>

Script: 仅做 composable 解构和事件转发,< 80 行


3.2 ComposeConfigSidebar.vue — 组卷配置栏

职责: 数据源选择、难度设置、题型数量配置、生成触发

Props

{
  modelValue: {                    // composeForm 对象
    difficulty: Number,            // 1-5
    include_personal: Boolean,
    single_choice_count: Number,
    multiple_choice_count: Number,
    true_false_count: Number,
    fill_blank_count: Number,
    subjective_count: Number
  },
  selectedFiles: Array,            // 已选文件列表
  allFiles: Array,                 // 全量文件列表
  loadingFiles: Boolean,
  totalCount: Number               // 计算属性:各题型数量之和
}

Emits

{
  'update:modelValue': [form],    // 表单变更
  'toggle-file': [file],          // 文件选择切换
  'remove-file': [id],            // 移除已选文件
  'open-picker': [],              // 打开文件选择器
  'close-picker': [],             // 收起文件选择器
  'generate': []                  // 触发生成试卷
}

视觉改进点

改进项 当前实现 新设计
Step 标识 圆形数字 badge (1/2/3) 分隔线 + 小标题,减少视觉噪音
题型数量控件 [-] 数字 [+] 按钮 Native <select> 下拉 (0-50),更紧凑
难度选择 5 个并排按钮 保持按钮组,使用 design-tokens 颜色
文件选择器 展开内嵌列表 限制 max-height: 180px滚动溢出
总计行 底部小字 "共 N 题" 加粗数字 + --bg-subtle 色块突出
生成按钮 全宽黑色 #212529 使用 --color-primary token

布局草图

┌──────────────────────────────┐
│  📋 配置                      │
├──────────────────────────────┤
│                              │
│  ── 数据源 ──────────────    │
│  [从知识库选择文件]           │
│  [chip: 制度A ×] [chip: 规范B ×] │
│  💡 不选则从全部题库出题      │
│                              │
│  ── 难度等级 ────────────    │
│  [简单] [中等✓] [较难] [困难]  │
│                              │
│  ── 题型配置 ────────────    │
│  单选题    [▼ 5 ▲]           │
│  多选题    [▼ 3 ▲]           │
│  判断题    [▼ 0 ▲]           │
│  填空题    [▼ 2 ▲]           │
│  简答题    [▼ 0 ▲]           │
│                              │
│  ┌──────────────────────┐   │
│  │   共 10 题            │   │
│  └──────────────────────┘   │
│                              │
│  ┌──────────────────────┐   │
│  │   ✨ 生成试卷          │   │
│  └──────────────────────┘   │
└──────────────────────────────┘

3.3 ComposePreviewPane.vue — 组卷预览区

职责: 展示组卷结果(空态/加载态/题目预览)+ 保存操作

Props

{
  result: Object | null,        // composeResult
  composing: Boolean,
  totalScore: Number
}

Emits

{ save: [] }

三态设计

  1. 空态 (!result && !composing):

    • SVG 图标 + 引导文案 "选择文件(可选)、设置题型数量 → 点击「生成试卷」"
    • 使用 --text-muted 颜色
  2. 加载态 (composing):

    • Spinner + "正在从题库中选题组卷..."
  3. 结果态 (result && !composing):

    • 试卷预览卡片:
      • 标题 + 统计信息(题目数、总分)
      • 题目列表(每题:序号圆圈 + 类型标签 + 难度 + 题干)
      • hover 时边框高亮
    • 保存区域: 保存按钮 + 提示文字

视觉改进

改进项 当前 新设计
预览卡边框 2px solid #228BE6 (蓝色粗边框) 1px solid var(--border-default) + 微妙阴影
预览卡头部 渐变蓝背景 var(--bg-subtle) 纯色背景
题号圆圈 #F1F9FA 灰底 var(--color-primary) 主色底 + 白字
保存按钮 黑色全宽 var(--color-primary) token

3.4 PaperListTab.vue — 复用型列表组件

职责: 通过 mode prop 切换草稿/已发布两种列表模式

Props

{
  mode: 'drafts' | 'published',
  papers: Array,
  loading: Boolean
}

Emits

{
  view: [paper],
  publish: [paperId],       // 仅 drafts 模式
  delete: [paperId],        // 仅 drafts 模式
  revoke: [paperId]         // 仅 published 模式
}

视觉改进

改进项 当前 新设计
行左侧 无标识 彩色竖条(草稿=--color-warning, 发布=--color-success
操作按钮 文字按钮 ("查看"/"发布"/"删除") 图标按钮 + hover tooltip
Badge 样式 自定义 CSS 使用 design-token 颜色
空态 内联 SVG 统一空态组件风格

条件渲染逻辑

<!-- 草稿模式独有 -->
<button v-if="mode === 'drafts'" @click="$emit('publish', id)">发布</button>
<button v-if="mode === 'drafts'" @click="$emit('delete', id)">删除</button>

<!-- 已发布模式独有 -->
<button v-if="mode === 'published'" @click="$emit('revoke', id)">撤回</button>

3.5 PaperDetailModal.vue — 试卷详情弹窗

职责: 展示试卷完整题目列表(选项/答案/解析/难度)

Props

{
  modelValue: Boolean,         // 弹窗显隐
  rawData: Object | null       // 原始 API 返回数据
}

内部处理

  • 使用 useQuestionParser() composable 解析原始数据
  • 解析逻辑包括: content 多格式兼容、选项提取、答案匹配、类型判断

展示区块

  1. 发布信息区 (如有): 目标部门/用户/时间/截止
  2. 概要行: 总题数 + 总分
  3. 题目列表 (每题):
    • 头部: 序号(主色圆圈) + 类型标签 + 分数
    • 题干文本
    • 选择题: 选项列表 (正确选项绿色左边框 + ✓)
    • 判断题: 正确/错误文本 填空/简答: 参考答案文本
    • 解析区: --color-warning 背景 (与 ExamPanel.vue 统一)
    • 难度标签 (如有)
  4. Fallback 区: 原始数据兜底展示 (当自动解析失败时)

3.6 PublishDialog.vue — 发布弹窗

职责: 设置发布参数(部门/用户范围/时间)

Props

{
  modelValue: Boolean,
  paperId: String | Number
}

Emits

{
  'update:modelValue': [Boolean],
  confirm: [payload]           // { deptIds, userIds, excludeUserIds, publishTime?, deadline? }
}

内部状态管理

所有发布相关状态(部门列表、用户列表、选择状态、模式等)从 usePaperManagement 中获取。

子组件调用

  • 部门选择 → 调用 <EntityPickerModal type="dept" />
  • 用户选择 → 调用 <EntityPickerModal type="user" :mode="userMode" />

布局保持: 两列网格 (Grid 1fr 1fr)


3.7 EntityPickerModal.vue — 通用实体选择器

职责: 通用的部门/用户多选弹窗,可复用

Props

{
  modelValue: Boolean,
  type: 'dept' | 'user',
  mode: 'target' | 'exclude',   // 仅 type='user' 时有效
  sourceList: Array,             // 可选数据源
  selectedIds: Set,              // 当前已选 ID 集合
  loading: Boolean,
  searchPlaceholder: String
}

Emits

{
  'update:modelValue': [Boolean],
  confirm: [selectedItems]       // 选中的完整对象数组
}

功能特性

  • 搜索过滤 (实时)
  • 全选 / 清空
  • 已选计数显示
  • Checkbox 列表 + hover 高亮
  • 部门模式下显示部门名称
  • 用户模式下显示姓名 + 所属部门

4. Composables 设计

4.1 usePaperManagement.js

定位: PaperManagementPanel 的全局状态中心

状态分组:

┌─ Tab 状态
│   activeTab, tabs
│
├─ 组卷状态
│   composeForm, composeResult, composing
│   selectedFiles, allFiles, loadingFiles, showFilePicker
│
├─ 列表状态
│   drafts, publishedPapers, loadingDrafts
│
├─ 详情状态
│   previewRaw, previewQuestions, previewPaperTitle, previewPublishInfo
│
├─ 发布状态
│   publishDialog, pubPaperId, publishing
│   pub (reactive), pubUI (reactive)
│   pickerDialog (reactive)
│
└─ 缓存
    _deptCache, _deptCacheTime, _userCache, CACHE_TTL

方法分组:

分类 方法
Tab switchTab
组卷 handleGeneratePaper, handleSavePaper, increment, decrement
文件 toggleFile, removeFile, openFilePicker
列表 loadPapers, deleteDraft, revokePublishedPaper
详情 viewPaperDetail, clearPreview
发布 openPublishDialog, closePublish, confirmPublish
Picker openPicker, closePicker, confirmPicker
部门 loadDeptList, filterDeptList, toggleDept, removeDept, selectAllDepts, clearAllDepts
用户 loadUserListForPicker, refreshUsersForDepts, toggleCurrentUser, removeUser, selectAllUsers, clearAllUsers
工具 getId, getPaperTitle, formatQType, formatPubTime, isDeadlineNear

4.2 useQuestionParser.js

定位: 从 viewPaperDetail() 中提取的纯函数集合,供 PaperDetailModal 使用

导出函数:

export function useQuestionParser() {
  function parseQuestionContent(rawQ)  { stem, options, answer, explanation, ... }
  function extractStem(q)  string
  function extractOptions(q)  Array<{ key, text }>
  function extractAnswer(q)  string
  function isChoiceQuestion(q)  boolean
  function isTextQuestion(q)  boolean
  function isOptionAnswer(opt, q)  boolean
  function formatTfAnswer(q)  string
  function formatDifficulty(d)  string
  function formatQType(t)  string

  return { parseQuestionContent, extractStem, extractOptions, extractAnswer,
           isChoiceQuestion, isTextQuestion, isOptionAnswer,
           formatTfAnswer, formatDifficulty, formatQType }
}

5. 样式规范

5.1 Design Token 映射

所有新组件必须使用项目 design-tokens.css 中定义的 CSS 变量,禁止硬编码颜色值:

当前硬编码 替换 Token 用途
#212529 var(--text-primary) 主文字
#495057 var(--text-secondary) 次要文字
#868E96 var(--text-muted) 辅助文字
#ADB5BD / #6C757D var(--text-muted) 占位符/提示
#E9ECEF var(--border-light) 浅边框
#DEE2E6 / #CED4DA var(--border-default) 默认边框
#F8F9FA var(--bg-subtle) 浅背景
#F1F3F5 var(--bg-hover) Hover 背景
#FFF5F5 var(--bg-danger-subtle) 危险背景
#EBFBEE / #D3F9D8 var(--bg-success-subtle) 成功背景
#FFFBEB / #FFF9DB var(--bg-warning-subtle) 警告背景
#C92A2A var(--color-danger) 危险色
#2B8A3E var(--color-success) 成功色
#228BE6 / #1C7ED6 var(--color-primary) 主色调
#E67700 var(--color-warning) 警告色

5.2 CSS 命名规范

使用 BEM 变体命名,前缀按组件缩写:

.pmp-{主容器}__{元素}--{修饰符}
.cps-{ComposePanel}__{元素}--{修饰符}
.ccs-{ComposeConfigSidebar}__{元素}--{修饰符}
.cpp-{ComposePreviewPane}__{元素}--{修饰符}
.plt-{PaperListTab}__{元素}--{修饰符}
.pdm-{PaperDetailModal}__{元素}--{修饰符}
.pbd-{PublishDialog}__{元素}--{修饰符}
.epm-{EntityPickerModal}__{元素}--{修饰符}

5.3 响应式断点

复用项目已有断点体系:

断点 宽度 布局调整
Mobile ≤767px 组卷分栏→上下堆叠;弹窗全屏;列表单列
Tablet 768-1023px 组卷侧栏收窄至 280px
Desktop 1024-1439px 标准布局
Large ≥1440px 侧栏放宽至 380px预览区更大

5.4 清理项

以下原样式将在拆分时移除(属于其他组件或冗余):

  • .answer-comparison, .your-answer, .correct-answer — TrainingPanel 残留
  • .wrong, .correct — 错题本遗留(已删除功能)
  • .explanation — 与新 .dqc-explanation 重复
  • 重复定义的 .loading-spinner@keyframes spin — 统一到公共样式中
  • 所有 console.log / console.warn 调试语句 — 移除或改为条件编译

6. 实施顺序建议

  1. Phase 1 - 基础设施

    • 创建 useQuestionParser.js(纯函数,无依赖)
    • 创建 usePaperManagement.js(从原组件提取状态和方法)
  2. Phase 2 - 核心组件

    • 创建 ComposeConfigSidebar.vue
    • 创建 ComposePreviewPane.vue
    • 创建 ComposePanel.vue(组合上述两个)
    • 创建 PaperListTab.vue
  3. Phase 3 - 弹窗组件

    • 创建 EntityPickerModal.vue(通用,无业务依赖)
    • 创建 PublishDialog.vue(依赖 EntityPickerModal
    • 创建 PaperDetailModal.vue(依赖 useQuestionParser
  4. Phase 4 - 主容器整合

    • 重构 PaperManagementPanel.vue 为薄容器
    • 接入所有子组件
    • 样式清理与 token 替换
  5. Phase 5 - 验证

    • 功能回归测试(组卷/保存/发布/查看/删除/撤回)
    • 响应式布局测试
    • 构建验证 (npm run build)