新编辑器功能设计.md 20 KB

新编辑器功能设计文档

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调用:

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后自动保存
  • 退出保存: 关闭编辑器前提示保存

保存接口:

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

<div class="table-block" style="display: grid; grid-template-columns: 1fr 1fr 1fr;">
  <div class="table-cell">单元格1</div>
  <div class="table-cell">单元格2</div>
  <div class="table-cell">单元格3</div>
  ...
</div>

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 │
└───────┴───┘

数据结构变化:

// 合并前
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 插入/删除行列

右键菜单:

单元格右键 →
  - 在上方插入行
  - 在下方插入行
  - 在左侧插入列
  - 在右侧插入列
  - 删除当前行
  - 删除当前列
  - 合并单元格
  - 拆分单元格

插入行逻辑:

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  │  ← 实时显示新宽度

实现:

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

转换流程:

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显示:

未选中:
┌─────────────┐
│   [图片]     │
└─────────────┘

选中状态:
■─────────────■  ← 四角调整点
│             │
│   [图片]     │  ← 中心图片
│             │
■─────────────■

尺寸计算:

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 图片对齐

对齐选项:

  • 左对齐
  • 居中对齐
  • 右对齐

实现:

<div className={`image-block align-${block.style.align}`}>
  <img src={block.content} alt={block.metadata.alt} />
</div>

/* 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

实现:

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)的快捷键协商避免冲突

实现:

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 操作历史栈

数据结构:

interface HistoryState {
  blocks: DocumentBlock[];
  timestamp: number;
}

interface HistoryStack {
  past: HistoryState[];     // 历史状态
  present: HistoryState;    // 当前状态
  future: HistoryState[];   // 重做栈
}

9.2 可撤销操作

  • 添加/删除块
  • 修改块内容
  • 移动块位置
  • 表格操作(插入行列、合并单元格等)
  • 图片调整

9.3 不可撤销操作

  • 保存文档(持久化操作)
  • 导出文档
  • 打开/关闭编辑器

9.4 实现

const editorStore = create<EditorStore>((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扩展:

┌─────────────────────────────┐
│ 查找: [旧文本___]           │
│ 替换为: [新文本___]          │
│ [替换] [全部替换]           │
└─────────────────────────────┘

替换逻辑:

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)
  • 表格导航辅助

示例:

<div 
  role="article"
  aria-label={`${block.type} block, ${getBlockSummary(block)}`}
  tabIndex={0}
>
  <BlockRenderer block={block} />
</div>

11.3 色彩对比

  • 文本和背景对比度≥4.5:1
  • 选中状态明显区分
  • 支持高对比度模式

12. 移动端适配(未来)

12.1 响应式布局

  • 编辑器宽度自适应
  • 表格横向滚动
  • 工具栏折叠为下拉菜单

12.2 触控优化

  • 拖拽手柄增大触控面积
  • 长按显示右键菜单
  • 双指缩放图片

文档版本: v1.0
创建日期: 2026-07-03
下一步: 参考《新编辑器开发手册.md》开始编码实现