# TOC (目录) 功能完整指南 > **最后更新**: 2026-07-10 > **文档版本**: v2.0 > **状态**: ✅ 已完成并上线 --- ## 目录 1. [功能概述](#功能概述) 2. [快速开始](#快速开始) 3. [功能需求与设计](#功能需求与设计) 4. [实现细节](#实现细节) 5. [页码编号机制](#页码编号机制) 6. [自动更新功能](#自动更新功能) 7. [测试验证](#测试验证) 8. [已知问题与限制](#已知问题与限制) 9. [故障排查](#故障排查) --- ## 功能概述 ### 背景问题 原系统在解析 Word 文档时**完全跳过了 SDT 目录控件**(Structured Document Tag),导致: - 目录信息完全丢失 - 用户在前端编辑器中看不到目录 - 导出时无法生成目录 ### 解决方案 实现了完整的 TOC Block 支持: - ✅ **解析识别** - 正确识别 Word SDT 目录控件 - ✅ **数据存储** - 保存目录配置(层级、超链接、页码等) - ✅ **前端展示** - 只读占位符,不可编辑但可删除 - ✅ **智能导出** - 插入 TOC 域代码,自动生成目录 - ✅ **页码控制** - 目录单独一页,正文从第 1 页开始 - ✅ **自动更新** - 使用 WPS/Word API 自动更新域 ### 用户价值 1. **文档完整性** - 保留原始文档的目录结构 2. **编辑便利性** - 在编辑器中看到目录位置 3. **导出一致性** - 导出文档自动包含目录 4. **开箱即用** - 下载的文档已生成目录,无需手动更新 --- ## 快速开始 ### 1. 上传包含目录的文档 ```bash curl -X POST http://localhost:8000/api/v1/documents \ -F "file=@document_with_toc.docx" ``` ### 2. 查看解析结果 ```json { "blocks": [ { "id": "toc-001", "type": "toc", "content": {"title": "目录"}, "metadata": { "toc_config": { "levels": "1-3", "use_hyperlinks": true, "use_page_numbers": true }, "readonly": true, "deletable": true } } ] } ``` ### 3. 导出文档 ```bash curl -X POST http://localhost:8000/api/v1/documents/{id}/export/doc \ -o output.docx ``` 生成的文档会: - 自动包含目录(已生成,无需更新) - 目录单独一页 - 页码从正文第一页开始(第 1 页) - 支持超链接点击跳转 --- ## 功能需求与设计 ### TOC Block 数据结构 ```json { "id": "toc-001", "blockOrder": 0, "type": "toc", "level": 0, "index": 0, "content": { "title": "目录" }, "wordStyle": "TOC", "metadata": { "toc_config": { "levels": "1-3", // 包含的标题层级 "use_hyperlinks": true, // 是否使用超链接 "use_page_numbers": true, // 是否显示页码 "hide_page_numbers_in_web": true, "use_outline_levels": true, "show_leader_dots": true, "leader_char": "." }, "is_auto_generated": true, // 标记为自动生成 "readonly": true, // 前端不可编辑 "deletable": true // 前端可删除 } } ``` ### 核心功能需求 | 功能 | 需求 | 状态 | |------|------|------| | FR-1 | 解析 Word 文档目录 | ✅ | | FR-2 | 存储目录配置 | ✅ | | FR-3 | 前端只读展示 | ✅ | | FR-4 | 删除目录 | ✅ | | FR-5 | 导出包含目录 | ✅ | | FR-6 | 禁止编辑内容 | ✅ | | FR-7 | 自动更新域 | ✅ | | FR-8 | 页码从正文开始 | ✅ | --- ## 实现细节 ### 1. 解析阶段 (word_parser.py) #### 检测 SDT 目录控件 ```python # 遍历 body 元素,检测 SDT for child in body: tag = child.tag if tag.endswith('sdt'): # 提取 TOC 信息 toc_block = _extract_toc_from_sdt(child, block_order, parent_stack) if toc_block: blocks.append(toc_block) ``` #### 提取目录配置 从 SDT XML 中提取: - 目录标题(第一个段落的文本) - TOC 域代码参数(`TOC \o "1-3" \h \z \u`) - 层级、超链接、页码等配置 ### 2. 导出阶段 (export_service.py) #### 主要函数 | 函数 | 功能 | 位置 | |------|------|------| | `_render_toc_block()` | 渲染 TOC block | Line 1275 | | `_create_toc_field()` | 创建 TOC 域代码 | Line 1315 | | `_set_update_fields_on_open()` | 设置自动更新 | Line 1176 | | `_add_page_number_footer_with_restart()` | 添加页码(重启编号) | Line 1443 | | `update_document_fields()` | 使用 WPS/Word API 更新域 | Line 17 | #### 渲染流程 ``` 1. 检测 TOC block → 设置自动更新域 ↓ 2. 在目录前添加分页符 ↓ 3. 添加目录标题(居中,Normal 样式) ↓ 4. 插入 TOC 域代码 ↓ 5. 添加分节符(目录后开始新节) ↓ 6. 为新节添加页码页脚(从 1 开始) ↓ 7. 渲染其他 blocks(heading, paragraph 等) ↓ 8. 保存文档 ↓ 9. 使用 WPS/Word API 自动更新域(可选) ``` ### 3. Word TOC 域结构 ```xml TOC \o "1-3" \h \z \u 占位文本 ``` #### TOC 域参数说明 | 参数 | 说明 | 示例 | |------|------|------| | `\o "1-3"` | 包含标题层级 1-3 | 一、二、三级标题 | | `\h` | 使用超链接 | 可点击跳转 | | `\z` | Web 视图中隐藏页码 | - | | `\u` | 使用 Unicode | 支持中文 | --- ## 页码编号机制 ### 问题:目录计入页码 **修改前**: ``` 文档(单节) ├── 目录 → 第 1 页 ├── 正文 → 第 2 页 ✗ 不符合需求 ``` ### 解决方案:使用分节符 **修改后**: ``` 文档 ├── [第一节:目录] │ ├── 目录内容 │ └── 页脚:无页码 │ ├── [分节符 - 下一页] │ └── [第二节:正文] ├── 一、基本数据 → 第 1 页 ✓ ├── 正文内容... └── 页脚:第 X 页 / 共 Y 页 ``` ### 关键技术 #### 1. 创建新节 ```python from docx.enum.section import WD_SECTION # 添加分节符(下一页开始新节) new_section = doc.add_section(WD_SECTION.NEW_PAGE) ``` #### 2. 断开页脚链接 ```python # 新节的页脚不链接到前一节 footer = new_section.footer footer.is_linked_to_previous = False ``` #### 3. 设置页码从 1 开始 ```python # 通过 XML 设置页码起始值 sectPr = section._sectPr pgNumType = OxmlElement('w:pgNumType') pgNumType.set(qn('w:start'), '1') sectPr.append(pgNumType) ``` #### 4. 使用 SECTIONPAGES 域 **修改前**(错误): ```python add_field(r4._r, ' NUMPAGES ') # 整个文档总页数(包括目录) ``` **修改后**(正确): ```python add_field(r4._r, ' SECTIONPAGES ') # 当前节总页数(只计算正文) ``` **效果对比**: | 域代码 | 计算范围 | 示例结果 | |--------|----------|----------| | `NUMPAGES` | 整个文档 | 共 23 页(含目录) | | `SECTIONPAGES` | 当前节(正文) | 共 20 页(不含目录) | ### 页码格式 **页脚内容**: ``` 第 {PAGE} 页 / 共 {SECTIONPAGES} 页 ``` **实际显示**: ``` 第 1 页 / 共 20 页 第 2 页 / 共 20 页 ... 第 20 页 / 共 20 页 ``` --- ## 自动更新功能 ### 功能说明 导出后使用 WPS/Word COM API 自动更新文档域,确保用户下载的文档已包含完整目录,无需手动更新。 ### 实现原理 参考 `update_toc.py` 的逻辑: ```python # 1. 启动 WPS/Word app = win32com.client.Dispatch("Kwps.Application") # 或 Word.Application app.Visible = False app.DisplayAlerts = False # 2. 打开文档 doc = app.Documents.Open(file_path) # 3. 更新所有域 doc.Fields.Update() # 4. 再次更新 TOC 域(确保页码正确) for field in doc.Fields: if field.Type == 13: # wdFieldTOC field.Update() # 5. 保存并关闭 doc.Save() doc.Close() app.Quit() ``` ### 集成到导出流程 ```python # 在 export.py 中 # 5. 写入文件 file_path.write_bytes(doc_bytes) # 5.5. 如果文档包含 TOC,自动更新域 has_toc = any(block.get('type') == 'toc' for block in blocks) if has_toc: update_success = update_document_fields(str(file_path)) if update_success: # 域更新成功,重新读取文件大小 file_size = file_path.stat().st_size # 6. 返回下载链接(已包含更新后的文档) ``` ### 容错机制 | 场景 | 处理方式 | 结果 | |------|---------|------| | 非 Windows 平台 | 跳过更新 | 文档仍可用,用户打开时自动更新 | | 未安装 pywin32 | 跳过更新 | 文档仍可用,用户打开时自动更新 | | WPS/Word 不可用 | 跳过更新 | 文档仍可用,用户打开时自动更新 | | 更新过程出错 | 跳过更新 | 文档仍可用,用户打开时自动更新 | ### 性能影响 | 操作 | 耗时 | |------|------| | 启动 WPS | ~1-2秒 | | 打开文档 | ~0.5秒 | | 更新域 | ~0.5秒 | | 保存文档 | ~0.5秒 | | **总计** | **~2-3秒** | **文件大小优化**:更新后文件减小约 20%(去除冗余 XML) ### 依赖安装 ```bash # 安装 pywin32(Windows COM API) pip install pywin32 # 系统要求 # - Windows 操作系统 # - WPS Office 或 Microsoft Word ``` --- ## 测试验证 ### 自动化测试 #### 1. 基础导出测试 ```bash python test_toc_export.py ``` **预期结果**: - ✅ 生成 `tmp/test_toc_export.docx` - ✅ 文档大小约 45KB - ✅ 无错误日志 #### 2. 自动更新测试 ```bash python test_toc_update.py ``` **预期结果**: ``` ✓ 使用 WPS 成功更新文档域 ✓ 域更新成功! ✓ 最终文件大小: 35,373 字节 → 文件大小变化: -9,774 字节 ``` ### 手动验证清单 打开生成的 `tmp/test_toc_with_update.docx`,检查: #### 目录页 - [ ] 目录单独占一页 - [ ] 目录标题居中显示 - [ ] 目录已自动生成(无需手动更新) - [ ] 目录包含所有一级标题 - [ ] 目录项页码正确 #### 正文页 - [ ] 正文从新的一页开始 - [ ] 第一个一级标题显示"第 1 页" - [ ] 页码连续递增 - [ ] 页脚显示"第 X 页 / 共 Y 页" - [ ] "共 Y 页"只计算正文(不包括目录) #### 交互功能 - [ ] 点击目录项可跳转到对应标题 - [ ] 修改标题后右键"更新域"可刷新目录 - [ ] 页码自动更新 --- ## 已知问题与限制 ### 1. 仅支持 SDT 目录 **问题**:只识别由 Word"插入目录"功能生成的目录 **影响**:手动创建的目录表格不会被识别 **解决方案**:提示用户使用 Word 的标准目录功能 ### 2. 目录项不存储 **问题**:不存储目录中的具体项(标题文本、页码等) **原因**:目录项由 Word 自动生成,存储后可能不一致 **影响**:前端无法预览目录内容 **解决方案**:前端可从 heading blocks 动态生成预览 ### 3. 复杂配置支持有限 **问题**:高级配置(自定义样式、特殊开关)可能丢失 **影响**:复杂目录的某些格式可能不完全保留 **解决方案**:优先支持常用配置,复杂配置在后续版本迭代 ### 4. WPS vs Microsoft Word | 软件 | 自动更新 | 手动更新 | |------|---------|---------| | WPS Office | ✅ 提示更新 | ✅ F9 或右键 | | Microsoft Word | ⚠️ 可能需手动 | ✅ F9 或右键 | | LibreOffice | ⚠️ 部分支持 | ✅ 手动更新 | ### 5. 非 Windows 平台 **问题**:Linux/macOS 无法使用 COM API 自动更新 **影响**:导出文档需用户手动更新域 **解决方案**:文档已设置自动更新标记,打开时会提示 --- ## 故障排查 ### 问题 1:目录未生成 **症状**:打开文档后目录区域是空的 **排查步骤**: 1. 检查是否自动更新:WPS 通常会提示"是否更新域" 2. 手动更新:右键点击目录区域 → 选择"更新域" 3. 按 F9 键强制更新所有域 **常见原因**: - Word 安全设置禁用自动更新 - 文档标题未使用标准样式(Heading 1, 2, 3) ### 问题 2:页码错误(从 2 开始) **症状**:正文第一页显示"第 2 页"而不是"第 1 页" **排查步骤**: 1. 检查文档是否使用分节符(不是简单分页符) 2. 检查新节的 `pgNumType` 是否设置 `start="1"` 3. 检查页脚是否断开链接(`is_linked_to_previous = False`) **解决方案**: - 确保使用最新版本的代码(已修复) - 重新导出文档 ### 问题 3:"共 X 页"包括目录页 **症状**:页脚显示"共 23 页",但实际正文只有 20 页 **排查步骤**: 1. 检查是否使用 `SECTIONPAGES` 域(不是 `NUMPAGES`) 2. 查看页脚域代码:右键 → 切换域代码 **解决方案**: - 确保使用最新版本(已使用 `SECTIONPAGES`) - 重新导出文档 ### 问题 4:自动更新失败 **症状**:测试日志显示"域更新失败" **排查步骤**: 1. 检查是否安装 pywin32:`pip list | grep pywin32` 2. 检查是否安装 WPS/Word 3. 检查文件路径是否为绝对路径 4. 检查文件权限 **解决方案**: ```bash # 安装 pywin32 pip install pywin32 # 检查 WPS 是否可用 python -c "import win32com.client; app = win32com.client.Dispatch('Kwps.Application'); print('WPS OK')" # 检查 Word 是否可用 python -c "import win32com.client; app = win32com.client.Dispatch('Word.Application'); print('Word OK')" ``` ### 问题 5:目录超链接无法跳转 **症状**:点击目录项无反应 **排查步骤**: 1. 检查 TOC 域是否包含 `\h` 参数 2. 检查标题是否有书签(自动生成) 3. 更新域后重新测试 **解决方案**: - 确保 `toc_config.use_hyperlinks = true` - 重新导出文档 - 在 Word 中手动更新域(F9) --- ## 附录 ### A. 相关文件清单 #### 核心代码文件 | 文件 | 说明 | 关键函数 | |------|------|---------| | `app/services/export_service.py` | 导出服务 | `_render_toc_block`, `update_document_fields` | | `app/api/v1/export.py` | 导出 API | `export_document` | | `app/schemas/block.py` | Block Schema | `TOCBlock`, `TOCConfig` | #### 测试文件 | 文件 | 说明 | |------|------| | `test_toc_export.py` | 基础导出测试 | | `test_toc_update.py` | 自动更新测试 | #### 文档文件 | 文件 | 说明 | |------|------| | `docs/features/toc-complete-guide.md` | 本文档(综合指南) | | `update_toc.py` | 参考实现 | ### B. API 参考 #### 创建文档(包含目录) **请求**: ```http POST /api/v1/documents Content-Type: multipart/form-data file: ``` **响应**: ```json { "code": 0, "data": { "id": "doc-123", "name": "document.docx", "created_at": "2026-07-10T10:00:00Z" } } ``` #### 获取 Blocks(含 TOC) **请求**: ```http GET /api/v1/documents/{documentId}/blocks ``` **响应**: ```json { "code": 0, "data": { "blocks": [ { "id": "toc-001", "type": "toc", "content": {"title": "目录"}, "metadata": { "toc_config": {...}, "readonly": true } } ] } } ``` #### 导出文档 **请求**: ```http POST /api/v1/export/doc Content-Type: application/json { "document_id": "doc-123", "style_id": null } ``` **响应**: ```json { "code": 0, "data": { "record_id": "export-456", "download_url": "http://localhost:8000/api/v1/export/records/export-456/download", "file_name": "document_1720598400000.doc" } } ``` ### C. Word 域代码参考 | 域类型 | 域代码 | 说明 | |--------|--------|------| | 目录 | `TOC \o "1-3" \h \z \u` | 生成目录 | | 当前页码 | `PAGE` | 第 X 页 | | 总页数 | `NUMPAGES` | 整个文档页数 | | 节页数 | `SECTIONPAGES` | 当前节页数 | | 日期 | `DATE \@ "yyyy-MM-dd"` | 当前日期 | ### D. 联系方式 如有问题或建议,请联系: - 开发团队:dev@example.com - 技术支持:support@example.com - 项目仓库:https://github.com/example/ax-backend --- **文档结束**