link content selection to preview exports
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# HIS_Sur_Data_Deal
|
# 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`。
|
患者编号类型可选择自动识别。自动识别会读取 `Patients_info.csv` 中的 `pat_no`,并与 `Tests_List` 文件名或患者目录名比对:若更匹配 10 位补零编号,则使用 `pat_no`;若更匹配原始编号,则使用 `zhuyuanhao`。
|
||||||
|
|
||||||
导出的压缩包默认只包含 Excel 结果,不包含处理日志。系统默认输出完整结果;处理完成后可在结果页选择导出时是否保留:
|
系统默认输出完整结果;处理完成后可在结果页通过“内容选择”立即切换预览内容,并同步影响右侧“导出此 Excel”的文件内容。可选择是否保留:
|
||||||
|
|
||||||
- 基本工作表
|
- 基本工作表
|
||||||
- 未匹配检测内容项
|
- 未匹配检测内容项
|
||||||
|
|||||||
117
app/main.py
117
app/main.py
@@ -111,14 +111,32 @@ async def process(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/result/{job_id}", response_class=HTMLResponse)
|
@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)
|
job_dir = _get_job_dir(job_id)
|
||||||
try:
|
try:
|
||||||
rows = _clean_preview_rows(preview_rows)
|
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:
|
except ProcessingError as exc:
|
||||||
raise HTTPException(status_code=404, detail=str(exc)) from 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}")
|
@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))
|
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_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)
|
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)
|
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:
|
|||||||
<div class="metric"><span>\u5de5\u4f5c\u8868</span><strong>{total_sheets}</strong></div>
|
<div class="metric"><span>\u5de5\u4f5c\u8868</span><strong>{total_sheets}</strong></div>
|
||||||
<div class="metric"><span>\u6570\u636e\u884c</span><strong>{total_rows}</strong></div>
|
<div class="metric"><span>\u6570\u636e\u884c</span><strong>{total_rows}</strong></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="actions">
|
|
||||||
<a class="button export-link" data-base="/download/all/{html.escape(result.job_id)}" href="/download/all/{html.escape(result.job_id)}">\u5bfc\u51fa\u5168\u90e8 Excel</a>
|
|
||||||
<a class="ghost" href="/">\u7ee7\u7eed\u5904\u7406\u65b0\u6587\u4ef6</a>
|
|
||||||
</section>
|
|
||||||
<section class="export-options">
|
<section class="export-options">
|
||||||
<strong>\u5bfc\u51fa\u5185\u5bb9\u9009\u62e9</strong>
|
<strong>\u5185\u5bb9\u9009\u62e9</strong>
|
||||||
<label><input class="export-option" type="checkbox" data-param="include_basic_sheets" checked> \u57fa\u672c\u5de5\u4f5c\u8868</label>
|
<label><input class="export-option" type="checkbox" data-param="include_basic_sheets" {_checked(include_basic_sheets)}> \u57fa\u672c\u5de5\u4f5c\u8868</label>
|
||||||
<label><input class="export-option" type="checkbox" data-param="include_unmatched_items" checked> \u672a\u5339\u914d\u68c0\u6d4b\u5185\u5bb9\u9879</label>
|
<label><input class="export-option" type="checkbox" data-param="include_unmatched_items" {_checked(include_unmatched_items)}> \u672a\u5339\u914d\u68c0\u6d4b\u5185\u5bb9\u9879</label>
|
||||||
<label><input class="export-option" type="checkbox" data-param="include_summary_sheet" checked> \u672a\u68c0\u6d4b\u5230\u5185\u5bb9\u6c47\u603b\u8868</label>
|
<label><input class="export-option" type="checkbox" data-param="include_summary_sheet" {_checked(include_summary_sheet)}> \u672a\u68c0\u6d4b\u5230\u5185\u5bb9\u6c47\u603b\u8868</label>
|
||||||
</section>
|
</section>
|
||||||
<form class="row-control" method="get" action="/result/{html.escape(result.job_id)}">
|
<form class="row-control" method="get" action="/result/{html.escape(result.job_id)}">
|
||||||
<label for="preview_rows">\u6bcf\u4e2a\u5de5\u4f5c\u8868\u9884\u89c8\u884c\u6570</label>
|
<label for="preview_rows">\u6bcf\u4e2a\u5de5\u4f5c\u8868\u9884\u89c8\u884c\u6570</label>
|
||||||
@@ -217,7 +241,11 @@ def _render_result(result: ProcessingResult, preview_rows: int = 20) -> str:
|
|||||||
{file_items}
|
{file_items}
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
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:
|
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 = '<a class="top-action" href="/">\u9000\u51fa / \u7ee7\u7eed\u5904\u7406\u65b0\u6587\u4ef6</a>'
|
||||||
return f"""
|
return f"""
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
@@ -302,7 +337,13 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406
|
|||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
min-width: 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 {{
|
h1 {{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
@@ -400,6 +441,20 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: #fff;
|
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 {{
|
.note, .hint {{
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -511,18 +566,23 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406
|
|||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
}}
|
}}
|
||||||
@media (max-width: 760px) {{
|
@media (max-width: 760px) {{
|
||||||
|
header {{ flex-direction: column; }}
|
||||||
.grid, .summary {{ grid-template-columns: 1fr; }}
|
.grid, .summary {{ grid-template-columns: 1fr; }}
|
||||||
.actions, .file-head, .row-control, .export-options {{ align-items: stretch; flex-direction: column; }}
|
.actions, .file-head, .row-control, .export-options {{ align-items: stretch; flex-direction: column; }}
|
||||||
.row-control input {{ width: 100%; }}
|
.row-control input {{ width: 100%; }}
|
||||||
button, .button, .small-button, .ghost {{ width: 100%; }}
|
button, .button, .small-button, .ghost {{ width: 100%; }}
|
||||||
|
.top-action {{ width: 100%; }}
|
||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="shell">
|
<main class="shell">
|
||||||
<header>
|
<header>
|
||||||
<h1>{APP_TITLE}</h1>
|
<div>
|
||||||
<div class="sub">{html.escape(subtitle)}</div>
|
<h1>{APP_TITLE}</h1>
|
||||||
|
<div class="sub">{html.escape(subtitle)}</div>
|
||||||
|
</div>
|
||||||
|
{action_html}
|
||||||
</header>
|
</header>
|
||||||
{body}
|
{body}
|
||||||
</main>
|
</main>
|
||||||
@@ -537,7 +597,28 @@ def _page_shell(body: str, subtitle: str = "\u4e0a\u4f20\u201c\u5f85\u5904\u7406
|
|||||||
link.href = base + sep + params.toString();
|
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();
|
updateExportLinks();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -183,11 +183,30 @@ def create_result_zip(
|
|||||||
return 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"
|
output_dir = job_dir / "output"
|
||||||
if not output_dir.exists():
|
if not output_dir.exists():
|
||||||
raise ProcessingError("结果目录不存在。")
|
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:
|
if not xlsx_files:
|
||||||
raise ProcessingError("结果文件不存在。")
|
raise ProcessingError("结果文件不存在。")
|
||||||
result_zip = job_dir / "result.zip"
|
result_zip = job_dir / "result.zip"
|
||||||
@@ -195,9 +214,9 @@ def summarize_job(job_dir: Path, preview_rows: int = 20) -> ProcessingResult:
|
|||||||
return ProcessingResult(
|
return ProcessingResult(
|
||||||
job_id=job_dir.name,
|
job_id=job_dir.name,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
output_dir=output_dir,
|
output_dir=preview_dir,
|
||||||
zip_path=result_zip,
|
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],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user