2026-05-24-15-55-48 增加项目锁定与切片控件修正

This commit is contained in:
2026-05-24 16:15:52 +08:00
parent e9f0823281
commit 3bedf204c8
14 changed files with 586 additions and 62 deletions

View File

@@ -9,6 +9,7 @@ import {
ChevronDown,
ChevronUp,
Eye,
Lock,
Maximize2,
RefreshCcw,
Save,
@@ -2283,8 +2284,7 @@ export function VoxelizationMappingView({
const stepSlice = (delta: number) => {
onSliceChange(clamp(safeSlice + delta, 0, maxSlice));
};
const sliderSliceValue = maxSlice - safeSlice;
const slicePercent = maxSlice > 0 ? (sliderSliceValue / maxSlice) * 100 : 0;
const sliderSliceValue = safeSlice;
const displaySliceNumber = getDicomDisplaySliceNumber(safeSlice, Math.max(totalSlices, 1));
const resetMappingViewport = () => {
setMappingViewport({ scale: 1, offsetX: 0, offsetY: 0 });
@@ -2423,16 +2423,12 @@ export function VoxelizationMappingView({
<aside className="flex min-h-0 flex-col items-center gap-3 border-l border-white/10 bg-[#0f172a] px-2 py-5">
<div className="relative min-h-[220px] w-8 flex-1">
<div className="absolute inset-y-0 left-1/2 w-1.5 -translate-x-1/2 rounded-full bg-white/10" />
<div
className="absolute bottom-0 left-1/2 w-1.5 -translate-x-1/2 rounded-full bg-cyan-400"
style={{ height: `${slicePercent}%` }}
/>
<input
type="range"
min="0"
max={maxSlice}
value={sliderSliceValue}
onChange={(event) => onSliceChange(maxSlice - Number(event.target.value))}
onChange={(event) => onSliceChange(Number(event.target.value))}
className="mapping-slice-dark-vertical-input"
aria-label="项目库逆向分割映射视图切片导航"
/>
@@ -2533,8 +2529,8 @@ export function VoxelizationMappingView({
</span>
</div>
<button
onClick={() => stepSlice(-1)}
disabled={safeSlice <= 0}
onClick={() => stepSlice(1)}
disabled={safeSlice >= maxSlice}
className="flex h-8 w-8 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 shadow-sm hover:border-blue-300 hover:bg-blue-50 hover:text-blue-600 disabled:opacity-35"
title="上一层"
>
@@ -2542,32 +2538,28 @@ export function VoxelizationMappingView({
</button>
<div className="relative min-h-[240px] w-10 flex-1">
<div className="absolute inset-y-0 left-1/2 w-2 -translate-x-1/2 rounded-full bg-slate-200" />
<div
className="absolute bottom-0 left-1/2 w-2 -translate-x-1/2 rounded-full bg-blue-600"
style={{ height: `${slicePercent}%` }}
/>
<input
type="range"
min="0"
max={maxSlice}
value={sliderSliceValue}
onChange={(event) => onSliceChange(maxSlice - Number(event.target.value))}
onChange={(event) => onSliceChange(Number(event.target.value))}
className="mapping-slice-vertical-input"
aria-label="逆向分割映射视图切片导航"
/>
</div>
<button
onClick={() => stepSlice(1)}
disabled={safeSlice >= maxSlice}
onClick={() => stepSlice(-1)}
disabled={safeSlice <= 0}
className="flex h-8 w-8 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 shadow-sm hover:border-blue-300 hover:bg-blue-50 hover:text-blue-600 disabled:opacity-35"
title="下一层"
>
<ChevronDown size={16} />
</button>
<div className="grid w-full grid-cols-1 gap-1 text-center text-[9px] font-bold text-slate-500">
<span> {Math.max(totalSlices, 1)}</span>
<span> 1</span>
<span className="text-blue-600"> {displaySliceNumber}</span>
<span> 1</span>
<span> {Math.max(totalSlices, 1)}</span>
</div>
</aside>
</div>
@@ -2759,6 +2751,9 @@ export default function ReverseWorkspace({
if (!project) {
return true;
}
if (project.locked) {
return true;
}
if (savedWorkspaceSnapshotRef.current === getCurrentWorkspaceSnapshot()) {
return true;
}
@@ -2918,6 +2913,19 @@ export default function ReverseWorkspace({
});
api.getProject(projectId).then((item) => {
setProject(item);
if (item.locked) {
setFusionVolume(null);
setWorkspaceLoadState({
ready: false,
phase: '项目已锁定',
loaded: 1,
total: 1,
startedAt: Date.now(),
error: '项目已锁定,请在项目库解锁后再进入逆向工作区。',
});
savedWorkspaceSnapshotRef.current = '';
return;
}
const maxIndex = Math.max((item.dicomCount || 1) - 1, 0);
const latestResult = item.segmentationResults?.[item.segmentationResults.length - 1];
const restoredSliceStart = clamp(latestResult?.sliceStart ?? 0, 0, maxIndex);
@@ -3167,6 +3175,21 @@ export default function ReverseWorkspace({
restoreVisualToolbarScroll(scrollTop);
};
const allModulesVisible = Boolean(project?.stlFiles?.length) && (project?.stlFiles ?? []).every((fileName) => moduleStyles[fileName]?.visible !== false);
const toggleAllModules = () => {
const stlFiles = project?.stlFiles ?? [];
const nextVisible = !allModulesVisible;
const next = { ...moduleStyles };
stlFiles.forEach((fileName, index) => {
next[fileName] = makeDefaultModuleStyle(index, {
...(next[fileName] ?? project?.moduleStyles?.[fileName]),
visible: nextVisible,
});
});
commitModuleStyles(next);
};
const updateModuleStyle = (fileName: string, partial: Partial<ModuleStyle>) => {
const stlFiles = project?.stlFiles ?? [];
const index = Math.max(0, stlFiles.indexOf(fileName));
@@ -3280,7 +3303,7 @@ export default function ReverseWorkspace({
const workspaceLoadSpeed = workspaceLoadState.loaded / workspaceElapsedSeconds;
useEffect(() => {
if (!project?.dicomCount) {
if (!project?.dicomCount || project.locked) {
return undefined;
}
if (workspaceLoadProjectRef.current === project.id) {
@@ -3371,6 +3394,7 @@ export default function ReverseWorkspace({
}, [
project?.id,
project?.dicomCount,
project?.locked,
project?.stlFiles?.join('|'),
displayStart,
displayEnd,
@@ -3398,6 +3422,27 @@ export default function ReverseWorkspace({
modelPose.rotateZ,
]);
if (project?.locked) {
return (
<div className="flex h-full min-h-0 items-center justify-center overflow-hidden pr-2">
<div className="w-full max-w-2xl rounded-3xl border border-amber-100 bg-white p-8 text-center shadow-sm">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-amber-50 text-amber-600">
<Lock size={24} />
</div>
<h2 className="mt-5 text-2xl font-black text-slate-900"></h2>
<p className="mx-auto mt-3 max-w-lg text-sm font-semibold leading-6 text-slate-500">
姿
</p>
<div className="mt-6 rounded-2xl bg-slate-50 px-4 py-3 text-left text-xs font-bold text-slate-500">
<p>{project.name}</p>
<p className="mt-1">{project.lockedAt ? new Date(project.lockedAt).toLocaleString('zh-CN') : '未记录'}</p>
<p className="mt-1">{project.lockedPoseSnapshotPath ?? '项目数据/锁定结果'}</p>
</div>
</div>
</div>
);
}
if (!workspaceLoadState.ready) {
return (
<div className="flex h-full min-h-0 items-center justify-center overflow-hidden pr-2">
@@ -3943,7 +3988,17 @@ export default function ReverseWorkspace({
<div>
<div className="mb-2 flex items-center justify-between">
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400"></p>
<span className="text-[10px] font-mono text-slate-400">{project?.stlFiles?.length ?? 0}</span>
<div className="flex items-center gap-2">
<span className="text-[10px] font-mono text-slate-400">{project?.stlFiles?.length ?? 0}</span>
<button
onClick={toggleAllModules}
disabled={!project?.stlFiles?.length}
className={`rounded p-1 transition ${allModulesVisible ? 'text-blue-500' : 'text-slate-300'} hover:bg-white disabled:opacity-40`}
title={allModulesVisible ? '隐藏所有构件' : '显示所有构件'}
>
<Eye size={14} />
</button>
</div>
</div>
<div className="space-y-2">
{(project?.stlFiles ?? []).map((fileName, index) => {