feat: 数据列表交互

This commit is contained in:
mozzie 2024-09-11 21:59:47 +08:00
parent 5aa710431d
commit e25ff341cd
4 changed files with 185 additions and 34 deletions

View File

@ -18,10 +18,11 @@ export interface StudyInfo {
MainDicomTags: { MainDicomTags: {
AccessionNumber: string; AccessionNumber: string;
StudyDate: string; StudyDate: string;
StudyDescription: string; ReferringPhysicianName: string;
StudyID: string; StudyID: string;
StudyInstanceUID: string; StudyInstanceUID: string;
StudyTime: string; StudyTime: string;
InstitutionName: string
}; };
ParentPatient: string; ParentPatient: string;
Series: string[]; Series: string[];
@ -34,16 +35,19 @@ export interface SeriesInfo {
ID: string; ID: string;
Instances: string[]; Instances: string[];
MainDicomTags: { MainDicomTags: {
Manufacturer: string; ContrastBolusAgent: string; // 对比剂类型,用于增强成像质量
Modality: string; ImageOrientationPatient: string; // 患者图像的方向参数
NumberOfSlices: string; Manufacturer: string; // 设备制造商
ProtocolName: string; Modality: string; // 医疗成像的模态类型,如 CT、MRI 等
SeriesDate: string; OperatorsName: string; // 操作者的名称
SeriesDescription: string; SeriesDate: string; // 成像系列的日期
SeriesInstanceUID: string; SeriesInstanceUID: string; // 成像系列的唯一标识符
SeriesNumber: string; SeriesNumber: number; // 成像系列的编号
SeriesTime: string; SeriesTime: string; // 成像系列的具体时间
StationName: string; SeriesDescription: string // 描述
BodyPartExamined: string // 检查的身体部位
ProtocolName: string // 成像协议
StationName: string // 成像操作的设备站点
}; };
ParentStudy: string; ParentStudy: string;
Status: string; Status: string;

View File

@ -0,0 +1,103 @@
import { Card } from "@/components/ui/card"
import {
Carousel,
CarouselContent,
CarouselItem,
type CarouselApi,
} from "@/components/ui/carousel"
import { EllipsisIcon, FileDown, Rotate3DIcon, Sparkles, Torus } from "lucide-react";
import { useEffect, useState } from "react"
import { SeriesInfo } from "./type"
import { Badge } from "@/components/ui/badge"
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuShortcut, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
export type CarouselAction = 'viewMpr' | 'view3D' | 'exportMeaurement' | 'viewReport'
interface CarouselSeriesProps {
seriesList: SeriesInfo[]
onClickItem?: (series: SeriesInfo, action: CarouselAction) => void
}
export function CarouselSeries(props: CarouselSeriesProps) {
const [api, setApi] = useState<CarouselApi>()
const [current, setCurrent] = useState(0)
const [count, setCount] = useState(0)
useEffect(() => {
if (!api) return
setCount(api.scrollSnapList().length)
setCurrent(api.selectedScrollSnap() + 1)
api.on("select", () => {
setCurrent(api.selectedScrollSnap() + 1)
})
}, [api])
return (
<div className="mx-auto">
<Carousel setApi={setApi} className="w-full">
<CarouselContent>
{props.seriesList.map((series) => (
<CarouselItem key={series.ID}>
<Card
className={`flex shadow-none flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent`}
>
<div className="flex w-full flex-col gap-1">
<div className="flex items-center">
<div className="flex items-center gap-2">
<div className="font-semibold">
{series.MainDicomTags.SeriesNumber}
</div>
</div>
<div className="ml-auto text-xs text-foreground">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<EllipsisIcon className="w-4 h-4" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-36">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => props.onClickItem?.(series, 'viewMpr')}>
MPR
<DropdownMenuShortcut><Rotate3DIcon className="w-4 h-4" /></DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => props.onClickItem?.(series, 'view3D')}>
3D
<DropdownMenuShortcut><Torus className="w-4 h-4" /></DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => props.onClickItem?.(series, 'exportMeaurement')}>
<DropdownMenuShortcut><FileDown className="w-4 h-4" /></DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => props.onClickItem?.(series, 'viewReport')}>
<DropdownMenuShortcut><Sparkles className="w-4 h-4" /></DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="text-xs font-medium">
{series.MainDicomTags.SeriesDescription}
</div>
</div>
<div className="line-clamp-2 text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<Badge variant="secondary">{series.Instances.length}</Badge>
{series.MainDicomTags.BodyPartExamined && <Badge variant="secondary">{series.MainDicomTags.BodyPartExamined}</Badge>}
</div>
</div>
</Card>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="text-center text-sm text-muted-foreground">
{current} / {count}
</div>
</div>
)
}

View File

@ -7,21 +7,25 @@ import {
} from "@/components/ui/resizable"; } from "@/components/ui/resizable";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { PatientInfo } from "./type"; import { PatientInfo, SeriesInfo } from "./type";
import { CarouselAction, CarouselSeries } from "./CarouselSeries";
export const Datasource = () => { export const Datasource = () => {
const [patients, setPatients] = useState< const [patients, setPatients] = useState<PatientInfo[]>([]);
(PatientInfo & { active: boolean })[]
>([]);
useEffect(() => { useEffect(() => {
window.ipcRenderer window.ipcRenderer
.invoke("dicom:select") .invoke("dicom:select")
.then((patients: PatientInfo[]) => { .then((patients: PatientInfo[]) => {
console.log(patients); console.log(patients);
setPatients(patients.map((p) => ({ ...p, active: false }))); setPatients(patients.map((p) => ({ ...p, active: false, children: p.children.map(s => ({ ...s, active: false })) })));
}); });
}, []); }, []);
const onClickSeriesItem = (series: SeriesInfo, action: CarouselAction) => {
console.log(action, series)
}
return ( return (
<motion.div <motion.div
className="h-full" className="h-full"
@ -44,9 +48,8 @@ export const Datasource = () => {
p.map((i) => ({ ...i, active: i.ID === patient.ID })) p.map((i) => ({ ...i, active: i.ID === patient.ID }))
) )
} }
className={`flex shadow-none flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent hover:cursor-pointer ${ className={`flex shadow-none flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent hover:cursor-pointer ${patient.active ? "bg-accent" : ""
patient.active ? "bg-accent" : "" }`}
}`}
> >
<div className="flex w-full flex-col gap-1"> <div className="flex w-full flex-col gap-1">
<div className="flex items-center"> <div className="flex items-center">
@ -75,12 +78,47 @@ export const Datasource = () => {
</ResizablePanel> </ResizablePanel>
<ResizableHandle withHandle /> <ResizableHandle withHandle />
<ResizablePanel defaultSize={24}> <ResizablePanel defaultSize={24}>
123 <div className="flex flex-col h-full">
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
{patients.find(p => p.active) && patients.find(p => p.active)?.children.map(study => <Card
key={study.ID}
onClick={() =>
setPatients((p) =>
p.map((i) => ({ ...i, children: i.children.map(s => ({ ...s, active: s.ID === study.ID })) }))
)
}
className={`flex shadow-none flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent hover:cursor-pointer ${study.active ? "bg-accent" : ""}`}
>
<div className="flex w-full flex-col gap-1">
<div className="flex items-center">
<div className="flex items-center gap-2">
<div className="font-semibold">
{study.MainDicomTags.StudyID}
</div>
</div>
<div className="ml-auto text-xs text-foreground">
{study.MainDicomTags.StudyDate}
</div>
</div>
</div>
<div className="line-clamp-2 text-xs text-muted-foreground">
{study.MainDicomTags.InstitutionName}
</div>
<div className="w-full py-2">
<CarouselSeries seriesList={study.children} onClickItem={onClickSeriesItem} />
</div>
</Card>)}
</ScrollArea>
</div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle withHandle /> <ResizableHandle withHandle />
<ResizablePanel defaultSize={52}>33</ResizablePanel> <ResizablePanel defaultSize={52}>
<div className="px-4">
123
</div>
</ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
</div> </div>
</motion.div> </motion.div >
); );
}; };

View File

@ -11,6 +11,7 @@ export interface PatientInfo {
Studies: string[]; Studies: string[];
Type: string; Type: string;
children: StudyInfo[]; children: StudyInfo[];
active: boolean
} }
export interface StudyInfo { export interface StudyInfo {
@ -18,14 +19,16 @@ export interface StudyInfo {
MainDicomTags: { MainDicomTags: {
AccessionNumber: string; AccessionNumber: string;
StudyDate: string; StudyDate: string;
StudyDescription: string; ReferringPhysicianName: string;
StudyID: string; StudyID: string;
StudyInstanceUID: string; StudyInstanceUID: string;
StudyTime: string; StudyTime: string;
InstitutionName: string
}; };
ParentPatient: string; ParentPatient: string;
Series: string[]; Series: string[];
Type: string; Type: string;
active: boolean
children: SeriesInfo[]; children: SeriesInfo[];
} }
@ -34,17 +37,20 @@ export interface SeriesInfo {
ID: string; ID: string;
Instances: string[]; Instances: string[];
MainDicomTags: { MainDicomTags: {
Manufacturer: string; ContrastBolusAgent: string; // 对比剂类型,用于增强成像质量
Modality: string; ImageOrientationPatient: string; // 患者图像的方向参数
NumberOfSlices: string; Manufacturer: string; // 设备制造商
ProtocolName: string; Modality: string; // 医疗成像的模态类型,如 CT、MRI 等
SeriesDate: string; OperatorsName: string; // 操作者的名称
SeriesDescription: string; SeriesDate: string; // 成像系列的日期
SeriesInstanceUID: string; SeriesInstanceUID: string; // 成像系列的唯一标识符
SeriesNumber: string; SeriesNumber: number; // 成像系列的编号
SeriesTime: string; SeriesTime: string; // 成像系列的具体时间
StationName: string; SeriesDescription: string // 描述
}; BodyPartExamined: string // 检查的身体部位
ProtocolName: string // 成像协议
StationName: string // 成像操作的设备站点
}
ParentStudy: string; ParentStudy: string;
Status: string; Status: string;
Type: string; Type: string;