diff --git a/README.md b/README.md index 3fa0ca2..c038208 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HIS_Sur_Data_Deal -网页端检测数据处理工具。上传 `待处理检测数据.zip` 后,服务会自动识别 V1/V2 数据结构,调用原处理脚本生成 Excel,并在网页中展示结果摘要、工作表统计和数据预览;用户可调整每个工作表的预览行数,并按需导出单个 Excel 或全部 Excel 压缩包。 +网页端检测数据处理工具。上传 `待处理检测数据.zip` 后,服务会自动识别 V1/V2 数据结构,调用原处理脚本生成 Excel,并在网页中展示结果摘要、工作表统计和数据预览;用户可调整每个工作表的预览行数,并按需导出单个 Excel。 ## 本地运行 @@ -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 942f364..b5c66d0 100644 --- a/app/main.py +++ b/app/main.py @@ -111,14 +111,32 @@ async def process( @app.get("/result/{job_id}", response_class=HTMLResponse) -def result_page(job_id: str, preview_rows: int = Query(20, ge=5, le=200)) -> str: +def result_page( + job_id: str, + preview_rows: int = Query(20, ge=5, le=200), + include_basic_sheets: bool = Query(True), + include_unmatched_items: bool = Query(True), + include_summary_sheet: bool = Query(True), +) -> str: job_dir = _get_job_dir(job_id) try: rows = _clean_preview_rows(preview_rows) - result = summarize_job(job_dir, rows) + result = summarize_job( + job_dir, + rows, + 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 _render_result(result, preview_rows=rows) + return _render_result( + result, + preview_rows=rows, + include_basic_sheets=include_basic_sheets, + include_unmatched_items=include_unmatched_items, + include_summary_sheet=include_summary_sheet, + ) @app.get("/download/all/{job_id}") @@ -185,7 +203,17 @@ def _clean_preview_rows(preview_rows: int) -> int: return max(5, min(int(preview_rows or 20), 200)) -def _render_result(result: ProcessingResult, preview_rows: int = 20) -> str: +def _checked(value: bool) -> str: + return "checked" if value else "" + + +def _render_result( + result: ProcessingResult, + preview_rows: int = 20, + include_basic_sheets: bool = True, + include_unmatched_items: bool = True, + include_summary_sheet: bool = True, +) -> str: total_sheets = sum(len(file.sheets) for file in result.files) total_rows = sum(max(sheet.rows - 1, 0) for file in result.files for sheet in file.sheets) file_items = "\n".join(_render_file(file, result.job_id) for file in result.files) @@ -197,15 +225,11 @@ def _render_result(result: ProcessingResult, preview_rows: int = 20) -> str:
\u5de5\u4f5c\u8868{total_sheets}
\u6570\u636e\u884c{total_rows}
-
- \u5bfc\u51fa\u5168\u90e8 Excel - \u7ee7\u7eed\u5904\u7406\u65b0\u6587\u4ef6 -
- \u5bfc\u51fa\u5185\u5bb9\u9009\u62e9 - - - + \u5185\u5bb9\u9009\u62e9 + + +
@@ -217,7 +241,11 @@ def _render_result(result: ProcessingResult, preview_rows: int = 20) -> str: {file_items} """ - return _page_shell(body, subtitle="\u5904\u7406\u5b8c\u6210\uff0c\u53ef\u5411\u4e0b\u6eda\u52a8\u67e5\u770b\u66f4\u591a\u5de5\u4f5c\u8868\uff0c\u4e5f\u53ef\u8c03\u6574\u9884\u89c8\u884c\u6570\u540e\u5237\u65b0\u3002") + return _page_shell( + body, + subtitle="\u5904\u7406\u5b8c\u6210\uff0c\u53ef\u5411\u4e0b\u6eda\u52a8\u67e5\u770b\u66f4\u591a\u5de5\u4f5c\u8868\uff0c\u4e5f\u53ef\u8c03\u6574\u9884\u89c8\u884c\u6570\u540e\u5237\u65b0\u3002", + top_action=True, + ) def _render_file(file, job_id: str) -> str: @@ -266,7 +294,14 @@ def _render_sheet(sheet) -> str: """ -def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406\u68c0\u6d4b\u6570\u636e.zip\u201d\uff0c\u5904\u7406\u5b8c\u6210\u540e\u5728\u7f51\u9875\u4e2d\u67e5\u770b\u7ed3\u679c\u3002") -> str: +def _page_shell( + body: str, + subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406\u68c0\u6d4b\u6570\u636e.zip\u201d\uff0c\u5904\u7406\u5b8c\u6210\u540e\u5728\u7f51\u9875\u4e2d\u67e5\u770b\u7ed3\u679c\u3002", + top_action: bool = False, +) -> str: + action_html = "" + if top_action: + action_html = '\u9000\u51fa / \u7ee7\u7eed\u5904\u7406\u65b0\u6587\u4ef6' return f""" @@ -302,7 +337,13 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406 padding: 24px 0; min-width: 0; }} - header {{ margin-bottom: 18px; }} + header {{ + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 18px; + }} h1 {{ margin: 0; font-size: 26px; @@ -400,6 +441,20 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406 color: var(--text); background: #fff; }} + .top-action {{ + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 40px; + border-radius: 6px; + padding: 0 14px; + background: var(--primary); + color: #fff; + font-weight: 700; + text-decoration: none; + white-space: nowrap; + }} + .top-action:hover {{ background: var(--primary-dark); }} .note, .hint {{ color: var(--muted); font-size: 13px; @@ -511,18 +566,23 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406 padding: 12px 14px; }} @media (max-width: 760px) {{ + header {{ flex-direction: column; }} .grid, .summary {{ grid-template-columns: 1fr; }} .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%; }} + .top-action {{ width: 100%; }} }}
-

{APP_TITLE}

-
{html.escape(subtitle)}
+
+

{APP_TITLE}

+
{html.escape(subtitle)}
+
+ {action_html}
{body}
@@ -537,7 +597,28 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406 link.href = base + sep + params.toString(); }}); }} - document.querySelectorAll('.export-option').forEach((option) => option.addEventListener('change', updateExportLinks)); + function refreshResultWithOptions() {{ + const resultForm = document.querySelector('.row-control'); + if (!resultForm) return; + const url = new URL(resultForm.action, window.location.origin); + const previewRows = document.querySelector('#preview_rows'); + if (previewRows) url.searchParams.set('preview_rows', previewRows.value || '20'); + document.querySelectorAll('.export-option').forEach((option) => {{ + url.searchParams.set(option.dataset.param, option.checked ? 'true' : 'false'); + }}); + window.location.href = url.toString(); + }} + const resultForm = document.querySelector('.row-control'); + if (resultForm) {{ + resultForm.addEventListener('submit', (event) => {{ + event.preventDefault(); + refreshResultWithOptions(); + }}); + }} + document.querySelectorAll('.export-option').forEach((option) => option.addEventListener('change', () => {{ + updateExportLinks(); + refreshResultWithOptions(); + }})); updateExportLinks(); diff --git a/app/processor.py b/app/processor.py index e19c6fe..28661a9 100644 --- a/app/processor.py +++ b/app/processor.py @@ -183,11 +183,30 @@ def create_result_zip( return result_zip -def summarize_job(job_dir: Path, preview_rows: int = 20) -> ProcessingResult: +def summarize_job( + job_dir: Path, + preview_rows: int = 20, + include_basic_sheets: bool = True, + include_unmatched_items: bool = True, + include_summary_sheet: bool = True, +) -> ProcessingResult: output_dir = job_dir / "output" if not output_dir.exists(): raise ProcessingError("结果目录不存在。") - xlsx_files = sorted(output_dir.rglob("*.xlsx")) + preview_dir = _new_export_dir(job_dir) + for path in sorted(output_dir.rglob("*.xlsx")): + if path.is_file(): + target = preview_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, + ) + + xlsx_files = sorted(preview_dir.rglob("*.xlsx")) if not xlsx_files: raise ProcessingError("结果文件不存在。") result_zip = job_dir / "result.zip" @@ -195,9 +214,9 @@ def summarize_job(job_dir: Path, preview_rows: int = 20) -> ProcessingResult: return ProcessingResult( job_id=job_dir.name, mode=mode, - output_dir=output_dir, + output_dir=preview_dir, zip_path=result_zip, - files=[_summarize_workbook(path, output_dir, preview_rows) for path in xlsx_files], + files=[_summarize_workbook(path, preview_dir, preview_rows) for path in xlsx_files], )