完善模板编辑与模型管理体验

This commit is contained in:
zwt13703
2026-07-03 11:14:24 +08:00
parent 52eb058070
commit a796301ae8
19 changed files with 1065 additions and 246 deletions
+129 -22
View File
@@ -56,6 +56,34 @@ def _copy_run_format(target_run, source_paragraph: Paragraph | None):
break
def _extract_first_run_format(source_paragraph: Paragraph | None):
if source_paragraph is None:
return None
for source_run in source_paragraph.runs:
if source_run._element.rPr is not None:
return deepcopy(source_run._element.rPr)
return None
def _set_paragraph_text(
paragraph: Paragraph,
text: str,
style_name: str | None = None,
template_paragraph: Paragraph | None = None,
):
run_format = _extract_first_run_format(template_paragraph)
_clear_paragraph(paragraph)
if style_name:
try:
paragraph.style = style_name
except Exception:
pass
if text:
run = paragraph.add_run(text)
if run_format is not None:
run._element.insert(0, run_format)
def _append_paragraph_after(
paragraph: Paragraph,
text: str,
@@ -166,16 +194,12 @@ def _find_heading_paragraph(document: DocumentObject, heading_text: str, after_e
return None
def _replace_section_content(document: DocumentObject, heading_title: str, content: dict, after_element=None):
heading = _find_heading_paragraph(document, heading_title, after_element)
if heading is None:
return after_element
def _collect_section_templates(heading: Paragraph):
first_body_style = None
paragraph_template = None
table_template = None
blocks = []
current = heading._element.getnext()
blocks_to_remove = []
while current is not None:
if isinstance(current, CT_P):
current_paragraph = Paragraph(current, heading._parent)
@@ -185,49 +209,132 @@ def _replace_section_content(document: DocumentObject, heading_title: str, conte
first_body_style = current_paragraph.style.name
if paragraph_template is None:
paragraph_template = current_paragraph
blocks_to_remove.append(current_paragraph)
blocks.append(current_paragraph)
elif isinstance(current, CT_Tbl):
current_table = Table(current, heading._parent)
if table_template is None:
table_template = current_table
blocks_to_remove.append(current_table)
blocks.append(current_table)
current = current.getnext()
return first_body_style, paragraph_template, table_template, blocks
for block in blocks_to_remove:
_delete_block(block)
insert_after = heading
def _insert_content_after(
insert_after: Paragraph,
content: dict,
first_body_style: str | None,
paragraph_template: Paragraph | None,
table_template: Table | None,
):
current_anchor: Paragraph = insert_after
content_blocks = content.get("content", [])
for block in content_blocks:
block_type = block.get("type")
if block_type == "table":
rows = [list(row) for row in block.get("rows", [])]
headers = block.get("headers") or []
table = _append_table_after(insert_after, rows, headers, table_template)
insert_after = _append_empty_paragraph_after_table(table, first_body_style)
table = _append_table_after(current_anchor, rows, headers, table_template)
current_anchor = _append_empty_paragraph_after_table(table, first_body_style)
else:
text = block.get("text", "")
text_parts = [item for item in text.split("\n") if item] or [text]
for text_part in text_parts:
insert_after = _append_paragraph_after(
insert_after,
current_anchor = _append_paragraph_after(
current_anchor,
text_part,
first_body_style,
paragraph_template,
)
return current_anchor
def _replace_section_content(
document: DocumentObject,
anchor_title: str,
target_title: str,
content: dict,
write_mode: str,
after_element=None,
):
heading = _find_heading_paragraph(document, anchor_title, after_element)
if heading is None:
return after_element
_set_paragraph_text(heading, target_title, heading.style.name if heading.style is not None else None, heading)
first_body_style, paragraph_template, table_template, blocks_to_remove = _collect_section_templates(heading)
if write_mode == "replace_heading_only":
return heading._element
if write_mode == "replace_section":
for block in blocks_to_remove:
_delete_block(block)
_insert_content_after(
heading,
content,
first_body_style,
paragraph_template,
table_template,
)
return heading._element
def _group_logs(logs: list[dict]) -> list[list[dict]]:
groups: list[list[dict]] = []
for item in logs:
anchor_title = item.get("anchor_title") or item.get("title") or ""
if not groups:
groups.append([item])
continue
last_group = groups[-1]
last_anchor = last_group[0].get("anchor_title") or last_group[0].get("title") or ""
if anchor_title == last_anchor:
last_group.append(item)
else:
groups.append([item])
return groups
def _replace_section_group(
document: DocumentObject,
items: list[dict],
after_element=None,
):
first_item = items[0]
anchor_title = first_item.get("anchor_title") or first_item.get("title") or ""
target_title = first_item.get("title") or anchor_title
heading = _find_heading_paragraph(document, anchor_title, after_element)
if heading is None:
return after_element
_set_paragraph_text(heading, target_title, heading.style.name if heading.style is not None else None, heading)
first_body_style, paragraph_template, table_template, blocks_to_remove = _collect_section_templates(heading)
if len(items) == 1 and first_item.get("write_mode") == "replace_heading_only":
return heading._element
preserve_existing = len(items) == 1 and first_item.get("write_mode") == "append_after_heading"
if not preserve_existing:
for block in blocks_to_remove:
_delete_block(block)
current_anchor = heading
for item in items:
current_anchor = _insert_content_after(
current_anchor,
item.get("content") or {"content": []},
first_body_style,
paragraph_template,
table_template,
)
return heading._element
def export_document_bytes(template_bytes: bytes, logs: list[dict]) -> bytes:
document = Document(BytesIO(template_bytes))
last_heading_element = None
for item in logs:
last_heading_element = _replace_section_content(
document,
item["title"],
item["content"],
last_heading_element,
)
for group in _group_logs(logs):
last_heading_element = _replace_section_group(document, group, last_heading_element)
output = BytesIO()
document.save(output)
+8
View File
@@ -15,11 +15,13 @@ from docx.enum.text import WD_ALIGN_PARAGRAPH
@dataclass
class ParsedParagraph:
sort_index: int
anchor_title: str
title: str
content: str
style_json: str
is_table: bool
table_json: str
write_mode: str
def _iter_block_items(document: DocumentObject) -> Iterator[Paragraph | Table]:
@@ -173,11 +175,13 @@ def parse_template(file_path: str) -> list[ParsedParagraph]:
if level is not None:
current_item = ParsedParagraph(
sort_index=len(parsed) + 1,
anchor_title=text,
title=text,
content="",
style_json=json.dumps(_capture_paragraph_style(block, level), ensure_ascii=False),
is_table=False,
table_json="{}",
write_mode="replace_section",
)
parsed.append(current_item)
continue
@@ -185,11 +189,13 @@ def parse_template(file_path: str) -> list[ParsedParagraph]:
if current_item is None:
current_item = ParsedParagraph(
sort_index=len(parsed) + 1,
anchor_title="未命名段落",
title="未命名段落",
content=text,
style_json=json.dumps(_capture_paragraph_style(block, 0), ensure_ascii=False),
is_table=False,
table_json="{}",
write_mode="replace_section",
)
parsed.append(current_item)
else:
@@ -201,11 +207,13 @@ def parse_template(file_path: str) -> list[ParsedParagraph]:
loose_table_count += 1
current_item = ParsedParagraph(
sort_index=len(parsed) + 1,
anchor_title=f"表格_{loose_table_count}",
title=f"表格_{loose_table_count}",
content=table_text,
style_json="{}",
is_table=True,
table_json=json.dumps(table_data, ensure_ascii=False),
write_mode="replace_section",
)
parsed.append(current_item)
else: