前端项目初始化提交

This commit is contained in:
2026-06-03 13:16:30 +08:00
commit 0910ba9cbe
163 changed files with 110032 additions and 0 deletions

View File

@@ -0,0 +1,562 @@
# 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)