From 022828095bd7d5dac08e9133306cd659a5df7ab1 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Sun, 3 May 2026 01:43:52 +0800 Subject: [PATCH] show zip packaging progress on download --- WebSite/src/App.tsx | 126 ++++++++++++++++++++++++++++++++++++++------ web_backend.py | 41 +++++++++++++- 2 files changed, 149 insertions(+), 18 deletions(-) diff --git a/WebSite/src/App.tsx b/WebSite/src/App.tsx index f015590..e281756 100644 --- a/WebSite/src/App.tsx +++ b/WebSite/src/App.tsx @@ -47,7 +47,7 @@ type Page = 'overview' | 'library' | 'workspace' | 'users'; type BackendJob = { id: string; - kind: 'deformation' | 'video'; + kind: 'deformation' | 'video' | 'zip'; owner?: string; status: 'running' | 'completed' | 'failed'; message: string; @@ -56,6 +56,8 @@ type BackendJob = { error?: string; }; +type ZipJobsByTarget = Record; + type LibraryItem = { id: string; patientId: string; @@ -246,6 +248,7 @@ export default function App() { const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [deformationJob, setDeformationJob] = useState(restoredDeformationJob?.job || null); const [videoJob, setVideoJob] = useState(null); + const [zipJobs, setZipJobs] = useState({}); const [videoMaxAngle, setVideoMaxAngle] = useState(20); const [videoDuration, setVideoDuration] = useState(6); @@ -295,14 +298,22 @@ export default function App() { }; const fileUrl = (path?: string) => path ? `${API_BASE}/api/file?path=${encodeURIComponent(path)}` : ''; - const deformationDownloadUrl = (target: string) => deformationJob?.id - ? `${API_BASE}/api/deformation/download?job=${encodeURIComponent(deformationJob.id)}&target=${encodeURIComponent(target)}` - : ''; + + const triggerDownload = (path?: string, name?: string) => { + if (!path) return; + const link = document.createElement('a'); + link.href = fileUrl(path); + if (name) link.download = name; + document.body.appendChild(link); + link.click(); + link.remove(); + }; const clearDeformationTask = () => { setDeformationJob(null); setProgress(0); setIsSimulating(false); + setZipJobs({}); if (typeof window !== 'undefined') { window.localStorage.removeItem(DEFORMATION_JOB_STORAGE_KEY); } @@ -314,6 +325,34 @@ export default function App() { setIsSimulating(job.status === 'running'); }; + const handlePackageDownload = async (target: string) => { + if (!deformationJob?.id || zipJobs[target]?.status === 'running') return; + try { + const job = await apiRequest('/api/deformation/package', { + method: 'POST', + body: JSON.stringify({ + username: currentUser?.username || 'anonymous', + jobId: deformationJob.id, + target + }) + }) as BackendJob; + setZipJobs(current => ({ ...current, [target]: job })); + showToast(target === 'all' ? '四状态 ZIP 开始打包' : '本状态 ZIP 开始打包'); + } catch (error) { + setZipJobs(current => ({ + ...current, + [target]: { + id: `failed_${Date.now()}`, + kind: 'zip', + status: 'failed', + message: '打包失败。', + error: (error as Error).message, + } + })); + showToast((error as Error).message); + } + }; + const loadLibrary = async () => { const data = await apiRequest('/api/library'); const items = data.items || []; @@ -427,6 +466,52 @@ export default function App() { return () => clearInterval(timer); }, [videoJob?.id, videoJob?.status]); + useEffect(() => { + const runningEntries = (Object.entries(zipJobs) as [string, BackendJob][]) + .filter(([, job]) => job.status === 'running'); + if (!runningEntries.length) return; + + let isActive = true; + const pollZipJobs = async () => { + await Promise.all(runningEntries.map(async ([target, currentJob]) => { + try { + const job = await apiRequest(`/api/job?id=${currentJob.id}`) as BackendJob; + if (!isActive) return; + const displayJob = job.status === 'running' + ? { + ...job, + progress: Math.min( + 95, + Math.max(progressFromJob(currentJob, 10) + 8, progressFromJob(job, 10)) + ) + } + : job; + setZipJobs(current => ({ ...current, [target]: displayJob })); + if (job.status === 'completed') { + triggerDownload(job.result?.file?.path, job.result?.file?.name); + showToast('ZIP 打包完成,已开始下载'); + } + if (job.status === 'failed') { + showToast(job.error || 'ZIP 打包失败'); + } + } catch (error) { + if (!isActive) return; + setZipJobs(current => ({ + ...current, + [target]: { ...currentJob, status: 'failed', error: (error as Error).message } + })); + } + })); + }; + + pollZipJobs(); + const timer = setInterval(pollZipJobs, 1500); + return () => { + isActive = false; + clearInterval(timer); + }; + }, [zipJobs]); + const handleLogin = (e: React.FormEvent) => { e.preventDefault(); const user = users.find(u => u.username === loginUsername && u.password === loginPassword); @@ -833,14 +918,18 @@ export default function App() { {deformationJob?.error &&

{deformationJob.error}

} {deformationJob?.status === 'completed' && deformationJob.result?.outputs && ( - handlePackageDownload('all')} + disabled={zipJobs.all?.status === 'running'} + className="mt-3 w-full py-3 bg-green-600 text-white rounded-xl text-xs font-bold hover:bg-green-700 transition-all flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-wait" > - 下载四状态 ZIP - + + {zipJobs.all?.status === 'running' + ? `四状态 ZIP 打包中 ${progressFromJob(zipJobs.all, 0)}%` + : '下载四状态 ZIP'} + )} + {zipJobs.all?.status === 'failed' &&

{zipJobs.all.error || '四状态 ZIP 打包失败'}

} @@ -920,6 +1009,7 @@ export default function App() { ].map(t => { const screenshotDir = deformationJob?.result?.previews?.screenshots; const imagePath = screenshotDir ? `${screenshotDir}/${t.key}.png` : ''; + const zipJob = zipJobs[t.key]; return (
@@ -937,14 +1027,18 @@ export default function App() { )}
{deformationJob?.status === 'completed' && deformationJob.result?.outputs?.[t.key] && ( - handlePackageDownload(t.key)} + disabled={zipJob?.status === 'running'} + className="mt-3 py-2.5 bg-slate-100 text-slate-600 rounded-xl text-[11px] font-black hover:bg-green-600 hover:text-white transition-all flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-wait" > - 下载本状态 DICOM ZIP - + + {zipJob?.status === 'running' + ? `打包中 ${progressFromJob(zipJob, 0)}%` + : '下载本状态 DICOM ZIP'} + )} + {zipJob?.status === 'failed' &&

{zipJob.error || '打包失败'}

}
); })} diff --git a/web_backend.py b/web_backend.py index c4dc841..ec3e8de 100644 --- a/web_backend.py +++ b/web_backend.py @@ -508,7 +508,7 @@ def get_job(job_id): return dict(job) if job else None -def start_job(kind, worker, owner=None, params=None): +def start_job(kind, worker, owner=None, params=None, remember_user_task=True): job_id = uuid.uuid4().hex[:12] owner = normalized_username(owner) with JOBS_LOCK: @@ -526,7 +526,8 @@ def start_job(kind, worker, owner=None, params=None): "updatedAt": time.strftime("%Y-%m-%d %H:%M:%S"), } persist_jobs_locked() - set_user_task(owner, kind, job_id) + if remember_user_task: + set_user_task(owner, kind, job_id) def run(): try: @@ -573,6 +574,15 @@ def serialize_outputs(output_paths, preview_paths): } +def file_metadata(file_path): + file_path = Path(file_path).resolve() + return { + "path": str(file_path), + "name": file_path.name, + "size": file_path.stat().st_size, + } + + def prepare_deformation_zip(job_id, target): job = get_job(job_id) if not job: @@ -712,6 +722,33 @@ class Handler(BaseHTTPRequestHandler): self.send_json(make_preview(body["inputDir"], body.get("angleDegrees", 12))) return + if parsed.path == "/api/deformation/package": + source_job_id = body["jobId"] + target = body.get("target", "all") + 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) + set_job(job_id, message="打包完成,准备下载...", progress=95) + return {"file": file_metadata(zip_path)} + + self.send_json( + start_job( + "zip", + worker, + owner=username, + params={ + "sourceJobId": source_job_id, + "target": target, + }, + remember_user_task=False, + ), + status=202, + ) + return + if parsed.path == "/api/deformation": input_dir = body["inputDir"] angle_degrees = float(body.get("angleDegrees", 12))