# 新编辑器架构设计文档 ## 1. 概述 ### 1.1 背景 当前前端使用MDXEditor组件编辑Markdown文档,后端基于旧架构。新后端已升级为基于SQLite的blocks存储架构,使用结构化的blocks数据来表示文档内容,支持更精细的内容管理和样式控制。 **现有应用架构**: 双面板布局 - **左侧面板(40%)**: ChatPanel - 聊天消息列表和输入 - **右侧面板(60%)**: 动态切换 - 默认: ExportRecordList (导出记录列表) - 编辑时: EditorPanel (文档编辑器) **集成目标**: 保留聊天面板和双面板布局,仅替换EditorPanel内部的编辑器内核(MDXEditor → BlockEditor)。 ### 1.2 核心目标 - **自研编辑器组件**:脱离MDXEditor依赖,实现完全自主可控的编辑器 - **blocks数据模型适配**:前端数据结构与后端blocks模型无缝对接 - **表格编辑增强**:支持表格渲染、编辑、合并单元格、列宽行高调整 - **图片编辑增强**:支持图片插入、尺寸调整、对齐方式设置 - **富文本支持**:段落和单元格内支持部分文本样式覆盖(加粗、颜色、字号等) - **保持现有布局**:聊天面板和ResizableLayout布局完全不变 ### 1.3 技术栈选型 | 技术 | 选型 | 理由 | |------|------|------| | 编辑器核心 | **自研基于contenteditable** | 完全控制渲染和交互逻辑 | | 状态管理 | Zustand | 保持现有技术栈一致 | | UI组件库 | Ant Design | 保持现有技术栈一致 | | 表格渲染 | 原生div+CSS Grid | 灵活控制布局和样式 | | 图片处理 | 原生img+Base64 | 简化上传流程 | --- ## 2. 后端blocks数据模型 ### 2.1 document_blocks表结构 ```sql CREATE TABLE document_blocks ( id TEXT PRIMARY KEY, -- 如 "block-h1-0" block_order INTEGER NOT NULL, -- 文档中的绝对位置(稀疏排序) type TEXT NOT NULL, -- heading / paragraph / table / image level INTEGER DEFAULT 0, -- 标题级别 1-6, 段落为0 "index" INTEGER DEFAULT 0, -- 该级别的序号 content TEXT, -- 文本内容或JSON结构 word_style TEXT, -- Word样式名 style TEXT, -- 自定义样式JSON metadata TEXT -- 元数据JSON ); ``` ### 2.2 Block类型说明 #### Heading(标题) ```typescript interface HeadingBlock { id: string; // "block-h1-0" block_order: number; // 100 type: "heading"; level: number; // 1-6 index: number; // 该级别序号 content: string | RichText[]; // 纯文本或富文本数组 word_style: string; // "Heading 1" style: StyleOverrides; // 自定义样式 metadata: { parent_id: string | null; // 父标题ID }; } ``` #### Paragraph(段落) ```typescript interface ParagraphBlock { id: string; block_order: number; type: "paragraph"; level: 0; index: 0; content: string | RichText[]; word_style: "Normal"; style: StyleOverrides; metadata: { parent_heading_id: string | null; }; } ``` #### Table(表格) ```typescript interface TableBlock { id: string; block_order: number; type: "table"; level: 0; index: 0; content: { rows: TableRow[]; }; word_style: "Table Grid"; style: StyleOverrides; metadata: { cols: number; rows: number; table_width: number; table_width_unit: "percent" | "cm" | "inch"; col_widths: number[]; row_heights?: number[]; // 可选,单位cm parent_heading_id: string | null; }; } interface TableRow { cells: TableCell[]; } interface TableCell { text: string | RichText[]; rowspan: number; colspan: number; style: CellStyleOverrides; } ``` #### Image(图片) ```typescript interface ImageBlock { id: string; block_order: number; type: "image"; level: 0; index: 0; content: string; // Base64 Data URL word_style: "Normal"; style: { width: number; height: number; unit: "cm" | "inch" | "px"; align: "left" | "center" | "right"; }; metadata: { alt: string; para_style: string; parent_heading_id: string | null; }; } ``` ### 2.3 富文本格式(RichText) 块内部分文本可有不同样式: ```typescript interface RichText { text: string; style: { bold?: boolean; italic?: boolean; underline?: boolean; color?: string; // 十六进制,如"FF0000" font_name?: string; font_size?: number; // 磅 }; } // 示例 content: [ { text: "普通文本", style: {} }, { text: "红色加粗", style: { bold: true, color: "FF0000" } } ] ``` --- ## 3. 前端数据层设计 ### 3.1 应用架构(保留现有布局) ``` ┌─────────────────────────────────────────────────────────┐ │ App.tsx (根组件) │ ├──────────────────────┬──────────────────────────────────┤ │ ChatPanel (左40%) │ 右侧面板(动态切换): │ │ ✅ 完全保留不变 │ - ExportRecordList (默认) │ │ - MessageList │ - EditorPanel (编辑时) ⭐ │ │ - MessageInput │ └── BlockEditor (新内核) │ │ - chatStore │ └── blocks编辑 │ └──────────────────────┴──────────────────────────────────┘ ``` **集成策略**: - App.tsx和ResizableLayout **完全不变** - ChatPanel及其所有子组件 **完全不变** - EditorPanel **保留外壳**,内部根据feature flag切换编辑器: ```typescript {USE_NEW_EDITOR ? : } ``` ### 3.2 数据流向 ``` 用户在Chat中点击"预览"按钮 ↓ MessageItem触发 onPreviewDocument(documentId) ↓ uiStore.openDocumentPreview(documentId) ✅ 现有逻辑保持 ↓ App.tsx检测到previewDocumentId变化 ↓ 右侧面板从ExportRecordList切换到EditorPanel ✅ 现有逻辑保持 ↓ EditorPanel加载文档 ⭐ 内部切换到BlockEditor ↓ BlockEditor: GET /api/v1/documents/{id}/blocks ↓ 前端Store(blocks数组按block_order排序) ↓ 编辑器渲染(BlockRenderer组件树) ↓ 用户编辑操作(插入/删除/修改) ↓ Store更新+本地block_order重算 ↓ PUT /api/v1/documents/{id}/blocks ↓ 后端更新SQLite ``` ### 3.3 Zustand Store设计 ```typescript // src/stores/editorStore.ts interface EditorStore { // 文档元数据 documentId: string | null; documentTitle: string; // blocks数据(按block_order排序) blocks: DocumentBlock[]; // 选中状态 selectedBlockId: string | null; // 操作方法 loadDocument: (id: string) => Promise; addBlock: (block: Partial, afterBlockId?: string) => void; updateBlock: (id: string, updates: Partial) => void; deleteBlock: (id: string) => void; moveBlock: (id: string, targetOrder: number) => void; saveDocument: () => Promise; // 辅助方法 getBlockById: (id: string) => DocumentBlock | undefined; getBlocksByType: (type: BlockType) => DocumentBlock[]; recomputeBlockOrders: () => void; // 重新分配稀疏block_order } ``` **与现有Store的关系**: - **uiStore**: 继续管理`previewDocumentId`、打开/关闭编辑器等UI状态 - **chatStore**: 继续管理聊天消息列表 - **editorStore**: ⭐ 新增,专门管理blocks编辑状态 **Store之间不直接耦合**,通过documentId传递关联。 ### 3.4 API Service层 ```typescript // src/services/blockService.ts export const blockService = { // 获取文档所有blocks async getBlocks(documentId: string): Promise { const res = await apiClient.get(`/documents/${documentId}/blocks`); return res.data.data.blocks; }, // 批量更新blocks async updateBlocks(documentId: string, blocks: DocumentBlock[]): Promise { await apiClient.put(`/documents/${documentId}/blocks`, { blocks }); }, // 单块更新 async updateBlock(documentId: string, blockId: string, updates: Partial): Promise { await apiClient.patch(`/documents/${documentId}/blocks/${blockId}`, updates); } }; ``` --- ## 4. 编辑器组件架构 ### 4.1 组件层级结构 ``` // 顶层编辑器容器 ├── // 工具栏(插入块、格式化等) ├── // 画布区域 │ ├── // 块渲染器(根据type渲染) │ │ ├── // 标题块 │ │ ├── // 段落块 │ │ ├── // 表格块 │ │ │ └── // 单元格(支持富文本) │ │ └── // 图片块 │ └── // 块级控制(拖拽、删除等) └── // 浮动工具栏(文本选中时) ``` ### 4.2 核心组件设计 #### BlockEditor(主编辑器) ```tsx // src/components/Editor/BlockEditor.tsx interface BlockEditorProps { documentId: string; readOnly?: boolean; onSave?: () => void; } export const BlockEditor: React.FC = ({ documentId, readOnly }) => { const { blocks, loadDocument, saveDocument } = useEditorStore(); useEffect(() => { loadDocument(documentId); }, [documentId]); return (
); }; ``` #### BlockRenderer(块渲染器) ```tsx // src/components/Editor/BlockRenderer.tsx interface BlockRendererProps { block: DocumentBlock; readOnly?: boolean; } export const BlockRenderer: React.FC = ({ block, readOnly }) => { switch (block.type) { case 'heading': return ; case 'paragraph': return ; case 'table': return ; case 'image': return ; default: return null; } }; ``` --- ## 5. 表格编辑实现 ### 5.1 表格渲染结构 使用div+CSS Grid实现灵活的表格布局: ```tsx // src/components/Editor/TableBlock.tsx export const TableBlock: React.FC<{ block: TableBlock }> = ({ block }) => { const { rows } = block.content; const { cols, col_widths, table_width, table_width_unit } = block.metadata; return (
`${w}${table_width_unit === 'percent' ? '%' : 'px'}`).join(' '), width: `${table_width}${table_width_unit === 'percent' ? '%' : 'px'}` }} > {rows.map((row, rowIdx) => row.cells.map((cell, colIdx) => ( handleCellUpdate(rowIdx, colIdx, updates)} /> )) )}
); }; ``` ### 5.2 单元格编辑 ```tsx // src/components/Editor/TableCell.tsx interface TableCellProps { cell: TableCell; onUpdate: (updates: Partial) => void; } export const TableCell: React.FC = ({ cell, onUpdate }) => { const [isEditing, setIsEditing] = useState(false); const handleDoubleClick = () => { setIsEditing(true); }; return (
{isEditing ? ( onUpdate({ text: newContent })} onBlur={() => setIsEditing(false)} /> ) : ( )}
); }; ``` ### 5.3 表格操作功能 - **插入行/列**: 在指定位置插入新行或列 - **删除行/列**: 删除选中的行或列 - **合并单元格**: 调整`rowspan`和`colspan` - **调整列宽**: 拖拽列边界,更新`col_widths`数组 - **调整行高**: 拖拽行边界,更新`row_heights`数组(可选) ```typescript // 表格操作辅助函数 export const tableOperations = { insertRow: (table: TableBlock, afterRow: number): TableBlock => { 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 } }; }, // insertColumn, deleteRow, deleteColumn, mergeCells... }; ``` --- ## 6. 图片编辑实现 ### 6.1 图片上传和插入 ```tsx // src/components/Editor/ImageBlock.tsx export const ImageBlock: React.FC<{ block: ImageBlock }> = ({ block }) => { const [isResizing, setIsResizing] = useState(false); const { width, height, align } = block.style; const handleImageUpload = async (file: File) => { const base64 = await fileToBase64(file); const dataUrl = `data:image/${file.type.split('/')[1]};base64,${base64}`; // 创建新的image block const imageBlock: ImageBlock = { id: generateBlockId('img'), block_order: computeNextOrder(), type: 'image', level: 0, index: 0, content: dataUrl, word_style: 'Normal', style: { width: 10, // 默认10cm height: 7, unit: 'cm', align: 'center' }, metadata: { alt: file.name, para_style: 'Normal', parent_heading_id: getCurrentHeadingId() } }; addBlock(imageBlock); }; return (
{block.metadata.alt} {!readOnly && ( { updateBlock(block.id, { style: { ...block.style, width: newWidth, height: newHeight } }); }} /> )}
); }; ``` ### 6.2 图片调整功能 - **尺寸调整**: 拖拽边角控制点 - **对齐方式**: 左对齐/居中/右对齐 - **替换图片**: 点击图片重新上传 - **删除图片**: 删除整个image block --- ## 7. 富文本编辑实现 ### 7.1 RichTextEditor组件 ```tsx // src/components/Editor/RichTextEditor.tsx interface RichTextEditorProps { content: string | RichText[]; onChange: (content: RichText[]) => void; onBlur?: () => void; } export const RichTextEditor: React.FC = ({ content, onChange }) => { const editorRef = useRef(null); // 初始化编辑器内容 useEffect(() => { if (editorRef.current) { editorRef.current.innerHTML = richTextToHtml(content); } }, []); // 处理格式化命令 const execCommand = (command: string, value?: string) => { document.execCommand(command, false, value); updateContent(); }; const updateContent = () => { if (editorRef.current) { const richText = htmlToRichText(editorRef.current.innerHTML); onChange(richText); } }; return (
execCommand('foreColor', e.target.value)} title="文字颜色" />
); }; ``` ### 7.2 富文本格式转换 ```typescript // src/utils/richTextConverter.ts // RichText数组 → HTML字符串(用于contenteditable渲染) export function richTextToHtml(content: string | RichText[]): string { if (typeof content === 'string') { return escapeHtml(content); } return content.map(segment => { let html = escapeHtml(segment.text); const { bold, italic, underline, color, font_name, font_size } = segment.style; const styles: string[] = []; if (color) styles.push(`color: #${color}`); if (font_name) styles.push(`font-family: ${font_name}`); if (font_size) styles.push(`font-size: ${font_size}pt`); if (bold) html = `${html}`; if (italic) html = `${html}`; if (underline) html = `${html}`; if (styles.length > 0) html = `${html}`; return html; }).join(''); } // HTML字符串 → RichText数组(从contenteditable提取) export function htmlToRichText(html: string): RichText[] { const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; const richText: RichText[] = []; function traverseNode(node: Node) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent || ''; if (text) { richText.push({ text, style: extractStyleFromParent(node.parentElement) }); } } else if (node.nodeType === Node.ELEMENT_NODE) { node.childNodes.forEach(traverseNode); } } traverseNode(tempDiv); return mergeAdjacentSameStyle(richText); } function extractStyleFromParent(element: HTMLElement | null): RichTextStyle { const style: RichTextStyle = {}; let el = element; while (el) { if (el.tagName === 'STRONG' || el.tagName === 'B') style.bold = true; if (el.tagName === 'EM' || el.tagName === 'I') style.italic = true; if (el.tagName === 'U') style.underline = true; const computedStyle = window.getComputedStyle(el); if (computedStyle.color) { const color = rgbToHex(computedStyle.color); if (color !== '000000') style.color = color; } if (computedStyle.fontFamily) style.font_name = computedStyle.fontFamily.replace(/['"]/g, ''); if (computedStyle.fontSize) style.font_size = parseFloat(computedStyle.fontSize) * 0.75; // px to pt el = el.parentElement; } return style; } ``` --- ## 8. 块操作和block_order管理 ### 8.1 稀疏排序策略 后端使用稀疏排序(间隔100),避免频繁更新block_order: ``` 初始: 0, 100, 200, 300, 400 插入: 0, 100, 150(新), 200, 300, 400 // 无需更新其他块 ``` 前端插入块时计算中间值: ```typescript export function computeInsertOrder(prevOrder: number, nextOrder: number): number { const gap = nextOrder - prevOrder; if (gap > 1) { // 有间隙,直接取中间值 return Math.floor((prevOrder + nextOrder) / 2); } else { // 间隙不足,触发局部重排 return -1; // 标记需要重排 } } ``` ### 8.2 局部重排 ```typescript export function rebalanceBlockOrders(blocks: DocumentBlock[]): DocumentBlock[] { // 按block_order排序 const sorted = [...blocks].sort((a, b) => a.block_order - b.block_order); // 重新分配:每个块间隔100 return sorted.map((block, index) => ({ ...block, block_order: index * 100 })); } // Store中的应用 export const editorStore = create((set, get) => ({ // ... addBlock: (block, afterBlockId) => { const blocks = get().blocks; const afterIndex = afterBlockId ? blocks.findIndex(b => b.id === afterBlockId) : -1; const prevOrder = afterIndex >= 0 ? blocks[afterIndex].block_order : 0; const nextOrder = afterIndex + 1 < blocks.length ? blocks[afterIndex + 1].block_order : prevOrder + 200; let newOrder = computeInsertOrder(prevOrder, nextOrder); if (newOrder === -1) { // 需要重排 const newBlocks = rebalanceBlockOrders([...blocks, { ...block, block_order: 0 }]); set({ blocks: newBlocks }); } else { set({ blocks: [...blocks, { ...block, block_order: newOrder }].sort((a, b) => a.block_order - b.block_order) }); } } })); ``` --- ## 9. 样式系统适配 ### 9.1 样式继承层级 ``` word_style(样式文件) → block.style(块级覆盖) → richText.style(文本片段覆盖) ``` ### 9.2 样式应用逻辑 ```typescript // src/utils/styleResolver.ts export function resolveBlockStyle(block: DocumentBlock): CSSProperties { // 1. 从word_style加载基础样式(后续可扩展为从样式库查询) const baseStyle = getWordStyleDefinition(block.word_style); // 2. 应用block.style覆盖 const blockOverrides = parseStyleJSON(block.style); // 3. 合并 return { ...baseStyle, ...blockOverrides }; } export function resolveRichTextStyle( blockStyle: CSSProperties, richTextStyle: RichTextStyle ): CSSProperties { return { ...blockStyle, fontWeight: richTextStyle.bold ? 'bold' : blockStyle.fontWeight, fontStyle: richTextStyle.italic ? 'italic' : blockStyle.fontStyle, textDecoration: richTextStyle.underline ? 'underline' : blockStyle.textDecoration, color: richTextStyle.color ? `#${richTextStyle.color}` : blockStyle.color, fontFamily: richTextStyle.font_name || blockStyle.fontFamily, fontSize: richTextStyle.font_size ? `${richTextStyle.font_size}pt` : blockStyle.fontSize }; } ``` --- ## 10. 交互设计 ### 10.1 键盘快捷键 | 快捷键 | 功能 | |--------|------| | Ctrl+B | 加粗 | | Ctrl+I | 斜体 | | Ctrl+U | 下划线 | | Ctrl+S | 保存 | | Enter | 新建段落块 | | Shift+Enter | 块内换行 | | Ctrl+Shift+T | 插入表格 | | Ctrl+Shift+I | 插入图片 | | Delete | 删除选中块 | ### 10.2 拖拽排序 使用`react-beautiful-dnd`或`dnd-kit`实现块级拖拽重排: ```tsx import { DndContext, closestCenter } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; export const BlockCanvas: React.FC = () => { const { blocks, moveBlock } = useEditorStore(); const handleDragEnd = (event) => { const { active, over } = event; if (active.id !== over.id) { moveBlock(active.id, over.id); } }; return ( b.id)} strategy={verticalListSortingStrategy}> {blocks.map(block => ( ))} ); }; ``` --- ## 11. 导出功能适配 ### 11.1 导出流程 ``` 用户点击"导出" → 前端调用 POST /api/v1/export/doc ↓ 后端读取document_blocks表 → 按blocks生成Word ↓ 返回下载链接 → 前端触发下载 ``` 前端只需调用API,无需处理blocks到Word的转换(后端负责): ```typescript // src/services/exportService.ts export const exportService = { async exportToDoc(documentId: string, styleId?: string): Promise { const res = await apiClient.post('/api/v1/export/doc', { documentId, styleId: styleId || null }); return res.data.data; } }; // 使用 const handleExport = async () => { const { downloadUrl, fileName } = await exportService.exportToDoc(documentId); // 触发浏览器下载 const link = document.createElement('a'); link.href = downloadUrl; link.download = fileName; link.click(); }; ``` ### 11.2 导出记录管理 ```tsx // src/components/ExportHistory.tsx export const ExportHistory: React.FC = () => { const [records, setRecords] = useState([]); useEffect(() => { loadRecords(); }, []); const loadRecords = async () => { const res = await apiClient.get('/api/v1/export/records', { params: { userId: currentUserId } }); setRecords(res.data.data.records); }; const handleDelete = async (recordId: string) => { await apiClient.delete(`/api/v1/export/records/${recordId}`, { params: { userId: currentUserId } }); loadRecords(); }; return ( ( <> 下载 )} />
); }; ``` --- ## 12. 性能优化策略 ### 12.1 虚拟滚动 大文档(1000+ blocks)使用虚拟列表渲染: ```tsx import { VariableSizeList } from 'react-window'; export const BlockCanvas: React.FC = () => { const { blocks } = useEditorStore(); return ( getBlockHeight(blocks[index])} width="100%" > {({ index, style }) => (
)}
); }; ``` ### 12.2 防抖保存 编辑过程中自动保存,防抖500ms: ```typescript const debouncedSave = useDebouncedCallback( () => { saveDocument(); }, 500 ); // 在updateBlock中调用 updateBlock: (id, updates) => { // ...更新逻辑 debouncedSave(); } ``` ### 12.3 图片懒加载 ```tsx export const ImageBlock: React.FC = ({ block }) => { return ( {block.metadata.alt} ); }; ``` --- ## 13. 迁移路径 ### 13.1 阶段性迁移 #### 阶段1: 并行开发 - 保留`WYSIWYGEditor.tsx`(MDXEditor) - 新建`BlockEditor.tsx`(新编辑器) - 通过feature flag切换: ```tsx export const DocumentEditorModal: React.FC = () => { const useNewEditor = useFeatureFlag('new-block-editor'); return useNewEditor ? : ; }; ``` #### 阶段2: 数据迁移 - 后端提供迁移脚本:旧Markdown文档 → blocks格式 - 前端调用迁移API: ```typescript const migrateDocument = async (documentId: string) => { await apiClient.post(`/api/v1/documents/${documentId}/migrate-to-blocks`); }; ``` #### 阶段3: 全面切换 - 移除MDXEditor依赖 - 删除旧编辑器代码 - 更新所有引用 ### 13.2 兼容性处理 对于尚未迁移的旧文档,前端自动检测并提示: ```tsx const { blocks, isLegacyDocument } = await loadDocument(documentId); if (isLegacyDocument) { showModal({ title: '文档需要升级', content: '此文档使用旧格式,需要升级后才能编辑', onOk: () => migrateDocument(documentId) }); } ``` --- ## 14. 测试策略 ### 14.1 单元测试 ```typescript // src/components/Editor/__tests__/BlockRenderer.test.tsx describe('BlockRenderer', () => { it('应正确渲染标题块', () => { const block: HeadingBlock = { id: 'block-h1-0', type: 'heading', level: 1, content: '测试标题', // ... }; const { getByText } = render(); expect(getByText('测试标题')).toBeInTheDocument(); }); it('应正确渲染富文本内容', () => { const block: ParagraphBlock = { id: 'block-p-0', type: 'paragraph', content: [ { text: '普通', style: {} }, { text: '加粗', style: { bold: true } } ], // ... }; const { container } = render(); expect(container.querySelector('strong')).toHaveTextContent('加粗'); }); }); ``` ### 14.2 集成测试 ```typescript // src/components/Editor/__tests__/BlockEditor.integration.test.tsx describe('BlockEditor集成测试', () => { it('应能完整流程:加载→编辑→保存', async () => { mockApiGet('/documents/doc-123/blocks', { blocks: [/* mock数据 */] }); const { getByText, getByRole } = render(); // 等待加载完成 await waitFor(() => expect(getByText('标题1')).toBeInTheDocument()); // 编辑操作 const heading = getByText('标题1'); fireEvent.doubleClick(heading); fireEvent.input(heading, { target: { textContent: '新标题' } }); // 保存 mockApiPut('/documents/doc-123/blocks', {}); const saveBtn = getByRole('button', { name: '保存' }); fireEvent.click(saveBtn); await waitFor(() => { expect(mockApiPut).toHaveBeenCalledWith('/documents/doc-123/blocks', { blocks: expect.arrayContaining([ expect.objectContaining({ content: '新标题' }) ]) }); }); }); }); ``` ### 14.3 E2E测试(Playwright) ```typescript // e2e/block-editor.spec.ts test('表格编辑流程', async ({ page }) => { await page.goto('/editor/doc-123'); // 插入表格 await page.click('[aria-label="插入表格"]'); await page.fill('[name="rows"]', '3'); await page.fill('[name="cols"]', '3'); await page.click('text=确定'); // 编辑单元格 const cell = page.locator('.table-cell').first(); await cell.dblclick(); await cell.fill('测试内容'); await cell.press('Escape'); // 保存 await page.click('text=保存'); await expect(page.locator('text=保存成功')).toBeVisible(); }); ``` --- ## 15. 技术风险和应对 ### 15.1 contenteditable的坑 **风险**: contenteditable在不同浏览器行为不一致 **应对**: - 使用`document.execCommand`标准API - 禁用浏览器默认的格式化行为 - 自行管理光标位置和选区 ```typescript // 统一处理contenteditable const makeContentEditable = (element: HTMLElement) => { element.contentEditable = 'true'; // 禁用默认格式化 element.addEventListener('paste', (e) => { e.preventDefault(); const text = e.clipboardData?.getData('text/plain'); document.execCommand('insertText', false, text); }); // 阻止浏览器自动添加div/span element.addEventListener('input', () => { normalizeContentEditable(element); }); }; ``` ### 15.2 大表格性能 **风险**: 大表格(100x100单元格)渲染卡顿 **应对**: - 虚拟滚动渲染可视区域 - 单元格使用`React.memo`防止无效重渲染 - 延迟加载表格内图片 ### 15.3 数据同步冲突 **风险**: 多标签页同时编辑导致数据冲突 **应对**: - 使用`BroadcastChannel` API实现标签页间通信 - 编辑时加乐观锁(version字段) - 冲突时提示用户选择保留哪个版本 ```typescript // 标签页同步 const channel = new BroadcastChannel('editor-sync'); channel.onmessage = (event) => { if (event.data.documentId === currentDocumentId) { showNotification('其他标签页已更新文档,是否重新加载?'); } }; // 保存时广播 const saveDocument = async () => { await apiClient.put(`/documents/${documentId}`, { blocks, version }); channel.postMessage({ documentId, action: 'saved' }); }; ``` --- ## 16. 未来扩展方向 ### 16.1 协同编辑(阶段2) 使用Yjs实现CRDT协同: ```typescript import * as Y from 'yjs'; import { WebsocketProvider } from 'y-websocket'; const ydoc = new Y.Doc(); const provider = new WebsocketProvider('ws://api.axonix.com/collab', documentId, ydoc); const yblocks = ydoc.getArray('blocks'); // 监听远程更新 yblocks.observe(() => { const blocks = yblocks.toArray(); updateStore(blocks); }); // 本地修改同步 const updateBlock = (id, updates) => { const index = yblocks.toArray().findIndex(b => b.id === id); yblocks.delete(index, 1); yblocks.insert(index, [{ ...yblocks.get(index), ...updates }]); }; ``` ### 16.2 评论和批注 在block级别添加评论: ```typescript interface CommentThread { id: string; blockId: string; comments: Comment[]; resolved: boolean; } interface Comment { id: string; userId: string; content: string; createdAt: number; } ``` ### 16.3 版本对比 可视化显示两个版本的diff: ```tsx export const VersionDiff: React.FC<{ fromVersion, toVersion }> = ({ fromVersion, toVersion }) => { const diff = computeDiff(fromVersion.blocks, toVersion.blocks); return (
{diff.map(change => (
{change.type === 'added' && } {change.type === 'removed' && } {change.type === 'modified' && }
))}
); }; ``` --- ## 17. 参考资料 ### 17.1 后端文档 - [content-sqlite-design.md](../../ax-backend-v1/docs/content-sqlite-design.md) - SQLite数据库设计 - [text-editor-backend-api.md](../../ax-backend-v1/docs/text-editor-backend-api.md) - 后端API文档 - [export-doc-content-mapping.md](../../ax-backend-v1/docs/export-doc-content-mapping.md) - 导出功能说明 ### 17.2 技术参考 - [contenteditable最佳实践](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable) - [CSS Grid布局指南](https://css-tricks.com/snippets/css/complete-guide-grid/) - [react-beautiful-dnd文档](https://github.com/atlassian/react-beautiful-dnd) - [Yjs协同编辑框架](https://docs.yjs.dev/) --- **文档版本**: v1.0 **创建日期**: 2026-07-03 **作者**: AI Assistant **状态**: 设计阶段