From 1369d87afb4c6519c5a375d4ce1ae8fa70a8f05b Mon Sep 17 00:00:00 2001 From: zwt13703 Date: Fri, 3 Jul 2026 17:13:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=AE=B5=E8=90=BD=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E4=B8=8E=E7=A7=BB=E5=8A=A8=E6=8E=92=E5=BA=8F=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AF=BC=E5=87=BA=E6=AE=8B=E7=95=99=E4=B8=8E?= =?UTF-8?q?=E5=A4=96=E9=94=AE=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端:段落列表/配置区/手动编辑区新增上移、下移、删除按钮,hover 显示 - 前端:移动和删除操作后自动保存到后端,修正 canDeleteBlock 判定逻辑 - 后端:保存段落时级联删除关联的 generation_logs 再删段落 - 后端:导出时清理未被引用的标题段落及其内容,避免已删段落残留在 Word 中 - 后端:删除模板时级联清理 generation_logs/documents/paragraphs --- backend/routers/templates.py | 28 +++++- backend/services/document_export.py | 49 ++++++++++ web/src/views/TemplateEditor.vue | 138 +++++++++++++++++++++++++++- 3 files changed, 208 insertions(+), 7 deletions(-) diff --git a/backend/routers/templates.py b/backend/routers/templates.py index 2bd0314..8651a10 100644 --- a/backend/routers/templates.py +++ b/backend/routers/templates.py @@ -6,11 +6,13 @@ from datetime import datetime from io import BytesIO from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile -from sqlalchemy import func, select +from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession from config import settings from database import get_db +from models.document import Document +from models.generation_log import GenerationLog from models.paragraph import Paragraph from models.template import Template from schemas.schemas import Response, TemplateSave @@ -194,8 +196,14 @@ async def save_template_paragraphs( paragraph_map = {item.id: item for item in existing_paragraphs} incoming_ids = {config.id for config in body.paragraphs if config.id} + print(f"[SAVE] template_id={template_id}, incoming_ids={incoming_ids}, existing_ids={[p.id for p in existing_paragraphs]}") + for paragraph in existing_paragraphs: if paragraph.id not in incoming_ids: + print(f"[SAVE] Deleting paragraph id={paragraph.id} title={paragraph.title}") + await db.execute( + delete(GenerationLog).where(GenerationLog.paragraph_id == paragraph.id) + ) await db.delete(paragraph) for index, config in enumerate(body.paragraphs, start=1): @@ -229,7 +237,23 @@ async def delete_template(template_id: int, db: AsyncSession = Depends(get_db)): raise HTTPException(status_code=404, detail="模板不存在") result = await db.execute(select(Paragraph).where(Paragraph.template_id == template_id)) - for paragraph in result.scalars().all(): + paragraphs_to_delete = result.scalars().all() + paragraph_ids = [p.id for p in paragraphs_to_delete] + + doc_result = await db.execute(select(Document).where(Document.template_id == template_id)) + documents_to_delete = doc_result.scalars().all() + + if paragraph_ids: + await db.execute( + delete(GenerationLog).where(GenerationLog.paragraph_id.in_(paragraph_ids)) + ) + for document in documents_to_delete: + await db.execute( + delete(GenerationLog).where(GenerationLog.document_id == document.id) + ) + await db.delete(document) + + for paragraph in paragraphs_to_delete: await db.delete(paragraph) file_path = template.file_path or "" diff --git a/backend/services/document_export.py b/backend/services/document_export.py index 86c0aef..fdba938 100644 --- a/backend/services/document_export.py +++ b/backend/services/document_export.py @@ -32,6 +32,43 @@ def _delete_block(block): parent.remove(element) +def _delete_heading_section(heading: Paragraph): + blocks = [heading] + current = heading._element.getnext() + while current is not None: + if isinstance(current, CT_P): + para = Paragraph(current, heading._parent) + if _is_heading(para): + break + blocks.append(para) + elif isinstance(current, CT_Tbl): + blocks.append(Table(current, heading._parent)) + current = current.getnext() + for block in blocks: + _delete_block(block) + + +def _remove_unreferenced_headings(document: DocumentObject, referenced_anchors: set[str]): + headings_to_remove: list[Paragraph] = [] + found_first_heading = False + pre_heading_blocks: list = [] + print(f"[EXPORT] referenced_anchors: {referenced_anchors}") + for block in _iter_block_items(document): + if isinstance(block, Paragraph) and _is_heading(block): + found_first_heading = True + text = block.text.strip() + if text not in referenced_anchors: + print(f"[EXPORT] Unreferenced heading found, will remove: '{text}'") + headings_to_remove.append(block) + elif not found_first_heading: + pre_heading_blocks.append(block) + for heading in headings_to_remove: + _delete_heading_section(heading) + if not referenced_anchors: + for block in pre_heading_blocks: + _delete_block(block) + + def _clear_paragraph(paragraph: Paragraph): element = paragraph._element for child in list(element): @@ -332,10 +369,22 @@ def _replace_section_group( def export_document_bytes(template_bytes: bytes, logs: list[dict]) -> bytes: document = Document(BytesIO(template_bytes)) + + referenced_anchors: set[str] = set() + for item in logs: + for key in ("anchor_title", "title"): + val = (item.get(key) or "").strip() + if val: + referenced_anchors.add(val) + + print(f"[EXPORT] logs count={len(logs)}, anchor_titles={[(l.get('anchor_title'), l.get('title')) for l in logs]}") + last_heading_element = None for group in _group_logs(logs): last_heading_element = _replace_section_group(document, group, last_heading_element) + _remove_unreferenced_headings(document, referenced_anchors) + output = BytesIO() document.save(output) return output.getvalue() diff --git a/web/src/views/TemplateEditor.vue b/web/src/views/TemplateEditor.vue index cb6152d..c024c39 100644 --- a/web/src/views/TemplateEditor.vue +++ b/web/src/views/TemplateEditor.vue @@ -33,6 +33,17 @@ {{ paragraph.edit_mode === 'ai' ? 'AI 生成' : '人工编辑' }} + + + + + + + + + + + @@ -61,6 +72,17 @@