2026-05-20-21-25-19 项目库结果视图与加载缓存优化

This commit is contained in:
2026-05-20 21:47:18 +08:00
parent 6c9787803c
commit cc137437bc
7 changed files with 643 additions and 96 deletions

View File

@@ -26,6 +26,9 @@ import { api, downloadDicomArchive, downloadProjectExportBundle, ProjectExportTa
import {
FusionThreeView,
VoxelizationMappingView,
getCachedDicomFusionVolume,
getCachedDicomPreview,
getCachedModelPreview,
dicomOpacityOptions as reverseDicomOpacityOptions,
displayOptions as reverseDisplayOptions,
} from './ReverseWorkspace';
@@ -433,11 +436,7 @@ function NativeStlViewer({
Promise.allSettled(
visibleFiles.map((fileName) =>
fetch(`/api/projects/${projectId}/models/${encodeURIComponent(fileName)}/preview?limit=3500`)
.then((response) => {
if (!response.ok) throw new Error('模型预览数据加载失败');
return response.json() as Promise<ModelPreviewPayload>;
})
getCachedModelPreview(projectId, fileName, 3500)
.then((payload) => ({
payload,
style: styles[fileName] ?? { color: '#3b82f6', opacity: 0.72, visible: true, partId: 1 },
@@ -490,13 +489,7 @@ function NativeStlViewer({
const loadedBounds: Array<{ min: THREE.Vector3; max: THREE.Vector3 }> = [];
visibleFiles.forEach((fileName) => {
fetch(`/api/projects/${projectId}/models/${encodeURIComponent(fileName)}/preview?limit=${detailLimit}`)
.then((response) => {
if (!response.ok) {
throw new Error('模型预览数据加载失败');
}
return response.json() as Promise<ModelPreviewPayload>;
})
getCachedModelPreview(projectId, fileName, detailLimit)
.then((payload) => {
if (disposed) return;
const geometry = new THREE.BufferGeometry();
@@ -697,11 +690,11 @@ export default function ProjectLibrary({
preloadedProjectIdsRef.current.add(project.id);
const maxSlice = Math.max((project.dicomCount || 1) - 1, 0);
if (project.dicomCount > 0) {
void api.getDicomPreview(project.id, maxSlice, 'axial', 'default').catch(() => undefined);
void api.getDicomFusionVolume(project.id, maxSlice, maxSlice, 'soft').catch(() => undefined);
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 fetch(`/api/projects/${project.id}/models/${encodeURIComponent(fileName)}/preview?limit=3500`).catch(() => undefined);
void getCachedModelPreview(project.id, fileName, 3500).catch(() => undefined);
});
};
@@ -860,7 +853,7 @@ export default function ProjectLibrary({
dicomRequestRef.current = requestId;
setDicomError('');
setIsSliceChanging(true);
api.getDicomPreview(selectedProject.id, sliceIndex, plane, displayMode)
getCachedDicomPreview(selectedProject.id, sliceIndex, plane, displayMode)
.then((preview) => {
if (!cancelled && requestId === dicomRequestRef.current) {
setDicomPreview(preview);
@@ -891,7 +884,7 @@ export default function ProjectLibrary({
const start = Math.min(resultCutStart, resultCutEnd);
const end = Math.max(resultCutStart, resultCutEnd);
setResultFusionError('');
api.getDicomFusionVolume(selectedProject.id, start, end, 'soft')
getCachedDicomFusionVolume(selectedProject.id, start, end, 'soft')
.then((volume) => {
if (!cancelled) {
setResultFusionVolume(volume);
@@ -1566,8 +1559,8 @@ export default function ProjectLibrary({
)}
{viewMode === 'mask' && (
<div className="h-full grid grid-cols-1 gap-6 2xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_340px]">
<div className="min-h-[560px]">
<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}
@@ -1581,9 +1574,10 @@ export default function ProjectLibrary({
cutEnabled={latestSegmentationResult.cutEnabled ?? false}
cutStart={resultCutStart}
cutEnd={resultCutEnd}
viewPreset="libraryResult"
/>
) : (
<div className="flex h-full min-h-[560px] 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 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>
)}
@@ -1594,42 +1588,7 @@ export default function ProjectLibrary({
)}
</div>
<div className="min-h-[560px]">
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<h3 className="flex items-center gap-2 font-bold text-slate-700">
<Layers size={18} className="text-cyan-500" />
</h3>
<div className="flex flex-wrap items-center justify-end gap-1.5">
<div className="flex rounded-xl bg-slate-100 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-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
}`}
>
{mode.label}
</button>
))}
</div>
<button
onClick={() => setResultRotation((value) => (value + 270) % 360)}
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-500 hover:text-cyan-600"
title="左转 90°"
>
<RotateCcw size={14} />
</button>
<button
onClick={() => setResultRotation((value) => (value + 90) % 360)}
className="flex h-7 w-7 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-500 hover:text-cyan-600"
title="右转 90°"
>
<RotateCw size={14} />
</button>
</div>
</div>
<div className="flex min-h-[620px] min-w-0 flex-col">
{latestSegmentationResult ? (
<VoxelizationMappingView
project={selectedProject}
@@ -1641,9 +1600,41 @@ export default function ProjectLibrary({
onSliceChange={setResultPreviewSlice}
displayMode={resultDisplayMode}
rotation={resultRotation}
variant="library"
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-[520px] 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 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>
)}