2026-05-04-03-21-40 增加前后端协同和NIfTI导出
This commit is contained in:
@@ -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%]" />
|
||||
|
||||
Reference in New Issue
Block a user