2026-05-04-03-21-40 增加前后端协同和NIfTI导出

This commit is contained in:
2026-05-04 03:29:54 +08:00
parent a6f3836460
commit a9b6d2d76a
15 changed files with 1040 additions and 67 deletions

View File

@@ -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'}`} />