From 2fcab9c71a2024d3c93afca60f6ccc15c65d3b1e Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Sun, 3 May 2026 02:45:45 +0800 Subject: [PATCH] select contents for four-state zip package --- WebSite/src/App.tsx | 168 +++++++++++++++++++++++++++++++++++++++++++- web_backend.py | 62 +++++++++++++++- 2 files changed, 225 insertions(+), 5 deletions(-) diff --git a/WebSite/src/App.tsx b/WebSite/src/App.tsx index b7d3ca2..f4c02b8 100644 --- a/WebSite/src/App.tsx +++ b/WebSite/src/App.tsx @@ -60,6 +60,31 @@ type BackendJob = { type ZipJobsByTarget = Record; +type PackageOptions = { + dicom: string[]; + images: string[]; +}; + +const DICOM_PACKAGE_OPTIONS = [ + { key: 'original', label: '原始序列 DICOM' }, + { key: 'hard_boundary', label: '硬边界 DICOM' }, + { key: 'gaussian_smooth', label: '高斯平滑 DICOM' }, + { key: 'soft_transition', label: '软过渡重建 DICOM' }, +]; + +const IMAGE_PACKAGE_OPTIONS = [ + { key: 'comparison', label: '原始图片四状态过程对比图' }, + { key: 'original', label: '原始序列' }, + { key: 'hard_boundary', label: '硬边界' }, + { key: 'gaussian_smooth', label: '高斯平滑' }, + { key: 'soft_transition', label: '软过渡重建' }, +]; + +const DEFAULT_PACKAGE_OPTIONS: PackageOptions = { + dicom: DICOM_PACKAGE_OPTIONS.map(option => option.key), + images: IMAGE_PACKAGE_OPTIONS.map(option => option.key), +}; + type LibraryItem = { id: string; patientId: string; @@ -263,6 +288,8 @@ export default function App() { const [deformationJob, setDeformationJob] = useState(restoredDeformationJob?.job || null); const [videoJob, setVideoJob] = useState(null); const [zipJobs, setZipJobs] = useState({}); + const [isPackageDialogOpen, setIsPackageDialogOpen] = useState(false); + const [packageOptions, setPackageOptions] = useState(DEFAULT_PACKAGE_OPTIONS); const [videoMaxAngle, setVideoMaxAngle] = useState(20); const [videoDuration, setVideoDuration] = useState(6); @@ -339,7 +366,7 @@ export default function App() { setIsSimulating(job.status === 'running'); }; - const handlePackageDownload = async (target: string) => { + const handlePackageDownload = async (target: string, selectedPackageOptions?: PackageOptions) => { if (!deformationJob?.id || zipJobs[target]?.status === 'running') return; try { const job = await apiRequest('/api/deformation/package', { @@ -347,7 +374,8 @@ export default function App() { body: JSON.stringify({ username: currentUser?.username || 'anonymous', jobId: deformationJob.id, - target + target, + packageOptions: selectedPackageOptions }) }) as BackendJob; setZipJobs(current => ({ ...current, [target]: job })); @@ -367,6 +395,38 @@ export default function App() { } }; + const togglePackageOption = (group: keyof PackageOptions, key: string) => { + setPackageOptions(current => { + const exists = current[group].includes(key); + return { + ...current, + [group]: exists + ? current[group].filter(value => value !== key) + : [...current[group], key], + }; + }); + }; + + const setPackageGroupSelection = (group: keyof PackageOptions, keys: string[]) => { + setPackageOptions(current => ({ ...current, [group]: keys })); + }; + + const invertPackageGroupSelection = (group: keyof PackageOptions, allKeys: string[]) => { + setPackageOptions(current => ({ + ...current, + [group]: allKeys.filter(key => !current[group].includes(key)), + })); + }; + + const confirmPackageDownload = () => { + if (!packageOptions.dicom.length && !packageOptions.images.length) { + showToast('请至少选择一个打包内容'); + return; + } + setIsPackageDialogOpen(false); + handlePackageDownload('all', packageOptions); + }; + const loadLibrary = async () => { const data = await apiRequest('/api/library'); const items = data.items || []; @@ -947,7 +1007,10 @@ export default function App() { {deformationJob?.error &&

{deformationJob.error}

} {deformationJob?.status === 'completed' && deformationJob.result?.outputs && ( + + +
+ {[ + { + key: 'dicom' as keyof PackageOptions, + title: 'DICOM 类', + options: DICOM_PACKAGE_OPTIONS, + allKeys: DICOM_PACKAGE_OPTIONS.map(option => option.key), + }, + { + key: 'images' as keyof PackageOptions, + title: '图片类', + options: IMAGE_PACKAGE_OPTIONS, + allKeys: IMAGE_PACKAGE_OPTIONS.map(option => option.key), + }, + ].map(group => ( +
+
+

{group.title}

+
+ + +
+
+
+ {group.options.map(option => { + const checked = packageOptions[group.key].includes(option.key); + return ( + + ); + })} +
+
+ ))} + +
+

+ 已选择 {packageOptions.dicom.length + packageOptions.images.length} 项内容 +

+
+ + +
+
+
+ + + )} + {(libraryInfo || isLibraryInfoLoading) && (
diff --git a/web_backend.py b/web_backend.py index bc5f178..a5a71a1 100644 --- a/web_backend.py +++ b/web_backend.py @@ -588,7 +588,60 @@ def file_metadata(file_path): } -def prepare_deformation_zip(job_id, target): +def zip_selected_deformation_outputs(job_root, outputs, previews, package_options): + dicom_keys = package_options.get("dicom") or [] + image_keys = package_options.get("images") or [] + if not dicom_keys and not image_keys: + raise RuntimeError("请至少选择一个要打包的内容。") + + zip_path = Path(job_root) / f"head_ct_morph_selected_{Path(job_root).name}.zip" + if zip_path.exists(): + zip_path.unlink() + + dicom_names = { + "original": "dicom/ct_original", + "hard_boundary": "dicom/ct_hard_boundary", + "gaussian_smooth": "dicom/ct_gaussian_smooth", + "soft_transition": "dicom/ct_soft_transition", + } + image_files = { + "comparison": Path(previews.get("comparison", "")), + "original": Path(previews.get("screenshots", "")) / "original.png", + "hard_boundary": Path(previews.get("screenshots", "")) / "hard_boundary.png", + "gaussian_smooth": Path(previews.get("screenshots", "")) / "gaussian_smooth.png", + "soft_transition": Path(previews.get("screenshots", "")) / "soft_transition.png", + } + image_names = { + "comparison": "images/process_comparison_4states.png", + "original": "images/original.png", + "hard_boundary": "images/hard_boundary.png", + "gaussian_smooth": "images/gaussian_smooth.png", + "soft_transition": "images/soft_transition.png", + } + + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for key in dicom_keys: + if key not in dicom_names: + continue + source_dir = Path(outputs.get(key, "")) + if not source_dir.exists(): + raise RuntimeError(f"{key} 的 DICOM 输出目录不存在。") + for file_path in source_dir.rglob("*"): + if file_path.is_file(): + archive.write(file_path, Path(dicom_names[key]) / file_path.relative_to(source_dir)) + + for key in image_keys: + if key not in image_files: + continue + source_file = image_files[key] + if not source_file.exists(): + raise RuntimeError(f"{key} 的图片输出不存在。") + archive.write(source_file, image_names[key]) + + return zip_path + + +def prepare_deformation_zip(job_id, target, package_options=None): job = get_job(job_id) if not job: raise RuntimeError("任务不存在。") @@ -599,8 +652,11 @@ def prepare_deformation_zip(job_id, target): result = job.get("result") or {} outputs = result.get("outputs") or {} + previews = result.get("previews") or {} job_root = RESULT_DIR / job_id if target == "all": + if package_options: + return zip_selected_deformation_outputs(job_root, outputs, previews, package_options) output_dir = job_root / "four_state_output" if not output_dir.exists(): raise RuntimeError("四状态输出目录不存在。") @@ -736,12 +792,13 @@ class Handler(BaseHTTPRequestHandler): if parsed.path == "/api/deformation/package": source_job_id = body["jobId"] target = body.get("target", "all") + package_options = body.get("packageOptions") username = normalized_username(body.get("username")) def worker(job_id): label = "四状态总 ZIP" if target == "all" else "本状态 DICOM ZIP" set_job(job_id, message=f"正在打包{label}...", progress=10) - zip_path = prepare_deformation_zip(source_job_id, target) + zip_path = prepare_deformation_zip(source_job_id, target, package_options) set_job(job_id, message="打包完成,准备下载...", progress=95) return {"file": file_metadata(zip_path)} @@ -753,6 +810,7 @@ class Handler(BaseHTTPRequestHandler): params={ "sourceJobId": source_job_id, "target": target, + "packageOptions": package_options, }, remember_user_task=False, ),