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

563 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 结构**:
```html
<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
```typescript
{
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
```typescript
{
'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
```typescript
{
result: Object | null, // composeResult
composing: Boolean,
totalScore: Number
}
```
#### Emits
```typescript
{ 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
```typescript
{
mode: 'drafts' | 'published',
papers: Array,
loading: Boolean
}
```
#### Emits
```typescript
{
view: [paper],
publish: [paperId], // 仅 drafts 模式
delete: [paperId], // 仅 drafts 模式
revoke: [paperId] // 仅 published 模式
}
```
#### 视觉改进
| 改进项 | 当前 | 新设计 |
|--------|------|--------|
| 行左侧 | 无标识 | 彩色竖条(草稿=`--color-warning`, 发布=`--color-success` |
| 操作按钮 | 文字按钮 ("查看"/"发布"/"删除") | 图标按钮 + hover tooltip |
| Badge 样式 | 自定义 CSS | 使用 design-token 颜色 |
| 空态 | 内联 SVG | 统一空态组件风格 |
#### 条件渲染逻辑
```html
<!-- 草稿模式独有 -->
<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
```typescript
{
modelValue: Boolean, // 弹窗显隐
rawData: Object | null // 原始 API 返回数据
}
```
#### 内部处理
- 使用 `useQuestionParser()` composable 解析原始数据
- 解析逻辑包括: content 多格式兼容、选项提取、答案匹配、类型判断
#### 展示区块
1. **发布信息区** (如有): 目标部门/用户/时间/截止
2. **概要行**: 总题数 + 总分
3. **题目列表** (每题):
- 头部: 序号(主色圆圈) + 类型标签 + 分数
- 题干文本
- 选择题: 选项列表 (正确选项绿色左边框 + ✓)
- 判断题: 正确/错误文本
填空/简答: 参考答案文本
- 解析区: `--color-warning` 背景 (与 ExamPanel.vue 统一)
- 难度标签 (如有)
4. **Fallback 区**: 原始数据兜底展示 (当自动解析失败时)
---
### 3.6 PublishDialog.vue — 发布弹窗
**职责**: 设置发布参数(部门/用户范围/时间)
#### Props
```typescript
{
modelValue: Boolean,
paperId: String | Number
}
```
#### Emits
```typescript
{
'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
```typescript
{
modelValue: Boolean,
type: 'dept' | 'user',
mode: 'target' | 'exclude', // 仅 type='user' 时有效
sourceList: Array, // 可选数据源
selectedIds: Set, // 当前已选 ID 集合
loading: Boolean,
searchPlaceholder: String
}
```
#### Emits
```typescript
{
'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 使用
**导出函数**:
```javascript
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)