From 5d0df4646b4fb1c0a4dd91301be7a5c8dd19a636 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Fri, 8 May 2026 23:06:00 +0800 Subject: [PATCH] move export options to results --- README.md | 2 +- app/main.py | 97 ++++++++++++++++++++++++++++++++++++++---------- app/processor.py | 81 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 156 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b8cb27d..3fa0ca2 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ V2:zip 解压后包含 `Patients_info.csv`,并按患者目录分别保存检 患者编号类型可选择自动识别。自动识别会读取 `Patients_info.csv` 中的 `pat_no`,并与 `Tests_List` 文件名或患者目录名比对:若更匹配 10 位补零编号,则使用 `pat_no`;若更匹配原始编号,则使用 `zhuyuanhao`。 -导出的压缩包默认只包含 Excel 结果,不包含处理日志。系统默认输出全部检测记录,并可选择是否保留: +导出的压缩包默认只包含 Excel 结果,不包含处理日志。系统默认输出完整结果;处理完成后可在结果页选择导出时是否保留: - 基本工作表 - 未匹配检测内容项 diff --git a/app/main.py b/app/main.py index 36c393f..942f364 100644 --- a/app/main.py +++ b/app/main.py @@ -62,11 +62,6 @@ def index() -> str: -
- - - -
\u9ed8\u8ba4\u8f93\u51fa\u5168\u90e8\u68c0\u6d4b\u8bb0\u5f55\uff0c\u5e76\u4fdd\u7559\u201c{UNMATCHED}\u201d\u5217\uff1b\u8be5\u5217\u4e3a\u989d\u5916\u8f85\u52a9\u4fe1\u606f\uff0c\u4fbf\u4e8e\u6838\u5bf9\u672a\u5f52\u7c7b\u9879\u76ee\u3002
@@ -82,9 +77,6 @@ async def process( data_type: str = Form("auto"), result_name: str = Form("Result"), preview_rows: int = Form(20), - include_basic_sheets: str | None = Form(None), - include_unmatched_items: str | None = Form(None), - include_summary_sheet: str | None = Form(None), ) -> str: if not file.filename or not file.filename.lower().endswith(".zip"): raise HTTPException(status_code=400, detail="\u8bf7\u4e0a\u4f20 zip \u6587\u4ef6\u3002") @@ -106,9 +98,9 @@ async def process( show_not_match=True, show_all_infos=True, preview_rows=rows, - include_basic_sheets=include_basic_sheets == "true", - include_unmatched_items=include_unmatched_items == "true", - include_summary_sheet=include_summary_sheet == "true", + include_basic_sheets=True, + include_unmatched_items=True, + include_summary_sheet=True, ) except ProcessingError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @@ -130,20 +122,42 @@ def result_page(job_id: str, preview_rows: int = Query(20, ge=5, le=200)) -> str @app.get("/download/all/{job_id}") -def download_all(job_id: str) -> FileResponse: +def download_all( + job_id: str, + include_basic_sheets: bool = Query(True), + include_unmatched_items: bool = Query(True), + include_summary_sheet: bool = Query(True), +) -> FileResponse: job_dir = _get_job_dir(job_id) try: - result_zip = create_result_zip(job_dir) + result_zip = create_result_zip( + job_dir, + include_basic_sheets=include_basic_sheets, + include_unmatched_items=include_unmatched_items, + include_summary_sheet=include_summary_sheet, + ) except ProcessingError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc return FileResponse(result_zip, media_type="application/zip", filename="result.zip") @app.get("/download/file/{job_id}") -def download_file(job_id: str, path: str = Query(...)) -> FileResponse: +def download_file( + job_id: str, + path: str = Query(...), + include_basic_sheets: bool = Query(True), + include_unmatched_items: bool = Query(True), + include_summary_sheet: bool = Query(True), +) -> FileResponse: job_dir = _get_job_dir(job_id) try: - target = find_output_file(job_dir, path) + target = find_output_file( + job_dir, + path, + include_basic_sheets=include_basic_sheets, + include_unmatched_items=include_unmatched_items, + include_summary_sheet=include_summary_sheet, + ) except ProcessingError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc return FileResponse( @@ -184,9 +198,15 @@ def _render_result(result: ProcessingResult, preview_rows: int = 20) -> str:
\u6570\u636e\u884c{total_rows}
- \u5bfc\u51fa\u5168\u90e8 Excel + \u5bfc\u51fa\u5168\u90e8 Excel \u7ee7\u7eed\u5904\u7406\u65b0\u6587\u4ef6
+
+ \u5bfc\u51fa\u5185\u5bb9\u9009\u62e9 + + + +
@@ -210,7 +230,7 @@ def _render_file(file, job_id: str) -> str:

{html.escape(file.filename)}

{len(file.sheets)} \u4e2a\u5de5\u4f5c\u8868

- \u5bfc\u51fa\u6b64 Excel + \u5bfc\u51fa\u6b64 Excel {sheet_items} @@ -227,12 +247,13 @@ def _render_sheet(sheet) -> str: unmatched_start = preview[0].index(UNMATCHED) rows = [] for index, row in enumerate(preview): + row_class = ' class="group-header"' if _is_summary_group_header(row) else "" cells = [] for col_index, value in enumerate((row + [""] * max_cols)[:max_cols]): tag = "th" if index == 0 else "td" css_class = ' class="extra-col"' if unmatched_start is not None and col_index >= unmatched_start else "" cells.append(f"<{tag}{css_class}>{html.escape(value)}") - rows.append("" + "".join(cells) + "") + rows.append(f"" + "".join(cells) + "") table = f'
{"".join(rows)}
' return f"""
@@ -330,6 +351,25 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406 color: var(--text); font-size: 14px; }} + .export-options {{ + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 14px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + padding: 10px 12px; + margin-bottom: 14px; + font-size: 14px; + }} + .export-options label {{ + display: inline-flex; + align-items: center; + gap: 8px; + margin: 0; + font-weight: 500; + }} .checks label {{ display: inline-flex; align-items: center; @@ -463,6 +503,7 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406 white-space: nowrap; }} th {{ background: #fbfcfd; font-weight: 700; }} + tr.group-header td {{ font-weight: 700; background: #fbfcfd; }} th.extra-col, td.extra-col {{ background: var(--extra); }} .empty {{ color: var(--muted); @@ -471,7 +512,7 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406 }} @media (max-width: 760px) {{ .grid, .summary {{ grid-template-columns: 1fr; }} - .actions, .file-head, .row-control {{ align-items: stretch; flex-direction: column; }} + .actions, .file-head, .row-control, .export-options {{ align-items: stretch; flex-direction: column; }} .row-control input {{ width: 100%; }} button, .button, .small-button, .ghost {{ width: 100%; }} }} @@ -485,6 +526,24 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406 {body} + """ + + +def _is_summary_group_header(row: list[str]) -> bool: + return len(row) >= 4 and row[0] == "\u59d3\u540d" and row[1] == "\u4f4f\u9662\u53f7" and row[2] == "\u91c7\u6837\u65f6\u95f4" and row[3].startswith("\u68c0\u6d4b\u539f\u56e0") diff --git a/app/processor.py b/app/processor.py index 93cf155..e19c6fe 100644 --- a/app/processor.py +++ b/app/processor.py @@ -3,6 +3,7 @@ import os import shutil import subprocess import sys +import uuid import zipfile from dataclasses import dataclass from pathlib import Path @@ -154,12 +155,31 @@ def run_processing( ) -def create_result_zip(job_dir: Path) -> Path: +def create_result_zip( + job_dir: Path, + include_basic_sheets: bool = True, + include_unmatched_items: bool = True, + include_summary_sheet: bool = True, +) -> Path: output_dir = job_dir / "output" result_zip = job_dir / "result.zip" if not output_dir.exists(): raise ProcessingError("结果目录不存在。") - _create_result_zip(output_dir, result_zip) + export_dir = _new_export_dir(job_dir) + for path in sorted(output_dir.rglob("*.xlsx")): + if path.is_file(): + target = export_dir / path.relative_to(output_dir) + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + _apply_export_options( + target, + include_basic_sheets=include_basic_sheets, + include_unmatched_items=include_unmatched_items, + include_summary_sheet=include_summary_sheet, + ) + + result_zip = export_dir / "result.zip" + _create_result_zip(export_dir, result_zip) return result_zip @@ -181,14 +201,67 @@ def summarize_job(job_dir: Path, preview_rows: int = 20) -> ProcessingResult: ) -def find_output_file(job_dir: Path, relpath: str) -> Path: +def find_output_file( + job_dir: Path, + relpath: str, + include_basic_sheets: bool = True, + include_unmatched_items: bool = True, + include_summary_sheet: bool = True, +) -> Path: output_dir = (job_dir / "output").resolve() target = (output_dir / relpath).resolve() if not str(target).startswith(str(output_dir)) or not target.is_file(): raise ProcessingError("结果文件不存在。") if target.suffix.lower() != ".xlsx": raise ProcessingError("只能导出 Excel 结果文件。") - return target + export_dir = _new_export_dir(job_dir) + export_target = export_dir / target.name + shutil.copy2(target, export_target) + _apply_export_options( + export_target, + include_basic_sheets=include_basic_sheets, + include_unmatched_items=include_unmatched_items, + include_summary_sheet=include_summary_sheet, + ) + return export_target + + +def _new_export_dir(job_dir: Path) -> Path: + export_root = job_dir / "exports" + export_root.mkdir(exist_ok=True) + export_dir = export_root / uuid.uuid4().hex + export_dir.mkdir() + return export_dir + + +def _apply_export_options( + path: Path, + include_basic_sheets: bool, + include_unmatched_items: bool, + include_summary_sheet: bool, +) -> None: + if not include_basic_sheets and not include_summary_sheet: + raise ProcessingError("\u81f3\u5c11\u9700\u8981\u5bfc\u51fa\u57fa\u672c\u5de5\u4f5c\u8868\u6216\u672a\u68c0\u6d4b\u5230\u5185\u5bb9\u6c47\u603b\u8868\u3002") + + workbook = load_workbook(path) + try: + if not include_unmatched_items: + _remove_unmatched_columns(workbook) + + if not include_summary_sheet and SUMMARY_SHEET_NAME in workbook.sheetnames: + workbook.remove(workbook[SUMMARY_SHEET_NAME]) + + if not include_basic_sheets: + for sheet in list(workbook.worksheets): + if sheet.title != SUMMARY_SHEET_NAME: + workbook.remove(sheet) + + if not workbook.worksheets: + workbook.create_sheet(SUMMARY_SHEET_NAME) + + workbook.save(path) + finally: + workbook.close() def _read_mode(job_dir: Path) -> str: