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

@@ -16,7 +16,8 @@ import {
} from 'lucide-react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Stage, PerspectiveCamera, Grid } from '@react-three/drei';
import { MaskMapping } from '../types';
import { MaskMapping, Project } from '../types';
import { api, downloadMask } from '../lib/api';
function InteractiveModel({ offset }: { offset: [number, number, number] }) {
return (
@@ -31,11 +32,14 @@ function InteractiveModel({ offset }: { offset: [number, number, number] }) {
);
}
export default function ReverseWorkspace() {
export default function ReverseWorkspace({ projectId }: { projectId: string }) {
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 [project, setProject] = useState<Project | null>(null);
const [exporting, setExporting] = useState(false);
const [exportMessage, setExportMessage] = useState('准备就绪');
const [mappings, setMappings] = useState<MaskMapping[]>([
{ className: '骨样组织', color: '#ff4d4f', maskId: 1 },
@@ -48,6 +52,23 @@ export default function ReverseWorkspace() {
setProgress(0);
};
const handleExport = async (format: 'nii' | 'nii.gz') => {
setExporting(true);
setExportMessage(`正在生成 ${format.toUpperCase()} 分割 Mask...`);
try {
await downloadMask(projectId, format);
setExportMessage(`${format.toUpperCase()} 分割 Mask 已生成并开始下载`);
} catch (err) {
setExportMessage(err instanceof Error ? err.message : '导出失败');
} finally {
setExporting(false);
}
};
useEffect(() => {
api.getProject(projectId).then(setProject).catch(() => setProject(null));
}, [projectId]);
useEffect(() => {
if (isRegistering && progress < 100) {
const timer = setTimeout(() => setProgress(p => p + 2), 50);
@@ -62,7 +83,9 @@ export default function ReverseWorkspace() {
<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>
<p className="text-slate-500 mt-1">
{project ? `${project.name} · ${project.dicomPath}${project.modelPath}` : '配准 DICOM 影像与三维模型,生成像素映射关系'}
</p>
</div>
<div className="flex gap-2">
<button
@@ -75,9 +98,13 @@ export default function ReverseWorkspace() {
) : <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">
<button
onClick={() => handleExport('nii.gz')}
disabled={exporting}
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 disabled:opacity-50"
>
<Download size={18} />
{exporting ? '正在导出' : '导出 NII.GZ'}
</button>
</div>
</div>
@@ -170,7 +197,7 @@ export default function ReverseWorkspace() {
<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 }`}
{`{ project: "${project?.id ?? projectId}", format: "nii.gz" }`}
</pre>
</div>
</div>
@@ -184,11 +211,19 @@ export default function ReverseWorkspace() {
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">
<button
onClick={() => handleExport('nii')}
disabled={exporting}
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 disabled:opacity-50"
>
<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">
<button
onClick={() => handleExport('nii.gz')}
disabled={exporting}
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 disabled:opacity-50"
>
<Download size={12} />
NII.GZ ()
</button>
@@ -250,7 +285,7 @@ export default function ReverseWorkspace() {
<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>
<span className="text-xs font-bold text-slate-700">{exportMessage} {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%]" />