# 新编辑器功能设计文档 ## 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
单元格1
单元格2
单元格3
...
``` ### 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
{block.metadata.alt}
/* 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》开始编码实现