import React, { useEffect, useMemo, useRef, useState } from 'react';
import { 
  Plus, 
  Search, 
  Eye, 
  FileArchive,
  RotateCw, 
  RotateCcw,
  Box,
  Image as ImageIcon,
  Info,
  ChevronRight,
  ChevronUp,
  ChevronDown,
  Edit2,
  FolderRoot,
  Download,
  Layers,
  X,
  Trash2,
  Upload,
  RefreshCcw,
  FlipHorizontal2,
  FlipVertical2,
  Move3d,
  Lock,
  Unlock
} from 'lucide-react';
import * as THREE from 'three';
import { DicomFusionVolume, DicomInfo, DicomPreview, ModuleStyle, Project, SegmentationExportScope } from '../types';
import { api, downloadDicomArchive, downloadProjectExportBundle, ProjectAssetImportKind, ProjectAssetImportProgress, ProjectExportTarget, SegmentationExportMode } from '../lib/api';
import {
  FusionThreeView,
  OverlayStats,
  VoxelizationMappingView,
  clearCachedProjectAssets,
  getCachedDicomFusionVolume,
  getCachedDicomPreview,
  getCachedModelPreview,
  dicomOpacityOptions as reverseDicomOpacityOptions,
  displayOptions as reverseDisplayOptions,
} from './ReverseWorkspace';

type Plane = 'axial' | 'sagittal' | 'coronal';
type DisplayMode = DicomPreview['mode'];
type SolidityLevel = 'standard' | 'fine' | 'ultra' | 'solid';

interface ModelPose {
  rotateX: number;
  rotateY: number;
  rotateZ: number;
  translateX: number;
  translateY: number;
  translateZ: number;
  scale: number;
  flipX: boolean;
  flipY: boolean;
  flipZ: boolean;
}

interface ModelPreviewPayload {
  fileName: string;
  triangleCount: number;
  sampledTriangles: number;
  vertices: number[];
  bounds?: {
    min: { x: number; y: number; z: number };
    max: { x: number; y: number; z: number };
  };
}

type ModelPoseKey = Exclude<keyof ModelPose, 'flipX' | 'flipY' | 'flipZ'>;
type ModelPoseFlipKey = Extract<keyof ModelPose, 'flipX' | 'flipY' | 'flipZ'>;

const defaultModuleColors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6', '#f97316', '#64748b', '#ec4899'];
const exportOptions: Array<{ id: ProjectExportTarget; label: string; description: string }> = [
  { id: 'dicom', label: 'DICOM 原始影像', description: '主影像 NII.GZ' },
  { id: 'stl', label: 'STL 原始模型', description: '原始三维构件' },
  { id: 'pose', label: '位姿数据', description: 'JSON 侧车' },
  { id: 'segmentation', label: '分割影像', description: '同维度 Label Map' },
];
const segmentationScopeOptions: Array<{ id: SegmentationExportScope; label: string; description: string }> = [
  { id: 'visible', label: '可见类别', description: '仅导出当前显示构件' },
  { id: 'all', label: '所有类别', description: '包含隐藏构件' },
];
const segmentationExportModeOptions: Array<{ id: SegmentationExportMode; label: string; description: string }> = [
  { id: 'combined', label: '构件整体导出', description: '生成一个多标签 Label Map' },
  { id: 'separate', label: '构件分别导出', description: '全部构件集中到同一目录' },
];
const solidityOptions: Array<{ id: SolidityLevel; label: string; limit: number }> = [
  { id: 'standard', label: '标准', limit: 16000 },
  { id: 'fine', label: '精细', limit: 36000 },
  { id: 'ultra', label: '超精细', limit: 72000 },
  { id: 'solid', label: '实体', limit: 200000 },
];
const defaultModelPose: ModelPose = {
  rotateX: 0,
  rotateY: 0,
  rotateZ: 0,
  translateX: 0,
  translateY: 0,
  translateZ: 0,
  scale: 1,
  flipX: false,
  flipY: false,
  flipZ: false,
};
const modelPoseFlipOptions: Array<{ key: ModelPoseFlipKey; label: string; axis: string; icon: typeof FlipHorizontal2 }> = [
  { key: 'flipX', label: '镜像 X', axis: 'X', icon: FlipHorizontal2 },
  { key: 'flipY', label: '镜像 Y', axis: 'Y', icon: FlipVertical2 },
  { key: 'flipZ', label: '镜像 Z', axis: 'Z', icon: Move3d },
];
const emptyOverlayStats: OverlayStats = {
  activeModules: 0,
  filledPixels: 0,
  segmentCount: 0,
  modules: [],
};
const modelPoseLimits: Record<ModelPoseKey, { min: number; max: number }> = {
  rotateX: { min: -180, max: 180 },
  rotateY: { min: -180, max: 180 },
  rotateZ: { min: -180, max: 180 },
  translateX: { min: -2, max: 2 },
  translateY: { min: -2, max: 2 },
  translateZ: { min: -2, max: 2 },
  scale: { min: 0.5, max: 2.5 },
};
const modelPoseStepPrecision: Partial<Record<ModelPoseKey, number>> = {
  scale: 3,
};

function clampModelPoseValue(key: ModelPoseKey, value: number) {
  const limit = modelPoseLimits[key];
  const clampedValue = Math.max(limit.min, Math.min(limit.max, value));
  const precision = modelPoseStepPrecision[key];
  return typeof precision === 'number' ? Number(clampedValue.toFixed(precision)) : clampedValue;
}

function getControlStepPrecision(step: number) {
  if (step >= 1) {
    return 0;
  }

  const text = step.toString();
  if (text.includes('e-')) {
    return Number(text.split('e-')[1] ?? 2);
  }

  return text.split('.')[1]?.length ?? 0;
}

function clampModelPose(next: ModelPose): ModelPose {
  return {
    rotateX: clampModelPoseValue('rotateX', next.rotateX),
    rotateY: clampModelPoseValue('rotateY', next.rotateY),
    rotateZ: clampModelPoseValue('rotateZ', next.rotateZ),
    translateX: clampModelPoseValue('translateX', next.translateX),
    translateY: clampModelPoseValue('translateY', next.translateY),
    translateZ: clampModelPoseValue('translateZ', next.translateZ),
    scale: clampModelPoseValue('scale', next.scale),
    flipX: Boolean(next.flipX),
    flipY: Boolean(next.flipY),
    flipZ: Boolean(next.flipZ),
  };
}

function normalizeModelPose(pose: Partial<ModelPose> | undefined): ModelPose {
  return clampModelPose({
    ...defaultModelPose,
    ...(pose ?? {}),
    flipX: typeof pose?.flipX === 'boolean' ? pose.flipX : defaultModelPose.flipX,
    flipY: typeof pose?.flipY === 'boolean' ? pose.flipY : defaultModelPose.flipY,
    flipZ: typeof pose?.flipZ === 'boolean' ? pose.flipZ : defaultModelPose.flipZ,
  });
}

function formatPoseCompactValue(value: number, digits = 2) {
  return Number.isFinite(value) ? Number(value).toFixed(digits).replace(/\.?0+$/, '') : '0';
}

function getProjectActivityTime(project: Project) {
  const latestResult = project.segmentationResults?.[project.segmentationResults.length - 1];
  const candidates = [
    project.lastProcessedAt,
    project.lockedAt,
    project.unlockedAt,
    latestResult?.createdAt,
    project.createTime,
  ];
  return Math.max(...candidates.map((value) => (value ? Date.parse(value) : 0)).filter((value) => Number.isFinite(value)), 0);
}

function sortProjectsByActivity(projects: Project[]) {
  return [...projects].sort((a, b) => getProjectActivityTime(b) - getProjectActivityTime(a));
}

function formatProjectActivity(project: Project) {
  const time = getProjectActivityTime(project);
  if (!time) {
    return project.createTime;
  }
  return new Intl.DateTimeFormat('zh-CN', {
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  }).format(new Date(time));
}

interface AssetImportProgressState {
  kind: ProjectAssetImportKind;
  fileCount: number;
  totalBytes: number;
  loadedBytes: number;
  percent: number;
  phase: 'uploading' | 'processing' | 'done' | 'failed';
  message?: string;
}

function formatFileSize(value: number) {
  if (!Number.isFinite(value) || value <= 0) {
    return '0 B';
  }
  const units = ['B', 'KB', 'MB', 'GB'];
  const index = Math.min(units.length - 1, Math.floor(Math.log(value) / Math.log(1024)));
  return `${(value / (1024 ** index)).toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
}

function describeImportKind(kind: ProjectAssetImportKind) {
  return kind === 'dicom' ? 'DICOM 影像' : '3D 模型';
}

function drawFallbackModelPreview(
  canvas: HTMLCanvasElement,
  previews: Array<{ payload: ModelPreviewPayload; style: ModuleStyle }>,
) {
  const rect = canvas.getBoundingClientRect();
  const parentRect = canvas.parentElement?.getBoundingClientRect();
  const width = Math.max(Math.floor(rect.width || parentRect?.width || 720), 1);
  const height = Math.max(Math.floor(rect.height || parentRect?.height || 460), 1);
  canvas.width = width * window.devicePixelRatio;
  canvas.height = height * window.devicePixelRatio;
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;

  const context = canvas.getContext('2d');
  if (!context) return;
  context.scale(window.devicePixelRatio, window.devicePixelRatio);
  context.fillStyle = '#f8fafc';
  context.fillRect(0, 0, width, height);

  const allPoints = previews.flatMap(({ payload }) => {
    const points: Array<[number, number]> = [];
    for (let index = 0; index < payload.vertices.length; index += 3) {
      points.push([payload.vertices[index], payload.vertices[index + 1]]);
    }
    return points;
  });

  if (!allPoints.length) return;

  const xs = allPoints.map((point) => point[0]);
  const ys = allPoints.map((point) => point[1]);
  const minX = Math.min(...xs);
  const maxX = Math.max(...xs);
  const minY = Math.min(...ys);
  const maxY = Math.max(...ys);
  const spanX = Math.max(maxX - minX, 1);
  const spanY = Math.max(maxY - minY, 1);
  const scale = Math.min((width * 0.78) / spanX, (height * 0.78) / spanY);
  const offsetX = width / 2 - ((minX + maxX) / 2) * scale;
  const offsetY = height / 2 + ((minY + maxY) / 2) * scale;

  previews.forEach(({ payload, style }) => {
    context.globalAlpha = Math.max(0.12, Math.min(style.opacity, 1));
    context.fillStyle = style.color;
    context.strokeStyle = style.color;
    for (let index = 0; index < payload.vertices.length; index += 9) {
      const x1 = payload.vertices[index] * scale + offsetX;
      const y1 = -payload.vertices[index + 1] * scale + offsetY;
      const x2 = payload.vertices[index + 3] * scale + offsetX;
      const y2 = -payload.vertices[index + 4] * scale + offsetY;
      const x3 = payload.vertices[index + 6] * scale + offsetX;
      const y3 = -payload.vertices[index + 7] * scale + offsetY;
      context.beginPath();
      context.moveTo(x1, y1);
      context.lineTo(x2, y2);
      context.lineTo(x3, y3);
      context.closePath();
      context.fill();
      context.stroke();
    }
  });
  context.globalAlpha = 1;
}

function drawDicomPreviewToCanvas(canvas: HTMLCanvasElement, preview: DicomPreview, rotation: number) {
  const normalizedRotation = ((rotation % 360) + 360) % 360;
  const sourceCanvas = document.createElement('canvas');
  sourceCanvas.width = preview.width;
  sourceCanvas.height = preview.height;
  const sourceContext = sourceCanvas.getContext('2d');
  const targetContext = canvas.getContext('2d');
  if (!sourceContext || !targetContext) {
    return;
  }

  const binary = atob(preview.pixels);
  const imageData = sourceContext.createImageData(preview.width, preview.height);
  for (let i = 0; i < binary.length; i += 1) {
    const value = binary.charCodeAt(i);
    const offset = i * 4;
    imageData.data[offset] = value;
    imageData.data[offset + 1] = value;
    imageData.data[offset + 2] = value;
    imageData.data[offset + 3] = 255;
  }
  sourceContext.putImageData(imageData, 0, 0);

  const isQuarterTurn = normalizedRotation === 90 || normalizedRotation === 270;
  canvas.width = isQuarterTurn ? preview.height : preview.width;
  canvas.height = isQuarterTurn ? preview.width : preview.height;
  targetContext.clearRect(0, 0, canvas.width, canvas.height);
  targetContext.save();
  targetContext.imageSmoothingEnabled = true;

  if (normalizedRotation === 90) {
    targetContext.translate(canvas.width, 0);
    targetContext.rotate(Math.PI / 2);
  } else if (normalizedRotation === 180) {
    targetContext.translate(canvas.width, canvas.height);
    targetContext.rotate(Math.PI);
  } else if (normalizedRotation === 270) {
    targetContext.translate(0, canvas.height);
    targetContext.rotate(-Math.PI / 2);
  }

  targetContext.drawImage(sourceCanvas, 0, 0);
  targetContext.restore();
}

function safeFilePart(value: string) {
  return value.trim().replace(/[^\u4e00-\u9fa5a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'dicom';
}

function displayDicomValue(value: string | number | null | undefined) {
  if (value === null || value === undefined || value === '') {
    return '未知';
  }
  return String(value);
}

function getDicomDisplaySliceNumber(sliceIndex: number, totalSlices: number) {
  const total = Math.max(Math.round(totalSlices), 0);
  if (!total) {
    return 0;
  }
  return total - Math.max(0, Math.min(total - 1, Math.round(sliceIndex)));
}

function DicomCanvas({ preview, rotation, resetSignal }: { preview: DicomPreview; rotation: number; resetSignal: number }) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const [viewport, setViewport] = useState({ scale: 1, offsetX: 0, offsetY: 0 });
  const [isPanning, setIsPanning] = useState(false);
  const panRef = useRef({
    active: false,
    pointerId: 0,
    startX: 0,
    startY: 0,
    offsetX: 0,
    offsetY: 0,
  });

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }
    drawDicomPreviewToCanvas(canvas, preview, rotation);
  }, [preview, rotation]);

  useEffect(() => {
    setViewport({ scale: 1, offsetX: 0, offsetY: 0 });
  }, [resetSignal]);

  const handleWheel = (event: React.WheelEvent<HTMLDivElement>) => {
    event.preventDefault();
    const scaleFactor = event.deltaY > 0 ? 0.9 : 1.1;
    setViewport((current) => ({
      ...current,
      scale: Math.max(0.35, Math.min(6, current.scale * scaleFactor)),
    }));
  };
  const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
    if (event.button !== 0) {
      return;
    }
    panRef.current = {
      active: true,
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      offsetX: viewport.offsetX,
      offsetY: viewport.offsetY,
    };
    setIsPanning(true);
    event.currentTarget.setPointerCapture(event.pointerId);
  };
  const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
    const dragState = panRef.current;
    if (!dragState.active || dragState.pointerId !== event.pointerId) {
      return;
    }
    setViewport((current) => ({
      ...current,
      offsetX: dragState.offsetX + event.clientX - dragState.startX,
      offsetY: dragState.offsetY + event.clientY - dragState.startY,
    }));
  };
  const stopPointerDrag = (event: React.PointerEvent<HTMLDivElement>) => {
    const dragState = panRef.current;
    if (!dragState.active || dragState.pointerId !== event.pointerId) {
      return;
    }
    panRef.current = { ...dragState, active: false };
    setIsPanning(false);
    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }
  };

  return (
    <div
      className={`relative flex h-full w-full items-center justify-center overflow-hidden rounded-xl bg-black ${isPanning ? 'cursor-grabbing' : 'cursor-grab'}`}
      onWheel={handleWheel}
      onPointerDown={handlePointerDown}
      onPointerMove={handlePointerMove}
      onPointerUp={stopPointerDrag}
      onPointerCancel={stopPointerDrag}
    >
      <div
        className="flex max-h-full max-w-full items-center justify-center"
        style={{
          transform: `translate3d(${viewport.offsetX}px, ${viewport.offsetY}px, 0) scale(${viewport.scale})`,
          transformOrigin: 'center center',
        }}
      >
        <canvas
          ref={canvasRef}
          className="max-h-full max-w-full select-none object-contain rounded-xl bg-black shadow-2xl ring-1 ring-white/25"
        />
      </div>
    </div>
  );
}

function OrientationGizmo({ pose }: { pose: ModelPose }) {
  const axes = useMemo(() => {
    const rotation = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(
      THREE.MathUtils.degToRad(pose.rotateX),
      THREE.MathUtils.degToRad(pose.rotateY),
      THREE.MathUtils.degToRad(pose.rotateZ),
      'XYZ',
    ));
    return [
      { id: 'X', color: '#ef4444', vector: new THREE.Vector3(pose.flipX ? -1 : 1, 0, 0).applyMatrix4(rotation) },
      { id: 'Y', color: '#10b981', vector: new THREE.Vector3(0, pose.flipY ? -1 : 1, 0).applyMatrix4(rotation) },
      { id: 'Z', color: '#3b82f6', vector: new THREE.Vector3(0, 0, pose.flipZ ? -1 : 1).applyMatrix4(rotation) },
    ]
      .map((axis) => ({
        ...axis,
        endX: 38 + axis.vector.x * 24 + axis.vector.z * 10,
        endY: 38 - axis.vector.y * 24 + axis.vector.z * 8,
        labelX: 38 + axis.vector.x * 30 + axis.vector.z * 12,
        labelY: 38 - axis.vector.y * 30 + axis.vector.z * 10,
        opacity: 0.55 + Math.max(-axis.vector.z, 0) * 0.45,
      }))
      .sort((a, b) => b.vector.z - a.vector.z);
  }, [pose.rotateX, pose.rotateY, pose.rotateZ, pose.flipX, pose.flipY, pose.flipZ]);

  return (
    <div className="pointer-events-none absolute bottom-4 right-4 rounded-xl border border-slate-200 bg-white/90 px-3 py-2 shadow-sm">
      <svg width="76" height="70" viewBox="0 0 76 70" className="mb-1 block">
        <circle cx="38" cy="38" r="2.5" fill="#0f172a" opacity="0.28" />
        {axes.map((axis) => (
          <g key={axis.id} opacity={axis.opacity}>
            <line x1="38" y1="38" x2={axis.endX} y2={axis.endY} stroke={axis.color} strokeWidth="2.2" strokeLinecap="round" />
            <circle cx={axis.endX} cy={axis.endY} r="2.4" fill={axis.color} />
            <text x={axis.labelX} y={axis.labelY} fill={axis.color} fontSize="10" fontWeight="900" textAnchor="middle" dominantBaseline="middle">
              {axis.id}
            </text>
          </g>
        ))}
      </svg>
      <div className="space-y-0.5 font-mono text-[9px] text-slate-500">
        <div>X {Math.round(pose.rotateX)}°</div>
        <div>Y {Math.round(pose.rotateY)}°</div>
        <div>Z {Math.round(pose.rotateZ)}°</div>
      </div>
    </div>
  );
}

function NativeStlViewer({
  projectId,
  files,
  styles,
  detailLimit,
  solidMode,
  pose,
  onPoseChange,
}: {
  projectId: string;
  files: string[];
  styles: Record<string, ModuleStyle>;
  detailLimit: number;
  solidMode: boolean;
  pose: ModelPose;
  onPoseChange: React.Dispatch<React.SetStateAction<ModelPose>>;
}) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const poseRef = useRef<ModelPose>(pose);
  const onPoseChangeRef = useRef(onPoseChange);
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState('准备加载模型');

  useEffect(() => {
    poseRef.current = pose;
  }, [pose]);

  useEffect(() => {
    onPoseChangeRef.current = onPoseChange;
  }, [onPoseChange]);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const dragState = {
      active: false,
      mode: 'rotate' as 'rotate' | 'pan',
      pointerId: 0,
      startX: 0,
      startY: 0,
      startPose: poseRef.current,
    };

    const handlePointerDown = (event: PointerEvent) => {
      dragState.active = true;
      dragState.mode = event.button === 2 || event.shiftKey ? 'pan' : 'rotate';
      dragState.pointerId = event.pointerId;
      dragState.startX = event.clientX;
      dragState.startY = event.clientY;
      dragState.startPose = poseRef.current;
      container.setPointerCapture(event.pointerId);
    };
    const handlePointerMove = (event: PointerEvent) => {
      if (!dragState.active || event.pointerId !== dragState.pointerId) return;
      const deltaX = event.clientX - dragState.startX;
      const deltaY = event.clientY - dragState.startY;
      if (dragState.mode === 'pan') {
        onPoseChangeRef.current(clampModelPose({
          ...dragState.startPose,
          translateX: dragState.startPose.translateX + deltaX * 0.006,
          translateY: dragState.startPose.translateY - deltaY * 0.006,
        }));
        return;
      }
      onPoseChangeRef.current(clampModelPose({
        ...dragState.startPose,
        rotateY: dragState.startPose.rotateY + deltaX * 0.35,
        rotateX: dragState.startPose.rotateX + deltaY * 0.35,
      }));
    };
    const stopPointerDrag = (event: PointerEvent) => {
      if (event.pointerId !== dragState.pointerId) return;
      dragState.active = false;
      if (container.hasPointerCapture(event.pointerId)) {
        container.releasePointerCapture(event.pointerId);
      }
    };
    const handleWheel = (event: WheelEvent) => {
      event.preventDefault();
      onPoseChangeRef.current(clampModelPose({
        ...poseRef.current,
        scale: poseRef.current.scale - event.deltaY * 0.001,
      }));
    };
    const preventContextMenu = (event: MouseEvent) => event.preventDefault();

    container.addEventListener('pointerdown', handlePointerDown);
    container.addEventListener('pointermove', handlePointerMove);
    container.addEventListener('pointerup', stopPointerDrag);
    container.addEventListener('pointercancel', stopPointerDrag);
    container.addEventListener('wheel', handleWheel, { passive: false });
    container.addEventListener('contextmenu', preventContextMenu);

    return () => {
      container.removeEventListener('pointerdown', handlePointerDown);
      container.removeEventListener('pointermove', handlePointerMove);
      container.removeEventListener('pointerup', stopPointerDrag);
      container.removeEventListener('pointercancel', stopPointerDrag);
      container.removeEventListener('wheel', handleWheel);
      container.removeEventListener('contextmenu', preventContextMenu);
    };
  }, []);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const visibleFiles = files.filter((file) => styles[file]?.visible !== false);
    container.innerHTML = '';
    setProgress(visibleFiles.length ? 5 : 0);
    setStatus(visibleFiles.length ? '正在加载 STL 模型...' : '没有可显示的模型');

    if (!visibleFiles.length) {
      return;
    }

    let disposed = false;
    let animationId = 0;
    const scene = new THREE.Scene();
    scene.background = new THREE.Color('#f8fafc');
    const camera = new THREE.PerspectiveCamera(45, Math.max(container.clientWidth, 1) / Math.max(container.clientHeight, 1), 0.1, 1000);
    camera.up.set(0, 1, 0);
    camera.position.set(0, 0, 6);
    camera.lookAt(0, 0, 0);
    let renderer: THREE.WebGLRenderer | null = null;
    try {
      renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    } catch {
      const fallbackCanvas = document.createElement('canvas');
      fallbackCanvas.className = 'absolute inset-0 h-full w-full';
      container.appendChild(fallbackCanvas);
      setStatus('WebGL 不可用，正在生成二维模型预览...');
      let fallbackPreviews: Array<{ payload: ModelPreviewPayload; style: ModuleStyle }> = [];

      Promise.allSettled(
        visibleFiles.map((fileName) =>
          getCachedModelPreview(projectId, fileName, 3500)
            .then((payload) => ({
              payload,
              style: styles[fileName] ?? { color: '#3b82f6', opacity: 0.72, visible: true, partId: 1 },
            })),
        ),
      ).then((results) => {
        if (disposed) return;
        const previews = results
          .filter((result): result is PromiseFulfilledResult<{ payload: ModelPreviewPayload; style: ModuleStyle }> => result.status === 'fulfilled')
          .map((result) => result.value);
        fallbackPreviews = previews;
        drawFallbackModelPreview(fallbackCanvas, previews);
        setProgress(100);
        setStatus(previews.length ? '二维模型预览已生成' : '模型预览加载失败');
      });

      const handleFallbackResize = () => {
        if (fallbackPreviews.length) {
          drawFallbackModelPreview(fallbackCanvas, fallbackPreviews);
        }
      };
      window.addEventListener('resize', handleFallbackResize);

      return () => {
        disposed = true;
        window.removeEventListener('resize', handleFallbackResize);
        container.innerHTML = '';
      };
    }

    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(container.clientWidth, container.clientHeight);
    container.appendChild(renderer.domElement);

    scene.add(new THREE.AmbientLight(0xffffff, 0.72));
    const keyLight = new THREE.DirectionalLight(0xffffff, 1.1);
    keyLight.position.set(4, 5, 6);
    scene.add(keyLight);
    const fillLight = new THREE.DirectionalLight(0x9cc4ff, 0.55);
    fillLight.position.set(-4, 2, -3);
    scene.add(fillLight);

    const poseGroup = new THREE.Group();
    const pivotGroup = new THREE.Group();
    poseGroup.add(pivotGroup);
    let baseScale = 1;
    scene.add(poseGroup);
    let loaded = 0;
    let failed = 0;
    const loadedBounds: Array<{ min: THREE.Vector3; max: THREE.Vector3 }> = [];

    visibleFiles.forEach((fileName) => {
      getCachedModelPreview(projectId, fileName, detailLimit)
        .then((payload) => {
          if (disposed) return;
          const geometry = new THREE.BufferGeometry();
          geometry.setAttribute('position', new THREE.Float32BufferAttribute(payload.vertices, 3));
          geometry.computeVertexNormals();
          const style = styles[fileName] ?? { color: '#3b82f6', opacity: 0.72, visible: true, partId: 1 };
          const materialOpacity = solidMode ? Math.max(style.opacity, 0.94) : style.opacity;
          const mesh = new THREE.Mesh(
            geometry,
            new THREE.MeshStandardMaterial({
              color: style.color,
              opacity: materialOpacity,
              transparent: materialOpacity < 1,
              roughness: solidMode ? 0.56 : 0.42,
              metalness: 0.04,
              side: THREE.DoubleSide,
            }),
          );
          pivotGroup.add(mesh);
          if (payload.bounds) {
            loadedBounds.push({
              min: new THREE.Vector3(payload.bounds.min.x, payload.bounds.min.y, payload.bounds.min.z),
              max: new THREE.Vector3(payload.bounds.max.x, payload.bounds.max.y, payload.bounds.max.z),
            });
          } else {
            geometry.computeBoundingBox();
            const geometryBox = geometry.boundingBox;
            if (geometryBox) {
              loadedBounds.push({
                min: geometryBox.min.clone(),
                max: geometryBox.max.clone(),
              });
            }
          }
          loaded += 1;
          setProgress(Math.round(((loaded + failed) / visibleFiles.length) * 100));
          setStatus(`已加载 ${loaded} / ${visibleFiles.length} 个 STL 预览`);

          if (loaded + failed === visibleFiles.length) {
            const box = new THREE.Box3();
            if (loadedBounds.length) {
              loadedBounds.forEach((bounds) => {
                box.expandByPoint(bounds.min);
                box.expandByPoint(bounds.max);
              });
            } else {
              box.setFromObject(pivotGroup);
            }
            const center = box.getCenter(new THREE.Vector3());
            const size = box.getSize(new THREE.Vector3());
            const maxSize = Math.max(size.x, size.y, size.z) || 1;
            pivotGroup.traverse((object) => {
              if (object instanceof THREE.Mesh) {
                object.geometry.translate(-center.x, -center.y, -center.z);
                object.geometry.computeBoundingBox();
                object.geometry.computeBoundingSphere();
                object.geometry.computeVertexNormals();
              }
            });
            poseGroup.position.set(0, 0, 0);
            pivotGroup.position.set(0, 0, 0);
            baseScale = 4.2 / maxSize;
            const initialPoseScale = baseScale * poseRef.current.scale;
            pivotGroup.scale.set(
              poseRef.current.flipX ? -initialPoseScale : initialPoseScale,
              poseRef.current.flipY ? -initialPoseScale : initialPoseScale,
              poseRef.current.flipZ ? -initialPoseScale : initialPoseScale,
            );
            camera.lookAt(0, 0, 0);
            setStatus(failed ? `完成，${failed} 个模型加载失败` : '模型加载完成');
          }
        })
        .catch(() => {
          if (disposed) return;
          failed += 1;
          setProgress(Math.round(((loaded + failed) / visibleFiles.length) * 100));
          setStatus(`有 ${failed} 个模型加载失败`);
        });
    });

    const handleResize = () => {
      if (!container.clientWidth || !container.clientHeight) return;
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    };
    window.addEventListener('resize', handleResize);

    const animate = () => {
      if (disposed) return;
      const currentPose = poseRef.current;
      poseGroup.position.set(currentPose.translateX, currentPose.translateY, currentPose.translateZ);
      pivotGroup.rotation.set(
        THREE.MathUtils.degToRad(currentPose.rotateX),
        THREE.MathUtils.degToRad(currentPose.rotateY),
        THREE.MathUtils.degToRad(currentPose.rotateZ),
      );
      const poseScale = baseScale * currentPose.scale;
      pivotGroup.scale.set(
        currentPose.flipX ? -poseScale : poseScale,
        currentPose.flipY ? -poseScale : poseScale,
        currentPose.flipZ ? -poseScale : poseScale,
      );
      renderer.render(scene, camera);
      animationId = window.requestAnimationFrame(animate);
    };
    animate();

    return () => {
      disposed = true;
      window.cancelAnimationFrame(animationId);
      window.removeEventListener('resize', handleResize);
      renderer.dispose();
      poseGroup.traverse((object) => {
        if (object instanceof THREE.Mesh) {
          object.geometry.dispose();
          const material = object.material;
          if (Array.isArray(material)) {
            material.forEach((item) => item.dispose());
          } else {
            material.dispose();
          }
        }
      });
      container.innerHTML = '';
    };
  }, [projectId, files.join('|'), JSON.stringify(styles), detailLimit, solidMode]);

  return (
    <div className="h-full w-full relative cursor-grab active:cursor-grabbing">
      <div ref={containerRef} className="absolute inset-0" />
      {progress < 100 && (
        <div className="absolute inset-x-8 top-8 z-10 rounded-xl bg-white/90 p-4 shadow-sm border border-slate-100">
          <div className="flex items-center justify-between text-xs font-bold text-slate-600 mb-2">
            <span>{status}</span>
            <span>{progress}%</span>
          </div>
          <div className="h-2 rounded-full bg-slate-100 overflow-hidden">
            <div className="h-full bg-blue-600 transition-all" style={{ width: `${progress}%` }} />
          </div>
        </div>
      )}
      {progress >= 100 && (
        <div className="absolute left-4 top-4 rounded-lg bg-white/80 px-3 py-1.5 text-[10px] font-bold text-slate-500 shadow-sm">
          {status}
        </div>
      )}
      <OrientationGizmo pose={pose} />
    </div>
  );
}

export default function ProjectLibrary({
  onReverse,
  initialViewMode = 'dicom',
}: {
  onReverse: (projId: string) => void;
  initialViewMode?: 'dicom' | 'model' | 'mask';
}) {
  const [search, setSearch] = useState('');
  const [projects, setProjects] = useState<Project[]>([]);
  const [loading, setLoading] = useState(true);
  const [selectedProject, setSelectedProject] = useState<Project | null>(null);
  const [showUnlockedOnly, setShowUnlockedOnly] = useState(false);
  const [viewMode, setViewMode] = useState<'dicom' | 'model' | 'mask'>(initialViewMode);
  const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
  const [sliceIndex, setSliceIndex] = useState(0);
  const [plane, setPlane] = useState<Plane>('axial');
  const [displayMode, setDisplayMode] = useState<DisplayMode>('default');
  const [rotation, setRotation] = useState(0);
  const [dicomViewportResetSignal, setDicomViewportResetSignal] = useState(0);
  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 [resultFusionVolume, setResultFusionVolume] = useState<DicomFusionVolume | null>(null);
  const [resultFusionError, setResultFusionError] = useState('');
  const [resultOverlayStats, setResultOverlayStats] = useState<OverlayStats>(emptyOverlayStats);
  const [resultVisibleModuleCount, setResultVisibleModuleCount] = useState(0);
  const [dicomInfo, setDicomInfo] = useState<DicomInfo | null>(null);
  const [dicomInfoError, setDicomInfoError] = useState('');
  const [isDicomInfoOpen, setIsDicomInfoOpen] = useState(false);
  const [dicomError, setDicomError] = useState('');
  const [newProjectName, setNewProjectName] = useState('');
  const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
  const [projectToDelete, setProjectToDelete] = useState<Project | null>(null);
  const [editingProjectId, setEditingProjectId] = useState('');
  const [editingName, setEditingName] = useState('');
  const [actionMessage, setActionMessage] = useState('');
  const [showMaskExportMenu, setShowMaskExportMenu] = useState(false);
  const [maskExportSelection, setMaskExportSelection] = useState<Record<ProjectExportTarget, boolean>>({
    dicom: false,
    segmentation: true,
    pose: true,
    stl: false,
  });
  const [maskSegmentationScope, setMaskSegmentationScope] = useState<SegmentationExportScope>('visible');
  const [maskSegmentationExportMode, setMaskSegmentationExportMode] = useState<SegmentationExportMode>('combined');
  const [maskExporting, setMaskExporting] = useState(false);
  const [assetImporting, setAssetImporting] = useState(false);
  const [lockChangingProjectId, setLockChangingProjectId] = useState('');
  const [assetImportProgress, setAssetImportProgress] = useState<AssetImportProgressState | null>(null);
  const importInputRef = useRef<HTMLInputElement | null>(null);
  const importKindRef = useRef<ProjectAssetImportKind>('dicom');
  const sliceRepeatRef = useRef<number | null>(null);
  const dicomRequestRef = useRef(0);
  const preloadedProjectIdsRef = useRef(new Set<string>());

  const preloadProjectAssets = (project: Project) => {
    if (preloadedProjectIdsRef.current.has(project.id)) {
      return;
    }
    preloadedProjectIdsRef.current.add(project.id);
    const maxSlice = Math.max((project.dicomCount || 1) - 1, 0);
    if (project.dicomCount > 0) {
      void getCachedDicomPreview(project.id, maxSlice, 'axial', 'default').catch(() => undefined);
      void getCachedDicomFusionVolume(project.id, maxSlice, maxSlice, 'soft').catch(() => undefined);
    }
    (project.stlFiles ?? []).slice(0, 3).forEach((fileName) => {
      void getCachedModelPreview(project.id, fileName, 3500).catch(() => undefined);
    });
  };

  const refreshProjects = () => {
    setLoading(true);
    return api.getProjects()
      .then((items) => {
        const orderedItems = sortProjectsByActivity(items);
        setProjects(orderedItems);
        orderedItems.slice(0, 2).forEach(preloadProjectAssets);
        setSelectedProject((current) => {
          if (!current) {
            return orderedItems[0] ?? null;
          }
          return orderedItems.find((item) => item.id === current.id) ?? orderedItems[0] ?? null;
        });
      })
      .finally(() => setLoading(false));
  };

  useEffect(() => {
    refreshProjects();
  }, []);

  useEffect(() => {
    if (selectedProject) {
      preloadProjectAssets(selectedProject);
      const latestResult = selectedProject.segmentationResults?.[selectedProject.segmentationResults.length - 1];
      setMaskSegmentationScope(latestResult?.segmentationScope ?? 'visible');
    }
  }, [selectedProject?.id]);

  const filteredProjects = useMemo(() => {
    const keyword = search.trim().toLowerCase();
    return sortProjectsByActivity(projects).filter((project) => {
      const matchesKeyword = !keyword || project.name.toLowerCase().includes(keyword);
      const matchesLockFilter = !showUnlockedOnly || project.locked !== true;
      return matchesKeyword && matchesLockFilter;
    });
  }, [projects, search, showUnlockedOnly]);

  const stlFiles = selectedProject?.stlFiles ?? [];
  const planeOptions: Array<{ id: Plane; label: string }> = [
    { id: 'axial', label: '横断面' },
    { id: 'sagittal', label: '矢状面' },
    { id: 'coronal', label: '冠状面' },
  ];
  const displayModes: Array<{ id: DisplayMode; label: string }> = [
    { id: 'default', label: '默认' },
    { id: 'bone', label: '骨窗' },
    { id: 'soft', label: '软组织' },
    { id: 'contrast', label: '高对比' },
  ];
  const allModulesVisible = stlFiles.length > 0 && stlFiles.every((file) => moduleStyles[file]?.visible !== false);
  const sliceTotal = dicomPreview?.total ?? selectedProject?.dicomCount ?? 0;
  const dicomSliceTotal = sliceTotal || selectedProject?.dicomCount || 0;
  const dicomMaxSlice = Math.max(dicomSliceTotal - 1, 0);
  const safeDicomSlice = Math.max(0, Math.min(dicomMaxSlice, sliceIndex));
  const dicomDisplaySlice = getDicomDisplaySliceNumber(safeDicomSlice, dicomSliceTotal);
  const dicomSliderValue = safeDicomSlice;
  const selectedSolidity = solidityOptions.find((option) => option.id === solidityLevel) ?? solidityOptions[0];
  const savedSegmentationResults = selectedProject?.segmentationResults ?? [];
  const latestSegmentationResult = savedSegmentationResults[savedSegmentationResults.length - 1];
  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, resultPreviewSlice));
  const resultDisplayOption = reverseDisplayOptions.find((option) => option.id === 'fine') ?? reverseDisplayOptions[0];
  const resultDicomOpacity = reverseDicomOpacityOptions.find((option) => option.id === 'high') ?? reverseDicomOpacityOptions[reverseDicomOpacityOptions.length - 1];
  const resultCutStart = Math.max(0, Math.min(resultMaxSlice, latestSegmentationResult?.sliceStart ?? 0));
  const resultCutEnd = Math.max(0, Math.min(resultMaxSlice, latestSegmentationResult?.sliceEnd ?? resultMaxSlice));
  const makeDefaultModuleStyle = (index: number, fallback?: Partial<ModuleStyle>): ModuleStyle => ({
    visible: fallback?.visible ?? true,
    color: fallback?.color ?? defaultModuleColors[index % defaultModuleColors.length],
    opacity: fallback?.opacity ?? 0.72,
    partId: Math.max(1, Math.min(255, Math.round(fallback?.partId ?? index + 1))),
  });

  const commitModuleStyles = (next: Record<string, ModuleStyle>) => {
    setModuleStyles(next);
    if (!selectedProject) {
      return;
    }
    api.updateProjectModuleStyles(selectedProject.id, next)
      .then((updated) => {
        setSelectedProject(updated);
        setProjects((items) => sortProjectsByActivity(items.map((item) => (item.id === updated.id ? updated : item))));
      })
      .catch((error) => {
        setActionMessage(error instanceof Error ? error.message : '构件样式保存失败');
      });
  };

  const handleMaskBundleExport = async () => {
    if (!selectedProject) {
      return;
    }

    const selectedTargets = exportOptions
      .filter((option) => maskExportSelection[option.id])
      .map((option) => option.id);
    if (!selectedTargets.length) {
      setActionMessage('请至少选择一个导出内容');
      return;
    }

    setMaskExporting(true);
    setActionMessage('');
    try {
      await downloadProjectExportBundle(selectedProject.id, selectedTargets, 'nii.gz', {
        pose: latestSegmentationResult?.pose ?? modelPose,
        segmentationScope: maskSegmentationScope,
        segmentationExportMode: maskSegmentationExportMode,
      });
      window.setTimeout(() => setMaskExporting(false), 900);
      setShowMaskExportMenu(false);
    } catch (error) {
      setActionMessage(error instanceof Error ? error.message : '导出失败');
      setMaskExporting(false);
    }
  };

  const triggerProjectAssetImport = () => {
    if (!selectedProject || viewMode === 'mask' || assetImporting) {
      return;
    }
    const kind: ProjectAssetImportKind = viewMode === 'model' ? 'stl' : 'dicom';
    const hasExistingAssets = kind === 'dicom'
      ? (selectedProject.dicomCount ?? 0) > 0
      : (selectedProject.stlFiles?.length ?? selectedProject.modelCount ?? 0) > 0;
    if (hasExistingAssets) {
      const confirmed = window.confirm(
        kind === 'dicom'
          ? '当前项目已有 DICOM 影像。继续导入会覆盖项目库中的现有 DICOM 影像，并清空当前逆向分割结果，是否继续？'
          : '当前项目已有 3D 模型。继续导入会覆盖项目库中的现有 STL 模型，并清空当前逆向分割结果，是否继续？',
      );
      if (!confirmed) {
        return;
      }
    }
    const input = importInputRef.current;
    if (!input) {
      setActionMessage('导入控件尚未就绪，请稍后重试');
      return;
    }
    importKindRef.current = kind;
    input.value = '';
    const archiveAccept = '.zip,.tar,.tar.gz,.tgz,.gz,application/zip,application/gzip,application/x-tar';
    input.accept = kind === 'dicom'
      ? `.dcm,.dicom,application/dicom,${archiveAccept}`
      : `.stl,model/stl,${archiveAccept}`;
    input.multiple = true;
    input.click();
  };

  const handleProjectAssetImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
    if (!selectedProject) {
      return;
    }
    const files = Array.from(event.target.files ?? []);
    event.target.value = '';
    if (!files.length) {
      return;
    }

    const kind = importKindRef.current;
    const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
    setAssetImporting(true);
    setAssetImportProgress({
      kind,
      fileCount: files.length,
      totalBytes,
      loadedBytes: 0,
      percent: 0,
      phase: 'uploading',
    });
    setActionMessage(`正在导入 ${describeImportKind(kind)}...`);
    try {
      const updated = await api.importProjectAssets(
        selectedProject.id,
        kind,
        files,
        (progress: ProjectAssetImportProgress) => {
          setAssetImportProgress({
            kind,
            fileCount: files.length,
            totalBytes: progress.total || totalBytes,
            loadedBytes: progress.loaded,
            percent: progress.percent,
            phase: progress.percent >= 100 ? 'processing' : 'uploading',
          });
        },
      );
      clearCachedProjectAssets(updated.id);
      preloadedProjectIdsRef.current.delete(updated.id);
      setSelectedProject(updated);
      setProjects((items) => sortProjectsByActivity(items.map((item) => (item.id === updated.id ? updated : item))));
      const latestResult = updated.segmentationResults?.[updated.segmentationResults.length - 1];
      const nextStyles: Record<string, ModuleStyle> = {};
      (updated.stlFiles ?? []).forEach((fileName, index) => {
        nextStyles[fileName] = makeDefaultModuleStyle(index, latestResult?.moduleStyles?.[fileName] ?? updated.moduleStyles?.[fileName]);
      });
      setModuleStyles(nextStyles);
      const nextPose = normalizeModelPose(latestResult?.pose);
      setModelPose(nextPose);
      setResultPose(nextPose);
      setSliceIndex(0);
      setDicomPreview(null);
      setDicomError('');
      setResultFusionVolume(null);
      setAssetImportProgress({
        kind,
        fileCount: files.length,
        totalBytes,
        loadedBytes: totalBytes,
        percent: 100,
        phase: 'done',
      });
      setActionMessage(kind === 'dicom' ? `已导入 ${updated.dicomCount} 张 DICOM 影像` : `已导入 ${updated.modelCount ?? 0} 个 STL 模型`);
      window.setTimeout(() => setAssetImportProgress(null), 1800);
    } catch (error) {
      const message = error instanceof Error ? error.message : '项目资产导入失败';
      setAssetImportProgress((current) => ({
        kind,
        fileCount: files.length,
        totalBytes: current?.totalBytes ?? totalBytes,
        loadedBytes: current?.loadedBytes ?? 0,
        percent: current?.percent ?? 0,
        phase: 'failed',
        message,
      }));
      setActionMessage(message);
    } finally {
      setAssetImporting(false);
    }
  };

  useEffect(() => {
    setViewMode(initialViewMode);
  }, [initialViewMode]);

  useEffect(() => {
    const latestResult = selectedProject?.segmentationResults?.[selectedProject.segmentationResults.length - 1];
    const maxIndex = Math.max((selectedProject?.dicomCount ?? 1) - 1, 0);
    const next: Record<string, ModuleStyle> = {};
    stlFiles.forEach((fileName, index) => {
      next[fileName] = makeDefaultModuleStyle(index, latestResult?.moduleStyles?.[fileName] ?? selectedProject?.moduleStyles?.[fileName] ?? moduleStyles[fileName]);
    });
    setModuleStyles(next);
    setSliceIndex(0);
    const nextPose = normalizeModelPose(latestResult?.pose);
    setModelPose(nextPose);
    setResultPose(nextPose);
    setResultPreviewSlice(Math.max(0, Math.min(maxIndex, latestResult?.mappingSlice ?? maxIndex)));
    setResultDisplayMode('soft');
    setResultRotation(0);
  }, [selectedProject?.id]);

  useEffect(() => {
    if (!selectedProject || viewMode !== 'dicom' || !selectedProject.dicomCount) {
      setDicomPreview(null);
      setIsSliceChanging(false);
      return;
    }

    let cancelled = false;
    const requestId = dicomRequestRef.current + 1;
    dicomRequestRef.current = requestId;
    setDicomError('');
    setIsSliceChanging(true);
    getCachedDicomPreview(selectedProject.id, sliceIndex, plane, displayMode)
      .then((preview) => {
        if (!cancelled && requestId === dicomRequestRef.current) {
          setDicomPreview(preview);
          setIsSliceChanging(false);
        }
      })
      .catch((error) => {
        if (!cancelled && requestId === dicomRequestRef.current) {
          setDicomPreview(null);
          setDicomError(error instanceof Error ? error.message : 'DICOM 预览失败');
          setIsSliceChanging(false);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [selectedProject?.id, selectedProject?.dicomCount, sliceIndex, plane, displayMode, viewMode]);

  useEffect(() => {
    if (!selectedProject || viewMode !== 'mask' || !latestSegmentationResult || !selectedProject.dicomCount) {
      setResultFusionVolume(null);
      setResultFusionError('');
      return;
    }

    let cancelled = false;
    const start = Math.min(resultCutStart, resultCutEnd);
    const end = Math.max(resultCutStart, resultCutEnd);
    setResultFusionError('');
    getCachedDicomFusionVolume(selectedProject.id, start, end, 'soft')
      .then((volume) => {
        if (!cancelled) {
          setResultFusionVolume(volume);
        }
      })
      .catch((error) => {
        if (!cancelled) {
          setResultFusionVolume(null);
          setResultFusionError(error instanceof Error ? error.message : 'DICOM 三维融合体载入失败');
        }
      });

    return () => {
      cancelled = true;
    };
  }, [selectedProject?.id, selectedProject?.dicomCount, viewMode, latestSegmentationResult?.id, resultCutStart, resultCutEnd]);

  useEffect(() => () => {
    if (sliceRepeatRef.current !== null) {
      window.clearInterval(sliceRepeatRef.current);
    }
  }, []);

  useEffect(() => {
    const max = Math.max(sliceTotal - 1, 0);
    if (sliceIndex > max) {
      setSliceIndex(max);
    }
  }, [sliceIndex, sliceTotal]);
  
  const updateModuleStyle = (fileName: string, partial: Partial<ModuleStyle>) => {
    const index = Math.max(0, stlFiles.indexOf(fileName));
    const next = {
      ...moduleStyles,
      [fileName]: makeDefaultModuleStyle(index, {
        ...(moduleStyles[fileName] ?? selectedProject?.moduleStyles?.[fileName]),
        ...partial,
      }),
    };
    commitModuleStyles(next);
  };

  const updateModulePartId = (fileName: string, value: number) => {
    const nextId = Math.max(1, Math.min(255, Math.round(Number.isFinite(value) ? value : 1)));
    updateModuleStyle(fileName, { partId: nextId });
  };

  const toggleAllModules = () => {
    const nextVisible = !allModulesVisible;
    const next = { ...moduleStyles };
    stlFiles.forEach((fileName, index) => {
      next[fileName] = makeDefaultModuleStyle(index, {
        ...(next[fileName] ?? selectedProject?.moduleStyles?.[fileName]),
        visible: nextVisible,
      });
    });
    commitModuleStyles(next);
  };

  const stepSlice = (delta: number) => {
    setSliceIndex((current) => {
      const max = Math.max((dicomPreview?.total ?? selectedProject?.dicomCount ?? 1) - 1, 0);
      return Math.max(0, Math.min(max, current + delta));
    });
  };

  const stopSliceStep = () => {
    if (sliceRepeatRef.current !== null) {
      window.clearInterval(sliceRepeatRef.current);
      sliceRepeatRef.current = null;
    }
  };

  const startSliceStep = (delta: number) => {
    stopSliceStep();
    stepSlice(delta);
    sliceRepeatRef.current = window.setInterval(() => stepSlice(delta), 95);
  };

  const updateModelPose = (partial: Partial<ModelPose>) => {
    setModelPose((current) => clampModelPose({
      ...current,
      ...partial,
    }));
  };

  const nudgeModelPose = (key: ModelPoseKey, delta: number) => {
    setModelPose((current) => clampModelPose({
      ...current,
      [key]: clampModelPoseValue(key, current[key] + delta),
    }));
  };

  const resetModelRotationPose = () => {
    setModelPose((current) => ({
      ...current,
      rotateX: 0,
      rotateY: 0,
      rotateZ: 0,
    }));
  };

  const resetModelTransformPose = () => {
    setModelPose((current) => ({
      ...current,
      translateX: 0,
      translateY: 0,
      translateZ: 0,
      scale: 1,
    }));
  };

  const toggleModelFlip = (key: ModelPoseFlipKey) => {
    setModelPose((current) => ({
      ...current,
      [key]: !current[key],
    }));
  };

  const resetModelFlipPose = () => {
    setModelPose((current) => ({
      ...current,
      flipX: false,
      flipY: false,
      flipZ: false,
    }));
  };

  const rotateDicom = (delta: number) => {
    setRotation((current) => ((current + delta) % 360 + 360) % 360);
  };

  const downloadCurrentDicomPng = () => {
    if (!dicomPreview || !selectedProject) {
      setActionMessage('当前没有可下载的 DICOM 图片');
      return;
    }

    const canvas = document.createElement('canvas');
    drawDicomPreviewToCanvas(canvas, dicomPreview, rotation);
    const link = document.createElement('a');
    const planeLabel = planeOptions.find((option) => option.id === plane)?.label ?? plane;
    const modeLabel = displayModes.find((mode) => mode.id === displayMode)?.label ?? displayMode;
    const displaySlice = getDicomDisplaySliceNumber(dicomPreview.slice, dicomPreview.total);
    link.href = canvas.toDataURL('image/png');
    link.download = `${safeFilePart(selectedProject.name)}_${planeLabel}_slice-${displaySlice}-of-${dicomPreview.total}_${modeLabel}_rot-${rotation}.png`;
    document.body.appendChild(link);
    link.click();
    link.remove();
    setActionMessage('已生成当前 DICOM 图片 PNG');
  };

  const openDicomInfo = async () => {
    if (!selectedProject) return;
    setIsDicomInfoOpen(true);
    setDicomInfoError('');
    try {
      setDicomInfo(await api.getDicomInfo(selectedProject.id));
    } catch (error) {
      setDicomInfo(null);
      setDicomInfoError(error instanceof Error ? error.message : 'DICOM 信息查询失败');
    }
  };

  const handleCreateProject = async () => {
    const name = newProjectName.trim();
    if (!name) {
      setActionMessage('请输入项目名称');
      return;
    }
    const created = await api.createProject(name);
    setNewProjectName('');
    setIsCreateModalOpen(false);
    setActionMessage(`已创建项目：${created.name}`);
    await refreshProjects();
    setSelectedProject(created);
  };

  const handleRenameProject = async (projectId: string) => {
    const name = editingName.trim();
    if (!name) {
      setActionMessage('项目名称不能为空');
      return;
    }
    const updated = await api.renameProject(projectId, name);
    setEditingProjectId('');
    setEditingName('');
    setActionMessage(`已更新项目名称：${updated.name}`);
    await refreshProjects();
    setSelectedProject(updated);
  };

  const handleEditBlur = (project: Project) => {
    if (editingProjectId !== project.id) {
      return;
    }
    if (editingName.trim() && editingName.trim() !== project.name) {
      handleRenameProject(project.id);
    } else {
      setEditingProjectId('');
      setEditingName('');
    }
  };

  const handleDeleteProject = async () => {
    if (!projectToDelete) {
      return;
    }
    await api.deleteProject(projectToDelete.id);
    setActionMessage(`已删除项目：${projectToDelete.name}`);
    setProjectToDelete(null);
    await refreshProjects();
  };

  const handleToggleProjectLock = async (project: Project) => {
    setLockChangingProjectId(project.id);
    setActionMessage('');
    try {
      const updated = await api.updateProjectLock(project.id, project.locked !== true);
      setProjects((items) => sortProjectsByActivity(items.map((item) => (item.id === updated.id ? updated : item))));
      setSelectedProject((current) => (current?.id === updated.id ? updated : current));
      setActionMessage(updated.locked
        ? `已锁定，位姿数据已保存到 ${updated.lockedPoseSnapshotPath ?? '项目数据/锁定结果'}`
        : '项目已解锁，可以进入逆向工作区');
    } catch (error) {
      setActionMessage(error instanceof Error ? error.message : '项目锁定状态更新失败');
    } finally {
      setLockChangingProjectId('');
    }
  };

  const handleEnterReverseWorkspace = (project: Project) => {
    if (project.locked) {
      setActionMessage('项目已锁定，请先点击「解锁项目」后再进入逆向工作区');
      return;
    }
    onReverse(project.id);
  };

  const tabs = [
    { id: 'dicom' as const, label: 'DICOM 影像', icon: ImageIcon },
    { id: 'model' as const, label: '3D 模型', icon: Box },
    { id: 'mask' as const, label: '逆向分割结果', icon: Layers },
  ];
  const renderMaskExportMenu = (widthClass = 'w-80') => (
    <div className={`absolute right-0 top-12 z-50 ${widthClass} rounded-2xl border border-slate-200 bg-white p-3 text-xs shadow-2xl`}>
      <div className="mb-2 flex items-center justify-between">
        <p className="font-bold text-slate-700">导出内容</p>
        <button
          onClick={() => setMaskExportSelection({ dicom: true, segmentation: true, pose: true, stl: true })}
          className="text-[10px] font-bold text-emerald-600 hover:text-emerald-700"
        >
          全选
        </button>
      </div>
      <div className="space-y-2">
        {exportOptions.map((option) => (
          <label key={option.id} className="flex items-center gap-3 rounded-xl bg-slate-50 px-3 py-2 font-bold text-slate-600">
            <input
              type="checkbox"
              checked={maskExportSelection[option.id]}
              onChange={(event) => setMaskExportSelection((current) => ({ ...current, [option.id]: event.target.checked }))}
              className="accent-emerald-600"
            />
            <span className="min-w-0 flex-1">
              <span className="block">{option.label}</span>
              <span className="block text-[10px] text-slate-400">{option.description}</span>
            </span>
          </label>
        ))}
      </div>
      {maskExportSelection.segmentation && (
        <div className="mt-3 rounded-xl border border-emerald-100 bg-emerald-50/70 p-2">
          <div className="mb-2 flex items-center justify-between gap-2">
            <p className="text-[10px] font-bold text-emerald-800">分割类别范围</p>
            <span className="text-[9px] font-bold text-emerald-600">附带 labels.json</span>
          </div>
          <div className="grid grid-cols-2 gap-1.5">
            {segmentationScopeOptions.map((option) => (
              <button
                key={option.id}
                onClick={() => setMaskSegmentationScope(option.id)}
                className={`rounded-lg px-2 py-1.5 text-left transition ${
                  maskSegmentationScope === option.id
                    ? 'bg-emerald-600 text-white shadow-sm'
                    : 'bg-white text-emerald-700 hover:bg-emerald-100'
                }`}
              >
                <span className="block text-[10px] font-bold">{option.label}</span>
                <span className={`block text-[9px] ${maskSegmentationScope === option.id ? 'text-emerald-50' : 'text-emerald-500'}`}>
                  {option.description}
                </span>
              </button>
            ))}
          </div>
          <div className="mt-2 border-t border-emerald-100 pt-2">
            <p className="mb-2 text-[10px] font-bold text-emerald-800">分割导出方式</p>
            <div className="grid grid-cols-2 gap-1.5">
              {segmentationExportModeOptions.map((option) => (
                <button
                  key={option.id}
                  onClick={() => setMaskSegmentationExportMode(option.id)}
                  className={`rounded-lg px-2 py-1.5 text-left transition ${
                    maskSegmentationExportMode === option.id
                      ? 'bg-slate-900 text-white shadow-sm'
                      : 'bg-white text-slate-600 hover:bg-emerald-100'
                  }`}
                >
                  <span className="block text-[10px] font-bold">{option.label}</span>
                  <span className={`block text-[9px] ${maskSegmentationExportMode === option.id ? 'text-slate-200' : 'text-slate-400'}`}>
                    {option.description}
                  </span>
                </button>
              ))}
            </div>
          </div>
        </div>
      )}
      <button
        onClick={handleMaskBundleExport}
        disabled={maskExporting}
        className="mt-3 flex h-9 w-full items-center justify-center rounded-xl bg-slate-900 text-[11px] font-bold text-white hover:bg-black disabled:opacity-50"
      >
        导出所选压缩包
      </button>
    </div>
  );
  const renderResultOverlaySummary = () => (
    <div className="rounded-2xl border border-slate-100 bg-slate-50 p-4">
      <div className="mb-3 flex items-center justify-between gap-3">
        <div>
          <p className="text-sm font-bold text-slate-800">Overlay Label Map</p>
          <p className="mt-1 font-mono text-[11px] font-bold text-cyan-700">
            {resultOverlayStats.activeModules}/{resultVisibleModuleCount} 构件 · {resultOverlayStats.segmentCount} 边 · {resultOverlayStats.filledPixels} px
          </p>
        </div>
        <span className="rounded-lg bg-white px-2 py-1 text-[10px] font-bold text-slate-400">当前切片</span>
      </div>
      {resultOverlayStats.modules.length ? (
        <div className="grid max-h-52 grid-cols-1 gap-2 overflow-auto pr-1">
          {resultOverlayStats.modules.map((item) => (
            <div key={item.fileName} className="grid grid-cols-[12px_1fr_auto] items-center gap-2 rounded-xl border border-slate-100 bg-white px-3 py-2 text-[10px] font-bold text-slate-600">
              <span className="h-3 w-3 rounded-full border border-white shadow-sm" style={{ backgroundColor: item.color, opacity: item.opacity }} />
              <span className="min-w-0 truncate">{item.name}</span>
              <span className="font-mono text-cyan-700">ID {item.partId}</span>
              <span className="col-start-2 font-mono text-slate-400">{item.segmentCount} 边</span>
              <span className="font-mono text-slate-400">{item.filledPixels} px</span>
            </div>
          ))}
        </div>
      ) : (
        <div className="rounded-xl border border-slate-100 bg-white px-3 py-2 text-[10px] font-bold text-slate-400">
          当前切片暂无可见构件
        </div>
      )}
    </div>
  );

  return (
    <div className="h-full flex gap-6 overflow-hidden">
      {/* Project Sidebar - Collapsible */}
      <div 
        className={`${
          isSidebarCollapsed ? 'w-12' : 'w-72'
        } flex flex-col bg-white rounded-2xl border border-slate-100 shadow-sm transition-all duration-300 relative overflow-visible shrink-0`}
      >
        <button 
          onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
          className="absolute -right-3 top-1/2 z-10 h-7 w-7 -translate-y-1/2 bg-white border border-slate-100 shadow-md hover:bg-slate-50 rounded-full text-slate-400 transition-colors flex items-center justify-center"
        >
          {isSidebarCollapsed ? <ChevronRight size={18} /> : <ChevronRight className="rotate-180" size={18} />}
        </button>

        {!isSidebarCollapsed && (
          <div className="p-4 flex flex-col h-full overflow-hidden">
            <div className="flex items-center justify-between mb-4 px-1">
              <h3 className="font-bold text-slate-800">项目列表</h3>
              <button
                onClick={() => setIsCreateModalOpen(true)}
                className="p-1.5 rounded-lg bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors"
                title="创建项目"
              >
                <Plus size={16} />
              </button>
            </div>
            <div className="relative mb-4">
              <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={14} />
              <input 
                type="text" 
                placeholder="搜索..."
                className="w-full pl-8 pr-4 py-2 bg-slate-50 border-none rounded-lg text-xs focus:ring-1 focus:ring-blue-500 outline-none"
                value={search}
                onChange={(e) => setSearch(e.target.value)}
              />
            </div>
            <div className="mb-3 grid grid-cols-2 gap-2">
              <button
                onClick={() => setShowUnlockedOnly(false)}
                className={`flex h-8 items-center justify-center gap-1.5 rounded-lg text-[10px] font-bold transition ${
                  !showUnlockedOnly ? 'bg-slate-900 text-white shadow-sm' : 'bg-slate-50 text-slate-500 hover:bg-slate-100'
                }`}
              >
                <FolderRoot size={12} />
                全部项目
              </button>
              <button
                onClick={() => setShowUnlockedOnly(true)}
                className={`flex h-8 items-center justify-center gap-1.5 rounded-lg text-[10px] font-bold transition ${
                  showUnlockedOnly ? 'bg-emerald-600 text-white shadow-sm' : 'bg-slate-50 text-slate-500 hover:bg-slate-100'
                }`}
              >
                <Unlock size={12} />
                未上锁
              </button>
            </div>
            <div className="flex-1 overflow-y-auto space-y-2 pr-1 scrollbar-hide">
              {loading && <p className="text-xs text-slate-400 px-2">正在从后端载入项目...</p>}
              {filteredProjects.map((proj) => (
                <div
                  key={proj.id}
                  onClick={() => setSelectedProject(proj)}
                  className={`w-full p-3 rounded-xl transition-all text-left cursor-pointer ${
                    selectedProject?.id === proj.id ? 'bg-blue-600 text-white shadow-md' : 'hover:bg-slate-50'
                  }`}
                >
                  <div className="flex items-start gap-2">
                    <div className="min-w-0 flex-1">
                      {editingProjectId === proj.id ? (
                        <input
                          value={editingName}
                          autoFocus
                          onClick={(event) => event.stopPropagation()}
                          onBlur={() => handleEditBlur(proj)}
                          onChange={(event) => setEditingName(event.target.value)}
                          onKeyDown={(event) => {
                            if (event.key === 'Enter') handleRenameProject(proj.id);
                            if (event.key === 'Escape') setEditingProjectId('');
                          }}
                          className="w-full rounded-md px-2 py-1 text-xs text-slate-900 outline-none ring-1 ring-blue-200"
                        />
                      ) : (
                        <div className="flex min-w-0 items-center gap-1.5">
                          <p className={`truncate text-xs font-bold ${selectedProject?.id === proj.id ? 'text-white' : 'text-slate-700'}`}>
                            {proj.name}
                          </p>
                          {proj.locked && (
                            <span className={`flex shrink-0 items-center gap-0.5 rounded px-1 py-0.5 text-[8px] font-black ${
                              selectedProject?.id === proj.id ? 'bg-white/15 text-blue-50' : 'bg-amber-50 text-amber-600'
                            }`}>
                              <Lock size={9} />
                              锁
                            </span>
                          )}
                        </div>
                      )}
                    </div>
                    {editingProjectId !== proj.id && (
                      <div className="flex gap-2">
                      <button
                        onClick={(event) => {
                          event.stopPropagation();
                          setEditingProjectId(proj.id);
                          setEditingName(proj.name);
                        }}
                        className={selectedProject?.id === proj.id ? 'text-blue-100 hover:text-white' : 'text-slate-300 hover:text-blue-600'}
                        title="修改项目名称"
                      >
                        <Edit2 size={14} />
                      </button>
                      <button
                        onClick={(event) => {
                          event.stopPropagation();
                          setProjectToDelete(proj);
                        }}
                        className={selectedProject?.id === proj.id ? 'text-blue-100 hover:text-white' : 'text-slate-300 hover:text-rose-600'}
                        title="删除项目"
                      >
                        <Trash2 size={14} />
                      </button>
                      </div>
                    )}
                  </div>
                  <p className={`text-[10px] mt-1 ${selectedProject?.id === proj.id ? 'text-blue-100' : 'text-slate-400'}`}>
                    处理 {formatProjectActivity(proj)} · DICOM {proj.dicomCount} · STL {proj.modelCount ?? 0}
                  </p>
                </div>
              ))}
            </div>
            {actionMessage && <p className="text-[10px] text-slate-400 mt-3 px-1">{actionMessage}</p>}
          </div>
        )}

        {isSidebarCollapsed && (
          <div className="flex flex-col items-center py-12 gap-4">
            {filteredProjects.map(p => (
              <div 
                key={p.id}
                onClick={() => setSelectedProject(p)}
                className={`relative w-8 h-8 rounded-lg flex items-center justify-center cursor-pointer transition-all ${
                  selectedProject?.id === p.id ? 'bg-blue-600 text-white shadow-md' : 'bg-slate-50 text-slate-400'
                }`}
              >
                <FolderRoot size={16} />
                {p.locked && <Lock size={9} className="absolute -right-1 -top-1 rounded-full bg-amber-100 p-0.5 text-amber-600" />}
              </div>
            ))}
          </div>
        )}
      </div>

      {/* Main Content Area */}
      <div className="flex-1 flex flex-col gap-6 overflow-hidden">
        {selectedProject ? (
          <>
            <input
              ref={importInputRef}
              type="file"
              className="hidden"
              onChange={handleProjectAssetImport}
            />
            <div className="flex items-center justify-between">
              <div className="flex bg-slate-100 p-1 rounded-xl">
                {tabs.map((tab) => (
                  <button
                    key={tab.id}
                    onClick={() => setViewMode(tab.id)}
                    className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${
                      viewMode === tab.id ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
                    }`}
                  >
                    <tab.icon size={16} /> {tab.label}
                  </button>
                ))}
              </div>
              <div className="flex gap-4">
                <button
                  onClick={() => void handleToggleProjectLock(selectedProject)}
                  disabled={lockChangingProjectId === selectedProject.id}
                  className={`px-5 py-2.5 rounded-xl text-sm font-bold flex items-center gap-2 transition-all shadow-lg disabled:opacity-60 ${
                    selectedProject.locked
                      ? 'bg-amber-500 text-white hover:bg-amber-600'
                      : 'bg-white text-slate-700 ring-1 ring-slate-200 hover:bg-slate-50'
                  }`}
                  title={selectedProject.locked ? '解除项目锁定' : '锁定项目并保存位姿数据'}
                >
                  {selectedProject.locked ? <Unlock size={18} /> : <Lock size={18} />}
                  {lockChangingProjectId === selectedProject.id
                    ? '处理中'
                    : selectedProject.locked ? '解锁项目' : '锁定项目'}
                </button>
                <button 
                  onClick={() => handleEnterReverseWorkspace(selectedProject)}
                  className={`px-6 py-2.5 rounded-xl text-sm font-bold flex items-center gap-2 transition-all shadow-lg ${
                    selectedProject.locked
                      ? 'bg-slate-300 text-slate-500 hover:bg-slate-300'
                      : 'bg-blue-600 text-white hover:bg-blue-700'
                  }`}
                >
                  <RotateCw size={18} /> 进入逆向工作区
                </button>
                {viewMode === 'mask' ? (
                  <div className="relative">
                    <button
                      onClick={() => setShowMaskExportMenu((value) => !value)}
                      disabled={maskExporting || !latestSegmentationResult}
                      className="bg-emerald-600 text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center gap-2 hover:bg-emerald-700 transition-all shadow-lg disabled:opacity-50"
                    >
                      <Download size={18} />
                      {maskExporting ? '正在导出' : '导出项目及结果'}
                    </button>
                    {showMaskExportMenu && renderMaskExportMenu('w-80')}
                  </div>
                ) : (
                  <button
                    onClick={triggerProjectAssetImport}
                    disabled={assetImporting}
                    className="bg-slate-800 text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center gap-2 hover:bg-slate-700 transition-all disabled:opacity-50"
                  >
                    <Upload size={18} /> {assetImporting ? '导入中' : '导入'}
                  </button>
                )}
              </div>
            </div>

            {assetImportProgress && (
              <div className={`rounded-2xl border px-5 py-3 shadow-sm ${
                assetImportProgress.phase === 'failed'
                  ? 'border-rose-200 bg-rose-50'
                  : assetImportProgress.phase === 'done'
                    ? 'border-emerald-100 bg-white'
                    : 'border-blue-100 bg-white'
              }`}>
                <div className="mb-2 flex items-center justify-between gap-4">
                  <div className="flex min-w-0 items-center gap-3">
                    <div className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ${
                      assetImportProgress.phase === 'failed'
                        ? 'bg-rose-100 text-rose-600'
                        : assetImportProgress.phase === 'done'
                          ? 'bg-emerald-50 text-emerald-600'
                          : 'bg-blue-50 text-blue-600'
                    }`}>
                      {assetImportProgress.phase === 'failed' ? <X size={17} /> : <FileArchive size={17} />}
                    </div>
                    <div className="min-w-0">
                      <p className="truncate text-sm font-bold text-slate-800">
                        {assetImportProgress.phase === 'failed'
                          ? `${describeImportKind(assetImportProgress.kind)}导入失败`
                          : assetImportProgress.phase === 'done'
                          ? `${describeImportKind(assetImportProgress.kind)}导入完成`
                          : assetImportProgress.phase === 'processing'
                            ? '上传完成，服务器正在解压与解析'
                            : `正在上传${describeImportKind(assetImportProgress.kind)}`}
                      </p>
                      <p className="mt-0.5 text-[11px] font-bold text-slate-400">
                        {assetImportProgress.fileCount} 个文件 · {formatFileSize(assetImportProgress.loadedBytes)} / {formatFileSize(assetImportProgress.totalBytes)}
                      </p>
                      {assetImportProgress.phase === 'failed' && assetImportProgress.message && (
                        <p className="mt-1 whitespace-normal text-[11px] font-bold leading-5 text-rose-700">
                          {assetImportProgress.message}
                        </p>
                      )}
                    </div>
                  </div>
                  <span className={`shrink-0 font-mono text-sm font-black ${
                    assetImportProgress.phase === 'failed'
                      ? 'text-rose-600'
                      : assetImportProgress.phase === 'done'
                        ? 'text-emerald-600'
                        : 'text-blue-600'
                  }`}>
                    {assetImportProgress.percent}%
                  </span>
                </div>
                <div className="h-2 overflow-hidden rounded-full bg-slate-100">
                  <div
                    className={`h-full rounded-full transition-all duration-300 ${
                      assetImportProgress.phase === 'failed'
                        ? 'bg-rose-500'
                        : assetImportProgress.phase === 'done'
                          ? 'bg-emerald-500'
                          : 'bg-blue-600'
                    }`}
                    style={{ width: `${assetImportProgress.percent}%` }}
                  />
                </div>
              </div>
            )}

            <div className="flex-1 bg-white rounded-3xl border border-slate-100 shadow-sm overflow-hidden p-8">
              {viewMode === 'dicom' && (
                <div className="h-full min-h-0 flex gap-8">
                  {/* Left: DICOM Viewer */}
                  <div className="flex-1 bg-slate-950 rounded-2xl relative border border-slate-800 flex items-center justify-center p-12">
                    <div className="absolute top-4 right-4 z-10 flex rounded-lg bg-white/5 p-1 backdrop-blur-sm border border-white/10">
                      {planeOptions.map((option) => (
                        <button
                          key={option.id}
                          onClick={() => {
                            setPlane(option.id);
                            setSliceIndex(0);
                          }}
                          className={`px-3 py-1.5 rounded-md text-[10px] font-bold transition-all ${
                            plane === option.id ? 'bg-blue-600 text-white' : 'text-white/50 hover:text-white'
                          }`}
                        >
                          {option.label}
                        </button>
                      ))}
                    </div>
                    <div className="absolute top-16 right-4 z-10 flex rounded-lg bg-white/5 p-1 backdrop-blur-sm border border-white/10">
                      {displayModes.map((mode) => (
                        <button
                          key={mode.id}
                          onClick={() => setDisplayMode(mode.id)}
                          className={`px-3 py-1.5 rounded-md text-[10px] font-bold transition-all ${
                            displayMode === mode.id ? 'bg-emerald-600 text-white' : 'text-white/50 hover:text-white'
                          }`}
                        >
                          {mode.label}
                        </button>
                      ))}
                    </div>
                    <div className="absolute top-28 right-4 z-10 flex rounded-lg bg-white/5 p-1 backdrop-blur-sm border border-white/10">
                      <button
                        onClick={() => rotateDicom(-90)}
                        className="px-3 py-1.5 rounded-md text-[10px] font-bold text-white/60 hover:bg-white/10 hover:text-white transition-all flex items-center gap-1"
                        title="左旋转 90°"
                      >
                        <RotateCcw size={12} /> 左转
                      </button>
                      <button
                        onClick={() => rotateDicom(90)}
                        className="px-3 py-1.5 rounded-md text-[10px] font-bold text-white/60 hover:bg-white/10 hover:text-white transition-all flex items-center gap-1"
                        title="右旋转 90°"
                      >
                        <RotateCw size={12} /> 右转
                      </button>
                      <button
                        onClick={() => setDicomViewportResetSignal((value) => value + 1)}
                        className="px-3 py-1.5 rounded-md text-[10px] font-bold text-white/60 hover:bg-white/10 hover:text-white transition-all flex items-center gap-1"
                        title="重置 DICOM 图片位置"
                      >
                        <RefreshCcw size={12} /> 位置重置
                      </button>
                    </div>
                    <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 transition-all duration-150 ${isSliceChanging ? 'scale-[1.01] opacity-85 brightness-110' : 'scale-100 opacity-100 brightness-100'}`}>
                       {dicomPreview ? (
                         <DicomCanvas preview={dicomPreview} rotation={rotation} resetSignal={dicomViewportResetSignal} />
                       ) : (
                         <p className="text-white/30 text-xs font-mono uppercase tracking-widest">
                           {selectedProject.dicomCount ? dicomError || '正在解析 DICOM 像素...' : '请导入DICOM影像'}
                         </p>
                       )}
                       {isSliceChanging && dicomPreview && (
                         <span className="absolute right-3 top-3 rounded-md bg-blue-500/20 px-2 py-1 text-[9px] font-bold text-blue-200 backdrop-blur-sm">
                           切片切换中
                         </span>
                       )}
                    </div>
                    <div className="absolute bottom-4 left-4 right-4 flex justify-between text-white/30 font-mono text-[10px]">
                    <span>WW/WL: {dicomPreview?.windowWidth ?? 400}/{dicomPreview?.windowCenter ?? 40} · {displayModes.find((mode) => mode.id === displayMode)?.label}</span>
                      <span>第 {selectedProject.dicomCount ? dicomDisplaySlice : 0} / {dicomSliceTotal || selectedProject.dicomCount} 张</span>
                    </div>
                  </div>
                  {/* Right: Vertical Progress Bar */}
                  <div className="w-24 h-full flex flex-col items-center py-4 bg-slate-50 rounded-2xl">
                    <span className="text-[10px] text-slate-400 font-bold mb-3">切片</span>
                    <span className="text-[10px] text-slate-500 font-bold mb-4 whitespace-nowrap">
                      {dicomDisplaySlice} / {dicomSliceTotal || selectedProject.dicomCount}
                    </span>
                    <button
                      onMouseDown={() => startSliceStep(1)}
                      onMouseUp={stopSliceStep}
                      onMouseLeave={stopSliceStep}
                      onTouchStart={(event) => {
                        event.preventDefault();
                        startSliceStep(1);
                      }}
                      onTouchEnd={stopSliceStep}
                      className="mb-3 h-8 w-8 rounded-full bg-white text-slate-500 shadow-sm border border-slate-100 hover:text-blue-600 hover:border-blue-100 flex items-center justify-center"
                      title="长按向上移动切片"
                    >
                      <ChevronUp size={16} />
                    </button>
                    <div className="relative min-h-[260px] w-10 flex-1">
                      <div className="absolute inset-y-0 left-1/2 w-2 -translate-x-1/2 rounded-full bg-slate-800/70" />
                      <input
                        type="range"
                        min="0"
                        max={dicomMaxSlice}
                        value={dicomSliderValue}
                        onChange={(event) => setSliceIndex(Number(event.target.value))}
                        className="mapping-slice-dark-vertical-input"
                        aria-label="项目库 DICOM 切片导航"
                      />
                    </div>
                    <button
                      onMouseDown={() => startSliceStep(-1)}
                      onMouseUp={stopSliceStep}
                      onMouseLeave={stopSliceStep}
                      onTouchStart={(event) => {
                        event.preventDefault();
                        startSliceStep(-1);
                      }}
                      onTouchEnd={stopSliceStep}
                      className="mt-3 h-8 w-8 rounded-full bg-white text-slate-500 shadow-sm border border-slate-100 hover:text-blue-600 hover:border-blue-100 flex items-center justify-center"
                      title="长按向下移动切片"
                    >
                      <ChevronDown size={16} />
                    </button>
                    <span className="text-[10px] text-blue-600 font-bold mt-4">#{dicomDisplaySlice}</span>
                    <div className="mt-5 flex w-full flex-col gap-2 px-2">
                      <button
                        onClick={downloadCurrentDicomPng}
                        className="h-8 rounded-lg bg-blue-600 text-white text-[10px] font-bold flex items-center justify-center gap-1 hover:bg-blue-700"
                        title="下载当前图片 PNG"
                      >
                        <Download size={12} /> PNG
                      </button>
                      <button
                        onClick={() => selectedProject && downloadDicomArchive(selectedProject.id)}
                        className="h-8 rounded-lg bg-white text-slate-600 text-[10px] font-bold flex items-center justify-center gap-1 border border-slate-200 hover:bg-slate-100"
                        title="下载 DICOM 影像压缩包"
                      >
                        <FileArchive size={12} /> DCM
                      </button>
                      <button
                        onClick={openDicomInfo}
                        className="h-8 rounded-lg bg-white text-slate-600 text-[10px] font-bold flex items-center justify-center gap-1 border border-slate-200 hover:bg-slate-100"
                        title="查询 DICOM 详细信息"
                      >
                        <Info size={12} /> 信息
                      </button>
                    </div>
                  </div>
                </div>
              )}

              {viewMode === 'model' && (
                <div className="h-full flex gap-8">
                  {/* Left: 3D Visualization */}
                  <div className="flex-1 bg-slate-50 rounded-2xl relative border border-slate-100 overflow-hidden">
                    <NativeStlViewer
                      projectId={selectedProject.id}
                      files={stlFiles}
                      styles={moduleStyles}
                      detailLimit={selectedSolidity.limit}
                      solidMode={solidityLevel === 'solid'}
                      pose={modelPose}
                      onPoseChange={setModelPose}
                    />
                    {!stlFiles.length && (
                      <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
                        <p className="rounded-2xl border border-slate-200 bg-white/85 px-5 py-3 text-xs font-bold text-slate-500 shadow-sm">
                          请导入STL模型
                        </p>
                      </div>
                    )}
                    <div className="absolute bottom-4 left-4 text-slate-400 font-mono text-[10px]">
                      MODEL PATH: {selectedProject.modelPath} | STL: {selectedProject.modelCount ?? 0} | {selectedSolidity.label}
                    </div>
                  </div>
                  {/* Right: Sub-module List */}
                  <div className="w-80 h-full min-h-0 overflow-y-auto pr-1 scrollbar-hide">
                    <div className="space-y-4 pb-4">
                      <div className="rounded-2xl bg-slate-50 border border-slate-100 p-4">
                        <div className="flex items-center justify-between mb-3">
                          <p className="text-xs font-bold text-slate-700">模型显示</p>
                          <span className="text-[10px] text-slate-400">左键旋转 · 右键/Shift 平移 · 滚轮缩放</span>
                        </div>
                        <div className="grid grid-cols-4 gap-1 rounded-xl bg-slate-100 p-1">
                          {solidityOptions.map((option) => (
                            <button
                              key={option.id}
                              onClick={() => setSolidityLevel(option.id)}
                              className={`rounded-lg px-2 py-1.5 text-[10px] font-bold transition-all ${
                                solidityLevel === option.id ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
                              }`}
                            >
                              {option.label}
                            </button>
                          ))}
                        </div>
                      </div>

                      <div className="rounded-2xl bg-slate-50 border border-slate-100 p-4 space-y-3">
                        <div className="flex items-center justify-between gap-2">
                          <p className="text-xs font-bold text-slate-700">模型位姿</p>
                          <div className="flex items-center gap-1">
                            <button
                              onClick={resetModelRotationPose}
                              className="rounded-md bg-white px-2 py-1 text-[10px] font-bold text-blue-600 shadow-sm border border-slate-100 hover:bg-blue-50"
                            >
                              旋转复位
                            </button>
                            <button
                              onClick={resetModelTransformPose}
                              className="rounded-md bg-white px-2 py-1 text-[10px] font-bold text-blue-600 shadow-sm border border-slate-100 hover:bg-blue-50"
                            >
                              平移缩放复位
                            </button>
                            <button
                              onClick={resetModelFlipPose}
                              className="rounded-md bg-white px-2 py-1 text-[10px] font-bold text-blue-600 shadow-sm border border-slate-100 hover:bg-blue-50"
                            >
                              镜像复位
                            </button>
                          </div>
                        </div>
                        <div className="grid grid-cols-3 gap-2">
                          {modelPoseFlipOptions.map((item) => {
                            const Icon = item.icon;
                            const enabled = modelPose[item.key];
                            return (
                              <button
                                key={item.key}
                                onClick={() => toggleModelFlip(item.key)}
                                className={`flex h-8 items-center justify-center gap-1.5 rounded-lg border text-[10px] font-bold transition ${
                                  enabled
                                    ? 'border-emerald-200 bg-emerald-600 text-white shadow-sm'
                                    : 'border-slate-100 bg-white text-slate-500 shadow-sm hover:border-emerald-200 hover:bg-emerald-50 hover:text-emerald-700'
                                }`}
                                title={`以模型中心沿 ${item.axis} 轴镜像翻转`}
                              >
                                <Icon size={13} />
                                {item.label}
                              </button>
                            );
                          })}
                        </div>
                        {[
                          { key: 'rotateX' as const, label: '旋转 X', min: -180, max: 180, step: 1, value: modelPose.rotateX, minus: '-90°', plus: '+90°', delta: 90 },
                          { key: 'rotateY' as const, label: '旋转 Y', min: -180, max: 180, step: 1, value: modelPose.rotateY, minus: '-90°', plus: '+90°', delta: 90 },
                          { key: 'rotateZ' as const, label: '旋转 Z', min: -180, max: 180, step: 1, value: modelPose.rotateZ, minus: '-90°', plus: '+90°', delta: 90 },
                          { key: 'translateX' as const, label: '平移 X', min: -2, max: 2, step: 0.05, value: modelPose.translateX, minus: '-X', plus: '+X', delta: 0.25 },
                          { key: 'translateY' as const, label: '平移 Y', min: -2, max: 2, step: 0.05, value: modelPose.translateY, minus: '-Y', plus: '+Y', delta: 0.25 },
                          { key: 'translateZ' as const, label: '平移 Z', min: -2, max: 2, step: 0.05, value: modelPose.translateZ, minus: '-Z', plus: '+Z', delta: 0.25 },
                          { key: 'scale' as const, label: '缩放', min: 0.5, max: 2.5, step: 0.005, value: modelPose.scale, minus: '-0.005', plus: '+0.005', delta: 0.005 },
                        ].map((item) => (
                          <div key={item.key} className="grid grid-cols-[48px_40px_1fr_40px_42px] items-center gap-2">
                            <span className="text-[10px] font-bold text-slate-500">{item.label}</span>
                            <button
                              onClick={() => nudgeModelPose(item.key, -item.delta)}
                              className="h-6 rounded-md bg-white text-[10px] font-bold text-slate-500 shadow-sm border border-slate-100 hover:text-blue-600 hover:bg-blue-50"
                              title={`${item.label} ${item.minus}`}
                            >
                              {item.minus}
                            </button>
                            <input
                              type="range"
                              min={item.min}
                              max={item.max}
                              step={item.step}
                              value={item.value}
                              onChange={(event) => updateModelPose({ [item.key]: Number(event.target.value) } as Partial<ModelPose>)}
                              className="w-full accent-blue-600"
                            />
                            <button
                              onClick={() => nudgeModelPose(item.key, item.delta)}
                              className="h-6 rounded-md bg-white text-[10px] font-bold text-slate-500 shadow-sm border border-slate-100 hover:text-blue-600 hover:bg-blue-50"
                              title={`${item.label} ${item.plus}`}
                            >
                              {item.plus}
                            </button>
                            <span className="text-[10px] font-mono text-slate-400 text-right">{Number(item.value).toFixed(getControlStepPrecision(item.step))}</span>
                          </div>
                        ))}
                      </div>
                    </div>

                    <div className="px-1 flex items-center justify-between mb-3 shrink-0">
                      <p className="text-xs font-bold text-slate-700 uppercase tracking-widest">构件层级 ({stlFiles.length})</p>
                      <button 
                        onClick={toggleAllModules}
                        className={`p-1 rounded hover:bg-slate-100 transition-colors ${allModulesVisible ? 'text-blue-500' : 'text-slate-400'}`}
                        title={allModulesVisible ? "全隐藏" : "全显示"}
                      >
                        <Eye size={16} />
                      </button>
                    </div>
                    <div className="space-y-2 pb-4">
                      {stlFiles.map((fileName, i) => {
                        const name = fileName.replace(/\.stl$/i, '');
                        const style = moduleStyles[fileName] ?? { visible: true, color: defaultModuleColors[i % defaultModuleColors.length], opacity: 0.72, partId: i + 1 };
                        return (
                        <div 
                          key={fileName} 
                          className={`p-3 rounded-xl border flex items-start gap-3 group transition-all bg-slate-50 border-transparent hover:border-slate-200 ${!style.visible ? 'opacity-50' : ''}`}
                        >
                          <input
                            type="color"
                            value={style.color}
                            onChange={(event) => updateModuleStyle(fileName, { color: event.target.value })}
                            className="w-8 h-8 rounded-lg border border-white bg-white p-0.5 cursor-pointer shrink-0"
                            title="模型颜色"
                          />
                          <div className="flex-1 min-w-0">
                            <p className="text-[11px] font-bold text-slate-700 truncate">{name}</p>
                            <div className="mt-0.5 flex items-center gap-2">
                              <p className="min-w-0 flex-1 text-[9px] text-slate-400 truncate">STL | {fileName}</p>
                              <label className="flex shrink-0 items-center gap-1 text-[9px] font-bold text-slate-400">
                                ID
                                <input
                                  type="number"
                                  min="1"
                                  max="255"
                                  value={style.partId}
                                  onChange={(event) => updateModulePartId(fileName, Number(event.target.value))}
                                  onBlur={(event) => updateModulePartId(fileName, Number(event.target.value))}
                                  className="h-5 w-12 rounded border border-slate-200 bg-white px-1 text-[9px] font-mono text-slate-600 outline-none focus:border-blue-400"
                                />
                              </label>
                            </div>
                            <div className="mt-2 flex items-center gap-2">
                              <span className="text-[9px] text-slate-400 shrink-0">透明度</span>
                              <input
                                type="range"
                                min="0.1"
                                max="1"
                                step="0.05"
                                value={style.opacity}
                                onChange={(event) => updateModuleStyle(fileName, { opacity: Number(event.target.value) })}
                                className="min-w-0 flex-1 accent-blue-600"
                              />
                              <span className="text-[9px] text-slate-400 w-7 text-right">{Math.round(style.opacity * 100)}%</span>
                            </div>
                          </div>
                          <div className="flex items-center gap-2">
                            <button 
                              onClick={(e) => {
                                e.stopPropagation();
                                updateModuleStyle(fileName, { visible: !style.visible });
                              }}
                              className={`p-1 rounded hover:bg-white transition-colors ${style.visible ? 'text-blue-500 underline decoration-2' : 'text-slate-300'}`}
                            >
                              <Eye size={14} />
                            </button>
                          </div>
                        </div>
                      )})}
                    </div>
                  </div>
                </div>
              )}

              {viewMode === 'mask' && (
                <div className="grid h-full min-h-0 grid-cols-1 items-stretch gap-6 2xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_340px]">
                  <div className="flex min-h-[620px] min-w-0 flex-col">
                    {latestSegmentationResult ? (
                      <FusionThreeView
                        project={selectedProject}
                        volume={resultFusionVolume}
                        modelPose={latestResultPose}
                        moduleStyles={latestResultStyles}
                        detailLimit={resultDisplayOption.limit}
                        solidMode={false}
                        dicomOpacity={resultDicomOpacity}
                        showBounds={latestSegmentationResult.showBounds ?? true}
                        cutEnabled={latestSegmentationResult.cutEnabled ?? false}
                        cutStart={resultCutStart}
                        cutEnd={resultCutEnd}
                        viewPreset="libraryResult"
                      />
                    ) : (
                      <div className="flex h-full min-h-[620px] items-center justify-center rounded-3xl border border-dashed border-slate-200 bg-slate-950 px-8 text-center text-sm font-bold text-white/35">
                        暂无保存结果，请在逆向工作区保存当前映射。
                      </div>
                    )}
                    {resultFusionError && (
                      <p className="mt-2 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-bold text-amber-700">
                        {resultFusionError}
                      </p>
                    )}
                  </div>

                  <div className="flex min-h-[620px] min-w-0 flex-col">
                    {latestSegmentationResult ? (
                      <VoxelizationMappingView
                        project={selectedProject}
                        moduleStyles={latestResultStyles}
                        modelPose={latestResultPose}
                        detailLimit={resultDisplayOption.limit}
                        slice={resultMappingSlice}
                        totalSlices={selectedProject.dicomCount}
                        onSliceChange={setResultPreviewSlice}
                        displayMode={resultDisplayMode}
                        rotation={resultRotation}
                        variant="library"
                        overlayPlacement="none"
                        onOverlayStatsChange={(stats, visibleCount) => {
                          setResultOverlayStats(stats);
                          setResultVisibleModuleCount(visibleCount);
                        }}
                        toolbar={(
                          <>
                            <div className="flex rounded-xl bg-white/10 p-1">
                              {displayModes.map((mode) => (
                                <button
                                  key={mode.id}
                                  onClick={() => setResultDisplayMode(mode.id)}
                                  className={`rounded-lg px-2 py-1 text-[10px] font-bold transition ${
                                    resultDisplayMode === mode.id ? 'bg-white text-cyan-700 shadow-sm' : 'text-white/55 hover:text-white'
                                  }`}
                                >
                                  {mode.label}
                                </button>
                              ))}
                            </div>
                            <button
                              onClick={() => setResultRotation((value) => (value + 270) % 360)}
                              className="flex h-8 w-8 items-center justify-center rounded-xl border border-white/10 bg-black/60 text-white/65 hover:border-cyan-300/30 hover:text-cyan-100"
                              title="左转 90°"
                            >
                              <RotateCcw size={14} />
                            </button>
                            <button
                              onClick={() => setResultRotation((value) => (value + 90) % 360)}
                              className="flex h-8 w-8 items-center justify-center rounded-xl border border-white/10 bg-black/60 text-white/65 hover:border-cyan-300/30 hover:text-cyan-100"
                              title="右转 90°"
                            >
                              <RotateCw size={14} />
                            </button>
                          </>
                        )}
                      />
                    ) : (
                      <div className="flex h-full min-h-[620px] items-center justify-center rounded-3xl border border-dashed border-slate-200 bg-slate-950 px-8 text-center text-sm font-bold text-white/35">
                        暂无逆向分割映射视图。
                      </div>
                    )}
                  </div>

                  <div className="flex flex-col gap-4">
                    <div className="rounded-2xl border border-slate-100 bg-slate-50 p-5">
                      <div className="flex items-center justify-between gap-3">
                        <div>
                          <h3 className="font-bold text-slate-800">逆向分割结果</h3>
                          <p className="mt-2 text-sm leading-6 text-slate-500">
                            项目库仅保留最新一次保存结果，导出时默认沿用该结果的模型位姿与构件样式。
                          </p>
                        </div>
                        <span className={`shrink-0 whitespace-nowrap rounded-lg px-2 py-1 text-[10px] font-bold ${latestSegmentationResult ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-200 text-slate-500'}`}>
                          {latestSegmentationResult ? '已保存' : '未保存'}
                        </span>
                      </div>
                      <div className="mt-4 grid grid-cols-2 gap-2 text-[10px] font-bold text-slate-500">
                        <span className="rounded-lg bg-white px-2 py-2">构件总数：{selectedProject.modelCount ?? stlFiles.length}</span>
                        <span className="rounded-lg bg-white px-2 py-2">
                          最后保存：{latestSegmentationResult ? new Date(latestSegmentationResult.createdAt).toLocaleString('zh-CN', { hour12: false }) : '等待结果'}
                        </span>
                      </div>
                      <div className="mt-3 rounded-xl bg-white p-3">
                        <p className="mb-2 text-[10px] font-black uppercase tracking-widest text-slate-400">模型位姿</p>
                        <div className="grid grid-cols-3 gap-1.5 text-[9px] font-bold text-slate-500">
                          <span className="rounded-lg bg-slate-50 px-2 py-1.5">RX {formatPoseCompactValue(latestResultPose.rotateX, 1)}°</span>
                          <span className="rounded-lg bg-slate-50 px-2 py-1.5">RY {formatPoseCompactValue(latestResultPose.rotateY, 1)}°</span>
                          <span className="rounded-lg bg-slate-50 px-2 py-1.5">RZ {formatPoseCompactValue(latestResultPose.rotateZ, 1)}°</span>
                          <span className="rounded-lg bg-slate-50 px-2 py-1.5">TX {formatPoseCompactValue(latestResultPose.translateX, 3)}</span>
                          <span className="rounded-lg bg-slate-50 px-2 py-1.5">TY {formatPoseCompactValue(latestResultPose.translateY, 3)}</span>
                          <span className="rounded-lg bg-slate-50 px-2 py-1.5">TZ {formatPoseCompactValue(latestResultPose.translateZ, 3)}</span>
                          <span className="col-span-3 rounded-lg bg-slate-50 px-2 py-1.5">Scale {formatPoseCompactValue(latestResultPose.scale, 3)}</span>
                          <span className="rounded-lg bg-slate-50 px-2 py-1.5">FX {latestResultPose.flipX ? '开' : '关'}</span>
                          <span className="rounded-lg bg-slate-50 px-2 py-1.5">FY {latestResultPose.flipY ? '开' : '关'}</span>
                          <span className="rounded-lg bg-slate-50 px-2 py-1.5">FZ {latestResultPose.flipZ ? '开' : '关'}</span>
                        </div>
                      </div>
                    </div>

                    {latestSegmentationResult && renderResultOverlaySummary()}
                  </div>
                </div>
              )}
            </div>
          </>
        ) : (
          <div className="flex-1 bg-white rounded-3xl border border-dashed border-slate-200 flex flex-col items-center justify-center text-slate-400">
             <div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mb-6">
                <FolderRoot size={40} />
             </div>
             <p className="font-bold">请从左侧选择一个项目开始阅览</p>
          </div>
        )}
      </div>

      {isCreateModalOpen && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/40 backdrop-blur-sm">
          <div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-2xl border border-slate-100">
            <div className="flex items-center justify-between mb-5">
              <h3 className="font-bold text-slate-900">创建项目</h3>
              <button
                onClick={() => setIsCreateModalOpen(false)}
                className="text-slate-400 hover:text-slate-700"
                title="关闭"
              >
                <X size={18} />
              </button>
            </div>
            <input
              value={newProjectName}
              autoFocus
              onChange={(event) => setNewProjectName(event.target.value)}
              onKeyDown={(event) => {
                if (event.key === 'Enter') {
                  handleCreateProject();
                }
              }}
              placeholder="请输入项目名称"
              className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-blue-500"
            />
            <div className="mt-6 flex justify-end gap-3">
              <button
                onClick={() => setIsCreateModalOpen(false)}
                className="px-4 py-2 rounded-xl text-sm font-bold text-slate-600 hover:bg-slate-100"
              >
                取消
              </button>
              <button
                onClick={handleCreateProject}
                className="px-4 py-2 rounded-xl text-sm font-bold bg-blue-600 text-white hover:bg-blue-700"
              >
                创建
              </button>
            </div>
          </div>
        </div>
      )}

      {isDicomInfoOpen && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/40 backdrop-blur-sm">
          <div className="w-full max-w-3xl max-h-[82vh] overflow-hidden rounded-2xl bg-white shadow-2xl border border-slate-100 flex flex-col">
            <div className="flex items-center justify-between border-b border-slate-100 px-6 py-4">
              <div>
                <h3 className="font-bold text-slate-900">DICOM 详细信息</h3>
                <p className="text-xs text-slate-400 mt-1">包含基础元数据、像素间距、切片间距和物理尺寸</p>
              </div>
              <button
                onClick={() => setIsDicomInfoOpen(false)}
                className="text-slate-400 hover:text-slate-700"
                title="关闭"
              >
                <X size={18} />
              </button>
            </div>
            <div className="overflow-y-auto p-6">
              {dicomInfoError && <p className="text-sm font-bold text-rose-600">{dicomInfoError}</p>}
              {!dicomInfo && !dicomInfoError && <p className="text-sm text-slate-400">正在解析 DICOM 信息...</p>}
              {dicomInfo && (
                <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                  {[
                    {
                      title: '患者与检查',
                      rows: [
                        ['患者姓名', dicomInfo.patient.name],
                        ['患者 ID', dicomInfo.patient.id],
                        ['检查日期', dicomInfo.study.date],
                        ['检查类型', dicomInfo.study.modality],
                        ['设备厂商', dicomInfo.study.manufacturer],
                      ],
                    },
                    {
                      title: '序列与文件',
                      rows: [
                        ['序列描述', dicomInfo.series.description],
                        ['文件数量', dicomInfo.series.files],
                        ['首文件', dicomInfo.series.firstFile],
                        ['末文件', dicomInfo.series.lastFile],
                        ['DICOM 路径', dicomInfo.project.dicomPath],
                      ],
                    },
                    {
                      title: '图像矩阵与窗宽窗位',
                      rows: [
                        ['Rows', dicomInfo.image.rows],
                        ['Columns', dicomInfo.image.columns],
                        ['Bits Allocated', dicomInfo.image.bitsAllocated],
                        ['Window Center', dicomInfo.image.windowCenter],
                        ['Window Width', dicomInfo.image.windowWidth],
                        ['Rescale', `${dicomInfo.image.rescaleSlope} / ${dicomInfo.image.rescaleIntercept}`],
                      ],
                    },
                    {
                      title: '空间距离',
                      rows: [
                        ['像素行间距', `${displayDicomValue(dicomInfo.spacing.row)} mm`],
                        ['像素列间距', `${displayDicomValue(dicomInfo.spacing.column)} mm`],
                        ['切片间距', `${displayDicomValue(dicomInfo.spacing.slice)} mm`],
                        ['间距来源', dicomInfo.spacing.sliceSource],
                        ['切片厚度', `${displayDicomValue(dicomInfo.spacing.sliceThickness)} mm`],
                        ['Spacing Between Slices', `${displayDicomValue(dicomInfo.spacing.spacingBetweenSlices)} mm`],
                      ],
                    },
                    {
                      title: '物理尺寸',
                      rows: [
                        ['宽度', `${displayDicomValue(dicomInfo.physicalSize.width)} ${dicomInfo.physicalSize.unit}`],
                        ['高度', `${displayDicomValue(dicomInfo.physicalSize.height)} ${dicomInfo.physicalSize.unit}`],
                        ['深度', `${displayDicomValue(dicomInfo.physicalSize.depth)} ${dicomInfo.physicalSize.unit}`],
                      ],
                    },
                    {
                      title: '空间位置',
                      rows: [
                        ['首张位置', dicomInfo.position.firstImagePosition?.join(', ') ?? '未知'],
                        ['末张位置', dicomInfo.position.lastImagePosition?.join(', ') ?? '未知'],
                      ],
                    },
                  ].map((section) => (
                    <div key={section.title} className="rounded-2xl bg-slate-50 border border-slate-100 p-4">
                      <h4 className="text-xs font-bold text-slate-800 mb-3">{section.title}</h4>
                      <div className="space-y-2">
                        {section.rows.map(([label, value]) => (
                          <div key={label} className="flex items-start justify-between gap-4 text-xs">
                            <span className="text-slate-400 shrink-0">{label}</span>
                            <span className="text-slate-700 font-semibold text-right break-all">{displayDicomValue(value)}</span>
                          </div>
                        ))}
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </div>
          </div>
        </div>
      )}

      {projectToDelete && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/40 backdrop-blur-sm">
          <div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-2xl border border-slate-100">
            <h3 className="font-bold text-slate-900 mb-2">确认删除项目</h3>
            <p className="text-sm text-slate-500 leading-6">
              将删除项目“{projectToDelete.name}”。该操作会从项目列表移除项目，需要恢复默认演示项目时可使用出厂设置。
            </p>
            <div className="mt-6 flex justify-end gap-3">
              <button
                onClick={() => setProjectToDelete(null)}
                className="px-4 py-2 rounded-xl text-sm font-bold text-slate-600 hover:bg-slate-100"
              >
                取消
              </button>
              <button
                onClick={handleDeleteProject}
                className="px-4 py-2 rounded-xl text-sm font-bold bg-rose-600 text-white hover:bg-rose-700"
              >
                确认删除
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
