feat: generate doc preview html
This commit is contained in:
@@ -1,5 +1,25 @@
|
|||||||
"""HTML 预览生成服务"""
|
"""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:
|
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 预览字符串。
|
将 block 列表转为带 data-block-id 的 HTML 预览字符串。
|
||||||
每个元素都会带上 data-block-id 属性供前端点击交互。
|
每个元素都会带上 data-block-id 属性供前端点击交互。
|
||||||
"""
|
"""
|
||||||
html_parts = ['<div class="doc-preview-content">']
|
html_parts = [f'<div class="doc-preview-content" style="{PAGE_STYLE}">']
|
||||||
|
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
block_id = block.get("block_id", "")
|
block_id = block.get("block_id", "")
|
||||||
block_type = block.get("type", "")
|
block_type = block.get("type") or block.get("block_type", "")
|
||||||
text = block.get("text", "")
|
text = block.get("text") or block.get("text_preview") or ""
|
||||||
|
|
||||||
if block_type == "heading":
|
if block_type == "heading":
|
||||||
html = f'<h2 data-block-id="{block_id}" style="font-size:16px;font-weight:600;margin:20px 0 10px;padding-bottom:6px;border-bottom:2px solid #1a1d24">{text}</h2>'
|
html = render_heading(block_id, text)
|
||||||
elif block_type == "table":
|
elif block_type == "table":
|
||||||
html = f'<div data-block-id="{block_id}" style="margin:12px 0;padding:8px;background:#f7f8fa;border:1px dashed #ccc;border-radius:4px;color:#5b626e">📊 {text[:80]}</div>'
|
html = render_table(block)
|
||||||
else:
|
else:
|
||||||
html = f'<p data-block-id="{block_id}" style="text-indent:2em;margin:8px 0;line-height:1.8;text-align:justify">{text}</p>'
|
html = render_paragraph(block_id, text)
|
||||||
|
|
||||||
html_parts.append(html)
|
html_parts.append(html)
|
||||||
|
|
||||||
html_parts.append('</div>')
|
html_parts.append('</div>')
|
||||||
return "\n".join(html_parts)
|
return "\n".join(html_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def render_heading(block_id: str, text: str) -> str:
|
||||||
|
return f'<h2 data-block-id="{escape(block_id)}" style="{HEADING_STYLE}">{escape(text)}</h2>'
|
||||||
|
|
||||||
|
|
||||||
|
def render_paragraph(block_id: str, text: str) -> str:
|
||||||
|
return f'<p data-block-id="{escape(block_id)}" style="{PARAGRAPH_STYLE}">{escape(text)}</p>'
|
||||||
|
|
||||||
|
|
||||||
|
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'<th style="{TH_STYLE}">{header}</th>' for header in headers[:cols])
|
||||||
|
if len(headers) < cols:
|
||||||
|
header_html += "".join(f'<th style="{TH_STYLE}"></th>' for _ in range(cols - len(headers)))
|
||||||
|
|
||||||
|
row_html = "".join(f'<td style="{TD_STYLE}"></td>' for _ in range(cols))
|
||||||
|
body_html = "\n".join(f"<tr>{row_html}</tr>" for _ in range(body_rows))
|
||||||
|
|
||||||
|
return (
|
||||||
|
f'<table data-block-id="{block_id}" style="{TABLE_STYLE}">'
|
||||||
|
f"<thead><tr>{header_html}</tr></thead>"
|
||||||
|
f"<tbody>{body_html}</tbody>"
|
||||||
|
"</table>"
|
||||||
|
)
|
||||||
|
|||||||
@@ -40,10 +40,10 @@
|
|||||||
| 024 | 实现层级构建(parent_block_id)+ 结构树 tree 输出 | 后端 | 0.25d | ✅ 已完成 |
|
| 024 | 实现层级构建(parent_block_id)+ 结构树 tree 输出 | 后端 | 0.25d | ✅ 已完成 |
|
||||||
| 025 | 将解析结果批量写入 template_block 表 | 后端 | 0.25d | ✅ 已完成 |
|
| 025 | 将解析结果批量写入 template_block 表 | 后端 | 0.25d | ✅ 已完成 |
|
||||||
| | **HTML 预览生成** | | | |
|
| | **HTML 预览生成** | | | |
|
||||||
| 026 | 标题 → `<h2 data-block-id>` 转换 + 居中加粗样式 | 后端 | 0.25d | ⏳ 未完成 |
|
| 026 | 标题 → `<h2 data-block-id>` 转换 + 居中加粗样式 | 后端 | 0.25d | ✅ 已完成 |
|
||||||
| 027 | 段落 → `<p data-block-id>` 转换 + 首行缩进样式 | 后端 | 0.25d | ⏳ 未完成 |
|
| 027 | 段落 → `<p data-block-id>` 转换 + 首行缩进样式 | 后端 | 0.25d | ✅ 已完成 |
|
||||||
| 028 | 表格 → `<table data-block-id>` 转换 + 边框样式 | 后端 | 0.25d | ⏳ 未完成 |
|
| 028 | 表格 → `<table data-block-id>` 转换 + 边框样式 | 后端 | 0.25d | ✅ 已完成 |
|
||||||
| 029 | 拼接完整 HTML 字符串(带内联 CSS) | 后端 | 0.25d | ⏳ 未完成 |
|
| 029 | 拼接完整 HTML 字符串(带内联 CSS) | 后端 | 0.25d | ✅ 已完成 |
|
||||||
| | **查询 + 配置接口** | | | |
|
| | **查询 + 配置接口** | | | |
|
||||||
| 030 | 实现 `GET /api/templates/{id}` 组装全部数据 | 后端 | 0.5d | ⏳ 未完成 |
|
| 030 | 实现 `GET /api/templates/{id}` 组装全部数据 | 后端 | 0.5d | ⏳ 未完成 |
|
||||||
| 031 | 实现 `POST /api/templates/{id}/blocks/{blockId}/config` upsert | 后端 | 0.25d | ⏳ 未完成 |
|
| 031 | 实现 `POST /api/templates/{id}/blocks/{blockId}/config` upsert | 后端 | 0.25d | ⏳ 未完成 |
|
||||||
|
|||||||
@@ -35,3 +35,15 @@
|
|||||||
5. 新增 `save_template_blocks`,上传模板后批量写入 `template_block`。
|
5. 新增 `save_template_blocks`,上传模板后批量写入 `template_block`。
|
||||||
6. 使用临时 docx 验证标题、段落、表格混排时的顺序和父子关系。
|
6. 使用临时 docx 验证标题、段落、表格混排时的顺序和父子关系。
|
||||||
- **执行结果**: 完成任务 019-025,上传模板时会解析 Word 并保存模板区域块。
|
- **执行结果**: 完成任务 019-025,上传模板时会解析 Word 并保存模板区域块。
|
||||||
|
|
||||||
|
## 会话 ID: 20260701-preview-html
|
||||||
|
- [2026-07-01 20:30:13]
|
||||||
|
- **执行原因**: 按任务清单继续完成 HTML 预览生成阶段 026-029。
|
||||||
|
- **执行过程**:
|
||||||
|
1. 重构 `html_generator.py`,为预览容器、标题、段落、表格定义内联样式。
|
||||||
|
2. 标题输出为 `<h2 data-block-id>`,支持居中、加粗和下边框样式。
|
||||||
|
3. 段落输出为 `<p data-block-id>`,支持首行缩进和两端对齐。
|
||||||
|
4. 表格输出为真实 `<table data-block-id>`,包含表头、单元格边框和占位网格。
|
||||||
|
5. 增加 HTML 转义,避免 Word 文本中的特殊字符破坏预览结构。
|
||||||
|
6. 使用示例 blocks 验证标题、段落、表格 HTML 均生成正确。
|
||||||
|
- **执行结果**: 完成任务 026-029,HTML 预览生成服务已满足基础预览要求。
|
||||||
|
|||||||
Reference in New Issue
Block a user