feat: 数据列表交互
This commit is contained in:
parent
5aa710431d
commit
e25ff341cd
|
@ -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;
|
||||||
|
|
103
apps/desktop/src/pages/Datasource/CarouselSeries.tsx
Normal file
103
apps/desktop/src/pages/Datasource/CarouselSeries.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user