add DICOM slice previews to image library

This commit is contained in:
2026-05-02 23:34:33 +08:00
parent 664fec7485
commit 676ef25106
2 changed files with 131 additions and 22 deletions

View File

@@ -70,6 +70,77 @@ const API_BASE = typeof window === 'undefined'
? 'http://127.0.0.1:8787'
: `${window.location.protocol}//${window.location.hostname}:8787`;
function LibraryDicomPreview({ item }: { item: LibraryItem }) {
const [sliceIndex, setSliceIndex] = useState(Math.max(0, Math.floor((item.fileCount || 1) / 2)));
const [previewImage, setPreviewImage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const count = Math.max(1, item.fileCount || 1);
useEffect(() => {
setSliceIndex(Math.max(0, Math.floor((item.fileCount || 1) / 2)));
}, [item.id, item.fileCount]);
useEffect(() => {
const controller = new AbortController();
setIsLoading(true);
setError('');
fetch(`${API_BASE}/api/library/preview?id=${encodeURIComponent(item.id)}&index=${sliceIndex}`, {
signal: controller.signal
})
.then(async response => {
const data = await response.json();
if (!response.ok) throw new Error(data.error || '预览生成失败');
setPreviewImage(data.image);
})
.catch(error => {
if ((error as Error).name !== 'AbortError') setError((error as Error).message);
})
.finally(() => {
if (!controller.signal.aborted) setIsLoading(false);
});
return () => controller.abort();
}, [item.id, sliceIndex]);
return (
<div className="h-52 bg-slate-950 relative flex items-center justify-center border-b border-slate-100 shadow-inner overflow-hidden">
<div className="absolute top-3 left-4 flex gap-1.5 z-10">
<div className="w-1.5 h-1.5 rounded-full bg-white/25"></div>
<div className="w-1.5 h-1.5 rounded-full bg-white/25"></div>
</div>
<div className="absolute top-3 right-4 z-10">
<span className={`text-[8px] font-black px-2 py-0.5 rounded border ${item.status === 'processed' ? 'bg-green-500/20 text-green-400 border-green-500/30' : 'bg-amber-500/20 text-amber-400 border-amber-500/30'}`}>
{item.status.toUpperCase()}
</span>
</div>
{previewImage && !error ? (
<img src={previewImage} className="w-full h-full object-contain" />
) : (
<div className="text-center text-white/35">
<ImageIcon size={34} className="mx-auto mb-2" />
<p className="text-[10px] font-bold">{error || (isLoading ? '正在生成预览...' : '等待预览')}</p>
</div>
)}
<div className="absolute inset-x-0 bottom-0 p-3 bg-gradient-to-t from-black/85 via-black/45 to-transparent">
<div className="flex items-center justify-between text-[8px] font-mono text-white/65 mb-2 uppercase tracking-[0.18em]">
<span>Axial DICOM</span>
<span>{sliceIndex + 1} / {count}</span>
</div>
<input
type="range"
min="0"
max={count - 1}
value={Math.min(sliceIndex, count - 1)}
onChange={event => setSliceIndex(parseInt(event.target.value, 10))}
className="w-full h-1 accent-blue-500 cursor-pointer"
/>
</div>
</div>
);
}
export default function App() {
// --- Authentication State ---
const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -798,28 +869,7 @@ export default function App() {
<div className="grid grid-cols-4 gap-8">
{libraryData.map(item => (
<div key={item.id} className="bg-white rounded-[2.5rem] border border-slate-200 overflow-hidden flex flex-col group hover:border-blue-400 hover:shadow-2xl hover:shadow-blue-500/10 transition-all duration-300">
{/* Simulated Preview Box */}
<div className={`h-44 ${item.previewColor} relative flex items-center justify-center p-6 border-b border-slate-100 shadow-inner group-hover:scale-[1.02] transition-transform`}>
<div className="absolute top-3 left-4 flex gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-white/20"></div>
<div className="w-1.5 h-1.5 rounded-full bg-white/20"></div>
</div>
{/* Abstract DICOM Scan Visualization */}
<div className="w-full h-full border border-white/5 rounded-xl flex items-center justify-center overflow-hidden">
<div className="w-24 h-24 border-2 border-dashed border-white/10 rounded-full animate-spin-slow opacity-30"></div>
<div className="absolute w-20 h-28 border-2 border-white/20 rounded-[40%] rotate-45 blur-[1px]"></div>
<div className="absolute w-28 h-20 border-2 border-white/20 rounded-[40%] -rotate-45 blur-[1px]"></div>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
<div className="absolute bottom-3 left-4 text-[7px] font-mono text-white/40 uppercase tracking-[0.2em] leading-none">
Axial View :: ID_{item.id.toUpperCase()}
</div>
<div className="absolute top-3 right-4">
<span className={`text-[8px] font-black px-2 py-0.5 rounded border ${item.status === 'processed' ? 'bg-green-500/20 text-green-400 border-green-500/30' : 'bg-amber-500/20 text-amber-400 border-amber-500/30'}`}>
{item.status.toUpperCase()}
</span>
</div>
</div>
<LibraryDicomPreview item={item} />
<div className="p-6 flex flex-col gap-4">
<div className="flex justify-between items-start">