persist deformation job progress across refresh

This commit is contained in:
2026-05-03 01:18:25 +08:00
parent d5a6b1c935
commit eb03bea7d4

View File

@@ -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' const API_BASE = typeof window === 'undefined'
? 'http://127.0.0.1:8787' ? 'http://127.0.0.1:8787'
: `${window.location.protocol}//${window.location.hostname}: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 }) { function LibraryDicomPreview({ item }: { item: LibraryItem }) {
const [sliceIndex, setSliceIndex] = useState(Math.max(0, Math.floor((item.fileCount || 1) / 2))); const [sliceIndex, setSliceIndex] = useState(Math.max(0, Math.floor((item.fileCount || 1) / 2)));
const [requestedSliceIndex, setRequestedSliceIndex] = useState(sliceIndex); const [requestedSliceIndex, setRequestedSliceIndex] = useState(sliceIndex);
@@ -166,6 +189,8 @@ function LibraryDicomPreview({ item }: { item: LibraryItem }) {
} }
export default function App() { export default function App() {
const restoredDeformationJob = useRef(readStoredDeformationJob()).current;
// --- Authentication State --- // --- Authentication State ---
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<User | null>(null);
@@ -202,14 +227,14 @@ export default function App() {
// --- Simulation State (Workspace) --- // --- Simulation State (Workspace) ---
const [cervicalRotation, setCervicalRotation] = useState(14.5); const [cervicalRotation, setCervicalRotation] = useState(14.5);
const [transitionWidth, setTransitionWidth] = useState(90); const [transitionWidth, setTransitionWidth] = useState(90);
const [isSimulating, setIsSimulating] = useState(false); const [isSimulating, setIsSimulating] = useState(restoredDeformationJob?.job.status === 'running');
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(restoredDeformationJob?.job.status === 'completed' ? 100 : restoredDeformationJob?.progress || 0);
const [toastMessage, setToastMessage] = useState(""); const [toastMessage, setToastMessage] = useState("");
const [backendOnline, setBackendOnline] = useState(false); const [backendOnline, setBackendOnline] = useState(false);
const [backendMessage, setBackendMessage] = useState('正在连接本地 Python 后端...'); const [backendMessage, setBackendMessage] = useState('正在连接本地 Python 后端...');
const [previewImage, setPreviewImage] = useState(''); const [previewImage, setPreviewImage] = useState('');
const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [isPreviewLoading, setIsPreviewLoading] = useState(false);
const [deformationJob, setDeformationJob] = useState<BackendJob | null>(null); const [deformationJob, setDeformationJob] = useState<BackendJob | null>(restoredDeformationJob?.job || null);
const [videoJob, setVideoJob] = useState<BackendJob | null>(null); const [videoJob, setVideoJob] = useState<BackendJob | null>(null);
const [videoMaxAngle, setVideoMaxAngle] = useState(20); const [videoMaxAngle, setVideoMaxAngle] = useState(20);
const [videoDuration, setVideoDuration] = useState(6); 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 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 loadLibrary = async () => {
const data = await apiRequest('/api/library'); const data = await apiRequest('/api/library');
const items = data.items || []; const items = data.items || [];
@@ -285,11 +319,30 @@ export default function App() {
refreshBackendDefaults(); 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(() => { useEffect(() => {
if (!deformationJob || deformationJob.status !== 'running') return; if (!deformationJob || deformationJob.status !== 'running') return;
const timer = setInterval(async () => {
let isActive = true;
const pollDeformationJob = async () => {
try { try {
const job = await apiRequest(`/api/job?id=${deformationJob.id}`) as BackendJob; const job = await apiRequest(`/api/job?id=${deformationJob.id}`) as BackendJob;
if (!isActive) return;
setDeformationJob(job); setDeformationJob(job);
setProgress(value => job.status === 'completed' ? 100 : Math.min(value + 8, 95)); setProgress(value => job.status === 'completed' ? 100 : Math.min(value + 8, 95));
if (job.status === 'completed') { if (job.status === 'completed') {
@@ -301,11 +354,18 @@ export default function App() {
showToast(job.error || '形变任务失败'); showToast(job.error || '形变任务失败');
} }
} catch (error) { } catch (error) {
if (!isActive) return;
setIsSimulating(false); setIsSimulating(false);
setDeformationJob(job => job ? { ...job, status: 'failed', error: (error as Error).message } : null); 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]); }, [deformationJob?.id, deformationJob?.status]);
useEffect(() => { useEffect(() => {
@@ -412,7 +472,7 @@ export default function App() {
setSelectedLibraryId(data.id || items[0]?.id || ''); setSelectedLibraryId(data.id || items[0]?.id || '');
setCurrentPage('workspace'); setCurrentPage('workspace');
setPreviewImage(''); setPreviewImage('');
setDeformationJob(null); clearDeformationTask();
showToast(`已上传 ${data.fileCount || files.length} 张 DICOM`); showToast(`已上传 ${data.fileCount || files.length} 张 DICOM`);
} catch (error) { } catch (error) {
const message = (error as Error).message === 'Failed to fetch' const message = (error as Error).message === 'Failed to fetch'
@@ -435,7 +495,7 @@ export default function App() {
if (selectedLibraryId === id) { if (selectedLibraryId === id) {
setSelectedLibraryId(data.items?.[0]?.id || ''); setSelectedLibraryId(data.items?.[0]?.id || '');
setPreviewImage(''); setPreviewImage('');
setDeformationJob(null); clearDeformationTask();
} }
showToast('影像已删除'); showToast('影像已删除');
} catch (error) { } catch (error) {
@@ -686,7 +746,7 @@ export default function App() {
onChange={event => { onChange={event => {
setSelectedLibraryId(event.target.value); setSelectedLibraryId(event.target.value);
setPreviewImage(''); 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" 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') { if (item.status === 'processed') {
setSelectedLibraryId(item.id); setSelectedLibraryId(item.id);
setPreviewImage(''); setPreviewImage('');
setDeformationJob(null); clearDeformationTask();
setCurrentPage('workspace'); setCurrentPage('workspace');
showToast(`已选择 ${item.patientId} 作为工作站数据源`); showToast(`已选择 ${item.patientId} 作为工作站数据源`);
} else { } else {