feat: 数据列表交互
This commit is contained in:
parent
5aa710431d
commit
e25ff341cd
|
@ -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;
|
||||
|
|
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";
|
||||
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,8 +48,7 @@ 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">
|
||||
|
@ -75,10 +78,45 @@ 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 >
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user