Files
REVOXELSEG_DICOM/WebSite/src/App.tsx
2026-05-21 11:18:50 +08:00

201 lines
6.2 KiB
TypeScript

/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useEffect, useRef } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import Login from './components/Login';
import Sidebar from './components/Sidebar';
import Overview from './components/Overview';
import ProjectLibrary from './components/ProjectLibrary';
import ReverseWorkspace from './components/ReverseWorkspace';
import UserManagement from './components/UserManagement';
import { ViewType } from './types';
import { api } from './lib/api';
export default function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [sessionLoading, setSessionLoading] = useState(true);
const [activeView, setActiveView] = useState<ViewType>(ViewType.OVERVIEW);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [activeProjectId, setActiveProjectId] = useState('head-ct-demo');
const [projectLibraryInitialView, setProjectLibraryInitialView] = useState<'dicom' | 'model' | 'mask'>('dicom');
const workspaceLeaveGuardRef = useRef<(() => Promise<boolean>) | null>(null);
const bootSessionResetRef = useRef(false);
// Automatically collapse main sidebar when entering Project Library or Workspace
useEffect(() => {
if (activeView === ViewType.PROJECTS || activeView === ViewType.WORKSPACE) {
setSidebarCollapsed(true);
} else {
setSidebarCollapsed(false);
}
}, [activeView]);
useEffect(() => {
let mounted = true;
const syncSession = async () => {
try {
if (!bootSessionResetRef.current) {
bootSessionResetRef.current = true;
const session = await api.logout();
if (!mounted) {
return;
}
setIsAuthenticated(session.authenticated);
setActiveView(ViewType.OVERVIEW);
return;
}
const session = await api.getSession();
if (!mounted) {
return;
}
setIsAuthenticated(session.authenticated);
if (!session.authenticated) {
setActiveView(ViewType.OVERVIEW);
}
} catch {
if (mounted) {
setIsAuthenticated(false);
}
} finally {
if (mounted) {
setSessionLoading(false);
}
}
};
syncSession();
const interval = window.setInterval(syncSession, 2500);
return () => {
mounted = false;
window.clearInterval(interval);
};
}, []);
const handleLogin = () => {
setIsAuthenticated(true);
};
const requestActiveView = (nextView: ViewType) => {
if (nextView === activeView) {
return;
}
const leaveWorkspace = activeView === ViewType.WORKSPACE && nextView !== ViewType.WORKSPACE;
const switchView = () => {
if (leaveWorkspace && nextView === ViewType.PROJECTS) {
setProjectLibraryInitialView('mask');
}
setActiveView(nextView);
};
if (!leaveWorkspace || !workspaceLeaveGuardRef.current) {
switchView();
return;
}
workspaceLeaveGuardRef.current()
.then((canLeave) => {
if (canLeave) {
switchView();
}
})
.catch(() => undefined);
};
const handleLogout = async () => {
if (activeView === ViewType.WORKSPACE && workspaceLeaveGuardRef.current) {
const canLeave = await workspaceLeaveGuardRef.current();
if (!canLeave) {
return;
}
}
await api.logout();
setIsAuthenticated(false);
setActiveView(ViewType.OVERVIEW);
};
if (sessionLoading) {
return (
<div className="min-h-screen bg-neutral-50 flex items-center justify-center text-slate-500 font-medium">
...
</div>
);
}
if (!isAuthenticated) {
return <Login onLogin={handleLogin} />;
}
return (
<div className="flex h-screen bg-[#f8fafc] overflow-hidden font-sans antialiased text-slate-900">
<Sidebar
activeView={activeView}
setActiveView={requestActiveView}
onLogout={handleLogout}
collapsed={sidebarCollapsed}
setCollapsed={setSidebarCollapsed}
/>
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
{/* Top Navigation */}
<header className="h-16 bg-white border-b border-slate-200 px-8 flex items-center justify-between z-10 shrink-0">
<div className="flex items-center gap-4 text-sm font-medium">
<span className="text-slate-900 font-bold capitalize">
{activeView === ViewType.OVERVIEW && '总体概况'}
{activeView === ViewType.PROJECTS && '项目库'}
{activeView === ViewType.WORKSPACE && '逆向工作区'}
{activeView === ViewType.SYSTEM && '系统管理工作区'}
</span>
</div>
<div className="flex items-center gap-6">
</div>
</header>
{/* Content Area */}
<div className="flex-1 overflow-y-auto overflow-x-hidden p-8">
<AnimatePresence mode="wait">
<motion.div
key={activeView}
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="h-full"
>
{activeView === ViewType.OVERVIEW && <Overview />}
{activeView === ViewType.PROJECTS && (
<ProjectLibrary
initialViewMode={projectLibraryInitialView}
onReverse={(projectId) => {
setActiveProjectId(projectId);
setActiveView(ViewType.WORKSPACE);
}}
/>
)}
{activeView === ViewType.WORKSPACE && (
<ReverseWorkspace
projectId={activeProjectId}
onLeaveGuardChange={(handler) => {
workspaceLeaveGuardRef.current = handler;
}}
/>
)}
{activeView === ViewType.SYSTEM && <UserManagement />}
</motion.div>
</AnimatePresence>
</div>
</main>
</div>
);
}