diff --git a/backend/database.py b/backend/database.py index ffc9fdb..a409e87 100644 --- a/backend/database.py +++ b/backend/database.py @@ -22,7 +22,6 @@ async def get_db(): async def init_db(): from models.template import Template from models.paragraph import Paragraph - from models.template_block import TemplateBlock from models.ai_model import AiModel from models.document import Document from models.generation_log import GenerationLog @@ -54,6 +53,3 @@ async def init_db(): await conn.execute(text("UPDATE paragraphs SET anchor_title = title WHERE anchor_title = '' OR anchor_title IS NULL")) if "write_mode" not in paragraph_columns: await conn.execute(text("ALTER TABLE paragraphs ADD COLUMN write_mode VARCHAR(30) DEFAULT 'replace_section'")) - block_tables = await conn.run_sync(lambda sync_conn: inspect(sync_conn).get_table_names()) - if "template_blocks" not in block_tables: - await conn.run_sync(lambda sync_conn: TemplateBlock.__table__.create(sync_conn)) diff --git a/backend/models/__init__.py b/backend/models/__init__.py index e7c3651..d79adf4 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -1,6 +1,5 @@ from models.template import Template from models.paragraph import Paragraph -from models.template_block import TemplateBlock from models.ai_model import AiModel from models.document import Document from models.generation_log import GenerationLog diff --git a/backend/routers/export.py b/backend/routers/export.py index e4c2c66..9d3e3ae 100644 --- a/backend/routers/export.py +++ b/backend/routers/export.py @@ -15,7 +15,6 @@ from models.document import Document from models.generation_log import GenerationLog from models.paragraph import Paragraph from models.template import Template -from models.template_block import TemplateBlock from services.document_export import export_document_bytes from services.minio_client import ( download_object_bytes, @@ -27,46 +26,6 @@ from services.minio_client import ( router = APIRouter() -def _block_text_content(block: TemplateBlock) -> str: - try: - payload = json.loads(block.content_json or "{}") - except Exception: - payload = {} - return payload.get("text") or block.default_value or "" - - -def _block_table_content(block: TemplateBlock) -> dict: - try: - payload = json.loads(block.content_json or "{}") - except Exception: - payload = {} - return payload.get("table") or {} - - -def _build_block_export_content(block: TemplateBlock) -> dict: - if block.block_type == "table": - table_data = _block_table_content(block) - matrix = table_data.get("data") or [] - headers = matrix[0] if matrix else [] - rows = matrix[1:] if len(matrix) > 1 else [] - return {"content": [{"type": "table", "headers": headers, "rows": rows}]} - return {"content": [{"type": "text", "text": _block_text_content(block)}]} - - -def _resolve_block_write_modes(blocks: list[TemplateBlock]) -> list[str]: - modes: list[str] = [] - anchor_counter: dict[str, int] = {} - for block in blocks: - if block.block_type == "heading": - modes.append("replace_heading_only") - continue - anchor = (block.anchor_ref or block.title or "").strip() - seen = anchor_counter.get(anchor, 0) - modes.append("replace_section" if seen == 0 else "append_after_heading") - anchor_counter[anchor] = seen + 1 - return modes - - @router.get("/{document_id}/docx") async def export_docx(document_id: int, db: AsyncSession = Depends(get_db)): document = await db.get(Document, document_id) @@ -80,49 +39,22 @@ async def export_docx(document_id: int, db: AsyncSession = Depends(get_db)): template_bucket, template_object = split_bucket_path(template.file_path) template_bytes = await asyncio.to_thread(download_object_bytes, template_bucket, template_object) - block_result = await db.execute( - select(TemplateBlock) - .where(TemplateBlock.template_id == template.id) - .order_by(TemplateBlock.sort_index.asc(), TemplateBlock.id.asc()) + result = await db.execute( + select(GenerationLog, Paragraph) + .join(Paragraph, Paragraph.id == GenerationLog.paragraph_id) + .where(GenerationLog.document_id == document_id) + .order_by(Paragraph.sort_index.asc(), Paragraph.id.asc()) ) - blocks = block_result.scalars().all() - - log_result = await db.execute( - select(GenerationLog).where(GenerationLog.document_id == document_id) - ) - generation_logs = log_result.scalars().all() - log_map = {item.paragraph_id: json.loads(item.content) if item.content else {"content": []} for item in generation_logs} - logs = [] - if blocks: - write_modes = _resolve_block_write_modes(blocks) - for block, write_mode in zip(blocks, write_modes): - generated_content = log_map.get(block.source_paragraph_id) if block.source_paragraph_id else None - content = generated_content if (block.edit_mode == "ai" or block.block_type == "ai_slot") and generated_content else _build_block_export_content(block) - logs.append( - { - "anchor_title": block.anchor_ref or block.title, - "title": block.title, - "write_mode": write_mode, - "content": content, - } - ) - else: - result = await db.execute( - select(GenerationLog, Paragraph) - .join(Paragraph, Paragraph.id == GenerationLog.paragraph_id) - .where(GenerationLog.document_id == document_id) - .order_by(Paragraph.sort_index.asc(), Paragraph.id.asc()) + for log, paragraph in result.all(): + logs.append( + { + "anchor_title": paragraph.anchor_title or paragraph.title, + "title": paragraph.title, + "write_mode": paragraph.write_mode, + "content": json.loads(log.content) if log.content else {"content": []}, + } ) - for log, paragraph in result.all(): - logs.append( - { - "anchor_title": paragraph.anchor_title or paragraph.title, - "title": paragraph.title, - "write_mode": paragraph.write_mode, - "content": json.loads(log.content) if log.content else {"content": []}, - } - ) exported_bytes = await asyncio.to_thread(export_document_bytes, template_bytes, logs) object_name = f"{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}.docx" diff --git a/backend/routers/templates.py b/backend/routers/templates.py index 74fbcae..8651a10 100644 --- a/backend/routers/templates.py +++ b/backend/routers/templates.py @@ -1,5 +1,4 @@ import asyncio -import json import os import tempfile import uuid @@ -16,10 +15,8 @@ from models.document import Document from models.generation_log import GenerationLog from models.paragraph import Paragraph from models.template import Template -from models.template_block import TemplateBlock from schemas.schemas import Response, TemplateSave -from services.document_export import export_document_bytes -from services.minio_client import download_object_bytes, minio_client, split_bucket_path, upload_bytes +from services.minio_client import minio_client from services.template_parser import parse_template router = APIRouter() @@ -67,270 +64,6 @@ def _serialize_template(template: Template) -> dict: } -def _build_block_from_paragraph(paragraph: Paragraph) -> dict: - block_type = "heading" if paragraph.write_mode == "replace_heading_only" else ("ai_slot" if paragraph.edit_mode == "ai" else ("table" if paragraph.is_table else "text")) - content_json = json.dumps({ - "text": paragraph.content or "", - "table": json.loads(paragraph.table_json or "{}") if paragraph.is_table else None, - }, ensure_ascii=False) - return { - "source_paragraph_id": paragraph.id, - "parent_block_id": None, - "sort_index": paragraph.sort_index, - "block_type": block_type, - "anchor_ref": paragraph.anchor_title or paragraph.title, - "title": paragraph.title, - "content_json": content_json, - "style_json": paragraph.style_json or "{}", - "edit_mode": paragraph.edit_mode, - "placeholder_key": "", - "variable_key": "", - "default_value": paragraph.content or "", - "model_id": paragraph.model_id, - "need_prompt": paragraph.need_prompt, - "prompt_text": paragraph.prompt_text, - "need_file": paragraph.need_file, - "file_note": paragraph.file_note, - "output_format": paragraph.output_format, - } - - -def _build_block_from_parsed_item(item, source_paragraph_id: int | None) -> dict: - content_json = json.dumps({ - "text": item.content or "", - "table": json.loads(item.table_json or "{}") if item.is_table else None, - }, ensure_ascii=False) - return { - "source_paragraph_id": source_paragraph_id, - "parent_block_id": None, - "sort_index": item.sort_index, - "block_type": item.block_type, - "anchor_ref": item.anchor_title or item.title, - "title": item.title, - "content_json": content_json, - "style_json": item.style_json or "{}", - "edit_mode": item.edit_mode, - "placeholder_key": item.placeholder_key, - "variable_key": item.variable_key, - "default_value": item.default_value, - "model_id": None, - "need_prompt": True, - "prompt_text": "", - "need_file": False, - "file_note": "", - "output_format": item.output_format, - } - - -def _serialize_block(block: TemplateBlock) -> dict: - try: - content_json = json.loads(block.content_json or "{}") - except Exception: - content_json = {} - return { - "id": block.id, - "template_id": block.template_id, - "source_paragraph_id": block.source_paragraph_id, - "parent_block_id": block.parent_block_id, - "sort_index": block.sort_index, - "block_type": block.block_type, - "anchor_ref": block.anchor_ref, - "title": block.title, - "content_json": content_json, - "style_json": block.style_json, - "edit_mode": block.edit_mode, - "placeholder_key": block.placeholder_key, - "variable_key": block.variable_key, - "default_value": block.default_value, - "model_id": block.model_id, - "need_prompt": block.need_prompt, - "prompt_text": block.prompt_text, - "need_file": block.need_file, - "file_note": block.file_note, - "output_format": block.output_format, - } - - -async def _load_blocks(db: AsyncSession, template_id: int) -> list[TemplateBlock]: - result = await db.execute( - select(TemplateBlock) - .where(TemplateBlock.template_id == template_id) - .order_by(TemplateBlock.sort_index.asc(), TemplateBlock.id.asc()) - ) - return result.scalars().all() - - -async def _sync_blocks_from_paragraphs(db: AsyncSession, template_id: int, paragraphs: list[Paragraph]): - existing_blocks = await _load_blocks(db, template_id) - for block in existing_blocks: - await db.delete(block) - await db.flush() - - block_rows: list[TemplateBlock] = [] - for paragraph in paragraphs: - block = TemplateBlock(template_id=template_id, **_build_block_from_paragraph(paragraph)) - db.add(block) - block_rows.append(block) - await db.flush() - return block_rows - - -async def _save_blocks( - db: AsyncSession, - template_id: int, - blocks_payload, -): - existing_blocks = await _load_blocks(db, template_id) - block_map = {item.id: item for item in existing_blocks} - incoming_ids = {config.id for config in blocks_payload if config.id} - - for block in existing_blocks: - if block.id not in incoming_ids: - await db.delete(block) - - for index, config in enumerate(blocks_payload, start=1): - block = block_map.get(config.id) if config.id else None - if block is None: - block = TemplateBlock(template_id=template_id) - db.add(block) - block.source_paragraph_id = config.source_paragraph_id - block.parent_block_id = config.parent_block_id - block.sort_index = index - block.block_type = config.block_type - block.anchor_ref = config.anchor_ref or config.title - block.title = config.title - block.content_json = json.dumps(config.content_json or {}, ensure_ascii=False) - block.style_json = config.style_json or "{}" - block.edit_mode = config.edit_mode - block.placeholder_key = config.placeholder_key - block.variable_key = config.variable_key - block.default_value = config.default_value - block.model_id = config.model_id - block.need_prompt = config.need_prompt - block.prompt_text = config.prompt_text - block.need_file = config.need_file - block.file_note = config.file_note - block.output_format = config.output_format - - await db.flush() - - -def _block_text_content(block: TemplateBlock) -> str: - try: - payload = json.loads(block.content_json or "{}") - except Exception: - payload = {} - return payload.get("text") or block.default_value or "" - - -def _block_table_content(block: TemplateBlock) -> dict: - try: - payload = json.loads(block.content_json or "{}") - except Exception: - payload = {} - return payload.get("table") or {} - - -async def _sync_paragraphs_from_blocks(db: AsyncSession, template_id: int) -> list[Paragraph]: - paragraph_result = await db.execute( - select(Paragraph) - .where(Paragraph.template_id == template_id) - .order_by(Paragraph.sort_index.asc(), Paragraph.id.asc()) - ) - existing_paragraphs = paragraph_result.scalars().all() - paragraph_map = {item.id: item for item in existing_paragraphs} - blocks = await _load_blocks(db, template_id) - write_modes = _resolve_block_write_modes(blocks) - kept_paragraph_ids: set[int] = set() - synced_rows: list[Paragraph] = [] - - for index, (block, write_mode) in enumerate(zip(blocks, write_modes), start=1): - paragraph = paragraph_map.get(block.source_paragraph_id) if block.source_paragraph_id else None - if paragraph is None: - paragraph = Paragraph(template_id=template_id) - db.add(paragraph) - await db.flush() - paragraph.sort_index = index - paragraph.anchor_title = block.anchor_ref or block.title - paragraph.title = block.title - paragraph.content = _block_text_content(block) - paragraph.style_json = block.style_json or "{}" - paragraph.is_table = block.block_type == "table" - paragraph.table_json = json.dumps(_block_table_content(block), ensure_ascii=False) if paragraph.is_table else "{}" - paragraph.edit_mode = "ai" if block.block_type == "ai_slot" or block.edit_mode == "ai" else "manual" - paragraph.write_mode = write_mode - paragraph.model_id = block.model_id - paragraph.need_prompt = block.need_prompt - paragraph.prompt_text = block.prompt_text - paragraph.need_file = block.need_file - paragraph.file_note = block.file_note - paragraph.output_format = block.output_format - block.source_paragraph_id = paragraph.id - kept_paragraph_ids.add(paragraph.id) - synced_rows.append(paragraph) - - for paragraph in existing_paragraphs: - if paragraph.id in kept_paragraph_ids: - continue - await db.execute(delete(GenerationLog).where(GenerationLog.paragraph_id == paragraph.id)) - await db.delete(paragraph) - - await db.flush() - return synced_rows - - -def _build_export_content_from_block(block: TemplateBlock) -> dict: - if block.block_type == "table": - table_data = _block_table_content(block) - matrix = table_data.get("data") or [] - headers = matrix[0] if matrix else [] - rows = matrix[1:] if len(matrix) > 1 else [] - return {"content": [{"type": "table", "headers": headers, "rows": rows}]} - return {"content": [{"type": "text", "text": _block_text_content(block)}]} - - -def _resolve_block_write_modes(blocks: list[TemplateBlock]) -> list[str]: - modes: list[str] = [] - anchor_counter: dict[str, int] = {} - for block in blocks: - if block.block_type == "heading": - modes.append("replace_heading_only") - continue - anchor = (block.anchor_ref or block.title or "").strip() - seen = anchor_counter.get(anchor, 0) - modes.append("replace_section" if seen == 0 else "append_after_heading") - anchor_counter[anchor] = seen + 1 - return modes - - -async def _write_template_snapshot_to_docx(db: AsyncSession, template: Template): - blocks = await _load_blocks(db, template.id) - if not blocks: - return - write_modes = _resolve_block_write_modes(blocks) - logs = [] - for block, write_mode in zip(blocks, write_modes): - logs.append( - { - "anchor_title": block.anchor_ref or block.title, - "title": block.title, - "write_mode": write_mode, - "content": _build_export_content_from_block(block), - } - ) - - template_bucket, template_object = split_bucket_path(template.file_path) - template_bytes = await asyncio.to_thread(download_object_bytes, template_bucket, template_object) - exported_bytes = await asyncio.to_thread(export_document_bytes, template_bytes, logs) - await asyncio.to_thread( - upload_bytes, - template_bucket, - template_object, - exported_bytes, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - ) - - @router.get("") async def list_templates( page: int = Query(1, ge=1), @@ -367,16 +100,9 @@ async def get_template(template_id: int, db: AsyncSession = Depends(get_db)): .where(Paragraph.template_id == template_id) .order_by(Paragraph.sort_index.asc(), Paragraph.id.asc()) ) - paragraph_rows = result.scalars().all() - paragraphs = [_serialize_paragraph(item) for item in paragraph_rows] - block_rows = await _load_blocks(db, template_id) - if not block_rows and paragraph_rows: - block_rows = await _sync_blocks_from_paragraphs(db, template_id, paragraph_rows) - await db.commit() - blocks = [_serialize_block(item) for item in block_rows] + paragraphs = [_serialize_paragraph(item) for item in result.scalars().all()] payload = _serialize_template(template) payload["paragraphs"] = paragraphs - payload["blocks"] = blocks return Response(data=payload) @@ -435,35 +161,19 @@ async def upload_template(file: UploadFile = File(...), db: AsyncSession = Depen style_json=item.style_json, is_table=item.is_table, table_json=item.table_json, - edit_mode=item.edit_mode, + edit_mode="manual", write_mode=item.write_mode, - need_prompt=item.edit_mode == "ai", - output_format=item.output_format, ) db.add(paragraph) paragraph_rows.append(paragraph) - await db.flush() - existing_blocks = await _load_blocks(db, template.id) - for block in existing_blocks: - await db.delete(block) - await db.flush() - block_rows: list[TemplateBlock] = [] - for item, paragraph in zip(parsed_items, paragraph_rows): - block = TemplateBlock(template_id=template.id, **_build_block_from_parsed_item(item, paragraph.id)) - db.add(block) - block_rows.append(block) - await db.flush() await db.commit() await db.refresh(template) for paragraph in paragraph_rows: await db.refresh(paragraph) - for block in block_rows: - await db.refresh(block) payload = _serialize_template(template) payload["paragraphs"] = [_serialize_paragraph(item) for item in paragraph_rows] - payload["blocks"] = [_serialize_block(item) for item in block_rows] return Response(data=payload) @@ -514,24 +224,10 @@ async def save_template_paragraphs( paragraph.file_note = config.file_note paragraph.output_format = config.output_format - await db.flush() - refreshed_result = await db.execute( - select(Paragraph) - .where(Paragraph.template_id == template_id) - .order_by(Paragraph.sort_index.asc(), Paragraph.id.asc()) - ) - refreshed_paragraphs = refreshed_result.scalars().all() - if body.save_mode == "manual" and body.blocks: - await _save_blocks(db, template_id, body.blocks) - refreshed_paragraphs = await _sync_paragraphs_from_blocks(db, template_id) - else: - await _sync_blocks_from_paragraphs(db, template_id, refreshed_paragraphs) - refreshed_paragraphs = await _sync_paragraphs_from_blocks(db, template_id) - template.paragraph_count = len(refreshed_paragraphs) - await _write_template_snapshot_to_docx(db, template) await db.commit() - blocks = [_serialize_block(item) for item in await _load_blocks(db, template_id)] - return Response(data={"template_id": template_id, "saved": len(refreshed_paragraphs), "blocks": blocks}) + template.paragraph_count = len(body.paragraphs) + await db.commit() + return Response(data={"template_id": template_id, "saved": len(body.paragraphs)}) @router.delete("/{template_id}") diff --git a/backend/schemas/schemas.py b/backend/schemas/schemas.py index b713c4d..32f7b3a 100644 --- a/backend/schemas/schemas.py +++ b/backend/schemas/schemas.py @@ -43,32 +43,8 @@ class ParagraphConfig(BaseModel): file_note: str = "" output_format: str = "text" - -class TemplateBlockConfig(BaseModel): - id: int = 0 - source_paragraph_id: Optional[int] = None - parent_block_id: Optional[int] = None - sort_index: int = 0 - block_type: str = "text" - anchor_ref: str = "" - title: str = "" - content_json: dict[str, Any] = Field(default_factory=dict) - style_json: str = "{}" - edit_mode: str = "manual" - placeholder_key: str = "" - variable_key: str = "" - default_value: str = "" - model_id: Optional[int] = None - need_prompt: bool = True - prompt_text: str = "" - need_file: bool = False - file_note: str = "" - output_format: str = "text" - class TemplateSave(BaseModel): - save_mode: str = "paragraph" paragraphs: list[ParagraphConfig] = [] - blocks: list[TemplateBlockConfig] = [] # 模型 class AiModelCreate(BaseModel): diff --git a/backend/services/document_export.py b/backend/services/document_export.py index 34fb58f..fdba938 100644 --- a/backend/services/document_export.py +++ b/backend/services/document_export.py @@ -69,77 +69,6 @@ def _remove_unreferenced_headings(document: DocumentObject, referenced_anchors: _delete_block(block) -def _ordered_unique_anchors(logs: list[dict]) -> list[str]: - ordered: list[str] = [] - seen: set[str] = set() - for item in logs: - anchor = (item.get("anchor_title") or item.get("title") or "").strip() - if not anchor or anchor in seen: - continue - seen.add(anchor) - ordered.append(anchor) - return ordered - - -def _reorder_heading_sections(document: DocumentObject, ordered_anchors: list[str]): - body = document.element.body - elements = list(body.iterchildren()) - pre_heading: list = [] - sections: list[tuple[str, list]] = [] - found_heading = False - index = 0 - - while index < len(elements): - child = elements[index] - if isinstance(child, CT_P): - paragraph = Paragraph(child, document) - if _is_heading(paragraph): - found_heading = True - anchor = paragraph.text.strip() - section_elements = [child] - index += 1 - while index < len(elements): - current = elements[index] - if isinstance(current, CT_P): - current_paragraph = Paragraph(current, document) - if _is_heading(current_paragraph): - break - section_elements.append(current) - index += 1 - sections.append((anchor, section_elements)) - continue - if not found_heading: - pre_heading.append(child) - index += 1 - - if not sections: - return - - section_map: dict[str, list[list]] = {} - for anchor, section_elements in sections: - section_map.setdefault(anchor, []).append(section_elements) - - all_section_elements = [element for _, section_elements in sections for element in section_elements] - for element in all_section_elements: - parent = element.getparent() - if parent is not None: - parent.remove(element) - - sect_pr = None - for child in list(body.iterchildren()): - if not isinstance(child, (CT_P, CT_Tbl)): - sect_pr = child - break - - for anchor in ordered_anchors: - for section_elements in section_map.pop(anchor, []): - for element in section_elements: - if sect_pr is not None: - sect_pr.addprevious(element) - else: - body.append(element) - - def _clear_paragraph(paragraph: Paragraph): element = paragraph._element for child in list(element): @@ -440,8 +369,6 @@ def _replace_section_group( def export_document_bytes(template_bytes: bytes, logs: list[dict]) -> bytes: document = Document(BytesIO(template_bytes)) - ordered_anchors = _ordered_unique_anchors(logs) - _reorder_heading_sections(document, ordered_anchors) referenced_anchors: set[str] = set() for item in logs: diff --git a/backend/services/template_parser.py b/backend/services/template_parser.py index 792741f..9a8ae40 100644 --- a/backend/services/template_parser.py +++ b/backend/services/template_parser.py @@ -1,5 +1,4 @@ import json -import re from collections.abc import Iterator from dataclasses import dataclass @@ -23,12 +22,6 @@ class ParsedParagraph: is_table: bool table_json: str write_mode: str - block_type: str = "text" - placeholder_key: str = "" - variable_key: str = "" - default_value: str = "" - edit_mode: str = "manual" - output_format: str = "text" def _iter_block_items(document: DocumentObject) -> Iterator[Paragraph | Table]: @@ -166,35 +159,11 @@ def _extract_table_data(table: Table) -> dict: } -PLACEHOLDER_PATTERN = re.compile(r"^\{\{\s*([a-zA-Z0-9_\-\.]+)\s*\}\}$") - - -def _build_block_title(text: str, fallback: str) -> str: - normalized = " ".join((text or "").split()) - if not normalized: - return fallback - return normalized[:24] + ("..." if len(normalized) > 24 else "") - - -def _classify_placeholder(text: str) -> tuple[str, str, str]: - matched = PLACEHOLDER_PATTERN.match(text.strip()) - if not matched: - return "text", "", "" - key = matched.group(1) - lowered = key.lower() - if any(token in lowered for token in ("summary", "opening", "section", "content", "analysis")): - return "ai_slot", key, "" - return "variable", "", key - - def parse_template(file_path: str) -> list[ParsedParagraph]: document = Document(file_path) parsed: list[ParsedParagraph] = [] - current_heading: str | None = None - current_heading_style_json = "{}" + current_item: ParsedParagraph | None = None loose_table_count = 0 - body_block_count = 0 - preface_count = 0 for block in _iter_block_items(document): if isinstance(block, Paragraph): @@ -204,79 +173,52 @@ def parse_template(file_path: str) -> list[ParsedParagraph]: level = _heading_level(block.style.name if block.style is not None else "") if level is not None: - current_heading = text - current_heading_style_json = json.dumps(_capture_paragraph_style(block, level), ensure_ascii=False) - body_block_count = 0 - parsed.append(ParsedParagraph( + current_item = ParsedParagraph( sort_index=len(parsed) + 1, anchor_title=text, title=text, content="", - style_json=current_heading_style_json, + style_json=json.dumps(_capture_paragraph_style(block, level), ensure_ascii=False), is_table=False, table_json="{}", - write_mode="replace_heading_only", - block_type="heading", - edit_mode="manual", - output_format="text", - )) + write_mode="replace_section", + ) + parsed.append(current_item) continue - block_type, placeholder_key, variable_key = _classify_placeholder(text) - if current_heading is None: - preface_count += 1 - anchor_title = f"文档起始_{preface_count}" - title = _build_block_title(text, anchor_title) - write_mode = "replace_section" + 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: - body_block_count += 1 - anchor_title = current_heading - title = _build_block_title(text, f"{current_heading}-正文{body_block_count}") - write_mode = "append_after_heading" - - parsed.append(ParsedParagraph( - sort_index=len(parsed) + 1, - anchor_title=anchor_title, - title=title, - content=text, - style_json=json.dumps(_capture_paragraph_style(block, 0), ensure_ascii=False), - is_table=False, - table_json="{}", - write_mode=write_mode, - block_type=block_type, - placeholder_key=placeholder_key, - variable_key=variable_key, - default_value="" if variable_key else text, - edit_mode="ai" if block_type == "ai_slot" else "manual", - output_format="text", - )) + current_item.content = "\n".join(filter(None, [current_item.content, text])) else: table_data = _extract_table_data(block) table_text = f"[表格] {table_data['rows']} 行 {table_data['cols']} 列" - if current_heading is None: + if current_item is None: loose_table_count += 1 - anchor_title = f"表格_{loose_table_count}" - title = anchor_title - write_mode = "replace_section" + 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: - body_block_count += 1 - anchor_title = current_heading - title = f"{current_heading}-表格{body_block_count}" - write_mode = "append_after_heading" - - parsed.append(ParsedParagraph( - sort_index=len(parsed) + 1, - anchor_title=anchor_title, - title=title, - content=table_text, - style_json=current_heading_style_json if current_heading else "{}", - is_table=True, - table_json=json.dumps(table_data, ensure_ascii=False), - write_mode=write_mode, - block_type="table", - default_value=table_text, - edit_mode="manual", - output_format="table", - )) + current_item.is_table = True + current_item.table_json = json.dumps(table_data, ensure_ascii=False) + current_item.content = "\n".join(filter(None, [current_item.content, table_text])) return parsed diff --git a/web/src/stores/template.ts b/web/src/stores/template.ts index efd4e99..12af6b6 100644 --- a/web/src/stores/template.ts +++ b/web/src/stores/template.ts @@ -1,45 +1,19 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { templateApi } from '@/api/template' -import type { Template, Paragraph, TemplateBlock } from '@/types' +import type { Template, Paragraph } from '@/types' export const useTemplateStore = defineStore('template', () => { const templates = ref([]) const currentTemplate = ref - + {{ editorMode === 'paragraph' ? '点击左侧段落或文档中的段落块查看配置' : '可直接编辑标题、正文,并手动拆块插入 AI 内容' }} -
-
+
@@ -156,41 +142,32 @@