/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useEffect, useRef } from 'react'; import { Activity, Check, Download, Layers, ActivitySquare, MonitorPlay, Save, LayoutDashboard, Database, Image as ImageIcon, Users, LogOut, UserPlus, Key, ChevronRight, ShieldCheck, Search, Settings, Plus, Play as PlayIcon, RefreshCw, Film, FolderOpen, Server, AlertCircle, Info, X } from 'lucide-react'; // --- Types --- type User = { id: string; username: string; password: string; role: 'admin' | 'user'; createdAt: string; }; type Page = 'overview' | 'library' | 'workspace' | 'users'; type BackendJob = { id: string; kind: 'deformation' | 'video' | 'zip'; owner?: string; status: 'running' | 'completed' | 'failed'; message: string; progress?: number; result?: any; error?: string; }; type ZipJobsByTarget = Record; type LibraryItem = { id: string; patientId: string; date: string; version: string; status: 'processed' | 'pending'; size: number; previewColor: string; dicomPath: string; fileCount?: number; source?: 'seed' | 'upload'; }; type LibraryInfo = { id: string; patientId: string; fileCount: number; groups: { title: string; items: { label: string; value: string }[]; }[]; }; 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 progressFromJob(job: BackendJob, fallback = 0) { if (job.status === 'completed') return 100; if (typeof job.progress === 'number') { return Math.max(0, Math.min(100, Math.round(job.progress))); } return Math.max(0, Math.min(95, fallback)); } 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); const [previewImage, setPreviewImage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const count = Math.max(1, item.fileCount || 1); useEffect(() => { const middleIndex = Math.max(0, Math.floor((item.fileCount || 1) / 2)); setSliceIndex(middleIndex); setRequestedSliceIndex(middleIndex); }, [item.id, item.fileCount]); useEffect(() => { const timer = window.setTimeout(() => { setRequestedSliceIndex(sliceIndex); }, 120); return () => window.clearTimeout(timer); }, [sliceIndex]); useEffect(() => { const controller = new AbortController(); setIsLoading(true); setError(''); fetch(`${API_BASE}/api/library/preview?id=${encodeURIComponent(item.id)}&index=${requestedSliceIndex}`, { signal: controller.signal }) .then(async response => { const data = await response.json(); if (!response.ok) throw new Error(data.error || '预览生成失败'); setPreviewImage(`${API_BASE}${data.imageUrl}`); (data.neighbors || []).forEach((url: string) => { fetch(`${API_BASE}${url}`, { signal: controller.signal }).catch(() => {}); }); }) .catch(error => { if ((error as Error).name !== 'AbortError') setError((error as Error).message); }) .finally(() => { if (!controller.signal.aborted) setIsLoading(false); }); return () => controller.abort(); }, [item.id, requestedSliceIndex]); return (
{item.status.toUpperCase()}
{previewImage && !error ? ( ) : (

{error || (isLoading ? '正在生成预览...' : '等待预览')}

)}
Axial DICOM {sliceIndex + 1} / {count}
setSliceIndex(parseInt(event.target.value, 10))} className="w-full h-1 accent-blue-500 cursor-pointer" />
); } export default function App() { const restoredDeformationJob = useRef(readStoredDeformationJob()).current; // --- Authentication State --- const [isLoggedIn, setIsLoggedIn] = useState(false); const [currentUser, setCurrentUser] = useState(null); const [loginUsername, setLoginUsername] = useState('admin'); const [loginPassword, setLoginPassword] = useState('123456'); const [loginError, setLoginError] = useState(''); // --- App Flow State --- const [currentPage, setCurrentPage] = useState('workspace'); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); useEffect(() => { // Automatically collapse sidebar when on workspace page to give more space if (currentPage === 'workspace') { setIsSidebarCollapsed(true); } else { setIsSidebarCollapsed(false); } }, [currentPage]); const [users, setUsers] = useState([ { id: '1', username: 'admin', password: '123456', role: 'admin', createdAt: '2024-01-01' }, { id: '2', username: 'doctor1', password: 'password123', role: 'user', createdAt: '2024-05-01' } ]); const [libraryData, setLibraryData] = useState([]); const [selectedLibraryId, setSelectedLibraryId] = useState(''); const [isUploadingDicom, setIsUploadingDicom] = useState(false); const [libraryInfo, setLibraryInfo] = useState(null); const [isLibraryInfoLoading, setIsLibraryInfoLoading] = useState(false); const folderUploadInputRef = useRef(null); const zipUploadInputRef = useRef(null); // --- Simulation State (Workspace) --- const [cervicalRotation, setCervicalRotation] = useState(14.5); const [transitionWidth, setTransitionWidth] = useState(90); const [isSimulating, setIsSimulating] = useState(restoredDeformationJob?.job.status === 'running'); const [progress, setProgress] = useState(restoredDeformationJob ? progressFromJob(restoredDeformationJob.job, 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(restoredDeformationJob?.job || null); const [videoJob, setVideoJob] = useState(null); const [zipJobs, setZipJobs] = useState({}); const [videoMaxAngle, setVideoMaxAngle] = useState(20); const [videoDuration, setVideoDuration] = useState(6); // --- User Management Shared State --- const [newUsername, setNewUsername] = useState(''); const [newPassword, setNewPassword] = useState(''); const [isChangingOwnPass, setIsChangingOwnPass] = useState(false); const [pwChangeInput, setPwChangeInput] = useState(''); const [showAddUser, setShowAddUser] = useState(false); const [activeUserMenu, setActiveUserMenu] = useState(null); const selectedDataset = libraryData.find(item => item.id === selectedLibraryId) || libraryData[0]; const selectedInputDir = selectedDataset?.dicomPath || ''; useEffect(() => { if (!activeUserMenu) return; const closeUserMenuOnOutsideClick = (event: PointerEvent) => { const target = event.target as HTMLElement | null; if (!target?.closest('[data-user-menu-root="true"]')) { setActiveUserMenu(null); } }; document.addEventListener('pointerdown', closeUserMenuOnOutsideClick); return () => document.removeEventListener('pointerdown', closeUserMenuOnOutsideClick); }, [activeUserMenu]); const showToast = (message: string) => { setToastMessage(message); setTimeout(() => setToastMessage(""), 3000); }; const apiRequest = async (path: string, options?: RequestInit) => { const response = await fetch(`${API_BASE}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...(options?.headers || {}) } }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '本地 Python 后端请求失败'); } return data; }; const fileUrl = (path?: string) => path ? `${API_BASE}/api/file?path=${encodeURIComponent(path)}` : ''; 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); } }; const applyDeformationJob = (job: BackendJob) => { setDeformationJob(job); setProgress(current => progressFromJob(job, current)); 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 || []; setLibraryData(items); setSelectedLibraryId(current => items.some((item: LibraryItem) => item.id === current) ? current : items[0]?.id || ''); return items; }; const refreshBackendDefaults = async () => { try { const data = await apiRequest('/api/defaults'); await loadLibrary(); setBackendOnline(true); setBackendMessage('本地 Python 后端已连接'); } catch (error) { setBackendOnline(false); setBackendMessage('未连接:请先运行 python web_backend.py'); } }; useEffect(() => { refreshBackendDefaults(); }, []); useEffect(() => { if (!isLoggedIn || !currentUser?.username) return; let isActive = true; const restoreUserDeformationJob = async () => { try { const data = await apiRequest( `/api/user/job?username=${encodeURIComponent(currentUser.username)}&kind=deformation` ) as { job?: BackendJob | null }; if (!isActive || !data.job) return; applyDeformationJob(data.job); if (data.job.status === 'running') { showToast('已恢复当前账号的四状态任务进度'); } } catch { // 账号任务恢复失败不影响正常使用工作站。 } }; restoreUserDeformationJob(); return () => { isActive = false; }; }, [isLoggedIn, currentUser?.username]); 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; 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 => progressFromJob(job, Math.min(value + 8, 95))); if (job.status === 'completed') { setIsSimulating(false); showToast('四状态 DICOM 与过程图已生成'); } if (job.status === 'failed') { setIsSimulating(false); showToast(job.error || '形变任务失败'); } } catch (error) { if (!isActive) return; setIsSimulating(false); setDeformationJob(job => job ? { ...job, status: 'failed', error: (error as Error).message } : null); } }; pollDeformationJob(); const timer = setInterval(pollDeformationJob, 1500); return () => { isActive = false; clearInterval(timer); }; }, [deformationJob?.id, deformationJob?.status]); useEffect(() => { if (!videoJob || videoJob.status !== 'running') return; const timer = setInterval(async () => { try { const job = await apiRequest(`/api/job?id=${videoJob.id}`) as BackendJob; setVideoJob(job); if (job.status === 'completed') showToast('仰头变化视频已生成'); if (job.status === 'failed') showToast(job.error || '视频生成失败'); } catch (error) { setVideoJob(job => job ? { ...job, status: 'failed', error: (error as Error).message } : null); } }, 1500); 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); if (user) { setIsLoggedIn(true); setCurrentUser(user); setLoginError(''); showToast(`欢迎, ${user.username}`); } else { setLoginError('用户名或密码错误'); } }; const handleLogout = () => { setIsLoggedIn(false); setCurrentUser(null); setCurrentPage('workspace'); }; const addUser = () => { if (!newUsername || !newPassword) return; if (users.find(u => u.username === newUsername)) { showToast('用户名已存在'); return; } const newUser: User = { id: Date.now().toString(), username: newUsername, password: newPassword, role: 'user', createdAt: new Date().toISOString().split('T')[0] }; setUsers([...users, newUser]); setNewUsername(''); setNewPassword(''); setShowAddUser(false); showToast('用户创建成功'); }; const uploadImage = () => { folderUploadInputRef.current?.click(); }; const uploadZip = () => { zipUploadInputRef.current?.click(); }; const handleLibraryFilesSelected = async (event: React.ChangeEvent) => { const selectedFiles: File[] = []; const fileList = event.currentTarget.files; if (fileList) { for (let index = 0; index < fileList.length; index += 1) { const file = fileList.item(index); if (file) selectedFiles.push(file); } } const files = selectedFiles.filter(file => { const name = file.name.toLowerCase(); return name.endsWith('.dcm') || name.endsWith('.zip'); }); event.currentTarget.value = ''; if (!files.length) { showToast('请选择 .dcm 文件或 .zip 压缩包'); return; } setIsUploadingDicom(true); try { const formData = new FormData(); const firstRelativePath = (files[0] as any).webkitRelativePath || files[0].name; const patientId = firstRelativePath.includes('/') ? firstRelativePath.split('/')[0] : files[0].name.replace(/\.(dcm|zip)$/i, '') || `WEB_${new Date().getTime()}`; formData.append('patientId', patientId); files.forEach(file => { formData.append('files', file, (file as any).webkitRelativePath || file.name); }); const response = await fetch(`${API_BASE}/api/library/upload`, { method: 'POST', body: formData }); const data = await response.json(); if (!response.ok) throw new Error(data.error || '上传失败'); const items = await loadLibrary(); setSelectedLibraryId(data.id || items[0]?.id || ''); setCurrentPage('workspace'); setPreviewImage(''); clearDeformationTask(); showToast(`已上传 ${data.fileCount || files.length} 张 DICOM`); } catch (error) { const message = (error as Error).message === 'Failed to fetch' ? `无法连接 Python 后端:${API_BASE}` : (error as Error).message; showToast(message); } finally { setIsUploadingDicom(false); } }; const deleteImage = async (id: string) => { try { const response = await fetch(`${API_BASE}/api/library?id=${encodeURIComponent(id)}`, { method: 'DELETE' }); const data = await response.json(); if (!response.ok) throw new Error(data.error || '删除失败'); setLibraryData(data.items || []); if (selectedLibraryId === id) { setSelectedLibraryId(data.items?.[0]?.id || ''); setPreviewImage(''); clearDeformationTask(); } showToast('影像已删除'); } catch (error) { showToast((error as Error).message); } }; const showLibraryInfo = async (item: LibraryItem) => { setIsLibraryInfoLoading(true); setLibraryInfo(null); try { const data = await apiRequest(`/api/library/info?id=${encodeURIComponent(item.id)}`) as LibraryInfo; setLibraryInfo(data); } catch (error) { showToast((error as Error).message); } finally { setIsLibraryInfoLoading(false); } }; const changePassword = (userId: string, newPass: string) => { setUsers(users.map(u => u.id === userId ? { ...u, password: newPass } : u)); setPwChangeInput(''); setIsChangingOwnPass(false); showToast('密码更新成功'); }; useEffect(() => { if (currentPage !== 'workspace' || !selectedInputDir) return; const controller = new AbortController(); const timer = window.setTimeout(async () => { setIsPreviewLoading(true); try { const response = await fetch(`${API_BASE}/api/preview`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ inputDir: selectedInputDir, angleDegrees: cervicalRotation }), signal: controller.signal }); const data = await response.json(); if (!response.ok) throw new Error(data.error || '预览生成失败'); setPreviewImage(data.image); setBackendOnline(true); setBackendMessage('预览已自动更新'); } catch (error) { if ((error as Error).name !== 'AbortError') { setBackendOnline(false); setBackendMessage((error as Error).message); } } finally { if (!controller.signal.aborted) setIsPreviewLoading(false); } }, 300); return () => { controller.abort(); window.clearTimeout(timer); }; }, [currentPage, selectedInputDir, cervicalRotation]); const handleRunSimulation = async () => { if (isSimulating) return; setIsSimulating(true); setProgress(0); try { const job = await apiRequest('/api/deformation', { method: 'POST', body: JSON.stringify({ username: currentUser?.username || 'anonymous', inputDir: selectedInputDir, angleDegrees: cervicalRotation, transitionWidth }) }) as BackendJob; applyDeformationJob(job); setBackendOnline(true); setBackendMessage('run_deformation 任务已提交'); showToast('形变任务已提交'); } catch (error) { setIsSimulating(false); setBackendOnline(false); showToast((error as Error).message); } }; const handleGenerateVideo = async () => { if (videoJob?.status === 'running') return; try { const job = await apiRequest('/api/video', { method: 'POST', body: JSON.stringify({ inputDir: selectedInputDir, maxAngle: videoMaxAngle, durationSeconds: videoDuration }) }) as BackendJob; setVideoJob(job); setBackendOnline(true); setBackendMessage('generate_head_extension_video.py 任务已提交'); showToast('视频任务已提交'); } catch (error) { setBackendOnline(false); showToast((error as Error).message); } }; // --- Login Screen --- if (!isLoggedIn) { return (

颅颈特定体位CT影像智慧变形平台

头部CT变形平台

setLoginUsername(e.target.value)} className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all" />
setLoginPassword(e.target.value)} className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all" />
{loginError &&

{loginError}

}
); } // --- Sidebar Component --- const NavItem = ({ icon: Icon, label, page }: { icon: any, label: string, page: Page }) => ( ); return (
{/* Sidebar */} {/* Main Content */}
{isSidebarCollapsed && ( )}

{currentPage === 'overview' && '控制台总览'} {currentPage === 'library' && '临床数据存档'} {currentPage === 'workspace' && '影像变换工作站'} {currentPage === 'users' && '系统访问权限管理'}

算网协同系统 :: 稳定
{/* --- WORKSPACE VIEW --- */} {currentPage === 'workspace' && (
{backendOnline ? : } {backendMessage}
当前 DICOM 数据集
仰头角度{cervicalRotation.toFixed(1)}°
setCervicalRotation(parseFloat(e.target.value))} className="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600 opacity-80 hover:opacity-100 transition-opacity" />
过渡平滑宽度{transitionWidth}
setTransitionWidth(parseInt(e.target.value, 10))} className="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600 opacity-80 hover:opacity-100 transition-opacity" />
{deformationJob?.message || 'head_extension_app.py / run_deformation'} {deformationJob?.status === 'completed' ? '100%' : `${progress}%`}
{deformationJob?.error &&

{deformationJob.error}

} {deformationJob?.status === 'completed' && deformationJob.result?.outputs && ( )} {zipJobs.all?.status === 'failed' &&

{zipJobs.all.error || '四状态 ZIP 打包失败'}

}
最大角度{videoMaxAngle}°
setVideoMaxAngle(parseInt(e.target.value, 10))} className="w-full accent-blue-600" />
时长{videoDuration}s
setVideoDuration(parseInt(e.target.value, 10))} className="w-full accent-blue-600" />
{videoJob?.status === 'completed' && videoJob.result?.video?.path ? ( ) : (

{videoJob?.message || 'generate_head_extension_video.py / generate_video'}

)} {videoJob?.error &&

{videoJob.error}

}

快速 2D 预览

对应 head_extension_app.py 的 preview_deform_2d

{cervicalRotation.toFixed(1)} DEG {isPreviewLoading &&

自动更新中...

}
{previewImage ? ( ) : (

{isPreviewLoading ? '正在自动生成预览...' : '选择影像库数据后自动生成预览'}

)}
{[ { key: 'original', label: '原始序列', sub: 'ct_original' }, { key: 'hard_boundary', label: '硬边界', sub: 'ct_hard_boundary' }, { key: 'gaussian_smooth', label: '高斯平滑', sub: 'ct_gaussian_smooth' }, { key: 'soft_transition', label: '软过渡重建', sub: 'ct_soft_transition' } ].map(t => { const screenshotDir = deformationJob?.result?.previews?.screenshots; const imagePath = screenshotDir ? `${screenshotDir}/${t.key}.png` : ''; const zipJob = zipJobs[t.key]; return (
{t.label} {t.sub}
{imagePath ? ( ) : (

等待 Python 输出

)}
{deformationJob?.status === 'completed' && deformationJob.result?.outputs?.[t.key] && ( )} {zipJob?.status === 'failed' &&

{zipJob.error || '打包失败'}

}
); })}
{deformationJob?.result?.previews?.comparison && (

四状态过程对比图

{deformationJob.result.previews.comparison}
)}
)} {/* --- OVERVIEW VIEW --- */} {currentPage === 'overview' && (
{[ { l: '已存储影像数', v: libraryData.length, i: Database, c: 'blue' }, { l: '存储容量占用', v: `${(libraryData.reduce((acc, curr) => acc + curr.size, 0) / 1024).toFixed(2)} GB`, i: ImageIcon, c: 'indigo' }, { l: '待处理任务', v: libraryData.filter(i => i.status === 'pending').length, i: Activity, c: 'amber' }, { l: '已完成计算', v: libraryData.filter(i => i.status === 'processed').length, i: Check, c: 'green' } ].map((s, i) => (

{s.l}

{s.v}

))}

近期活跃影像流

{libraryData.slice(0, 4).map(item => (

{item.patientId}

{item.date} · {item.size}MB

{item.status === 'processed' ? 'COMPLETED' : 'QUEUEING'}
))}
)} {/* --- LIBRARY VIEW (Card Grid) --- */} {currentPage === 'library' && (

影像资产引擎

网页影像库中的 DICOM 3D 序列数据集

{libraryData.map(item => (

病例标识码

{item.patientId}

SIZE

{item.size}MB

{item.version}
{item.date}
{item.fileCount || 0} 张
))} {/* Empty Upload Slot */}

新增影像数据

支持 DICOM 文件夹和 ZIP 压缩包

)} {/* --- USERS VIEW --- */} {currentPage === 'users' && (
{/* My Profile */}
{currentUser?.username[0].toUpperCase()}

{currentUser?.username}

{currentUser?.role} ACCESS
{isChangingOwnPass && (

密钥重置通道

setPwChangeInput(e.target.value)} className="flex-1 px-5 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm" />
)} {/* User List Header */}

系统权限生命周期管理

{currentUser?.role === 'admin' && ( )}
{/* Add User Inline Form */} {showAddUser && currentUser?.role === 'admin' && (

创建新的医疗访问帐户

setNewUsername(e.target.value)} className="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500 font-medium" />
setNewPassword(e.target.value)} className="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-2xl outline-none focus:ring-2 focus:ring-blue-500 font-mono" />
)} {/* Improved User List with Gear Actions */}
{users.map(u => (
{u.username[0].toUpperCase()}

{u.username}

{u.role === 'admin' && }

{u.role} · 接入于 {u.createdAt}

{currentUser?.role === 'admin' || u.id === currentUser?.id ? (
{activeUserMenu === u.id && (

操作选单

{currentUser?.role === 'admin' && u.id !== currentUser.id && ( )}
)}
) : (
无权操作
)}
))}
)}
{(libraryInfo || isLibraryInfoLoading) && (

DICOM 基本信息

{libraryInfo?.patientId || '正在读取影像信息'}

{isLibraryInfoLoading ? (
正在读取 DICOM 头信息...
) : (
{libraryInfo?.groups.map(group => (

{group.title}

{group.items.map(item => (
{item.label} {item.value || '-'}
))}
))}
)}
)} {/* Toast */} {toastMessage && (
{toastMessage}
)}
); } const Play = (props: any) => ( );