persist deformation job progress across refresh
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user