diff --git a/backend/app/services/html_generator.py b/backend/app/services/html_generator.py
index b95942c..1bc6c8f 100644
--- a/backend/app/services/html_generator.py
+++ b/backend/app/services/html_generator.py
@@ -1,5 +1,25 @@
"""HTML 预览生成服务"""
-from typing import List, Dict, Any
+from html import escape
+from typing import Any, Dict, List
+
+
+PAGE_STYLE = (
+ "width:794px;min-height:1123px;margin:0 auto;background:#fff;"
+ "padding:72px 64px;box-sizing:border-box;color:#1a1d24;"
+ "font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;"
+ "font-size:14px;line-height:1.8;"
+)
+
+HEADING_STYLE = (
+ "font-size:16px;font-weight:700;text-align:center;margin:24px 0 14px;"
+ "padding-bottom:6px;border-bottom:2px solid #1a1d24;"
+)
+
+PARAGRAPH_STYLE = "text-indent:2em;margin:8px 0;line-height:1.8;text-align:justify;"
+
+TABLE_STYLE = "width:100%;border-collapse:collapse;margin:14px 0;font-size:13px;"
+TH_STYLE = "padding:7px 10px;border:1px solid #d8dbe2;background:#f0f1f3;text-align:left;font-weight:600;"
+TD_STYLE = "padding:7px 10px;border:1px solid #d8dbe2;text-align:left;min-height:24px;"
def generate_preview_html(blocks: List[Dict[str, Any]]) -> str:
@@ -7,21 +27,56 @@ def generate_preview_html(blocks: List[Dict[str, Any]]) -> str:
将 block 列表转为带 data-block-id 的 HTML 预览字符串。
每个元素都会带上 data-block-id 属性供前端点击交互。
"""
- html_parts = ['
']
+ html_parts = [f'
']
for block in blocks:
block_id = block.get("block_id", "")
- block_type = block.get("type", "")
- text = block.get("text", "")
+ block_type = block.get("type") or block.get("block_type", "")
+ text = block.get("text") or block.get("text_preview") or ""
if block_type == "heading":
- html = f'
{text}
'
+ html = render_heading(block_id, text)
elif block_type == "table":
- html = f'
📊 {text[:80]}
'
+ html = render_table(block)
else:
- html = f'
{text}
'
+ html = render_paragraph(block_id, text)
html_parts.append(html)
html_parts.append('
')
return "\n".join(html_parts)
+
+
+def render_heading(block_id: str, text: str) -> str:
+ return f'
{escape(text)}
'
+
+
+def render_paragraph(block_id: str, text: str) -> str:
+ return f'
{escape(text)}
'
+
+
+def render_table(block: Dict[str, Any]) -> str:
+ block_id = escape(block.get("block_id", ""))
+ headers = [escape(str(header)) for header in block.get("headers", []) if str(header).strip()]
+ rows = int(block.get("rows") or block.get("table_rows") or 1)
+ cols = int(block.get("cols") or block.get("table_cols") or max(len(headers), 1))
+ cols = max(cols, len(headers), 1)
+ body_rows = max(rows - 1, 1)
+
+ if not headers:
+ text = block.get("text") or block.get("text_preview") or "表格"
+ headers = [escape(str(text))] + ["" for _ in range(cols - 1)]
+
+ header_html = "".join(f'
{header} | ' for header in headers[:cols])
+ if len(headers) < cols:
+ header_html += "".join(f'
| ' for _ in range(cols - len(headers)))
+
+ row_html = "".join(f'
| ' for _ in range(cols))
+ body_html = "\n".join(f"
{row_html}
" for _ in range(body_rows))
+
+ return (
+ f'
'
+ f"{header_html}
"
+ f"{body_html}"
+ "
"
+ )
diff --git a/docs/AI文档模板系统-像素级任务清单-20260701.md b/docs/AI文档模板系统-像素级任务清单-20260701.md
index 89c70f8..0e190f0 100644
--- a/docs/AI文档模板系统-像素级任务清单-20260701.md
+++ b/docs/AI文档模板系统-像素级任务清单-20260701.md
@@ -40,10 +40,10 @@
| 024 | 实现层级构建(parent_block_id)+ 结构树 tree 输出 | 后端 | 0.25d | ✅ 已完成 |
| 025 | 将解析结果批量写入 template_block 表 | 后端 | 0.25d | ✅ 已完成 |
| | **HTML 预览生成** | | | |
-| 026 | 标题 → `
` 转换 + 居中加粗样式 | 后端 | 0.25d | ⏳ 未完成 |
-| 027 | 段落 → `
` 转换 + 首行缩进样式 | 后端 | 0.25d | ⏳ 未完成 |
-| 028 | 表格 → `
` 转换 + 边框样式 | 后端 | 0.25d | ⏳ 未完成 |
-| 029 | 拼接完整 HTML 字符串(带内联 CSS) | 后端 | 0.25d | ⏳ 未完成 |
+| 026 | 标题 → `` 转换 + 居中加粗样式 | 后端 | 0.25d | ✅ 已完成 |
+| 027 | 段落 → `
` 转换 + 首行缩进样式 | 后端 | 0.25d | ✅ 已完成 |
+| 028 | 表格 → `
` 转换 + 边框样式 | 后端 | 0.25d | ✅ 已完成 |
+| 029 | 拼接完整 HTML 字符串(带内联 CSS) | 后端 | 0.25d | ✅ 已完成 |
| | **查询 + 配置接口** | | | |
| 030 | 实现 `GET /api/templates/{id}` 组装全部数据 | 后端 | 0.5d | ⏳ 未完成 |
| 031 | 实现 `POST /api/templates/{id}/blocks/{blockId}/config` upsert | 后端 | 0.25d | ⏳ 未完成 |
diff --git a/docs/tasks/task_detail_2026_07_01.md b/docs/tasks/task_detail_2026_07_01.md
index 6edeea9..23faf7d 100644
--- a/docs/tasks/task_detail_2026_07_01.md
+++ b/docs/tasks/task_detail_2026_07_01.md
@@ -35,3 +35,15 @@
5. 新增 `save_template_blocks`,上传模板后批量写入 `template_block`。
6. 使用临时 docx 验证标题、段落、表格混排时的顺序和父子关系。
- **执行结果**: 完成任务 019-025,上传模板时会解析 Word 并保存模板区域块。
+
+## 会话 ID: 20260701-preview-html
+- [2026-07-01 20:30:13]
+- **执行原因**: 按任务清单继续完成 HTML 预览生成阶段 026-029。
+- **执行过程**:
+ 1. 重构 `html_generator.py`,为预览容器、标题、段落、表格定义内联样式。
+ 2. 标题输出为 ``,支持居中、加粗和下边框样式。
+ 3. 段落输出为 `
`,支持首行缩进和两端对齐。
+ 4. 表格输出为真实 `
`,包含表头、单元格边框和占位网格。
+ 5. 增加 HTML 转义,避免 Word 文本中的特殊字符破坏预览结构。
+ 6. 使用示例 blocks 验证标题、段落、表格 HTML 均生成正确。
+- **执行结果**: 完成任务 026-029,HTML 预览生成服务已满足基础预览要求。