2026-05-08-03-03-52 修正DICOM切片范围为单范围条

This commit is contained in:
2026-05-08 03:07:21 +08:00
parent 22b0a93654
commit 765e4cc41a
6 changed files with 255 additions and 24 deletions

View File

@@ -818,6 +818,8 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) {
const safeSliceEnd = clamp(sliceEnd, 0, maxSlice);
const displayStart = Math.min(safeSliceStart, safeSliceEnd);
const displayEnd = Math.max(safeSliceStart, safeSliceEnd);
const rangeStartPercent = maxSlice > 0 ? (displayStart / maxSlice) * 100 : 0;
const rangeEndPercent = maxSlice > 0 ? (displayEnd / maxSlice) * 100 : 0;
const selectedDisplay = displayOptions.find((item) => item.id === displayLevel) ?? displayOptions[0];
const selectedDicomOpacity = dicomOpacityOptions.find((item) => item.id === dicomOpacityLevel) ?? dicomOpacityOptions[0];
const preloadPoints = [0.2, 0.4, 0.6, 0.8, 1].map((ratio) => clamp(Math.max(0, Math.round((project?.dicomCount ?? 1) * ratio) - 1), 0, maxSlice));
@@ -924,30 +926,43 @@ export default function ReverseWorkspace({ projectId }: { projectId: string }) {
{displayStart + 1} - {displayEnd + 1} / {project?.dicomCount ?? 0}
</span>
</div>
<label className="grid grid-cols-[76px_1fr_64px] items-center gap-3 text-[10px] font-bold text-slate-500">
<input
type="range"
min="0"
max={maxSlice}
value={safeSliceStart}
onChange={(event) => setSliceStart(Number(event.target.value))}
className="accent-blue-600"
/>
<span className="text-right font-mono">{safeSliceStart + 1}</span>
</label>
<label className="mt-2 grid grid-cols-[76px_1fr_64px] items-center gap-3 text-[10px] font-bold text-slate-500">
<input
type="range"
min="0"
max={maxSlice}
value={safeSliceEnd}
onChange={(event) => setSliceEnd(Number(event.target.value))}
className="accent-blue-600"
/>
<span className="text-right font-mono">{safeSliceEnd + 1}</span>
</label>
<div className="py-1">
<div className="relative h-10">
<div className="absolute inset-x-0 top-1/2 h-2 -translate-y-1/2 rounded-full bg-slate-200" />
<div
className="absolute top-1/2 h-2 -translate-y-1/2 rounded-full bg-blue-600"
style={{
left: `${rangeStartPercent}%`,
right: `${100 - rangeEndPercent}%`,
}}
/>
<input
type="range"
aria-label="DICOM 切片范围起点"
min="0"
max={maxSlice}
value={safeSliceStart}
onChange={(event) => setSliceStart(Number(event.target.value))}
className="dicom-range-input"
style={{ zIndex: safeSliceStart >= safeSliceEnd ? 5 : 4 }}
/>
<input
type="range"
aria-label="DICOM 切片范围终点"
min="0"
max={maxSlice}
value={safeSliceEnd}
onChange={(event) => setSliceEnd(Number(event.target.value))}
className="dicom-range-input"
style={{ zIndex: safeSliceStart >= safeSliceEnd ? 4 : 5 }}
/>
</div>
<div className="mt-1 grid grid-cols-3 text-[10px] font-bold text-slate-500">
<span> {safeSliceStart + 1}</span>
<span className="text-center text-blue-600"></span>
<span className="text-right"> {safeSliceEnd + 1}</span>
</div>
</div>
<p className="mt-3 text-[10px] leading-5 text-slate-400">
M-N姿
</p>

View File

@@ -1 +1,61 @@
@import "tailwindcss";
.dicom-range-input {
appearance: none;
-webkit-appearance: none;
background: transparent;
height: 100%;
inset: 0;
pointer-events: none;
position: absolute;
width: 100%;
}
.dicom-range-input:focus {
outline: none;
}
.dicom-range-input::-webkit-slider-runnable-track {
background: transparent;
border: 0;
height: 8px;
}
.dicom-range-input::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
background: #2563eb;
border: 3px solid #ffffff;
border-radius: 9999px;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.28);
cursor: grab;
height: 20px;
margin-top: -6px;
pointer-events: auto;
width: 20px;
}
.dicom-range-input::-moz-range-track {
background: transparent;
border: 0;
height: 8px;
}
.dicom-range-input::-moz-range-thumb {
background: #2563eb;
border: 3px solid #ffffff;
border-radius: 9999px;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.28);
cursor: grab;
height: 14px;
pointer-events: auto;
width: 14px;
}
.dicom-range-input:active::-webkit-slider-thumb {
cursor: grabbing;
}
.dicom-range-input:active::-moz-range-thumb {
cursor: grabbing;
}

View File

@@ -0,0 +1,53 @@
# 实现方案:单条 DICOM 范围滑块
时间戳2026-05-08-03-03-52
## 修改目标
将逆向体素化工作区“DICOM 切片范围”从两个上下排列的原生进度条,改为一条自绘轨道叠加两个可拖动端点的范围条。
## 涉及路径
- `WebSite/src/components/ReverseWorkspace.tsx`
- `WebSite/src/index.css`
- `工程分析/经验记录.md`
## 技术路线
1.`ReverseWorkspace.tsx` 中基于 `safeSliceStart``safeSliceEnd``displayStart``displayEnd` 计算范围条起止百分比。
2. 替换原有两个 `<label><input type="range"></label>`
3. 使用一个 `relative` 容器绘制:
- 灰色底轨;
- 蓝色选中范围;
- 两个透明轨道的 `range input`,只显示滑块端点。
4.`index.css` 中新增 `.dicom-range-input` 样式,隐藏原生轨道,保留 thumb并让 thumb 可点击拖动。
5. 端点重合时提高“起点”滑块层级,便于从默认 `300-300` 状态向左拖出范围。
## 数据流与交互流程
- 用户拖动起点端点:调用 `setSliceStart(Number(event.target.value))`
- 用户拖动终点端点:调用 `setSliceEnd(Number(event.target.value))`
- 当前显示范围继续由 `displayStart = min(start, end)``displayEnd = max(start, end)` 决定。
- 后续 `loadFusionVolume(displayStart/displayEnd)` 相关逻辑保持现状。
## 兼容性与回滚方案
- 若自定义样式在目标浏览器表现异常,可回滚本次对 `ReverseWorkspace.tsx``index.css` 的修改,恢复两个原生 range。
- 本次不修改后端 API 和数据结构,回滚风险较低。
## 风险控制
- 保留无障碍 `aria-label`,明确起点和终点端点。
- 通过 `npm run lint``npm run build` 检查 TypeScript 与构建。
- 部署后通过页面源码或构建产物搜索确认不存在“起点/终点两个 label + 两个进度条”的旧结构。
## 预计文件变更
- 新增本次需求、实现、测试方案文档。
- 修改 `ReverseWorkspace.tsx` 中 DICOM 范围控件 JSX。
- 修改 `index.css` 增加范围条浏览器兼容样式。
- 追加 `经验记录.md`
## 人工审核状态
用户已在项目工作流历史中确认后续直接执行,本次不等待二次人工审核。

View File

@@ -0,0 +1,45 @@
# 测试方案DICOM 单范围条修复
时间戳2026-05-08-03-03-52
## 静态检查
- 执行 `npm run lint`,验证 TypeScript 类型检查通过。
- 执行 `npm run build`,验证 Vite 生产构建通过。
## 关键业务场景验证
- 打开逆向体素化工作区查看“DICOM 切片范围”只显示一条范围轨道。
- 默认 `300 - 300 / 300` 状态下,拖动起点端点可向左形成 `M - 300 / 300`
- 拖动终点端点可更新终点,起点和终点允许交叉,显示值仍自动按小到大输出。
- 变化范围后,三维融合视角仍加载对应 DICOM 切片范围。
## 医学影像数据边界验证
- DICOM 总数为 1 时,范围条不除以 0端点显示 `1 - 1 / 1`
- DICOM 总数为 300 时,最大端点仍为第 300 张。
## 回归风险
- 本次只改 UI 控件,不改 DICOM 数据读取和 STL 叠加逻辑。
- 需要确认自定义 range 样式不会影响同页面其它原生滑块。
## 验收标准
- 页面不再出现上下两条“起点/终点”蓝色进度条。
- `ReverseWorkspace.tsx` 中不再保留旧的两个 label range 结构。
- 构建和重新部署成功。
## 无法测试的风险
- 当前无法在用户浏览器中直接确认缓存是否清除;部署后若仍看到旧界面,需要强制刷新浏览器缓存。
## 人工审核状态
用户已在项目工作流历史中确认后续直接执行,本次不等待二次人工审核。
## 执行结果
- `npm run lint`:通过。
- `npm run build`:通过;仅出现 Vite chunk 大小提示,不影响运行。
- `rg` 验证:`ReverseWorkspace.tsx` 中旧的 `grid grid-cols-[76px_1fr_64px]` 双 range 结构已移除,构建产物包含新的 `dicom-range-input` 单范围条结构。

View File

@@ -721,3 +721,21 @@ C. 解决问题方案
D. 后续如何避免问题
所有配准相关可视化都必须区分“显示范围”和“空间基准”:显示范围可以变化,但 DICOM 物理尺寸、模型缩放基准和坐标原点不能跟着变化;任何切割或 Mask 预览都要明确只是辅助显示,不能修改原始 DICOM/STL 位姿。
## 2026-05-08-03-03-52 项目定位错误导致控件未修复
A. 具体问题
用户要求将“DICOM 切片范围”的起点、终点合并为一个范围条,但页面仍显示两个上下排列的进度条。
B. 产生问题原因
前一次处理时依据当前工作目录误改了 `Head_CT_Morph`,而用户截图中的真实页面来自 `ReVoxelSeg_DICOM/WebSite/src/components/ReverseWorkspace.tsx`。没有先用截图中的页面文案和 DOM 片段全局定位真实渲染文件,导致改动没有作用到用户看到的程序。
C. 解决问题方案
`/home/wkmgc/Desktop` 范围内搜索 `DICOM 切片范围``三维融合场景已就绪`,定位到正确项目 `ReVoxelSeg_DICOM`。将 `ReverseWorkspace.tsx` 中两个独立 `range` 替换为单条自绘轨道叠加两个端点的范围控件,并在 `index.css` 中隐藏原生 range 轨道、保留可拖动 thumb。
D. 后续如何避免问题
收到 UI 截图或 DOM 片段时先用页面可见文案、class 片段和组件文本在所有相关项目目录中定位真实源码,再修改代码。若当前仓库内找不到截图中的文本,必须立即扩大搜索范围并向用户说明实际项目位置,不能默认当前工作目录就是目标项目。

View File

@@ -0,0 +1,40 @@
# 需求分析:修正 DICOM 切片范围控件
时间戳2026-05-08-03-03-52
## 原始需求
用户反馈页面仍然显示“起点”“终点”两个独立进度条,并指出当前截图中的 DOM
- `起点 <input type="range" ...>`
- `终点 <input type="range" ...>`
要求将其改成一个范围控件,而不是上下两条进度条。
## 业务目标
- 在逆向体素化工作区的“DICOM 切片范围”中,用一个范围条承载起点和终点两个端点。
- 起点、终点仍能独立拖动,并允许调整顺序。
- 显示范围仍使用 `M - N / 总数`,不改变 DICOM/STL 数据加载逻辑。
## 影响范围
- 页面路径:逆向体素化/影像与模型融合视角。
- 主要文件:`WebSite/src/components/ReverseWorkspace.tsx`
- 样式文件:`WebSite/src/index.css`
## 约束
- 必须保留现有 `sliceStart``sliceEnd` 状态和 `displayStart/displayEnd` 归一化逻辑。
- 只修复当前控件形态不改动模型切割、Mask 生成和数据 API。
- 本次是对上次误改错误项目的纠正,必须确认真实渲染文件来自 `ReVoxelSeg_DICOM`
## 风险点
- 双滑块叠在同一个轨道上时,端点相同的情况下可能难以拖动。
- 原生 range 轨道默认样式可能在浏览器中显示为两条蓝线,需用 CSS 隐藏原生轨道并自绘单条轨道。
- Vite 开发服务需要重新部署到当前项目实际端口 `4000`
## 待确认事项
- 用户已在历史工作流中确认后续直接执行;本次不再停等人工二次确认。