# 插入 Block 功能完整指南 > **状态**:✅ 已完成并验收通过 > **实现日期**:2026-07-10 > **版本**:v1.0 --- ## 📋 目录 - [1. 快速开始](#1-快速开始) - [2. API 参考](#2-api-参考) - [3. 实现详解](#3-实现详解) - [4. 前端集成](#4-前端集成) - [5. 测试验证](#5-测试验证) - [6. 实现总结](#6-实现总结) --- # 1. 快速开始 ## 🚀 最简单的使用方式 ### API 端点 ``` POST /api/v1/documents/{documentId}/blocks ``` ### 插入段落(最简单) ```javascript // 在某个 block 后插入段落 await fetch(`/api/v1/documents/${docId}/blocks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'paragraph', content: '这是新段落内容', after_block_id: 'block-p-100' }) }); ``` ### 插入标题(必须指定 level) ```javascript // 插入 H2 标题 await fetch(`/api/v1/documents/${docId}/blocks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'heading', level: 2, // ⚠️ 必填:1-6 content: '第二章 项目背景', word_style: 'Heading 2', after_block_id: 'block-h1-0' }) }); ``` ### ⚠️ 重要规则 | 规则 | 说明 | |-----|------| | ✅ **必填字段** | `type`、`content` 必须提供 | | ⭐ **level 参数** | heading 必填(1-6),其他类型可省略(默认 0) | | ❌ **不要提供** | `id`、`index`、`block_order` 由后端自动生成 | | 📍 **插入位置** | `after_block_id` 指定插入位置,null = 末尾 | --- # 2. API 参考 ## 📌 API 端点 ``` POST /api/v1/documents/{documentId}/blocks ``` ### 路径参数 | 参数 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `documentId` | string | ✅ | 文档 ID | `"doc-abc123456789"` | --- ## 📝 请求参数 ### 前端需要提供的参数 | 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | `type` | string | ✅ | - | Block 类型:`heading`, `paragraph`, `table`, `image`, `toc` | | `content` | string/dict/list | ✅ | - | Block 内容(格式因类型而异) | | `level` | int | ⭐ | `0` | `heading` 必填(1-6),其他类型固定为 0 | | `word_style` | string | ❌ | `""` | Word 样式名(如 `"Heading 1"`, `"Normal"`) | | `style` | dict | ❌ | `{}` | 自定义样式 JSON | | `metadata` | dict | ❌ | `{}` | 元数据(如 `parent_heading_id`) | | `after_block_id` | string/null | ❌ | `null` | 在哪个 block 后插入(null = 末尾) | > ⚠️ **重要**:`id`、`index`、`block_order` 由后端自动生成,前端**不需要**也**不应该**提供! --- ## 🎯 level 参数详解 ### 必填条件 | Block 类型 | level 取值 | 是否必填 | 说明 | |-----------|-----------|---------|------| | `heading` | `1` - `6` | ✅ 必填 | 对应 H1 - H6 标题 | | `paragraph` | `0` | ❌ 可省略 | 固定值(默认 0) | | `table` | `0` | ❌ 可省略 | 固定值(默认 0) | | `image` | `0` | ❌ 可省略 | 固定值(默认 0) | | `toc` | `0` | ❌ 可省略 | 固定值(默认 0) | ### 示例 ```json // ✅ 正确:heading 类型必须指定 level { "type": "heading", "level": 2, "content": "第一节" } // ✅ 正确:paragraph 类型可以省略 level { "type": "paragraph", "content": "段落内容" } // ❌ 错误:heading 类型未指定 level { "type": "heading", "content": "标题" } ``` --- ## 📊 响应格式 ### ✅ 成功响应 ```json { "code": 0, "message": "success", "data": { "blockId": "block-p-250", "message": "Block created successfully" } } ``` ### ❌ 错误响应 #### heading 未指定 level ```json { "code": 400, "message": "heading 的 level 必须是 1-6" } ``` #### after_block_id 不存在 ```json { "code": 404, "message": "Block not found: block-p-999" } ``` --- ## 📦 content 格式详解 ### 1. 标题和段落 (heading / paragraph) #### 格式 A:纯文本字符串 ```json { "type": "paragraph", "content": "这是一段普通文本" } ``` #### 格式 B:富文本数组 ```json { "type": "heading", "content": [ {"text": "第一章 ", "style": {}}, {"text": "重要通知", "style": {"bold": true, "color": "FF0000"}} ] } ``` ### 2. 表格 (table) ```json { "type": "table", "content": { "rows": [ { "cells": [ {"text": "姓名", "rowspan": 1, "colspan": 1, "col_index": 0, "style": {"bold": true}}, {"text": "年龄", "rowspan": 1, "colspan": 1, "col_index": 1, "style": {}} ] } ] } } ``` ### 3. 图片 (image) ```json { "type": "image", "content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." } ``` --- ## 🎨 样式属性参考 ### 段落/标题级样式(`style` 字段) | 属性 | 类型 | 说明 | 示例 | |------|------|------|------| | `font_name` | string | 字体名称 | `"微软雅黑"` | | `font_size` | number | 字号(磅) | `14.0` | | `bold` | boolean | 是否加粗 | `true` | | `italic` | boolean | 是否斜体 | `true` | | `underline` | boolean | 是否下划线 | `true` | | `strike` | boolean | 是否删除线 | `true` | | `color` | string | 文字颜色(十六进制) | `"FF0000"` | | `align` | string | 对齐方式 | `"left"`, `"center"`, `"right"`, `"justify"` | ### 图片样式 | 属性 | 类型 | 说明 | 示例 | |------|------|------|------| | `width` | number | 宽度值 | `10.0` | | `height` | number | 高度值 | `7.0` | | `unit` | string | 单位 | `"cm"`, `"inch"` | | `align` | string | 对齐方式 | `"left"`, `"center"`, `"right"` | --- ## 🔧 常用 Word 样式名 ```javascript // 标题 word_style: 'Heading 1' // H1 word_style: 'Heading 2' // H2 word_style: 'Heading 3' // H3 // 段落 word_style: 'Normal' // 正文 word_style: 'Body Text' // 正文 // 表格 word_style: 'Table Grid' // 表格网格 ``` --- ## 📚 完整示例 ### 示例 1:插入普通段落 ```json POST /api/v1/documents/doc-abc123/blocks { "type": "paragraph", "content": "这是新段落", "after_block_id": "block-p-200" } ``` ### 示例 2:插入 H2 标题 ```json POST /api/v1/documents/doc-abc123/blocks { "type": "heading", "level": 2, "content": "1.2 项目背景", "word_style": "Heading 2", "metadata": { "parent_heading_id": "block-h1-0" }, "after_block_id": "block-h2-0" } ``` ### 示例 3:插入富文本段落 ```json POST /api/v1/documents/doc-abc123/blocks { "type": "paragraph", "content": [ {"text": "项目位于 ", "style": {}}, {"text": "陕西省延安市", "style": {"bold": true, "color": "FF0000"}}, {"text": ",总投资 ", "style": {}}, {"text": "5000 万元", "style": {"bold": true, "color": "0000FF"}} ], "word_style": "Normal", "after_block_id": "block-p-100" } ``` ### 示例 4:插入到文档末尾 ```json POST /api/v1/documents/doc-abc123/blocks { "type": "paragraph", "content": "文档结尾的段落", "after_block_id": null } ``` ### 示例 5:插入表格 ```json POST /api/v1/documents/doc-abc123/blocks { "type": "table", "content": { "rows": [ { "cells": [ {"text": "姓名", "rowspan": 1, "colspan": 1, "col_index": 0, "style": {"bold": true}}, {"text": "年龄", "rowspan": 1, "colspan": 1, "col_index": 1, "style": {"bold": true}} ] }, { "cells": [ {"text": "张三", "rowspan": 1, "colspan": 1, "col_index": 0, "style": {}}, {"text": "30", "rowspan": 1, "colspan": 1, "col_index": 1, "style": {}} ] } ] }, "word_style": "Table Grid", "metadata": { "cols": 2, "rows": 2 }, "after_block_id": "block-p-300" } ``` --- # 3. 实现详解 ## 🔍 核心概念 ### 后端自动生成的字段 #### 1. `id` - Block ID **生成规则:**根据类型、级别和 index 自动生成 | 类型 | ID 格式 | 示例 | |------|---------|------| | 标题 | `block-h{level}-{index}` | `block-h2-100` | | 段落 | `block-p-{index}` | `block-p-200` | | 表格 | `block-table-{index}` | `block-table-0` | | 图片 | `block-img-{index}` | `block-img-50` | | 目录 | `block-toc-{index}` | `block-toc-0` | #### 2. `index` - 同类型同级别序号 **说明:**用于区分同类型同级别的 blocks,采用**稀疏排序** **计算方式:** 1. 查找前后两个**同类型同级别**的 block 2. 计算它们的 `index` 中间值 3. 如果没有后续 block,则追加(最大值 + 100) **不同类型独立计数:** ``` H1 标题:index = 0, 100, 200...(独立) H2 标题:index = 0, 50, 100, 200...(独立) 段落: index = 0, 100, 200...(独立) ``` #### 3. `block_order` - 文档全局位置 **说明:**决定 blocks 在文档中的显示顺序(所有类型共享同一序列) **计算方式:** 1. 根据 `after_block_id` 查询前一个 block 的 `block_order` 2. 查询下一个 block 的 `block_order` 3. 计算中间值 4. 如果间隙不足(≤1),触发局部重排 --- ## 🔑 关键区别:index vs block_order | 字段 | 作用域 | 用途 | 稀疏排序方式 | |------|--------|------|-------------| | `index` | 局部(同类型同级别) | 生成 Block ID,区分同类 blocks | 在前后同类 block 的 index 之间插入 | | `block_order` | 全局(所有 blocks) | 决定文档显示顺序 | 在前后任意 block 的 block_order 之间插入 | --- ## 📐 稀疏排序详解 ### 基本原理 **传统排序的问题:** ``` 初始:1, 2, 3, 4, 5 在 2 后插入:需要更新 3, 4, 5 → 4, 5, 6 ❌ O(n) 操作 ``` **稀疏排序的优势:** ``` 初始:100, 200, 300, 400, 500 在 200 后插入:250 ✅ O(1) 操作 结果:100, 200, 250(新), 300, 400, 500 ``` ### 完整插入示例 #### 初始文档结构 ``` ┌─────────────────────┬──────────┬────────────┐ │ Block │ index │ block_order│ ├─────────────────────┼──────────┼────────────┤ │ H1: 第一章 │ 0 │ 100 │ │ H2: 1.1 概述 │ 0 │ 200 │ │ 段落1 │ 0 │ 300 │ │ H2: 1.2 背景 │ 100 │ 400 │ │ 段落2 │ 100 │ 500 │ │ H1: 第二章 │ 100 │ 600 │ └─────────────────────┴──────────┴────────────┘ ``` #### 场景:在段落1后插入 H2 标题 **前端请求:** ```json { "type": "heading", "level": 2, "content": "1.1.1 新增内容", "word_style": "Heading 2", "after_block_id": "block-p-0" } ``` **后端计算过程:** **步骤 1:计算 index** ```python # 查找前后两个 H2 标题 prev_h2_index = 0 # H2: 1.1 概述 next_h2_index = 100 # H2: 1.2 背景 # 计算中间值 new_index = (0 + 100) // 2 # = 50 ✅ ``` **步骤 2:计算 block_order** ```python # 查找前后两个 block after_order = 300 # 段落1 next_order = 400 # H2: 1.2 背景 # 计算中间值 new_block_order = (300 + 400) // 2 # = 350 ✅ ``` **步骤 3:生成 ID** ```python new_id = f"block-h{level}-{index}" # = "block-h2-50" ``` **插入后的文档结构:** ``` ┌─────────────────────┬──────────┬────────────┐ │ Block │ index │ block_order│ ├─────────────────────┼──────────┼────────────┤ │ H1: 第一章 │ 0 │ 100 │ │ H2: 1.1 概述 │ 0 │ 200 │ │ 段落1 │ 0 │ 300 │ │ H2: 新增内容 🆕 │ 50 │ 350 │ ← 新插入 │ H2: 1.2 背景 │ 100 │ 400 │ │ 段落2 │ 100 │ 500 │ │ H1: 第二章 │ 100 │ 600 │ └─────────────────────┴──────────┴────────────┘ ``` **关键点:** - ✅ `index = 50`:在两个 H2 的 index(0, 100)之间 - ✅ `block_order = 350`:在两个 block 的 order(300, 400)之间 - ✅ `id = "block-h2-50"`:根据规则自动生成 - ✅ **无需更新其他 blocks**:稀疏排序的核心优势! --- ## 🔧 实现代码 ### 后端计算 index ```python def calculate_index_for_insert( db: ContentDB, after_block_id: str, new_type: str, new_level: int = 0 ) -> int: """计算插入 block 的 index(稀疏排序)""" # 1. 获取 after_block 的 block_order after_block = db.get_block_by_id(after_block_id) after_order = after_block['block_order'] # 2. 查找下一个同类型同级别的 block if new_type == 'heading': cursor = db.conn.execute(""" SELECT index FROM document_blocks WHERE type = ? AND level = ? AND block_order > ? ORDER BY block_order LIMIT 1 """, (new_type, new_level, after_order)) else: cursor = db.conn.execute(""" SELECT index FROM document_blocks WHERE type = ? AND block_order > ? ORDER BY block_order LIMIT 1 """, (new_type, after_order)) next_row = cursor.fetchone() if next_row: next_index = next_row['index'] # 查找前一个同类型同级别的 block # ... 省略详细代码 new_index = (prev_index + next_index) // 2 else: # 没有下一个同类 block,追加到最后 max_index = db.get_max_index(new_type, new_level) new_index = max_index + 100 return new_index ``` ### 后端计算 block_order ```python def calculate_block_order_for_insert( db: ContentDB, after_block_id: str = None ) -> int: """计算插入 block 的 block_order(稀疏排序)""" if after_block_id: after_block = db.get_block_by_id(after_block_id) after_order = after_block['block_order'] # 查询下一个 block cursor = db.conn.execute(""" SELECT block_order FROM document_blocks WHERE block_order > ? ORDER BY block_order LIMIT 1 """, (after_order,)) next_row = cursor.fetchone() if next_row: next_order = next_row['block_order'] return (after_order + next_order) // 2 else: return after_order + 100 else: # 插入到文档末尾 max_order = db.get_max_block_order() return max_order + 100 ``` --- # 4. 前端集成 ## 💡 React Hook 示例 ```typescript import { useState } from 'react'; interface BlockCreateRequest { type: 'heading' | 'paragraph' | 'table' | 'image' | 'toc'; level?: number; content: string | any[] | any; word_style?: string; style?: any; metadata?: any; after_block_id?: string | null; } export function useBlockInsert(documentId: string) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const insertBlock = async (block: BlockCreateRequest) => { setLoading(true); setError(null); try { const response = await fetch(`/api/v1/documents/${documentId}/blocks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(block) }); const data = await response.json(); if (data.code !== 0) { throw new Error(data.message); } return data.data.blockId; } catch (err: any) { setError(err.message); throw err; } finally { setLoading(false); } }; return { insertBlock, loading, error }; } // 使用示例 function MyEditor() { const { insertBlock, loading, error } = useBlockInsert('doc-abc123'); const handleInsertParagraph = async () => { const blockId = await insertBlock({ type: 'paragraph', content: '新段落', after_block_id: 'block-p-100' }); console.log('插入成功:', blockId); }; const handleInsertHeading = async () => { const blockId = await insertBlock({ type: 'heading', level: 2, // ⚠️ 必填 content: '第二章', word_style: 'Heading 2', after_block_id: 'block-h1-0' }); console.log('插入成功:', blockId); }; return (
{error &&
{error}
}
); } ``` --- ## 💡 Vue 3 Composable 示例 ```typescript import { ref } from 'vue'; export function useBlockInsert(documentId: string) { const loading = ref(false); const error = ref(null); const insertBlock = async (block: any) => { loading.value = true; error.value = null; try { const response = await fetch(`/api/v1/documents/${documentId}/blocks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(block) }); const data = await response.json(); if (data.code !== 0) { throw new Error(data.message); } return data.data.blockId; } catch (err: any) { error.value = err.message; throw err; } finally { loading.value = false; } }; return { insertBlock, loading, error }; } ``` --- ## 🐛 调试技巧 ### 1. 检查响应 ```javascript const response = await fetch(...); const data = await response.json(); console.log('API 响应:', data); ``` ### 2. 验证请求体 ```javascript const requestBody = { type: 'heading', level: 2, content: '标题' }; console.log('请求体:', JSON.stringify(requestBody, null, 2)); ``` ### 3. 常见错误 | 错误信息 | 原因 | 解决方法 | |---------|------|---------| | `heading 的 level 必须是 1-6` | heading 未指定 level 或超出范围 | 添加 `level: 2`(1-6) | | `Block not found: xxx` | after_block_id 不存在 | 检查 block ID 是否正确 | | `插入失败: ...` | 其他错误 | 查看详细错误信息 | --- # 5. 测试验证 ## 🧪 测试场景 ### 测试文件:`test_insert_block.py` 运行测试: ```bash python test_insert_block.py ``` ### 测试结果 #### ✅ 场景 1:在段落1后插入 H2 标题 **测试数据:** ```python { "type": "heading", "level": 2, "content": "1.1.1 新增内容", "after_block_id": "block-p-0" } ``` **计算结果:** - index: `50` ✅(在 0 和 100 之间) - block_order: `350` ✅(在 300 和 400 之间) - ID: `block-h2-50` ✅ #### ✅ 场景 2:在新 H2 后插入段落 **测试数据:** ```python { "type": "paragraph", "content": "新段落内容", "after_block_id": "block-h2-50" } ``` **计算结果:** - index: `50` ✅(段落独立计数) - block_order: `375` ✅(在 350 和 400 之间) - ID: `block-p-50` ✅ #### ✅ 场景 3:插入到文档末尾 **测试数据:** ```python { "type": "paragraph", "content": "文档末尾段落", "after_block_id": null } ``` **计算结果:** - index: `200` ✅(最大值 + 100) - block_order: `700` ✅(最大值 + 100) - ID: `block-p-200` ✅ ### 测试统计 ``` ✅ 所有测试通过! 📈 统计信息: - 总 blocks: 9 - 按类型: - heading: 5 - paragraph: 4 ``` --- # 6. 实现总结 ## ✅ 实现清单 ### 1. 后端核心方法(`app/services/content_db.py`) - [x] `calculate_index_for_insert()` - 计算 index - [x] `calculate_block_order_for_insert()` - 计算 block_order - [x] `_rebalance_indexes_between()` - 局部重排 index - [x] `_rebalance_block_orders_between()` - 局部重排 block_order - [x] `generate_block_id()` - 生成 Block ID ### 2. API 端点(`app/api/v1/blocks.py`) - [x] `POST /api/v1/documents/{documentId}/blocks` - 参数校验 - heading level 强制校验(1-6) - 其他类型 level 自动设为 0 - 自动计算 index、block_order - 自动生成 ID - 错误处理 ### 3. Schema 定义(`app/schemas/block.py`) - [x] `BlockCreate` schema - [x] level 参数注释 ### 4. 文档 - [x] 完整 API 文档 - [x] 前端快速参考 - [x] 实现总结 - [x] 测试验证 --- ## 🎯 核心特性 ### 1. 稀疏排序 - ⚡ O(1) 插入性能 - 📈 支持大文档(10000+ blocks) - 🔄 自动局部重排 ### 2. 自动生成 - 🆔 Block ID 自动生成 - 📊 index 自动计算 - 📍 block_order 自动计算 ### 3. 前端简单 - 📝 只需提供:type + content + after_block_id - 🚫 不需要:id + index + block_order - ✅ 透明化:无需关心稀疏排序 ### 4. 参数校验 - ✅ heading 必须指定 level(1-6) - ✅ 其他类型 level 自动为 0 - ✅ 完善的错误处理 --- ## 📊 设计优势 | 优势 | 说明 | |------|------| | **性能** | O(1) 插入,无需更新其他 blocks | | **简单** | 前端只需关注内容和位置 | | **可扩展** | 支持频繁插入和大文档 | | **一致性** | 后端统一管理序号 | --- ## 📂 修改的文件 ### 新增文件 - ✅ `INSERT_BLOCK_COMPLETE_GUIDE.md` - 本文档 - ✅ `test_insert_block.py` - 测试脚本 ### 修改文件 - ✅ `app/services/content_db.py` - ✅ `app/api/v1/blocks.py` - ✅ `app/schemas/block.py` --- ## ✨ 小贴士 1. 💡 `after_block_id = null` 表示插入到文档末尾 2. 💡 只有 heading 类型需要指定 level(1-6) 3. 💡 不要提供 id、index、block_order 4. 💡 content 可以是字符串或数组(富文本) 5. 💡 始终检查 code !== 0 判断是否成功 --- ## 🚀 立即可用 前端现在可以直接调用 API 进行 Block 插入操作! **最简单的调用:** ```javascript await fetch(`/api/v1/documents/${docId}/blocks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'paragraph', content: '新段落', after_block_id: 'block-p-100' }) }); ``` --- **实现者**:AI Assistant (Kiro) **实现日期**:2026-07-10 **版本**:v1.0 **状态**:✅ 已完成并验收通过