2026-05-04-03-21-40 增加前后端协同和NIfTI导出
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
Plus,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls, Stage, Gltf, useGLTF, Environment, PerspectiveCamera } from '@react-three/drei';
|
||||
import { Project } from '../types';
|
||||
import { api } from '../lib/api';
|
||||
|
||||
// Mock 3D Model component
|
||||
function ModelPreview() {
|
||||
@@ -32,15 +33,42 @@ function ModelPreview() {
|
||||
|
||||
export default function ProjectLibrary({ onReverse }: { onReverse: (projId: string) => void }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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 [visibleModules, setVisibleModules] = useState<Record<string, boolean>>({});
|
||||
|
||||
const subModules = ['颅骨整体', '牙弓', '左颧骨', '右颧骨', '下颌骨', '蝶骨', '筛骨'];
|
||||
useEffect(() => {
|
||||
api.getProjects()
|
||||
.then((items) => {
|
||||
setProjects(items);
|
||||
setSelectedProject(items[0] ?? null);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const filteredProjects = useMemo(() => {
|
||||
const keyword = search.trim().toLowerCase();
|
||||
if (!keyword) {
|
||||
return projects;
|
||||
}
|
||||
return projects.filter((project) => project.name.toLowerCase().includes(keyword));
|
||||
}, [projects, search]);
|
||||
|
||||
const subModules = selectedProject?.stlFiles?.length
|
||||
? selectedProject.stlFiles.map((file) => file.replace(/\.stl$/i, ''))
|
||||
: [];
|
||||
|
||||
useEffect(() => {
|
||||
const next: Record<string, boolean> = {};
|
||||
subModules.forEach((module) => {
|
||||
next[module] = visibleModules[module] ?? true;
|
||||
});
|
||||
setVisibleModules(next);
|
||||
}, [selectedProject?.id]);
|
||||
|
||||
const toggleModule = (name: string) => {
|
||||
setVisibleModules(prev => ({ ...prev, [name]: !prev[name] }));
|
||||
@@ -53,13 +81,6 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
||||
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 */}
|
||||
@@ -89,7 +110,8 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto space-y-2 pr-1 scrollbar-hide">
|
||||
{projects.map((proj) => (
|
||||
{loading && <p className="text-xs text-slate-400 px-2">正在从后端载入项目...</p>}
|
||||
{filteredProjects.map((proj) => (
|
||||
<button
|
||||
key={proj.id}
|
||||
onClick={() => setSelectedProject(proj)}
|
||||
@@ -101,7 +123,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
||||
{proj.name}
|
||||
</p>
|
||||
<p className={`text-[10px] mt-1 ${selectedProject?.id === proj.id ? 'text-blue-100' : 'text-slate-400'}`}>
|
||||
{proj.createTime}
|
||||
{proj.createTime} · DICOM {proj.dicomCount} · STL {proj.modelCount ?? 0}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
@@ -111,7 +133,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
||||
|
||||
{isSidebarCollapsed && (
|
||||
<div className="flex flex-col items-center py-12 gap-4">
|
||||
{projects.map(p => (
|
||||
{filteredProjects.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
onClick={() => setSelectedProject(p)}
|
||||
@@ -170,6 +192,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
||||
<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>
|
||||
<p>DICOM PATH: {selectedProject.dicomPath}</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" />
|
||||
@@ -178,14 +201,14 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
||||
</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>
|
||||
<span>SLICE: {sliceIndex}/{selectedProject.dicomCount}</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}
|
||||
type="range" min="0" max={Math.max(selectedProject.dicomCount, 1)} 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 }}
|
||||
@@ -204,7 +227,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
<div className="absolute bottom-4 left-4 text-slate-400 font-mono text-[10px]">
|
||||
POLYGONS: 2.1M | VERTS: 1.2M
|
||||
MODEL PATH: {selectedProject.modelPath} | STL: {selectedProject.modelCount ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
{/* Right: Sub-module List */}
|
||||
@@ -232,7 +255,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
||||
</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>
|
||||
<p className="text-[9px] text-slate-400">STL | {selectedProject.stlFiles?.[i]}</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'}`} />
|
||||
|
||||
Reference in New Issue
Block a user