# 新编辑器功能设计文档
## 1. 功能概览
### 1.1 核心功能模块
| 模块 | 功能 | 优先级 |
|------|------|--------|
| 文档管理 | 创建、加载、保存文档 | P0 |
| 基础编辑 | 标题、段落、文本格式化 | P0 |
| 表格编辑 | 创建、编辑、调整表格 | P0 |
| 图片管理 | 插入、调整、对齐图片 | P0 |
| 导出功能 | 导出Word文档 | P0 |
| 富文本编辑 | 单元格和段落内富文本 | P1 |
| 块操作 | 拖拽排序、复制粘贴 | P1 |
| 协同编辑 | 多人实时编辑 | P2 |
| 版本管理 | 历史版本、回滚 | P2 |
---
## 2. 文档管理功能
### 2.1 创建文档
**触发场景**: 用户在Chat中点击"编辑"按钮
**功能流程**:
1. 前端调用`POST /api/v1/documents`
2. 传入Word文档URL(后端下载解析为blocks)
3. 返回documentId
4. 打开编辑器Modal
**UI交互**:
```
[Chat消息]
这是AI生成的文档 [下载] [编辑]
↓点击
[加载中...解析文档]
↓
[打开编辑器窗口]
```
### 2.2 加载文档
**API调用**:
```typescript
const loadDocument = async (documentId: string) => {
const res = await apiClient.get(`/api/v1/documents/${documentId}/blocks`);
const blocks = res.data.data.blocks.sort((a, b) => a.block_order - b.block_order);
return blocks;
};
```
**加载状态显示**:
- 骨架屏显示文档结构
- 逐块渲染(避免长时间白屏)
### 2.3 保存文档
**保存策略**:
- **手动保存**: 点击"保存"按钮立即保存
- **自动保存**: 编辑停止500ms后自动保存
- **退出保存**: 关闭编辑器前提示保存
**保存接口**:
```typescript
const saveDocument = async (documentId: string, blocks: DocumentBlock[]) => {
await apiClient.put(`/api/v1/documents/${documentId}/blocks`, { blocks });
};
```
**保存状态提示**:
```
[保存中...] → [已保存于 14:32:15] → [保存失败,点击重试]
```
---
## 3. 基础编辑功能
### 3.1 标题编辑
**支持级别**: H1 ~ H6
**创建标题**:
- 工具栏点击"标题"按钮选择级别
- 快捷键: Ctrl+1 ~ Ctrl+6
**编辑标题**:
- 单击标题进入编辑模式
- contenteditable实现所见即所得
- 支持富文本(部分文字加粗、变色)
**UI示例**:
```
# 第一章 概述 ← H1
↑ 点击编辑
输入状态:
┌────────────────────┐
│# |第一章 概述 │ ← 光标闪烁
└────────────────────┘
[B] [I] [U] [颜色] ← 浮动工具栏
```
### 3.2 段落编辑
**创建段落**:
- 在标题后按Enter自动创建段落
- 工具栏"段落"按钮
**段落操作**:
- 单击编辑
- 支持富文本格式
- 支持多行文本
**空段落处理**:
- 显示占位符"输入段落内容..."
- 失焦时若为空则自动删除
### 3.3 文本格式化
**支持格式**:
| 格式 | 快捷键 | 效果 |
|------|--------|------|
| 加粗 | Ctrl+B | **粗体** |
| 斜体 | Ctrl+I | *斜体* |
| 下划线 | Ctrl+U | 下划线 |
| 文字颜色 | - | 红色 |
| 字号 | - | 字号14pt |
| 字体 | - | 微软雅黑 |
**格式化工具栏**:
```
文本选中时显示浮动工具栏:
┌────────────────────────────────┐
│ [B] [I] [U] [颜色▼] [字号▼] [字体▼] │
└────────────────────────────────┘
```
---
## 4. 表格编辑功能
### 4.1 创建表格
**创建方式**:
1. 工具栏点击"插入表格"按钮
2. 弹出对话框选择行列数
**对话框UI**:
```
┌─────────────────┐
│ 插入表格 │
│ │
│ 行数: [3] │
│ 列数: [4] │
│ │
│ [取消] [确定] │
└─────────────────┘
```
**默认值**:
- 行数: 3
- 列数: 3
- 列宽: 平均分配
- 表格宽度: 100%(占满编辑器宽度)
### 4.2 表格结构
**渲染方式**: div + CSS Grid
```html
```
### 4.3 单元格编辑
**进入编辑**:
- 单击单元格
- 显示contenteditable编辑框
- 支持富文本格式(加粗、颜色等)
**编辑状态UI**:
```
┌──────────────────┐
│ 单元格内容| │ ← 光标闪烁
│ [B] [I] [U] [色] │ ← 工具栏
└──────────────────┘
```
**退出编辑**:
- 点击单元格外部
- 按Esc键
- 按Tab键(跳转到下一单元格)
### 4.4 合并单元格
**操作流程**:
1. 选中多个单元格(拖拽或Shift+点击)
2. 右键菜单选择"合并单元格"
3. 更新第一个单元格的rowspan/colspan
4. 移除被合并的单元格
**合并示例**:
```
原始:
┌───┬───┬───┐
│ A │ B │ C │
├───┼───┼───┤
│ D │ E │ F │
└───┴───┴───┘
选中A、B、D、E后合并:
┌───────┬───┐
│ A │ C │ ← A单元格 rowspan=2, colspan=2
│ │ │
├───────┼───┤
│ G │ F │
└───────┴───┘
```
**数据结构变化**:
```typescript
// 合并前
cells: [
{ text: 'A', rowspan: 1, colspan: 1 },
{ text: 'B', rowspan: 1, colspan: 1 },
// ...
]
// 合并后
cells: [
{ text: 'A', rowspan: 2, colspan: 2 },
{ text: 'C', rowspan: 1, colspan: 1 },
// B、D、E被移除
]
```
### 4.5 插入/删除行列
**右键菜单**:
```
单元格右键 →
- 在上方插入行
- 在下方插入行
- 在左侧插入列
- 在右侧插入列
- 删除当前行
- 删除当前列
- 合并单元格
- 拆分单元格
```
**插入行逻辑**:
```typescript
const insertRow = (table: TableBlock, afterRow: number) => {
const newRow: TableRow = {
cells: Array(table.metadata.cols).fill(null).map(() => ({
text: '',
rowspan: 1,
colspan: 1,
style: {}
}))
};
const rows = [...table.content.rows];
rows.splice(afterRow + 1, 0, newRow);
return {
...table,
content: { rows },
metadata: { ...table.metadata, rows: rows.length }
};
};
```
### 4.6 调整列宽
**交互方式**:
- 鼠标悬停在列边界显示调整光标
- 拖拽调整宽度
- 实时更新`col_widths`数组
**UI反馈**:
```
正常状态:
│ 列1 │ 列2 │
悬停状态:
│ 列1 ║ 列2 │ ← 加粗竖线,光标变为↔
拖拽状态:
│ 列1 │ 列2 │ ← 实时显示新宽度
```
**实现**:
```typescript
const handleColumnResize = (colIndex: number, newWidth: number) => {
const col_widths = [...table.metadata.col_widths];
col_widths[colIndex] = newWidth;
updateBlock(table.id, {
metadata: { ...table.metadata, col_widths }
});
};
```
### 4.7 调整行高(可选)
**交互**:
- 类似列宽,拖拽行边界
- 更新`row_heights`数组
**注意**: 行高为可选功能,不设置时自动适应内容高度
---
## 5. 图片管理功能
### 5.1 插入图片
**插入方式**:
1. 工具栏点击"插入图片"按钮
2. 文件选择对话框
3. 图片转Base64嵌入content字段
**支持格式**: JPG、PNG、GIF、WebP
**文件大小限制**: 单张图片≤5MB
**转换流程**:
```typescript
const handleImageUpload = async (file: File) => {
// 1. 验证
if (file.size > 5 * 1024 * 1024) {
showError('图片大小不能超过5MB');
return;
}
// 2. 读取并转Base64
const reader = new FileReader();
reader.onload = (e) => {
const base64 = e.target.result as string;
// 3. 创建image block
const imageBlock: ImageBlock = {
id: generateBlockId('img'),
block_order: computeInsertOrder(),
type: 'image',
level: 0,
index: 0,
content: base64, // data:image/png;base64,...
word_style: 'Normal',
style: {
width: 10,
height: 7,
unit: 'cm',
align: 'center'
},
metadata: {
alt: file.name,
para_style: 'Normal',
parent_heading_id: getCurrentHeadingId()
}
};
addBlock(imageBlock);
};
reader.readAsDataURL(file);
};
```
### 5.2 调整图片尺寸
**交互方式**:
- 选中图片后显示8个调整手柄
- 拖拽角落等比缩放
- 拖拽边缘单方向缩放
**UI显示**:
```
未选中:
┌─────────────┐
│ [图片] │
└─────────────┘
选中状态:
■─────────────■ ← 四角调整点
│ │
│ [图片] │ ← 中心图片
│ │
■─────────────■
```
**尺寸计算**:
```typescript
const handleResize = (corner: 'nw' | 'ne' | 'sw' | 'se', deltaX: number, deltaY: number) => {
const { width, height } = block.style;
const aspectRatio = width / height;
let newWidth = width;
let newHeight = height;
switch (corner) {
case 'se': // 右下角:等比缩放
newWidth = width + deltaX * 0.01; // 转换px到cm
newHeight = newWidth / aspectRatio;
break;
// ...其他角落
}
updateBlock(block.id, {
style: { ...block.style, width: newWidth, height: newHeight }
});
};
```
### 5.3 图片对齐
**对齐选项**:
- 左对齐
- 居中对齐
- 右对齐
**实现**:
```tsx
/* CSS */
.image-block.align-left { text-align: left; }
.image-block.align-center { text-align: center; }
.image-block.align-right { text-align: right; }
```
### 5.4 替换图片
**操作**:
- 选中图片后点击"替换"按钮
- 或右键菜单选择"替换图片"
- 重新选择文件,保持原有尺寸和对齐
### 5.5 删除图片
**操作**:
- 选中图片后按Delete键
- 或右键菜单选择"删除"
- 弹出确认对话框
---
## 6. 导出功能
### 6.1 导出Word文档
**触发**:
- 编辑器工具栏点击"导出"按钮
- 或顶部菜单"文件 → 导出为Word"
**导出流程**:
```
用户点击"导出"
↓
弹出样式选择对话框(可选,默认使用default样式)
↓
调用 POST /api/v1/export/doc { documentId, styleId }
↓
后端从document_blocks读取 → 渲染Word → 保存到tmp/
↓
返回 { downloadUrl, fileName, recordId }
↓
前端触发浏览器下载
```
**样式选择对话框**(阶段1):
```
┌────────────────────────┐
│ 导出设置 │
│ │
│ 样式: [系统默认 ▼] │
│ - 系统默认 │
│ - 我的样式1 │
│ - 公司模板 │
│ │
│ [取消] [导出] │
└────────────────────────┘
```
### 6.2 下载历史
**查看历史**:
- 编辑器菜单"文件 → 下载历史"
- 打开侧边栏显示历史记录
**历史记录列表**:
```
┌───────────────────────────┐
│ 下载历史 │
├───────────────────────────┤
│ 📄 报告_1680000000.doc │
│ 204KB · 2小时前 │
│ [下载] [删除] │
├───────────────────────────┤
│ 📄 报告_1679950000.doc │
│ 198KB · 昨天 14:32 │
│ [下载] [删除] │
└───────────────────────────┘
```
**操作**:
- 点击"下载"重新下载
- 点击"删除"删除记录(后端同步删除文件)
---
## 7. 块操作功能
### 7.1 选中块
**选中方式**:
- 单击块边缘显示块边框
- 显示块操作工具栏
**选中状态UI**:
```
┌─────────────────────────┐
│ [⋮] [↑] [↓] [🗑] │ ← 块工具栏
├─────────────────────────┤
│ # 标题内容 │ ← 选中的块(带边框)
└─────────────────────────┘
```
### 7.2 拖拽排序
**实现库**: `@dnd-kit/core`
**交互**:
1. 点击块左侧拖拽手柄(⋮)
2. 拖拽到目标位置
3. 显示插入位置指示线
4. 松开鼠标完成排序
**UI反馈**:
```
拖拽前:
[块A]
[块B]
[块C]
拖拽中:
[块B] ← 拖拽中(半透明)
───────── ← 插入位置指示
[块A]
[块C]
拖拽后:
[块A]
[块B] ← 已移动
[块C]
```
### 7.3 复制粘贴块
**复制**:
- 选中块后 Ctrl+C
- 或右键菜单"复制"
- 将块数据存入剪贴板
**粘贴**:
- Ctrl+V
- 在当前块后插入复制的块
- 自动生成新ID和block_order
**实现**:
```typescript
const handleCopy = (block: DocumentBlock) => {
const clipboardData = {
type: 'ax-block',
block: { ...block, id: undefined, block_order: undefined } // 移除原ID
};
navigator.clipboard.writeText(JSON.stringify(clipboardData));
};
const handlePaste = async () => {
const text = await navigator.clipboard.readText();
try {
const data = JSON.parse(text);
if (data.type === 'ax-block') {
const newBlock = {
...data.block,
id: generateBlockId(data.block.type),
block_order: computeInsertOrder()
};
addBlock(newBlock);
}
} catch (e) {
// 普通文本粘贴,创建段落块
addBlock({
type: 'paragraph',
content: text,
// ...
});
}
};
```
### 7.4 删除块
**操作**:
- 选中块后按Delete键
- 或点击工具栏删除按钮🗑
- 弹出确认对话框(可选,由设置控制)
**批量删除**:
- 按住Shift选中多个块
- 按Delete删除所有选中块
---
## 8. 快捷键设计
### 8.1 快捷键清单
| 快捷键 | 功能 | 作用范围 |
|--------|------|---------|
| **文档操作** | | |
| Ctrl+S | 保存文档 | 全局 |
| Ctrl+Z | 撤销 | 全局 |
| Ctrl+Y / Ctrl+Shift+Z | 重做 | 全局 |
| Ctrl+P | 导出Word | 全局 |
| **文本格式** | | |
| Ctrl+B | 加粗 | 文本选中时 |
| Ctrl+I | 斜体 | 文本选中时 |
| Ctrl+U | 下划线 | 文本选中时 |
| **块操作** | | |
| Enter | 新建段落块 | 块末尾时 |
| Shift+Enter | 块内换行 | 块编辑时 |
| Ctrl+C | 复制块 | 块选中时 |
| Ctrl+V | 粘贴块 | 任意时 |
| Delete | 删除块 | 块选中时 |
| Ctrl+Up | 上移块 | 块选中时 |
| Ctrl+Down | 下移块 | 块选中时 |
| **标题** | | |
| Ctrl+1 ~ Ctrl+6 | 转为H1~H6 | 块选中时 |
| Ctrl+0 | 转为普通段落 | 块选中时 |
| **表格** | | |
| Tab | 跳转下一单元格 | 表格编辑时 |
| Shift+Tab | 跳转上一单元格 | 表格编辑时 |
| Ctrl+Shift+T | 插入表格 | 任意时 |
| **图片** | | |
| Ctrl+Shift+I | 插入图片 | 任意时 |
### 8.2 快捷键冲突处理
**优先级规则**:
1. 编辑器内部快捷键优先
2. 浏览器默认快捷键(如Ctrl+T新标签页)不覆盖
3. 与宿主应用(Chat)的快捷键协商避免冲突
**实现**:
```typescript
const handleKeyDown = (e: KeyboardEvent) => {
// 阻止浏览器默认行为
if ((e.ctrlKey || e.metaKey) && ['s', 'p', 'z', 'y'].includes(e.key.toLowerCase())) {
e.preventDefault();
}
// 分发快捷键
if (e.ctrlKey && e.key === 's') {
saveDocument();
} else if (e.ctrlKey && e.key === 'z') {
undo();
}
// ...
};
```
---
## 9. 撤销/重做功能
### 9.1 操作历史栈
**数据结构**:
```typescript
interface HistoryState {
blocks: DocumentBlock[];
timestamp: number;
}
interface HistoryStack {
past: HistoryState[]; // 历史状态
present: HistoryState; // 当前状态
future: HistoryState[]; // 重做栈
}
```
### 9.2 可撤销操作
- 添加/删除块
- 修改块内容
- 移动块位置
- 表格操作(插入行列、合并单元格等)
- 图片调整
### 9.3 不可撤销操作
- 保存文档(持久化操作)
- 导出文档
- 打开/关闭编辑器
### 9.4 实现
```typescript
const editorStore = create((set, get) => ({
history: {
past: [],
present: { blocks: [], timestamp: Date.now() },
future: []
},
pushHistory: () => {
const { blocks, history } = get();
set({
history: {
past: [...history.past, history.present],
present: { blocks: [...blocks], timestamp: Date.now() },
future: [] // 清空重做栈
}
});
},
undo: () => {
const { history } = get();
if (history.past.length === 0) return;
const previous = history.past[history.past.length - 1];
set({
blocks: previous.blocks,
history: {
past: history.past.slice(0, -1),
present: previous,
future: [history.present, ...history.future]
}
});
},
redo: () => {
const { history } = get();
if (history.future.length === 0) return;
const next = history.future[0];
set({
blocks: next.blocks,
history: {
past: [...history.past, history.present],
present: next,
future: history.future.slice(1)
}
});
}
}));
```
---
## 10. 搜索和替换
### 10.1 文档内搜索
**触发**: Ctrl+F 打开搜索框
**搜索框UI**:
```
┌─────────────────────────────┐
│ 查找: [关键词___] [↑] [↓] │
│ [x] 区分大小写 [x] 全词匹配│
│ 共找到 3 处,当前第 1 处 │
└─────────────────────────────┘
```
**搜索范围**:
- 标题内容
- 段落内容
- 表格单元格内容
**高亮显示**:
- 匹配文本背景黄色高亮
- 当前匹配项橙色高亮
### 10.2 替换功能
**UI扩展**:
```
┌─────────────────────────────┐
│ 查找: [旧文本___] │
│ 替换为: [新文本___] │
│ [替换] [全部替换] │
└─────────────────────────────┘
```
**替换逻辑**:
```typescript
const replaceAll = (searchText: string, replaceText: string) => {
const newBlocks = blocks.map(block => {
if (block.type === 'heading' || block.type === 'paragraph') {
return {
...block,
content: replaceInContent(block.content, searchText, replaceText)
};
} else if (block.type === 'table') {
return {
...block,
content: {
rows: block.content.rows.map(row => ({
cells: row.cells.map(cell => ({
...cell,
text: replaceInContent(cell.text, searchText, replaceText)
}))
}))
}
};
}
return block;
});
set({ blocks: newBlocks });
pushHistory();
};
```
---
## 11. 无障碍设计
### 11.1 键盘导航
- 所有功能均可通过键盘操作
- Tab键在块之间导航
- Enter键进入块编辑模式
- Esc键退出编辑模式
### 11.2 屏幕阅读器支持
- 块类型语义化标签(aria-label)
- 编辑状态通知(aria-live)
- 表格导航辅助
**示例**:
```tsx
```
### 11.3 色彩对比
- 文本和背景对比度≥4.5:1
- 选中状态明显区分
- 支持高对比度模式
---
## 12. 移动端适配(未来)
### 12.1 响应式布局
- 编辑器宽度自适应
- 表格横向滚动
- 工具栏折叠为下拉菜单
### 12.2 触控优化
- 拖拽手柄增大触控面积
- 长按显示右键菜单
- 双指缩放图片
---
**文档版本**: v1.0
**创建日期**: 2026-07-03
**下一步**: 参考《新编辑器开发手册.md》开始编码实现