2026-05-04-02-38-48 记录前端项目代码基线

This commit is contained in:
2026-05-04 02:44:14 +08:00
parent 3a47363a6c
commit 2017348cf1
20 changed files with 6713 additions and 0 deletions

View File

@@ -0,0 +1,268 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import {
Plus,
Search,
MoreHorizontal,
Eye,
RotateCw,
FileText,
Box,
Image as ImageIcon,
ChevronRight,
Filter,
Trash2,
Edit2,
FolderRoot,
Download
} from 'lucide-react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Stage, Gltf, useGLTF, Environment, PerspectiveCamera } from '@react-three/drei';
import { Project } from '../types';
// Mock 3D Model component
function ModelPreview() {
return (
<mesh>
<boxGeometry args={[1.5, 1.5, 1.5]} />
<meshStandardMaterial color="#3b82f6" metalness={0.5} roughness={0.2} />
</mesh>
);
}
export default function ProjectLibrary({ onReverse }: { onReverse: (projId: string) => void }) {
const [search, setSearch] = useState('');
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [viewMode, setViewMode] = useState<'dicom' | 'model'>('dicom');
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [sliceIndex, setSliceIndex] = useState(42);
const [visibleModules, setVisibleModules] = useState<Record<string, boolean>>({
'颅骨整体': true, '牙弓': true, '左颧骨': true, '右颧骨': true, '下颌骨': true, '蝶骨': true, '筛骨': true
});
const subModules = ['颅骨整体', '牙弓', '左颧骨', '右颧骨', '下颌骨', '蝶骨', '筛骨'];
const toggleModule = (name: string) => {
setVisibleModules(prev => ({ ...prev, [name]: !prev[name] }));
};
const toggleAllModules = () => {
const allVisible = Object.values(visibleModules).every(v => v);
const newState = { ...visibleModules };
subModules.forEach(m => newState[m] = !allVisible);
setVisibleModules(newState);
};
const projects: Project[] = [
{ id: '1', name: '大腿骨折复位三维重建', createTime: '2024-03-20', status: 'completed', dicomCount: 156, hasModel: true },
{ id: '2', name: '牙齿正畸扫描数据', createTime: '2024-03-21', status: 'pending', dicomCount: 42, hasModel: true },
{ id: '3', name: '测试项目_胸腔扫描', createTime: '2024-03-22', status: 'processing', dicomCount: 210, hasModel: false },
{ id: '4', name: '脑膜瘤切除规划', createTime: '2024-03-23', status: 'completed', dicomCount: 320, hasModel: true },
];
return (
<div className="h-full flex gap-6 overflow-hidden">
{/* Project Sidebar - Collapsible */}
<div
className={`${
isSidebarCollapsed ? 'w-12' : 'w-72'
} flex flex-col bg-white rounded-2xl border border-slate-100 shadow-sm transition-all duration-300 relative overflow-hidden shrink-0`}
>
<button
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
className="absolute right-1 top-4 z-10 p-1.5 hover:bg-slate-100 rounded-lg text-slate-400 transition-colors"
>
{isSidebarCollapsed ? <ChevronRight size={18} /> : <ChevronRight className="rotate-180" size={18} />}
</button>
{!isSidebarCollapsed && (
<div className="p-4 flex flex-col h-full overflow-hidden">
<h3 className="font-bold text-slate-800 mb-4 px-1"></h3>
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={14} />
<input
type="text"
placeholder="搜索..."
className="w-full pl-8 pr-4 py-2 bg-slate-50 border-none rounded-lg text-xs focus:ring-1 focus:ring-blue-500 outline-none"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="flex-1 overflow-y-auto space-y-2 pr-1 scrollbar-hide">
{projects.map((proj) => (
<button
key={proj.id}
onClick={() => setSelectedProject(proj)}
className={`w-full p-3 rounded-xl transition-all text-left ${
selectedProject?.id === proj.id ? 'bg-blue-600 text-white shadow-md' : 'hover:bg-slate-50'
}`}
>
<p className={`font-bold text-xs truncate ${selectedProject?.id === proj.id ? 'text-white' : 'text-slate-700'}`}>
{proj.name}
</p>
<p className={`text-[10px] mt-1 ${selectedProject?.id === proj.id ? 'text-blue-100' : 'text-slate-400'}`}>
{proj.createTime}
</p>
</button>
))}
</div>
</div>
)}
{isSidebarCollapsed && (
<div className="flex flex-col items-center py-12 gap-4">
{projects.map(p => (
<div
key={p.id}
onClick={() => setSelectedProject(p)}
className={`w-8 h-8 rounded-lg flex items-center justify-center cursor-pointer transition-all ${
selectedProject?.id === p.id ? 'bg-blue-600 text-white shadow-md' : 'bg-slate-50 text-slate-400'
}`}
>
<FolderRoot size={16} />
</div>
))}
</div>
)}
</div>
{/* Main Content Area */}
<div className="flex-1 flex flex-col gap-6 overflow-hidden">
{selectedProject ? (
<>
<div className="flex items-center justify-between">
<div className="flex bg-slate-100 p-1 rounded-xl">
<button
onClick={() => setViewMode('dicom')}
className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${
viewMode === 'dicom' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
}`}
>
<ImageIcon size={16} /> DICOM
</button>
<button
onClick={() => setViewMode('model')}
className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${
viewMode === 'model' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
}`}
>
<Box size={16} /> 3D
</button>
</div>
<div className="flex gap-4">
<button
onClick={() => onReverse(selectedProject.id)}
className="bg-blue-600 text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center gap-2 hover:bg-blue-700 transition-all shadow-lg"
>
<RotateCw size={18} />
</button>
<button className="bg-slate-800 text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center gap-2 hover:bg-slate-700 transition-all">
<Download size={18} />
</button>
</div>
</div>
<div className="flex-1 bg-white rounded-3xl border border-slate-100 shadow-sm overflow-hidden p-8">
{viewMode === 'dicom' ? (
<div className="h-full flex gap-8">
{/* Left: DICOM Viewer */}
<div className="flex-1 bg-slate-950 rounded-2xl relative border border-slate-800 flex items-center justify-center p-12">
<div className="absolute top-4 left-4 text-white/40 font-mono text-[10px] space-y-1">
<p>PATIENT ID: {selectedProject.id}_XYZ</p>
<p>SCAN DATE: {selectedProject.createTime}</p>
</div>
<div className="relative w-full h-full flex items-center justify-center">
<div className="w-64 h-64 border-2 border-white/5 rounded-full absolute animate-pulse" />
<div className="w-56 h-56 border border-white/10 rounded-full" />
<p className="absolute text-white/20 text-xs font-mono uppercase tracking-widest">DCM RENDER VIEW | #{sliceIndex}</p>
</div>
<div className="absolute bottom-4 left-4 right-4 flex justify-between text-white/30 font-mono text-[10px]">
<span>WW/WL: 400/40</span>
<span>SLICE: {sliceIndex}/128</span>
</div>
</div>
{/* Right: Vertical Progress Bar */}
<div className="w-16 h-full flex flex-col items-center py-2 bg-slate-50 rounded-2xl">
<span className="text-[10px] text-slate-400 font-bold mb-4">NAV</span>
<input
type="range" min="0" max="128" value={sliceIndex}
onChange={(e) => setSliceIndex(Number(e.target.value))}
className="flex-1 w-1.5 appearance-none bg-slate-200 rounded-full focus:outline-none accent-blue-600 cursor-pointer"
style={{ writingMode: 'bt-lr' as any }}
/>
<span className="text-[10px] text-blue-600 font-bold mt-4">#{sliceIndex}</span>
</div>
</div>
) : (
<div className="h-full flex gap-8">
{/* Left: 3D Visualization */}
<div className="flex-1 bg-slate-50 rounded-2xl relative border border-slate-100 overflow-hidden">
<Canvas>
<Stage environment="city" intensity={0.5}>
<ModelPreview />
</Stage>
<OrbitControls />
</Canvas>
<div className="absolute bottom-4 left-4 text-slate-400 font-mono text-[10px]">
POLYGONS: 2.1M | VERTS: 1.2M
</div>
</div>
{/* Right: Sub-module List */}
<div className="w-64 h-full flex flex-col overflow-hidden">
<div className="px-1 flex items-center justify-between mb-3 shrink-0">
<p className="text-xs font-bold text-slate-700 uppercase tracking-widest"> ({subModules.length})</p>
<button
onClick={toggleAllModules}
className={`p-1 rounded hover:bg-slate-100 transition-colors ${Object.values(visibleModules).every(v => v) ? 'text-blue-500' : 'text-slate-400'}`}
title={Object.values(visibleModules).every(v => v) ? "全隐藏" : "全显示"}
>
<Eye size={16} />
</button>
</div>
<div className="flex-1 overflow-y-auto space-y-2 pr-1 scrollbar-hide">
{subModules.map((m, i) => (
<div
key={m}
className={`p-3 rounded-xl border flex items-center gap-3 group transition-all ${
i === 0 ? 'bg-blue-50 border-blue-100' : 'bg-slate-50 border-transparent hover:border-slate-200'
} ${!visibleModules[m] ? 'opacity-50' : ''}`}
>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${i === 0 ? 'bg-blue-600 text-white' : 'bg-white text-slate-400'}`}>
<Box size={14} />
</div>
<div className="flex-1 min-w-0">
<p className="text-[11px] font-bold text-slate-700 truncate">{m}</p>
<p className="text-[9px] text-slate-400">STL | {i * 4 + 2} MB</p>
</div>
<div className="flex items-center gap-2">
<div className={`w-1.5 h-1.5 rounded-full ${i === 4 ? 'bg-amber-400' : 'bg-emerald-500'}`} />
<button
onClick={(e) => {
e.stopPropagation();
toggleModule(m);
}}
className={`p-1 rounded hover:bg-white transition-colors ${visibleModules[m] ? 'text-blue-500 underline decoration-2' : 'text-slate-300'}`}
>
<Eye size={14} />
</button>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
</>
) : (
<div className="flex-1 bg-white rounded-3xl border border-dashed border-slate-200 flex flex-col items-center justify-center text-slate-400">
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mb-6">
<FolderRoot size={40} />
</div>
<p className="font-bold"></p>
</div>
)}
</div>
</div>
);
}