From eb03bea7d400d4b1cd79112e71e576ccfefc9970 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Sun, 3 May 2026 01:18:25 +0800 Subject: [PATCH] persist deformation job progress across refresh --- WebSite/src/App.tsx | 80 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/WebSite/src/App.tsx b/WebSite/src/App.tsx index 871ef86..538330e 100644 --- a/WebSite/src/App.tsx +++ b/WebSite/src/App.tsx @@ -77,10 +77,33 @@ type LibraryInfo = { }[]; }; +type StoredDeformationJob = { + job: BackendJob; + progress: number; +}; + +const DEFORMATION_JOB_STORAGE_KEY = 'head_ct_morph_deformation_job'; + const API_BASE = typeof window === 'undefined' ? 'http://127.0.0.1:8787' : `${window.location.protocol}//${window.location.hostname}:8787`; +function readStoredDeformationJob(): StoredDeformationJob | null { + if (typeof window === 'undefined') return null; + try { + const rawValue = window.localStorage.getItem(DEFORMATION_JOB_STORAGE_KEY); + if (!rawValue) return null; + const parsed = JSON.parse(rawValue) as StoredDeformationJob; + if (!parsed?.job?.id) return null; + return { + job: parsed.job, + progress: Math.max(0, Math.min(100, parsed.progress || 0)), + }; + } catch { + return null; + } +} + function LibraryDicomPreview({ item }: { item: LibraryItem }) { const [sliceIndex, setSliceIndex] = useState(Math.max(0, Math.floor((item.fileCount || 1) / 2))); const [requestedSliceIndex, setRequestedSliceIndex] = useState(sliceIndex); @@ -166,6 +189,8 @@ function LibraryDicomPreview({ item }: { item: LibraryItem }) { } export default function App() { + const restoredDeformationJob = useRef(readStoredDeformationJob()).current; + // --- Authentication State --- const [isLoggedIn, setIsLoggedIn] = useState(false); const [currentUser, setCurrentUser] = useState(null); @@ -202,14 +227,14 @@ export default function App() { // --- Simulation State (Workspace) --- const [cervicalRotation, setCervicalRotation] = useState(14.5); const [transitionWidth, setTransitionWidth] = useState(90); - const [isSimulating, setIsSimulating] = useState(false); - const [progress, setProgress] = useState(0); + const [isSimulating, setIsSimulating] = useState(restoredDeformationJob?.job.status === 'running'); + const [progress, setProgress] = useState(restoredDeformationJob?.job.status === 'completed' ? 100 : restoredDeformationJob?.progress || 0); const [toastMessage, setToastMessage] = useState(""); const [backendOnline, setBackendOnline] = useState(false); const [backendMessage, setBackendMessage] = useState('正在连接本地 Python 后端...'); const [previewImage, setPreviewImage] = useState(''); const [isPreviewLoading, setIsPreviewLoading] = useState(false); - const [deformationJob, setDeformationJob] = useState(null); + const [deformationJob, setDeformationJob] = useState(restoredDeformationJob?.job || null); const [videoJob, setVideoJob] = useState(null); const [videoMaxAngle, setVideoMaxAngle] = useState(20); const [videoDuration, setVideoDuration] = useState(6); @@ -261,6 +286,15 @@ export default function App() { const fileUrl = (path?: string) => path ? `${API_BASE}/api/file?path=${encodeURIComponent(path)}` : ''; + const clearDeformationTask = () => { + setDeformationJob(null); + setProgress(0); + setIsSimulating(false); + if (typeof window !== 'undefined') { + window.localStorage.removeItem(DEFORMATION_JOB_STORAGE_KEY); + } + }; + const loadLibrary = async () => { const data = await apiRequest('/api/library'); const items = data.items || []; @@ -285,11 +319,30 @@ export default function App() { refreshBackendDefaults(); }, []); + useEffect(() => { + if (typeof window === 'undefined') return; + if (!deformationJob) { + window.localStorage.removeItem(DEFORMATION_JOB_STORAGE_KEY); + return; + } + + window.localStorage.setItem( + DEFORMATION_JOB_STORAGE_KEY, + JSON.stringify({ + job: deformationJob, + progress: deformationJob.status === 'completed' ? 100 : progress, + }) + ); + }, [deformationJob, progress]); + useEffect(() => { if (!deformationJob || deformationJob.status !== 'running') return; - const timer = setInterval(async () => { + + let isActive = true; + const pollDeformationJob = async () => { try { const job = await apiRequest(`/api/job?id=${deformationJob.id}`) as BackendJob; + if (!isActive) return; setDeformationJob(job); setProgress(value => job.status === 'completed' ? 100 : Math.min(value + 8, 95)); if (job.status === 'completed') { @@ -301,11 +354,18 @@ export default function App() { showToast(job.error || '形变任务失败'); } } catch (error) { + if (!isActive) return; setIsSimulating(false); setDeformationJob(job => job ? { ...job, status: 'failed', error: (error as Error).message } : null); } - }, 1500); - return () => clearInterval(timer); + }; + + pollDeformationJob(); + const timer = setInterval(pollDeformationJob, 1500); + return () => { + isActive = false; + clearInterval(timer); + }; }, [deformationJob?.id, deformationJob?.status]); useEffect(() => { @@ -412,7 +472,7 @@ export default function App() { setSelectedLibraryId(data.id || items[0]?.id || ''); setCurrentPage('workspace'); setPreviewImage(''); - setDeformationJob(null); + clearDeformationTask(); showToast(`已上传 ${data.fileCount || files.length} 张 DICOM`); } catch (error) { const message = (error as Error).message === 'Failed to fetch' @@ -435,7 +495,7 @@ export default function App() { if (selectedLibraryId === id) { setSelectedLibraryId(data.items?.[0]?.id || ''); setPreviewImage(''); - setDeformationJob(null); + clearDeformationTask(); } showToast('影像已删除'); } catch (error) { @@ -686,7 +746,7 @@ export default function App() { onChange={event => { setSelectedLibraryId(event.target.value); setPreviewImage(''); - setDeformationJob(null); + clearDeformationTask(); }} className="w-full px-3 py-2.5 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-xs font-bold text-slate-700" > @@ -979,7 +1039,7 @@ export default function App() { if (item.status === 'processed') { setSelectedLibraryId(item.id); setPreviewImage(''); - setDeformationJob(null); + clearDeformationTask(); setCurrentPage('workspace'); showToast(`已选择 ${item.patientId} 作为工作站数据源`); } else {