show zip packaging progress on download

This commit is contained in:
2026-05-03 01:43:52 +08:00
parent a795aa13bf
commit 022828095b
2 changed files with 149 additions and 18 deletions

View File

@@ -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<string, BackendJob>;
type LibraryItem = {
id: string;
patientId: string;
@@ -246,6 +248,7 @@ export default function App() {
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
const [deformationJob, setDeformationJob] = useState<BackendJob | null>(restoredDeformationJob?.job || null);
const [videoJob, setVideoJob] = useState<BackendJob | null>(null);
const [zipJobs, setZipJobs] = useState<ZipJobsByTarget>({});
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() {
</div>
{deformationJob?.error && <p className="text-[10px] text-red-500 font-bold mt-2 break-all">{deformationJob.error}</p>}
{deformationJob?.status === 'completed' && deformationJob.result?.outputs && (
<a
href={deformationDownloadUrl('all')}
download
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"
<button
onClick={() => 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"
>
<Download size={14} /> ZIP
</a>
<Download size={14} />
{zipJobs.all?.status === 'running'
? `四状态 ZIP 打包中 ${progressFromJob(zipJobs.all, 0)}%`
: '下载四状态 ZIP'}
</button>
)}
{zipJobs.all?.status === 'failed' && <p className="text-[10px] text-red-500 font-bold mt-2 break-all">{zipJobs.all.error || '四状态 ZIP 打包失败'}</p>}
</div>
</div>
@@ -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 (
<div key={t.key} className="bg-white p-4 rounded-2xl border flex flex-col hover:border-blue-200 transition-colors shadow-sm group min-h-[285px]">
<div className="flex justify-between items-center mb-3">
@@ -937,14 +1027,18 @@ export default function App() {
)}
</div>
{deformationJob?.status === 'completed' && deformationJob.result?.outputs?.[t.key] && (
<a
href={deformationDownloadUrl(t.key)}
download
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"
<button
onClick={() => 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"
>
<Download size={13} /> DICOM ZIP
</a>
<Download size={13} />
{zipJob?.status === 'running'
? `打包中 ${progressFromJob(zipJob, 0)}%`
: '下载本状态 DICOM ZIP'}
</button>
)}
{zipJob?.status === 'failed' && <p className="text-[10px] text-red-500 font-bold mt-2 break-all">{zipJob.error || '打包失败'}</p>}
</div>
);
})}