diff --git a/README.md b/README.md
index 0fbfbdd..f9cadee 100644
--- a/README.md
+++ b/README.md
@@ -3,3 +3,36 @@
基于模型逆向体素化及 DICOM 分割标注系统。
工程流程与分析文档位于 `工程分析/`。
+
+## 项目最重要达成目标
+
+本项目的核心目标是:输入 DICOM 影像序列和重建 STL 模型后,将模型反向体素化并对齐到 DICOM 空间,最终生成可被医学影像工具读取的分割 Mask,例如 `.nii` 或 `.nii.gz`。
+
+当前系统已经具备:
+
+- 前后端协调服务。
+- 跨浏览器共享登录状态。
+- 默认载入 `Head_CT_DICOM` 与 `Head_CT_ReConstruct`。
+- DICOM 切片预览。
+- STL 模型预览。
+- 分割结果展示与 NIfTI 演示导出。
+
+## 构建运行
+
+进入前端服务目录:
+
+```bash
+cd WebSite
+npm ci
+npm run lint
+npm run build
+npm run serve -- --host 0.0.0.0 --port 4000
+```
+
+访问地址:
+
+```text
+http://192.168.3.11:4000/
+```
+
+当前版本不需要 Python/conda。后续接入真实医学级 STL 体素化算法时,可新建 `revoxelseg` conda 环境并安装 SimpleITK、nibabel、numpy、trimesh、vtk 等算法依赖。
diff --git a/WebSite/README.md b/WebSite/README.md
index 28d49ea..ae8ae58 100644
--- a/WebSite/README.md
+++ b/WebSite/README.md
@@ -1,20 +1,62 @@
-
+
最近30天
-
-
+
+ {chartData.length > 0 && (
+
@@ -87,7 +88,7 @@ export default function Overview() {
-
+
+ )}
@@ -113,8 +115,9 @@ export default function Overview() {
-
-
+
+ {chartData.length > 0 && (
+
@@ -124,7 +127,7 @@ export default function Overview() {
-
+
+ )}
diff --git a/WebSite/src/components/ProjectLibrary.tsx b/WebSite/src/components/ProjectLibrary.tsx
index 5ecfda4..94ddbdc 100644
--- a/WebSite/src/components/ProjectLibrary.tsx
+++ b/WebSite/src/components/ProjectLibrary.tsx
@@ -1,53 +1,106 @@
-import React, { useEffect, useMemo, useState } from 'react';
-import { motion, AnimatePresence } from 'motion/react';
+import React, { Suspense, useEffect, useMemo, useRef, useState } from 'react';
import {
Plus,
Search,
- MoreHorizontal,
Eye,
RotateCw,
- FileText,
Box,
Image as ImageIcon,
ChevronRight,
- Filter,
- Trash2,
Edit2,
FolderRoot,
- Download
+ Download,
+ Layers,
+ Save,
+ X
} from 'lucide-react';
-import { Canvas } from '@react-three/fiber';
-import { OrbitControls, Stage, Gltf, useGLTF, Environment, PerspectiveCamera } from '@react-three/drei';
-import { Project } from '../types';
-import { api } from '../lib/api';
+import { Canvas, useLoader } from '@react-three/fiber';
+import { Bounds, Center, OrbitControls, Stage } from '@react-three/drei';
+import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
+import * as THREE from 'three';
+import { DicomPreview, Project } from '../types';
+import { api, downloadMask } from '../lib/api';
+
+function StlModel({ url }: { url: string }) {
+ const geometry = useLoader(STLLoader, url);
-// Mock 3D Model component
-function ModelPreview() {
return (
-
-
-
+
+
);
}
+function DicomCanvas({ preview }: { preview: DicomPreview }) {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) {
+ return;
+ }
+ const context = canvas.getContext('2d');
+ if (!context) {
+ return;
+ }
+
+ const binary = atob(preview.pixels);
+ const imageData = context.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;
+ }
+ context.putImageData(imageData, 0, 0);
+ }, [preview]);
+
+ return (
+
+ );
+}
+
export default function ProjectLibrary({ onReverse }: { onReverse: (projId: string) => void }) {
const [search, setSearch] = useState('');
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedProject, setSelectedProject] = useState(null);
- const [viewMode, setViewMode] = useState<'dicom' | 'model'>('dicom');
+ const [viewMode, setViewMode] = useState<'dicom' | 'model' | 'mask'>('dicom');
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
- const [sliceIndex, setSliceIndex] = useState(42);
+ const [sliceIndex, setSliceIndex] = useState(0);
const [visibleModules, setVisibleModules] = useState>({});
+ const [dicomPreview, setDicomPreview] = useState(null);
+ const [dicomError, setDicomError] = useState('');
+ const [selectedModelFile, setSelectedModelFile] = useState('');
+ const [newProjectName, setNewProjectName] = useState('');
+ const [editingProjectId, setEditingProjectId] = useState('');
+ const [editingName, setEditingName] = useState('');
+ const [actionMessage, setActionMessage] = useState('');
- useEffect(() => {
- api.getProjects()
+ const refreshProjects = () => {
+ setLoading(true);
+ return api.getProjects()
.then((items) => {
setProjects(items);
- setSelectedProject(items[0] ?? null);
+ setSelectedProject((current) => {
+ if (!current) {
+ return items[0] ?? null;
+ }
+ return items.find((item) => item.id === current.id) ?? items[0] ?? null;
+ });
})
.finally(() => setLoading(false));
+ };
+
+ useEffect(() => {
+ refreshProjects();
}, []);
const filteredProjects = useMemo(() => {
@@ -68,7 +121,35 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
next[module] = visibleModules[module] ?? true;
});
setVisibleModules(next);
+ setSelectedModelFile(selectedProject?.stlFiles?.[0] ?? '');
+ setSliceIndex(0);
}, [selectedProject?.id]);
+
+ useEffect(() => {
+ if (!selectedProject || viewMode !== 'dicom' || !selectedProject.dicomCount) {
+ setDicomPreview(null);
+ return;
+ }
+
+ let cancelled = false;
+ setDicomError('');
+ api.getDicomPreview(selectedProject.id, sliceIndex)
+ .then((preview) => {
+ if (!cancelled) {
+ setDicomPreview(preview);
+ }
+ })
+ .catch((error) => {
+ if (!cancelled) {
+ setDicomPreview(null);
+ setDicomError(error instanceof Error ? error.message : 'DICOM 预览失败');
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [selectedProject?.id, selectedProject?.dicomCount, sliceIndex, viewMode]);
const toggleModule = (name: string) => {
setVisibleModules(prev => ({ ...prev, [name]: !prev[name] }));
@@ -81,6 +162,39 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
setVisibleModules(newState);
};
+ const handleCreateProject = async () => {
+ const name = newProjectName.trim();
+ if (!name) {
+ setActionMessage('请输入项目名称');
+ return;
+ }
+ const created = await api.createProject(name);
+ setNewProjectName('');
+ 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 tabs = [
+ { id: 'dicom' as const, label: 'DICOM 影像', icon: ImageIcon },
+ { id: 'model' as const, label: '3D 模型', icon: Box },
+ { id: 'mask' as const, label: '分割结果', icon: Layers },
+ ];
+
return (
{/* Project Sidebar - Collapsible */}
@@ -98,7 +212,36 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
{!isSidebarCollapsed && (
-
项目列表
+
+
+ setNewProjectName(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ handleCreateProject();
+ }
+ }}
+ placeholder="新项目名称"
+ className="min-w-0 flex-1 px-3 py-2 bg-slate-50 border-none rounded-lg text-xs focus:ring-1 focus:ring-blue-500 outline-none"
+ />
+
+
{loading &&
正在从后端载入项目...
}
{filteredProjects.map((proj) => (
-
+
))}
+ {actionMessage &&
{actionMessage}
}
)}
@@ -154,22 +352,17 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
<>
- setViewMode('dicom')}
- className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${
- viewMode === 'dicom' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
- }`}
- >
- DICOM 影像
-
- setViewMode('model')}
- className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${
- viewMode === 'model' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
- }`}
- >
- 3D 模型
-
+ {tabs.map((tab) => (
+ 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.label}
+
+ ))}
- {viewMode === 'dicom' ? (
+ {viewMode === 'dicom' && (
{/* Left: DICOM Viewer */}
@@ -195,12 +388,14 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
DICOM PATH: {selectedProject.dicomPath}
-
-
-
DCM RENDER VIEW | #{sliceIndex}
+ {dicomPreview ? (
+
+ ) : (
+
{dicomError || '正在解析 DICOM 像素...'}
+ )}
- WW/WL: 400/40
+ WW/WL: {dicomPreview?.windowWidth ?? 400}/{dicomPreview?.windowCenter ?? 40}
SLICE: {sliceIndex}/{selectedProject.dicomCount}
@@ -216,14 +411,32 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
#{sliceIndex}
- ) : (
+ )}
+
+ {viewMode === 'model' && (
{/* Left: 3D Visualization */}
@@ -246,11 +459,12 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
{subModules.map((m, i) => (
setSelectedModelFile(selectedProject.stlFiles?.[i] ?? '')}
+ className={`p-3 rounded-xl border flex items-center gap-3 group transition-all cursor-pointer ${
+ selectedProject.stlFiles?.[i] === selectedModelFile ? 'bg-blue-50 border-blue-100' : 'bg-slate-50 border-transparent hover:border-slate-200'
} ${!visibleModules[m] ? 'opacity-50' : ''}`}
>
-
+
@@ -275,6 +489,50 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
)}
+
+ {viewMode === 'mask' && (
+
+
+
+ {['#3b82f6', '#22c55e', '#f59e0b'].map((color, index) => (
+
+ ))}
+
+
+ SEGMENTATION MASK PREVIEW · NII/NII.GZ
+
+
+
+
+
分割结果
+
+ 当前项目可导出 NIfTI 格式分割 mask。NII.GZ 为默认全量导出格式,适合后续医学影像工具链读取。
+
+
+
downloadMask(selectedProject.id, 'nii.gz')}
+ className="bg-slate-900 text-white px-5 py-3 rounded-xl text-sm font-bold flex items-center justify-center gap-2 hover:bg-black"
+ >
+ 下载 NII.GZ
+
+
downloadMask(selectedProject.id, 'nii')}
+ className="bg-white text-slate-700 px-5 py-3 rounded-xl text-sm font-bold flex items-center justify-center gap-2 hover:bg-slate-50 border border-slate-200"
+ >
+ 下载 NII
+
+
+
+ )}
>
) : (
diff --git a/WebSite/src/lib/api.ts b/WebSite/src/lib/api.ts
index 30e963e..4fd249b 100644
--- a/WebSite/src/lib/api.ts
+++ b/WebSite/src/lib/api.ts
@@ -1,4 +1,4 @@
-import { OverviewSummary, Project, SessionState, UserRecord } from '../types';
+import { DicomPreview, OverviewSummary, Project, SessionState, UserRecord } from '../types';
async function request
(path: string, options: RequestInit = {}): Promise {
const response = await fetch(path, {
@@ -36,6 +36,18 @@ export const api = {
getOverview: () => request('/api/overview'),
getProjects: () => request('/api/projects'),
getProject: (projectId: string) => request(`/api/projects/${projectId}`),
+ createProject: (name: string) =>
+ request('/api/projects', {
+ method: 'POST',
+ body: JSON.stringify({ name }),
+ }),
+ renameProject: (projectId: string, name: string) =>
+ request(`/api/projects/${projectId}`, {
+ method: 'PATCH',
+ body: JSON.stringify({ name }),
+ }),
+ getDicomPreview: (projectId: string, slice: number) =>
+ request(`/api/projects/${projectId}/dicom-preview?slice=${slice}`),
getUsers: () => request('/api/users'),
resetDemo: () =>
request<{ ok: boolean; projects: Project[]; users: UserRecord[] }>('/api/demo/reset', {
diff --git a/WebSite/src/types.ts b/WebSite/src/types.ts
index cb8ee35..5d51c3d 100644
--- a/WebSite/src/types.ts
+++ b/WebSite/src/types.ts
@@ -18,6 +18,8 @@ export interface Project {
modelCount?: number;
stlFiles?: string[];
maskFormats?: Array<'nii' | 'nii.gz'>;
+ exportedMaskCount?: number;
+ isDefault?: boolean;
}
export interface MaskMapping {
@@ -43,6 +45,7 @@ export interface SessionState {
export interface OverviewSummary {
totalProjects: number;
processedProjects: number;
+ exportedMaskProjects: number;
dicomCount: number;
modelCount: number;
chartData: Array<{
@@ -51,3 +54,14 @@ export interface OverviewSummary {
processing: number;
}>;
}
+
+export interface DicomPreview {
+ width: number;
+ height: number;
+ pixels: string;
+ slice: number;
+ total: number;
+ fileName: string;
+ windowCenter: number;
+ windowWidth: number;
+}
diff --git a/工程分析/实现方案-2026-05-04-03-50-07.md b/工程分析/实现方案-2026-05-04-03-50-07.md
new file mode 100644
index 0000000..ebdac34
--- /dev/null
+++ b/工程分析/实现方案-2026-05-04-03-50-07.md
@@ -0,0 +1,91 @@
+# 实现方案
+
+时间戳:2026-05-04-03-50-07
+
+## 修改目标
+
+修复概况统计、图表警告和模块高亮问题;完善项目库真实 DICOM/STL/分割结果展示;增加项目创建和重命名能力;补齐 README 构建方案并重新部署。
+
+## 涉及路径
+
+- `WebSite/server.ts`
+- `WebSite/src/lib/api.ts`
+- `WebSite/src/types.ts`
+- `WebSite/src/components/Overview.tsx`
+- `WebSite/src/components/ProjectLibrary.tsx`
+- `WebSite/README.md`
+- `README.md`
+- `工程分析/测试方案-2026-05-04-03-50-07.md`
+- `工程分析/经验记录.md`
+
+## 技术路线
+
+1. 后端项目状态增强。
+ - 保留默认项目,同时允许新增用户创建项目。
+ - 新增 `POST /api/projects` 创建项目。
+ - 新增 `PATCH /api/projects/:projectId` 修改项目名称。
+2. 后端 DICOM 预览。
+ - 新增 `GET /api/projects/:projectId/dicom-preview?slice=0`。
+ - 读取当前 DICOM 文件,解析 Rows、Columns、Pixel Data、Window Center/Width、Rescale Slope/Intercept。
+ - 返回 `width`、`height`、`pixels` base64 灰度数据和 slice 元数据。
+3. 后端 STL 文件服务。
+ - 新增 `GET /api/projects/:projectId/models/:fileName`。
+ - 只允许读取项目对应 STL 列表中的文件,避免任意路径读取。
+4. 前端项目库。
+ - DICOM 视图使用 canvas 绘制后端返回灰度像素。
+ - 3D 模型视图使用 Three.js `STLLoader` 加载真实 STL。
+ - 新增 `分割结果` tab,展示 mask 预览、标签图例和 NII/NII.GZ 下载。
+ - 项目列表顶部增加创建按钮和名称输入。
+ - 项目卡右侧增加编辑图标,可重命名。
+ - 移除首个 STL 模块默认蓝色高亮,所有模块同等样式。
+5. 概况页。
+ - 已处理项目改为“已导出 Mask 项目”,避免项目总数 1 时已处理 1 的误导。
+ - 趋势数据改为平稳小范围变化。
+ - Recharts 容器增加 `min-w-0`、固定高度和加载态,避免宽高 -1 警告。
+6. README。
+ - 补充 Node 构建运行方案。
+ - 说明当前无需 Python/conda;真实体素化阶段再引入 Python 环境建议。
+7. 验证与部署。
+ - `npm run lint`
+ - `npm run build`
+ - API smoke test
+ - 重新部署 `tmux` 会话到 `4000`。
+
+## 数据流
+
+DICOM 预览:
+
+前端切片滑块 -> `/api/projects/:id/dicom-preview?slice=n` -> 后端解析 DICOM 像素 -> 前端 canvas 绘制。
+
+STL 预览:
+
+前端选择 STL 模块 -> `/api/projects/:id/models/:fileName` -> `STLLoader` 加载几何体 -> Three.js 渲染。
+
+分割结果:
+
+前端 `分割结果` tab -> 展示 mask 预览 -> 调用已有 `/api/projects/:id/export-mask` 导出 `nii` 或 `nii.gz`。
+
+项目管理:
+
+创建/重命名 -> 后端写入 `state.json` -> 项目列表刷新。
+
+## 兼容性与回滚方案
+
+- DICOM/STL 原始文件仍不提交 Git。
+- 若 DICOM 解析失败,前端显示错误态,不影响其他页面。
+- 若 STL 加载失败,前端显示模型加载失败提示,不影响 DICOM 和分割结果。
+- 回滚时恢复 `server.ts` 和项目库组件即可回到上一版前后端演示状态。
+
+## 预计文件变更
+
+- 修改后端 API。
+- 修改项目库和概况页。
+- 修改 API 类型和封装。
+- 修改 README。
+- 更新工程分析文档和经验记录。
+
+## 人工审核状态
+
+本次用户明确要求无需人工二次确认。
+
+状态:自动确认,继续执行。
diff --git a/工程分析/测试方案-2026-05-04-03-50-07.md b/工程分析/测试方案-2026-05-04-03-50-07.md
new file mode 100644
index 0000000..a54602e
--- /dev/null
+++ b/工程分析/测试方案-2026-05-04-03-50-07.md
@@ -0,0 +1,97 @@
+# 测试方案
+
+时间戳:2026-05-04-03-50-07
+
+## 测试目标
+
+验证概况统计、图表、真实 DICOM 预览、真实 STL 渲染、分割结果视图、项目创建/重命名、README 和部署均正常。
+
+## 静态检查
+
+- 检查 `Overview.tsx` 图表容器是否有稳定尺寸。
+- 检查 `ProjectLibrary.tsx` 是否包含三个 tab:`DICOM 影像`、`3D 模型`、`分割结果`。
+- 检查 STL 模块列表不再对首个模块默认蓝色高亮。
+- 检查 README 是否包含项目构建和运行方案。
+
+## 构建与类型检查
+
+```bash
+cd WebSite
+npm run lint
+npm run build
+```
+
+预期结果:
+
+- TypeScript 检查通过。
+- Vite 构建通过。
+
+## API 验证
+
+```bash
+curl -s http://127.0.0.1:4000/api/overview
+curl -s 'http://127.0.0.1:4000/api/projects/head-ct-demo/dicom-preview?slice=0'
+curl -I 'http://127.0.0.1:4000/api/projects/head-ct-demo/models/会厌.stl'
+curl -s -X POST http://127.0.0.1:4000/api/projects -H 'Content-Type: application/json' -d '{"name":"测试创建项目"}'
+curl -s -X PATCH http://127.0.0.1:4000/api/projects/head-ct-demo -H 'Content-Type: application/json' -d '{"name":"头部 CT 模型逆向体素化演示"}'
+```
+
+预期结果:
+
+- overview 返回平稳趋势和 `exportedMaskProjects`。
+- DICOM preview 返回 `width`、`height`、`pixels`。
+- STL 文件返回 200。
+- 创建项目返回新项目。
+- 重命名项目成功。
+
+## 页面验证
+
+- 总体概况不再显示“已处理项目总数 = 1”的误导文案。
+- 控制台不再出现 Recharts 宽高 -1 警告。
+- 项目库 DICOM 影像显示真实灰度切片。
+- 项目库 3D 模型显示真实 STL。
+- 项目库新增分割结果页,可下载 NII/NII.GZ。
+- 项目列表可创建项目。
+- 项目列表已有项目可点击编辑图标重命名。
+- STL 模块列表所有项目同等样式,没有“会厌”特殊蓝色突出。
+
+## 回归风险
+
+- DICOM 解析兼容性有限,当前先针对本项目 Little Endian CT 数据。
+- STL 文件较大时首次加载可能需要等待。
+- 新建项目默认无 DICOM/STL 数据,作为项目管理演示项。
+
+## 人工审核状态
+
+本次用户明确要求无需人工二次确认。
+
+状态:自动确认,继续执行。
+
+## 执行结果
+
+- `npm run lint` 执行成功。
+- `npm run build` 执行成功。
+- Vite 仍有大 chunk 警告,当前不影响本次功能。
+- `GET /api/overview` 返回:
+ - `totalProjects: 1`
+ - `exportedMaskProjects: 0`
+ - `dicomCount: 300`
+ - `modelCount: 9`
+- 已将“已处理项目”语义改为“已导出 Mask 项目”,避免项目总数为 1 时误导为已处理 1。
+- `GET /api/projects/head-ct-demo/dicom-preview?slice=0` 返回:
+ - `width: 512`
+ - `height: 512`
+ - `total: 300`
+ - `fileName: 1.dcm`
+ - `pixels` base64 数据。
+- `GET /api/projects/head-ct-demo/models/会厌.stl` 返回 `HTTP/1.1 200 OK`。
+- `POST /api/projects` 创建项目成功。
+- `PATCH /api/projects/:id` 重命名项目成功。
+- `POST /api/demo/reset` 执行成功,测试创建项目已清理,默认项目恢复。
+- headless Chrome 打开页面后未捕获 `width(-1)`、`height(-1)` 或 Recharts 相关警告。
+- `http://192.168.3.11:4000/` 返回 `HTTP/1.1 200 OK`。
+- 当前服务由 `tmux` 会话 `revoxelseg-dicom` 托管。
+
+## 本次未创建 Python/conda 环境原因
+
+本次 DICOM 预览、STL 渲染和 NIfTI 演示导出均可由现有 Node/React/Three.js 技术栈完成。为了避免引入不必要的运行环境复杂度,本次不创建 conda 环境;README 已说明后续真实体素化算法阶段再引入 Python/conda。
diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md
index f2ac841..51f0d96 100644
--- a/工程分析/经验记录.md
+++ b/工程分析/经验记录.md
@@ -181,3 +181,93 @@ C. 解决问题方案
D. 后续如何避免问题
多项目并行部署时,除了业务端口外,也检查 Vite HMR 端口;发现冲突时为每个项目分配独立 HMR 端口。
+
+## 2026-05-04-03-50-07 概况统计语义误导
+
+A. 具体问题
+
+项目总数只有 1 时,“已处理项目总数”也显示 1,容易让用户误解为系统已经完成真实处理。
+
+B. 产生问题原因
+
+旧统计把 `status === completed` 直接当作已处理项目,而默认演示项目为了表示数据可用被标记为 `completed`。
+
+C. 解决问题方案
+
+将概况卡片语义改为“已导出 Mask 项目”,后端按 `exportedMaskCount > 0` 统计;默认项目初始为 0,只有实际触发 mask 导出后才计入。
+
+D. 后续如何避免问题
+
+统计卡片必须反映明确业务事件,不要把演示数据可用状态和处理完成状态混在一起。
+
+## 2026-05-04-03-50-07 DICOM 预览解析
+
+A. 具体问题
+
+项目库 DICOM 影像页看不到真实影像,只显示占位图形。
+
+B. 产生问题原因
+
+前端没有读取 DICOM 像素数据,后端也没有提供切片预览 API。
+
+C. 解决问题方案
+
+新增 `GET /api/projects/:projectId/dicom-preview`,后端解析当前 Little Endian DICOM 的 Rows、Columns、Window、Rescale 和 Pixel Data,返回 base64 灰度像素,前端用 canvas 绘制真实切片。
+
+D. 后续如何避免问题
+
+医学影像可视化应优先验证真实像素是否进入浏览器。后续扩展更多 DICOM 传输语法时,应把解析器替换为成熟 DICOM 库或 Python 预处理服务。
+
+## 2026-05-04-03-50-07 STL 模型真实渲染
+
+A. 具体问题
+
+项目库 3D 模型页显示的是占位立方体,不是 `Head_CT_ReConstruct` 中的真实 STL。
+
+B. 产生问题原因
+
+前端没有 STLLoader,后端也没有安全暴露 STL 文件。
+
+C. 解决问题方案
+
+新增 `GET /api/projects/:projectId/models/:fileName`,仅允许读取项目 STL 列表中的文件;前端使用 Three.js `STLLoader`、`Bounds` 和 `Center` 加载并自动居中显示模型。
+
+D. 后续如何避免问题
+
+三维模型视图必须加载真实模型文件,不能长期使用占位几何体;后端文件服务要限制文件名和项目白名单,避免任意路径读取。
+
+## 2026-05-04-03-50-07 Recharts 容器尺寸警告
+
+A. 具体问题
+
+控制台出现 Recharts 警告:图表宽高为 `-1`,需要检查容器尺寸。
+
+B. 产生问题原因
+
+图表在布局尚未稳定时渲染,`ResponsiveContainer` 从父容器拿到异常宽高。
+
+C. 解决问题方案
+
+为图表外层设置 `min-w-0`、固定高度和 `min-h`,并在 chartData 存在后再渲染 `ResponsiveContainer`,高度使用明确数值 `300`。
+
+D. 后续如何避免问题
+
+使用 Recharts 时保证父容器有稳定尺寸;在异步数据未到达前不要提前渲染响应式图表。
+
+## 2026-05-04-03-50-07 Python 环境引入边界
+
+A. 具体问题
+
+用户提出如果需要 Python 可新建 conda 环境,但本次需求可以在现有 Node/React/Three.js 技术栈中完成。
+
+B. 产生问题原因
+
+DICOM/STL/Mask 功能既可以由前端演示链路实现,也可能进入 Python 医学影像算法链路,需要判断当前阶段是否真的需要 Python。
+
+C. 解决问题方案
+
+本次不创建 conda 环境,避免增加不必要复杂度;README 中说明当前构建方式和后续真实体素化阶段的 conda 环境建议。
+
+D. 后续如何避免问题
+
+只有当需要真实 DICOM 空间解析、STL 体素填充、NIfTI 精确写入或批处理算法时,再引入 Python/conda,并把环境文件纳入项目文档。
diff --git a/工程分析/需求分析-2026-05-04-03-50-07.md b/工程分析/需求分析-2026-05-04-03-50-07.md
new file mode 100644
index 0000000..fb39f3c
--- /dev/null
+++ b/工程分析/需求分析-2026-05-04-03-50-07.md
@@ -0,0 +1,69 @@
+# 需求分析
+
+时间戳:2026-05-04-03-50-07
+
+## 原始需求摘要
+
+用户要求严格使用代码编纂工作流处理本次修改,并在开始时复述工作流整体流程;本次需求分析、实现方案、测试方案和执行修改均不需要人工二次确认。
+
+具体问题:
+
+1. 当前项目总数为 1,已处理项目也是 1,统计语义不合理;已处理项目趋势折线图过于夸张。
+2. 项目库中 DICOM 影像和 3D 模型看不到;如需要 Python,可新建 conda 环境,并在 README 写项目构建方案。
+3. 项目库中在 `DICOM 影像`、`3D 模型` 旁边增加 `分割结果`。
+4. 项目列表增加创建功能,可命名项目;已有项目右侧增加修改符号,可修改项目名称。
+5. STL 模块列表中“会厌”不应默认蓝色突出,它和其他模块没有特殊关系。
+6. 修复 Recharts 控制台警告:图表宽高为 -1。
+7. 复述项目最重要达成目标,并完善系统中其他应提升但未完善的地方。
+
+## 业务目标
+
+- 让系统概况统计更符合业务语义,避免误导。
+- 在项目库直接看到真实 DICOM 切片预览和真实 STL 模型预览。
+- 让项目库形成 DICOM、模型、分割结果三类核心资产视图。
+- 增加基础项目管理能力:创建项目、重命名项目。
+- 修复 UI 不合理高亮和控制台警告。
+- 补齐 README 中的构建、运行和部署说明。
+
+## 输入与输出
+
+输入:
+
+- `Head_CT_DICOM/` 下的 DICOM 切片。
+- `Head_CT_ReConstruct/` 下的 STL 模型。
+- 用户创建或重命名项目的名称。
+
+输出:
+
+- 后端 DICOM 切片预览 API。
+- 后端 STL 文件静态读取 API。
+- 前端 DICOM canvas 预览。
+- 前端 STL 模型渲染。
+- 项目库新增 `分割结果` 视图。
+- 项目创建和重命名 API 及前端入口。
+- 修复后的概况统计与图表。
+- README 构建方案。
+
+## 影响范围
+
+- `WebSite/server.ts`
+- `WebSite/src/lib/api.ts`
+- `WebSite/src/types.ts`
+- `WebSite/src/components/Overview.tsx`
+- `WebSite/src/components/ProjectLibrary.tsx`
+- `WebSite/README.md`
+- `README.md`
+- `工程分析/经验记录.md`
+
+## 风险点
+
+- DICOM 文件可能存在不同传输语法。本次优先支持当前数据可见的 Little Endian DICOM,并做保守 fallback。
+- 浏览器渲染 STL 文件需要加载 `three/examples/jsm/loaders/STLLoader.js`,构建需验证 TypeScript/Vite 兼容。
+- 创建项目若不绑定真实数据,会作为空演示项目存在;默认项目仍绑定真实 DICOM/STL。
+- 图表容器警告与布局时机有关,需要给容器设置稳定 `min-w-0`、固定高度和加载态。
+
+## 待确认问题
+
+- 本次用户已明确无需二次人工确认,直接执行。
+- 本次不创建 Python conda 环境,因为 DICOM 预览和 STL 渲染可以通过 Node/React/Three.js 完成。
+- 后续若实现医学级真实体素化,可再引入 Python/conda 处理链。