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,263 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'motion/react';
import {
Dices,
Settings2,
Maximize2,
Download,
Layers,
Move,
Rotate3d,
CheckCircle2,
AlertCircle,
FileJson,
Plus,
Play
} from 'lucide-react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Stage, PerspectiveCamera, Grid } from '@react-three/drei';
import { MaskMapping } from '../types';
function InteractiveModel({ offset }: { offset: [number, number, number] }) {
return (
<mesh position={offset}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#3b82f6" transparent opacity={0.6} />
<mesh position={[0, 0, 0]}>
<boxGeometry args={[2.05, 2.05, 2.05]} />
<meshBasicMaterial color="#ffffff" wireframe />
</mesh>
</mesh>
);
}
export default function ReverseWorkspace() {
const [slice, setSlice] = useState(50);
const [isRegistering, setIsRegistering] = useState(false);
const [progress, setProgress] = useState(0);
const [offset, setOffset] = useState<[number, number, number]>([0, 0, 0]);
const [mappings, setMappings] = useState<MaskMapping[]>([
{ className: '骨样组织', color: '#ff4d4f', maskId: 1 },
{ className: '神经根', color: '#52c41a', maskId: 2 },
{ className: '血管', color: '#1890ff', maskId: 3 },
]);
const handleStartRegistration = () => {
setIsRegistering(true);
setProgress(0);
};
useEffect(() => {
if (isRegistering && progress < 100) {
const timer = setTimeout(() => setProgress(p => p + 2), 50);
return () => clearTimeout(timer);
} else if (progress >= 100) {
setIsRegistering(false);
}
}, [isRegistering, progress]);
return (
<div className="h-full flex flex-col gap-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-slate-800"></h2>
<p className="text-slate-500 mt-1"> DICOM </p>
</div>
<div className="flex gap-2">
<button
onClick={handleStartRegistration}
disabled={isRegistering}
className="bg-indigo-600 text-white px-5 py-2.5 rounded-xl text-sm font-semibold hover:bg-indigo-700 transition-all shadow-lg flex items-center gap-2 disabled:opacity-50"
>
{isRegistering ? (
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : <Dices size={18} />}
{isRegistering ? `正在自动配准 (${progress}%)` : '开始自动配准'}
</button>
<button className="bg-emerald-600 text-white px-5 py-2.5 rounded-xl text-sm font-semibold hover:bg-emerald-700 transition-all shadow-lg flex items-center gap-2">
<Download size={18} />
</button>
</div>
</div>
<div className="flex-1 grid grid-cols-1 lg:grid-cols-12 gap-6 overflow-hidden">
{/* Left Column: Image Fusion (4/12) */}
<div className="lg:col-span-4 flex flex-col gap-4 overflow-hidden">
<div className="px-2 flex items-center justify-between shrink-0">
<h3 className="font-bold text-slate-700 flex items-center gap-2">
<Rotate3d size={18} className="text-blue-500" />
</h3>
<span className="text-[10px] font-mono text-slate-400">Layer: {slice}</span>
</div>
<div className="flex-1 bg-black rounded-3xl overflow-hidden relative border border-slate-800 shadow-xl group">
<div className="absolute inset-0 z-0 opacity-40">
<div className="w-full h-full flex items-center justify-center p-12">
<div className="w-full h-full border-2 border-white/5 rounded-full flex items-center justify-center anonymous-dicom-grid" />
</div>
</div>
<div className="absolute inset-0 z-10">
<Canvas>
<PerspectiveCamera makeDefault position={[3, 3, 3]} />
<Stage environment="city" intensity={0.5}>
<InteractiveModel offset={offset} />
</Stage>
<OrbitControls />
</Canvas>
</div>
<div className="absolute bottom-4 left-4 z-20 pointer-events-none">
<div className="pointer-events-auto bg-black/60 backdrop-blur-md border border-white/10 p-3 rounded-xl w-48 space-y-3">
<div className="flex items-center justify-between">
<span className="text-[9px] font-bold text-white uppercase opacity-60"></span>
<Settings2 size={10} className="text-blue-400" />
</div>
<input
type="range" min="-5" max="5" step="0.1"
value={offset[0]}
onChange={(e) => setOffset([Number(e.target.value), offset[1], offset[2]])}
className="w-full h-1 bg-white/20 rounded-lg appearance-none accent-blue-500"
/>
</div>
</div>
</div>
</div>
{/* Middle Column: Mask Selection (3/12) */}
<div className="lg:col-span-3 flex flex-col gap-4 overflow-hidden">
<div className="px-2 shrink-0">
<h3 className="font-bold text-slate-700 flex items-center gap-2">
<Layers size={18} className="text-emerald-500" />
Mask
</h3>
</div>
<div className="flex-1 bg-white rounded-3xl border border-slate-100 shadow-sm overflow-hidden flex flex-col p-4 gap-4">
<div className="flex-1 overflow-auto space-y-2 pr-1">
{mappings.map((m, i) => (
<button
key={i}
className={`w-full flex flex-col gap-2 p-3 rounded-xl border transition-all text-left group ${
i === 0 ? 'bg-blue-50 border-blue-200' : 'bg-slate-50 border-transparent hover:border-slate-200'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: m.color }} />
<span className="text-xs font-bold text-slate-700">{m.className}</span>
</div>
{i === 0 && <CheckCircle2 size={14} className="text-blue-500" />}
</div>
<div className="flex items-center justify-between text-[10px] text-slate-500 font-mono">
<span>ID: {m.maskId}</span>
<span className="font-bold text-emerald-600">Conf: 98%</span>
</div>
</button>
))}
<button className="w-full py-3 border-2 border-dashed border-slate-100 rounded-xl text-slate-400 flex items-center justify-center hover:bg-slate-50 transition-all">
<Plus size={18} />
</button>
</div>
<div className="p-3 bg-slate-900 rounded-2xl shrink-0">
<div className="flex items-center gap-2 mb-2">
<FileJson size={12} className="text-blue-400" />
<span className="text-[10px] font-bold text-white/50 uppercase tracking-widest">Metadata</span>
</div>
<pre className="text-[9px] text-blue-300/70 font-mono overflow-hidden">
{`{ id: "SM_091", voxel: 124K }`}
</pre>
</div>
</div>
</div>
{/* Right Column: Mask Image Display (5/12) */}
<div className="lg:col-span-5 flex flex-col gap-4 overflow-hidden">
<div className="px-2 flex items-center justify-between shrink-0">
<h3 className="font-bold text-slate-700 flex items-center gap-2">
<Play size={18} className="text-blue-500" />
Mask
</h3>
<div className="flex gap-2">
<button className="bg-slate-100 hover:bg-slate-200 text-slate-700 px-3 py-1 rounded-lg text-[10px] font-bold transition-all border border-slate-200 flex items-center gap-1">
<Download size={12} />
NII ()
</button>
<button className="bg-slate-900 hover:bg-black text-white px-3 py-1 rounded-lg text-[10px] font-bold transition-all flex items-center gap-1 shadow-lg">
<Download size={12} />
NII.GZ ()
</button>
</div>
</div>
<div className="flex-1 bg-slate-900 rounded-3xl border border-slate-800 shadow-2xl relative overflow-hidden flex items-center justify-center">
{/* The actual Mask result visualization */}
<div className="relative w-72 h-72">
{/* Base DICOM context (faint) */}
<div className="absolute inset-0 opacity-10 blur-xl bg-white rounded-full translate-x-4 translate-y-4" />
{/* Mask Layers */}
{mappings.map((m, i) => (
<motion.div
key={i}
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 0.8 }}
transition={{ delay: i * 0.2 }}
className="absolute inset-0 border-2"
style={{
borderColor: m.color,
borderRadius: i === 0 ? '30% 70% 70% 30% / 30% 30% 70% 70%' : '60% 40% 30% 70% / 60% 30% 70% 40%',
background: `${m.color}20`,
boxShadow: `inset 0 0 20px ${m.color}40`,
transform: `rotate(${i * 45 + slice}deg) scale(${1 - i * 0.1})`
}}
/>
))}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="w-full h-0.5 bg-blue-500/20 absolute" />
<div className="h-full w-0.5 bg-blue-500/20 absolute" />
</div>
</div>
<div className="absolute top-4 left-4 z-20 flex gap-2">
<span className="px-2 py-1 bg-blue-600/20 border border-blue-500/30 text-blue-400 text-[9px] font-bold rounded uppercase">Inferred Mask</span>
<span className="px-2 py-1 bg-emerald-600/20 border border-emerald-500/30 text-emerald-400 text-[9px] font-bold rounded uppercase">Verified</span>
</div>
<div className="absolute bottom-4 right-4">
<button className="p-2 bg-white/5 hover:bg-white/10 text-white/50 rounded-lg backdrop-blur-sm transition-all">
<Maximize2 size={16} />
</button>
</div>
{/* Legend Overlay */}
<div className="absolute top-4 right-4 flex flex-col gap-1 items-end">
{mappings.map((m, i) => (
<div key={i} className="flex items-center gap-2">
<span className="text-[9px] text-white/40 font-mono italic">#{m.maskId}</span>
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: m.color }} />
</div>
))}
</div>
</div>
<div className="h-16 shrink-0 bg-white rounded-2xl border border-slate-100 shadow-sm flex items-center justify-between px-6">
<div className="flex flex-col">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest"></span>
<span className="text-xs font-bold text-slate-700"> {mappings.length} </span>
</div>
<div className="w-32 bg-slate-100 h-1.5 rounded-full overflow-hidden">
<div className="bg-blue-600 h-full w-[100%]" />
</div>
</div>
</div>
</div>
</div>
);
}