2026-05-20-14-53-31 逆向结果复核与用户管理修复

This commit is contained in:
2026-05-20 15:08:20 +08:00
parent 2a599695e9
commit fd7f3387f7
12 changed files with 886 additions and 111 deletions

View File

@@ -9,6 +9,7 @@ import {
Box,
Image as ImageIcon,
Info,
ChevronLeft,
ChevronRight,
ChevronUp,
ChevronDown,
@@ -653,6 +654,10 @@ export default function ProjectLibrary({
const [isSliceChanging, setIsSliceChanging] = useState(false);
const [solidityLevel, setSolidityLevel] = useState<SolidityLevel>('standard');
const [modelPose, setModelPose] = useState<ModelPose>(defaultModelPose);
const [resultPose, setResultPose] = useState<ModelPose>(defaultModelPose);
const [resultPreviewSlice, setResultPreviewSlice] = useState(0);
const [resultDisplayMode, setResultDisplayMode] = useState<DisplayMode>('soft');
const [resultRotation, setResultRotation] = useState(0);
const [moduleStyles, setModuleStyles] = useState<Record<string, ModuleStyle>>({});
const [dicomPreview, setDicomPreview] = useState<DicomPreview | null>(null);
const [resultDicomPreview, setResultDicomPreview] = useState<DicomPreview | null>(null);
@@ -747,10 +752,11 @@ export default function ProjectLibrary({
const selectedSolidity = solidityOptions.find((option) => option.id === solidityLevel) ?? solidityOptions[0];
const savedSegmentationResults = selectedProject?.segmentationResults ?? [];
const latestSegmentationResult = savedSegmentationResults[savedSegmentationResults.length - 1];
const latestResultPose = latestSegmentationResult?.pose ?? modelPose;
const latestResultPose = latestSegmentationResult ? resultPose : modelPose;
const latestResultStyles = latestSegmentationResult?.moduleStyles ?? moduleStyles;
const resultMaxSlice = Math.max((selectedProject?.dicomCount ?? 1) - 1, 0);
const resultMappingSlice = Math.max(0, Math.min(resultMaxSlice, latestSegmentationResult?.mappingSlice ?? resultMaxSlice));
const resultMappingSlice = Math.max(0, Math.min(resultMaxSlice, resultPreviewSlice));
const resultFineDetailLimit = solidityOptions.find((option) => option.id === 'fine')?.limit ?? 36000;
const resultVisibleModules = stlFiles
.map((fileName, index) => ({
fileName,
@@ -763,7 +769,6 @@ export default function ProjectLibrary({
},
}))
.filter(({ style }) => style.visible !== false);
const readonlyPoseChange = useMemo<React.Dispatch<React.SetStateAction<ModelPose>>>(() => () => undefined, []);
const makeDefaultModuleStyle = (index: number, fallback?: Partial<ModuleStyle>): ModuleStyle => ({
visible: fallback?.visible ?? true,
@@ -828,6 +833,10 @@ export default function ProjectLibrary({
setModuleStyles(next);
setSliceIndex(0);
setModelPose(latestResult?.pose ?? defaultModelPose);
setResultPose(latestResult?.pose ?? defaultModelPose);
setResultPreviewSlice(Math.max(0, Math.min(Math.max((selectedProject?.dicomCount ?? 1) - 1, 0), latestResult?.mappingSlice ?? 0)));
setResultDisplayMode('soft');
setResultRotation(0);
}, [selectedProject?.id]);
useEffect(() => {
@@ -870,8 +879,8 @@ export default function ProjectLibrary({
let cancelled = false;
const maxSlice = Math.max(selectedProject.dicomCount - 1, 0);
const previewSlice = Math.max(0, Math.min(maxSlice, latestSegmentationResult?.mappingSlice ?? maxSlice));
api.getDicomPreview(selectedProject.id, previewSlice, 'axial', 'soft')
const previewSlice = Math.max(0, Math.min(maxSlice, resultPreviewSlice));
api.getDicomPreview(selectedProject.id, previewSlice, 'axial', resultDisplayMode)
.then((preview) => {
if (!cancelled) {
setResultDicomPreview(preview);
@@ -886,7 +895,7 @@ export default function ProjectLibrary({
return () => {
cancelled = true;
};
}, [selectedProject?.id, selectedProject?.dicomCount, viewMode, latestSegmentationResult?.id, latestSegmentationResult?.mappingSlice]);
}, [selectedProject?.id, selectedProject?.dicomCount, viewMode, latestSegmentationResult?.id, resultPreviewSlice, resultDisplayMode]);
useEffect(() => () => {
if (sliceRepeatRef.current !== null) {
@@ -1551,7 +1560,7 @@ export default function ProjectLibrary({
<div className="absolute left-4 top-4 z-10 rounded-xl border border-white/10 bg-black/45 px-3 py-2 backdrop-blur">
<p className="text-sm font-bold"></p>
<p className="mt-1 font-mono text-[10px] text-white/45">
{latestSegmentationResult ? `Z ${resultMappingSlice + 1}/${selectedProject.dicomCount}` : '等待保存结果'}
{latestSegmentationResult ? `模型显示 精细 · 融合显示 DICOM 高 · Z ${resultMappingSlice + 1}/${selectedProject.dicomCount}` : '等待保存结果'}
</p>
</div>
{latestSegmentationResult ? (
@@ -1559,10 +1568,10 @@ export default function ProjectLibrary({
projectId={selectedProject.id}
files={stlFiles}
styles={latestResultStyles}
detailLimit={selectedSolidity.limit}
solidMode={solidityLevel === 'solid'}
detailLimit={resultFineDetailLimit}
solidMode={false}
pose={latestResultPose}
onPoseChange={readonlyPoseChange}
onPoseChange={setResultPose}
/>
) : (
<div className="flex h-full items-center justify-center px-8 text-center text-sm font-bold text-white/35">
@@ -1571,22 +1580,50 @@ export default function ProjectLibrary({
)}
</div>
<div className="relative min-h-[520px] overflow-hidden rounded-2xl border border-slate-900 bg-black text-white shadow-sm">
<div className="absolute left-4 top-4 z-10 flex flex-wrap gap-2">
<span className="rounded-lg border border-white/10 bg-black/50 px-2.5 py-1 font-mono text-[10px] font-bold text-white/70 backdrop-blur">
BASE DICOM
</span>
<span className="rounded-lg border border-cyan-300/40 bg-cyan-400/15 px-2.5 py-1 font-mono text-[10px] font-bold text-cyan-100 backdrop-blur">
OVERLAY LABEL MAP
</span>
<span className="rounded-lg border border-white/10 bg-black/50 px-2.5 py-1 font-mono text-[10px] font-bold text-white/70 backdrop-blur">
Z {resultMappingSlice + 1}/{selectedProject.dicomCount}
</span>
</div>
<div className="absolute inset-0 flex items-center justify-center p-8">
<div className="relative flex min-h-[520px] flex-col overflow-hidden rounded-2xl border border-slate-900 bg-black text-white shadow-sm">
<div className="relative min-h-0 flex-1">
<div className="absolute left-4 top-4 z-10 flex flex-wrap gap-2">
<span className="rounded-lg border border-white/10 bg-black/50 px-2.5 py-1 font-mono text-[10px] font-bold text-white/70 backdrop-blur">
BASE DICOM
</span>
<span className="rounded-lg border border-cyan-300/40 bg-cyan-400/15 px-2.5 py-1 font-mono text-[10px] font-bold text-cyan-100 backdrop-blur">
OVERLAY LABEL MAP
</span>
<span className="rounded-lg border border-white/10 bg-black/50 px-2.5 py-1 font-mono text-[10px] font-bold text-white/70 backdrop-blur">
Z {resultMappingSlice + 1}/{selectedProject.dicomCount}
</span>
</div>
<div className="absolute right-4 top-4 z-10 flex flex-wrap justify-end gap-1.5">
<div className="flex rounded-lg border border-white/10 bg-black/50 p-1 backdrop-blur">
{displayModes.map((mode) => (
<button
key={mode.id}
onClick={() => setResultDisplayMode(mode.id)}
className={`rounded-md px-2 py-1 text-[9px] font-bold transition ${resultDisplayMode === mode.id ? 'bg-cyan-400 text-slate-950' : 'text-white/55 hover:text-white'}`}
>
{mode.label}
</button>
))}
</div>
<button
onClick={() => setResultRotation((value) => (value + 270) % 360)}
className="rounded-lg border border-white/10 bg-black/50 px-2 py-1 text-[9px] font-bold text-white/70 backdrop-blur hover:text-white"
title="左转 90°"
>
<RotateCcw size={12} />
</button>
<button
onClick={() => setResultRotation((value) => (value + 90) % 360)}
className="rounded-lg border border-white/10 bg-black/50 px-2 py-1 text-[9px] font-bold text-white/70 backdrop-blur hover:text-white"
title="右转 90°"
>
<RotateCw size={12} />
</button>
</div>
<div className="absolute inset-0 flex items-center justify-center p-8">
{latestSegmentationResult && resultDicomPreview ? (
<div className="relative flex h-full w-full items-center justify-center">
<DicomCanvas preview={resultDicomPreview} rotation={0} />
<DicomCanvas preview={resultDicomPreview} rotation={resultRotation} />
<div className="pointer-events-none absolute inset-x-10 bottom-10 rounded-2xl border border-white/10 bg-black/55 p-3 backdrop-blur">
<div className="mb-2 flex items-center justify-between gap-3 text-[11px] font-bold text-white/70">
<span></span>
@@ -1608,6 +1645,43 @@ export default function ProjectLibrary({
{latestSegmentationResult ? '正在载入逆向分割映射视图...' : '暂无逆向分割映射视图'}
</p>
)}
</div>
</div>
<div className="border-t border-white/10 bg-slate-950 px-4 py-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Slice Navigator</p>
<span className="font-mono text-[10px] font-bold text-cyan-100">
{resultMappingSlice + 1} / {Math.max(selectedProject.dicomCount, 1)}
</span>
</div>
<div className="grid grid-cols-[28px_1fr_28px] items-center gap-3">
<button
onClick={() => setResultPreviewSlice((value) => Math.max(0, value - 1))}
disabled={!latestSegmentationResult || resultMappingSlice <= 0}
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-700 bg-slate-900 text-slate-200 hover:border-cyan-400 hover:text-cyan-100 disabled:opacity-35"
title="上一层"
>
<ChevronLeft size={15} />
</button>
<input
type="range"
min="0"
max={resultMaxSlice}
value={resultMappingSlice}
disabled={!latestSegmentationResult}
onChange={(event) => setResultPreviewSlice(Number(event.target.value))}
className="h-2 w-full accent-cyan-400 disabled:opacity-35"
aria-label="项目库逆向分割结果切片导航"
/>
<button
onClick={() => setResultPreviewSlice((value) => Math.min(resultMaxSlice, value + 1))}
disabled={!latestSegmentationResult || resultMappingSlice >= resultMaxSlice}
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-700 bg-slate-900 text-slate-200 hover:border-cyan-400 hover:text-cyan-100 disabled:opacity-35"
title="下一层"
>
<ChevronRight size={15} />
</button>
</div>
</div>
</div>
</div>