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: {
AccessionNumber: string;
StudyDate: string;
StudyDescription: string;
ReferringPhysicianName: string;
StudyID: string;
StudyInstanceUID: string;
StudyTime: string;
InstitutionName: string
};
ParentPatient: string;
Series: string[];
@ -34,16 +35,19 @@ export interface SeriesInfo {
ID: string;
Instances: string[];
MainDicomTags: {
Manufacturer: string;
Modality: string;
NumberOfSlices: string;
ProtocolName: string;
SeriesDate: string;
SeriesDescription: string;
SeriesInstanceUID: string;
SeriesNumber: string;
SeriesTime: string;
StationName: string;
ContrastBolusAgent: string; // 对比剂类型,用于增强成像质量
ImageOrientationPatient: string; // 患者图像的方向参数
Manufacturer: string; // 设备制造商
Modality: string; // 医疗成像的模态类型,如 CT、MRI 等
OperatorsName: string; // 操作者的名称
SeriesDate: string; // 成像系列的日期
SeriesInstanceUID: string; // 成像系列的唯一标识符
SeriesNumber: number; // 成像系列的编号
SeriesTime: string; // 成像系列的具体时间
SeriesDescription: string // 描述
BodyPartExamined: string // 检查的身体部位
ProtocolName: string // 成像协议
StationName: string // 成像操作的设备站点
};
ParentStudy: 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";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Card } from "@/components/ui/card";
import { PatientInfo } from "./type";
import { PatientInfo, SeriesInfo } from "./type";
import { CarouselAction, CarouselSeries } from "./CarouselSeries";
export const Datasource = () => {
const [patients, setPatients] = useState<
(PatientInfo & { active: boolean })[]
>([]);
const [patients, setPatients] = useState<PatientInfo[]>([]);
useEffect(() => {
window.ipcRenderer
.invoke("dicom:select")
.then((patients: PatientInfo[]) => {
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 (
<motion.div
className="h-full"
@ -44,9 +48,8 @@ export const Datasource = () => {
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 ${
patient.active ? "bg-accent" : ""
}`}
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" : ""
}`}
>
<div className="flex w-full flex-col gap-1">
<div className="flex items-center">
@ -75,12 +78,47 @@ export const Datasource = () => {
</ResizablePanel>
<ResizableHandle withHandle />
<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>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={52}>33</ResizablePanel>
<ResizablePanel defaultSize={52}>
<div className="px-4">
123
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
</motion.div>
</motion.div >
);
};

View File

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