feat: 处理很多小bug
This commit is contained in:
parent
0e7d05d9a4
commit
2897d02c22
|
@ -29,7 +29,7 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
|||
|
||||
let win: BrowserWindow | null;
|
||||
let tray: Tray | null = null;
|
||||
const theme: "dark" | "light" = "light";
|
||||
const theme: "dark" | "light" = "dark";
|
||||
|
||||
const themeTitleBarStyles = {
|
||||
dark: { color: "rgb(32,32,32)", symbolColor: "#fff" },
|
||||
|
|
44
apps/desktop/src/pages/Datasource/PatientCard.tsx
Normal file
44
apps/desktop/src/pages/Datasource/PatientCard.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { Card } from "@/components/ui/card";
|
||||
import { PatientInfo } from "./type";
|
||||
|
||||
interface PatientCardProps {
|
||||
patient: PatientInfo;
|
||||
isSelected: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const PatientCard = ({
|
||||
patient,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: PatientCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
key={patient.ID}
|
||||
onClick={() => onSelect(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 ${
|
||||
isSelected ? "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">
|
||||
{patient.MainDicomTags.PatientName}
|
||||
</div>
|
||||
<span className="flex h-2 w-2 rounded-full bg-blue-600"></span>
|
||||
</div>
|
||||
<div className="ml-auto text-xs text-foreground">
|
||||
{patient.MainDicomTags.PatientSex}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-medium">
|
||||
{patient.MainDicomTags.PatientBirthDate}
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-clamp-2 text-xs text-muted-foreground">
|
||||
上次更新: {patient.LastUpdate}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
30
apps/desktop/src/pages/Datasource/PatientList.tsx
Normal file
30
apps/desktop/src/pages/Datasource/PatientList.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { PatientCard } from "./PatientCard";
|
||||
import { PatientInfo } from "./type";
|
||||
|
||||
interface PatientListProps {
|
||||
patients: PatientInfo[];
|
||||
selectedPatientId: string | null;
|
||||
onSelectPatient: (id: string) => void;
|
||||
}
|
||||
|
||||
export const PatientList = ({
|
||||
patients,
|
||||
selectedPatientId,
|
||||
onSelectPatient,
|
||||
}: PatientListProps) => {
|
||||
return (
|
||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{patients.map((patient) => (
|
||||
<PatientCard
|
||||
key={patient.ID}
|
||||
patient={patient}
|
||||
isSelected={patient.ID === selectedPatientId}
|
||||
onSelect={onSelectPatient}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
94
apps/desktop/src/pages/Datasource/SeriesCard.tsx
Normal file
94
apps/desktop/src/pages/Datasource/SeriesCard.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
EllipsisIcon,
|
||||
FileDown,
|
||||
Rotate3DIcon,
|
||||
Sparkles,
|
||||
Torus,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { SeriesInfo } from "./type";
|
||||
|
||||
interface SeriesCardProps {
|
||||
series: SeriesInfo;
|
||||
onAction: (action: string, series: SeriesInfo) => void;
|
||||
}
|
||||
|
||||
export const SeriesCard = ({ series, onAction }: SeriesCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
key={series.MainDicomTags.SeriesInstanceUID}
|
||||
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 flex-col gap-1 w-full">
|
||||
<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={() => onAction("viewMpr", series)}>
|
||||
MPR 阅片
|
||||
<DropdownMenuShortcut>
|
||||
<Rotate3DIcon className="w-4 h-4" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAction("view3D", series)}>
|
||||
3D 重建
|
||||
<DropdownMenuShortcut>
|
||||
<Torus className="w-4 h-4" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onAction("exportMeasurement", series)}
|
||||
>
|
||||
导出测量
|
||||
<DropdownMenuShortcut>
|
||||
<FileDown className="w-4 h-4" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onAction("viewReport", series)}
|
||||
>
|
||||
查看报告
|
||||
<DropdownMenuShortcut>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-clamp-2 text-xs text-muted-foreground">
|
||||
<span>{series.MainDicomTags.SeriesDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-clamp-2 text-xs text-muted-foreground flex gap-x-2">
|
||||
<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>
|
||||
);
|
||||
};
|
24
apps/desktop/src/pages/Datasource/SeriesList.tsx
Normal file
24
apps/desktop/src/pages/Datasource/SeriesList.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { SeriesCard } from "./SeriesCard";
|
||||
import { SeriesInfo } from "./type";
|
||||
|
||||
interface SeriesListProps {
|
||||
seriesList: SeriesInfo[];
|
||||
onAction: (action: string, series: SeriesInfo) => void;
|
||||
}
|
||||
|
||||
export const SeriesList = ({ seriesList, onAction }: SeriesListProps) => {
|
||||
return (
|
||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{seriesList.map((series) => (
|
||||
<SeriesCard
|
||||
key={series.MainDicomTags.SeriesInstanceUID}
|
||||
series={series}
|
||||
onAction={onAction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
37
apps/desktop/src/pages/Datasource/StudyCard.tsx
Normal file
37
apps/desktop/src/pages/Datasource/StudyCard.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
// StudyCard.tsx
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { StudyInfo } from "./type";
|
||||
|
||||
interface StudyCardProps {
|
||||
study: StudyInfo;
|
||||
isSelected: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const StudyCard = ({ study, isSelected, onSelect }: StudyCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
key={study.ID}
|
||||
onClick={() => onSelect(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 ${
|
||||
isSelected ? "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>
|
||||
</Card>
|
||||
);
|
||||
};
|
30
apps/desktop/src/pages/Datasource/StudyList.tsx
Normal file
30
apps/desktop/src/pages/Datasource/StudyList.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { StudyCard } from "./StudyCard";
|
||||
import { StudyInfo } from "./type";
|
||||
|
||||
interface StudyListProps {
|
||||
studies: StudyInfo[];
|
||||
selectedStudyId: string | null;
|
||||
onSelectStudy: (id: string) => void;
|
||||
}
|
||||
|
||||
export const StudyList = ({
|
||||
studies,
|
||||
selectedStudyId,
|
||||
onSelectStudy,
|
||||
}: StudyListProps) => {
|
||||
return (
|
||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{studies.map((study) => (
|
||||
<StudyCard
|
||||
key={study.ID}
|
||||
study={study}
|
||||
isSelected={study.ID === selectedStudyId}
|
||||
onSelect={onSelectStudy}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
|
@ -1,41 +1,26 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
// Datasource.tsx
|
||||
import { useEffect, useRef, useState, useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { PatientInfo, SeriesInfo, StudyInfo } from "./type";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search } from "lucide-react";
|
||||
import {
|
||||
EllipsisIcon,
|
||||
FileDown,
|
||||
Rotate3DIcon,
|
||||
Sparkles,
|
||||
Torus,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface SeriesClickItem {
|
||||
action: "viewMpr" | "view3D" | "exportMeaurement" | "viewReport";
|
||||
series: SeriesInfo;
|
||||
}
|
||||
import { PatientInfo, SeriesInfo } from "./type";
|
||||
import { PatientList } from "./PatientList";
|
||||
import { StudyList } from "./StudyList";
|
||||
import { SeriesList } from "./SeriesList";
|
||||
|
||||
export const Datasource = () => {
|
||||
const rawPatientsRef = useRef<PatientInfo[]>([]);
|
||||
const [patients, setPatients] = useState<PatientInfo[]>([]);
|
||||
const [selectedPatientId, setSelectedPatientId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -44,43 +29,47 @@ export const Datasource = () => {
|
|||
.then((patients: PatientInfo[]) => {
|
||||
console.log(patients);
|
||||
rawPatientsRef.current = patients;
|
||||
setPatients(
|
||||
patients.map((p) => ({
|
||||
...p,
|
||||
active: false,
|
||||
children: p.children.map((s) => ({ ...s, active: false })),
|
||||
}))
|
||||
);
|
||||
setPatients(patients);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onClickItem = (p: SeriesClickItem) => {
|
||||
const activeStudy = patients
|
||||
.find((p) => p.active)
|
||||
?.children.find((s) => s.active);
|
||||
const StudyInstanceUID = activeStudy?.MainDicomTags.StudyInstanceUID;
|
||||
const SeriesInstanceUID = p.series.MainDicomTags.SeriesInstanceUID;
|
||||
const handlePatientSearch = (filterValue: string) => {
|
||||
const raw = rawPatientsRef.current;
|
||||
const filteredPatients = raw.filter((p) =>
|
||||
p.MainDicomTags.PatientName.toUpperCase().includes(
|
||||
filterValue.toUpperCase()
|
||||
)
|
||||
);
|
||||
setPatients(filteredPatients);
|
||||
if (!filteredPatients.find((p) => p.ID === selectedPatientId)) {
|
||||
setSelectedPatientId(null);
|
||||
setSelectedStudyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedPatient = useMemo(() => {
|
||||
return patients.find((p) => p.ID === selectedPatientId) || null;
|
||||
}, [patients, selectedPatientId]);
|
||||
|
||||
const selectedStudy = useMemo(() => {
|
||||
return (
|
||||
selectedPatient?.children.find((s) => s.ID === selectedStudyId) || null
|
||||
);
|
||||
}, [selectedPatient, selectedStudyId]);
|
||||
|
||||
const handleSeriesAction = (action: string, series: SeriesInfo) => {
|
||||
const StudyInstanceUID = selectedStudy?.MainDicomTags.StudyInstanceUID;
|
||||
const SeriesInstanceUID = series.MainDicomTags.SeriesInstanceUID;
|
||||
const query = `StudyInstanceUID=${StudyInstanceUID}&SeriesInstanceUID=${SeriesInstanceUID}`;
|
||||
switch (p.action) {
|
||||
switch (action) {
|
||||
case "viewMpr":
|
||||
navigate(`/viewer?${query}`, { replace: true });
|
||||
break;
|
||||
|
||||
// 其他操作逻辑
|
||||
default:
|
||||
break;
|
||||
}
|
||||
console.log(StudyInstanceUID, p.action, p.series);
|
||||
};
|
||||
|
||||
const handlePatientSearch = (filterValue: string) => {
|
||||
const raw = rawPatientsRef.current;
|
||||
setPatients(
|
||||
raw.filter((p) =>
|
||||
p.MainDicomTags.PatientName.toUpperCase().includes(
|
||||
filterValue.toUpperCase()
|
||||
)
|
||||
)
|
||||
);
|
||||
console.log(StudyInstanceUID, action, series);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -93,7 +82,7 @@ export const Datasource = () => {
|
|||
>
|
||||
<div className="p-4 h-full flex flex-col">
|
||||
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
|
||||
<ResizablePanel defaultSize={24}>
|
||||
<ResizablePanel defaultSize={100 / 3}>
|
||||
<div className="flex flex-col h-full pt-2">
|
||||
<div className="px-4 pb-2">
|
||||
<div className="relative">
|
||||
|
@ -105,178 +94,37 @@ export const Datasource = () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{patients.map((patient) => (
|
||||
<Card
|
||||
key={patient.ID}
|
||||
onClick={() =>
|
||||
setPatients((p) =>
|
||||
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" : ""
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
{patient.MainDicomTags.PatientName}
|
||||
</div>
|
||||
<span className="flex h-2 w-2 rounded-full bg-blue-600"></span>
|
||||
</div>
|
||||
<div className="ml-auto text-xs text-foreground">
|
||||
{patient.MainDicomTags.PatientSex}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-medium">
|
||||
{patient.MainDicomTags.PatientBirthDate}
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-clamp-2 text-xs text-muted-foreground">
|
||||
上次更新: {patient.LastUpdate}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<PatientList
|
||||
patients={patients}
|
||||
selectedPatientId={selectedPatientId}
|
||||
onSelectPatient={(id) => {
|
||||
setSelectedPatientId(id);
|
||||
setSelectedStudyId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={24}>
|
||||
<ResizablePanel defaultSize={100 / 3}>
|
||||
<div className="flex flex-col h-full">
|
||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
||||
<div className="flex flex-col gap-y-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>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
{selectedPatient && (
|
||||
<StudyList
|
||||
studies={selectedPatient.children}
|
||||
selectedStudyId={selectedStudyId}
|
||||
onSelectStudy={(id) => setSelectedStudyId(id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={52}>
|
||||
<ResizablePanel defaultSize={100 / 3}>
|
||||
<div className="flex flex-col h-full">
|
||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{patients.find((p) => p.active) &&
|
||||
patients
|
||||
.find((p) => p.active)
|
||||
?.children.find((study) => study.active)
|
||||
?.children.map((series) => (
|
||||
<Card
|
||||
key={series.MainDicomTags.SeriesInstanceUID}
|
||||
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 flex-col gap-1 w-full">
|
||||
<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={() =>
|
||||
onClickItem({
|
||||
series,
|
||||
action: "viewMpr",
|
||||
})
|
||||
}
|
||||
>
|
||||
MPR 阅片
|
||||
<DropdownMenuShortcut>
|
||||
<Rotate3DIcon className="w-4 h-4" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
3D 重建
|
||||
<DropdownMenuShortcut>
|
||||
<Torus className="w-4 h-4" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
导出测量
|
||||
<DropdownMenuShortcut>
|
||||
<FileDown className="w-4 h-4" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
查看报告
|
||||
<DropdownMenuShortcut>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-clamp-2 text-xs text-muted-foreground">
|
||||
<span>
|
||||
{series.MainDicomTags.SeriesDescription}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-clamp-2 text-xs text-muted-foreground flex gap-x-2">
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
{selectedStudy && (
|
||||
<SeriesList
|
||||
seriesList={selectedStudy.children}
|
||||
onAction={handleSeriesAction}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as cornerstoneTools from "@cornerstonejs/tools";
|
||||
import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js";
|
||||
import { createImageIdsAndCacheMetaData } from "./CornerstoneDicomLoader/createImageIdsAndCacheMetaData";
|
||||
|
@ -20,17 +20,12 @@ import {
|
|||
ViewportId,
|
||||
} from "./Crosshair.config";
|
||||
|
||||
import GridLayout from 'react-grid-layout';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { DraftingCompassIcon, MoveIcon } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
const {
|
||||
ToolGroupManager,
|
||||
CrosshairsTool,
|
||||
StackScrollMouseWheelTool,
|
||||
WindowLevelTool,
|
||||
ZoomTool,
|
||||
Enums: csToolsEnums,
|
||||
} = cornerstoneTools;
|
||||
|
||||
|
@ -72,14 +67,22 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
|||
const viewportRef_SAGITTAL = useRef<HTMLDivElement | null>(null);
|
||||
const viewportRef_CORONAL = useRef<HTMLDivElement | null>(null);
|
||||
const renderingEngine = useRef<RenderingEngine>();
|
||||
const toolGroupIdRef = useRef<string>("");
|
||||
const toolGroupRef = useRef<cornerstoneTools.Types.IToolGroup | undefined>(
|
||||
undefined
|
||||
);
|
||||
const imageIds = useRef<string[]>();
|
||||
const ts = "-" + Date.now();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
cornerstoneTools.addTool(StackScrollMouseWheelTool);
|
||||
cornerstoneTools.addTool(CrosshairsTool);
|
||||
cornerstoneTools.addTool(WindowLevelTool);
|
||||
cornerstoneTools.addTool(ZoomTool);
|
||||
|
||||
const toolGroupId = props.SeriesInstanceUID + ts;
|
||||
toolGroupIdRef.current = toolGroupId;
|
||||
toolGroupRef.current = ToolGroupManager.createToolGroup(toolGroupId);
|
||||
|
||||
const run = async () => {
|
||||
if (
|
||||
|
@ -92,9 +95,6 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
|||
renderingEngine.current = new RenderingEngine(
|
||||
props.SeriesInstanceUID + ts
|
||||
);
|
||||
const toolGroup = ToolGroupManager.createToolGroup(
|
||||
props.SeriesInstanceUID + ts
|
||||
);
|
||||
|
||||
imageIds.current = await createImageIdsAndCacheMetaData({
|
||||
StudyInstanceUID: props.StudyInstanceUID,
|
||||
|
@ -124,7 +124,7 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
|||
element: viewportRef_AXIAL.current,
|
||||
defaultOptions: {
|
||||
orientation: CoreEnums.OrientationAxis.AXIAL,
|
||||
background: [0, 0, 0],
|
||||
background: [0, 0, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -169,38 +169,59 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
|||
[viewportId1, viewportId2, viewportId3]
|
||||
);
|
||||
|
||||
if (toolGroup) {
|
||||
if (toolGroupRef.current) {
|
||||
// For the crosshairs to operate, the viewports must currently be
|
||||
// added ahead of setting the tool active. This will be improved in the future.
|
||||
toolGroup.addViewport(viewportId1, props.SeriesInstanceUID + ts);
|
||||
toolGroup.addViewport(viewportId2, props.SeriesInstanceUID + ts);
|
||||
toolGroup.addViewport(viewportId3, props.SeriesInstanceUID + ts);
|
||||
toolGroupRef.current.addViewport(
|
||||
viewportId1,
|
||||
props.SeriesInstanceUID + ts
|
||||
);
|
||||
toolGroupRef.current.addViewport(
|
||||
viewportId2,
|
||||
props.SeriesInstanceUID + ts
|
||||
);
|
||||
toolGroupRef.current.addViewport(
|
||||
viewportId3,
|
||||
props.SeriesInstanceUID + ts
|
||||
);
|
||||
|
||||
/**
|
||||
* zoom影像
|
||||
*/
|
||||
toolGroupRef.current.addTool(ZoomTool.toolName);
|
||||
toolGroupRef.current.setToolActive(ZoomTool.toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Secondary, // 鼠标中键
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Manipulation Tools
|
||||
toolGroup.addTool(StackScrollMouseWheelTool.toolName);
|
||||
toolGroupRef.current.addTool(StackScrollMouseWheelTool.toolName);
|
||||
// Add Crosshairs tool and configure it to link the three viewports
|
||||
// These viewports could use different tool groups. See the PET-CT example
|
||||
// for a more complicated used case.
|
||||
|
||||
toolGroup.addTool(CrosshairsTool.toolName, {
|
||||
toolGroupRef.current.addTool(CrosshairsTool.toolName, {
|
||||
getReferenceLineColor,
|
||||
getReferenceLineControllable,
|
||||
getReferenceLineDraggableRotatable,
|
||||
getReferenceLineSlabThicknessControlsOn,
|
||||
});
|
||||
|
||||
toolGroup.setToolActive(CrosshairsTool.toolName, {
|
||||
toolGroupRef.current.setToolActive(CrosshairsTool.toolName, {
|
||||
bindings: [{ mouseButton: 1 }],
|
||||
});
|
||||
// As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback`
|
||||
// hook instead of mouse buttons, it does not need to assign any mouse button.
|
||||
toolGroup.setToolActive(StackScrollMouseWheelTool.toolName);
|
||||
toolGroupRef.current.setToolActive(StackScrollMouseWheelTool.toolName);
|
||||
|
||||
toolGroup.addTool(WindowLevelTool.toolName);
|
||||
toolGroup.setToolActive(WindowLevelTool.toolName, {
|
||||
toolGroupRef.current.addTool(WindowLevelTool.toolName);
|
||||
toolGroupRef.current.setToolActive(WindowLevelTool.toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Secondary,
|
||||
mouseButton: MouseBindings.Auxiliary,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -218,37 +239,50 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
|||
}
|
||||
|
||||
return () => {
|
||||
// 禁用视口
|
||||
renderingEngine.current?.disableElement(viewportId1);
|
||||
renderingEngine.current?.disableElement(viewportId2);
|
||||
renderingEngine.current?.disableElement(viewportId3);
|
||||
|
||||
// 销毁渲染引擎
|
||||
renderingEngine.current?.destroy();
|
||||
|
||||
// 从 ToolGroupManager 中移除工具组
|
||||
ToolGroupManager.destroyToolGroup(toolGroupIdRef.current);
|
||||
|
||||
// 移出工具注册
|
||||
cornerstoneTools.removeTool(StackScrollMouseWheelTool);
|
||||
cornerstoneTools.removeTool(CrosshairsTool);
|
||||
cornerstoneTools.removeTool(WindowLevelTool);
|
||||
renderingEngine.current?.destroy();
|
||||
ToolGroupManager.destroyToolGroup(props.SeriesInstanceUID + ts);
|
||||
cornerstoneTools.removeTool(ZoomTool);
|
||||
};
|
||||
}, [props, ts]);
|
||||
|
||||
/**
|
||||
* mpr resize
|
||||
*/
|
||||
useEffect(() => {
|
||||
const resize = () => renderingEngine.current?.resize()
|
||||
window.addEventListener("resize", resize);
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
let resizeTimeout: NodeJS.Timeout | null = null;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
console.log("mpr resize");
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => renderingEngine.current?.resize(), 100);
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
return () => {
|
||||
window.removeEventListener("resize", resize);
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
resizeObserver.unobserve(container);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onDBClickViewport = (viewportRef) => {
|
||||
console.log("dblcik", viewportRef);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full h-full flex flex-col">
|
||||
<Card className="rounded-none h-1/3">
|
||||
<div className="w-full h-full" onDoubleClick={() => onDBClickViewport(viewportRef_AXIAL)} ref={viewportRef_AXIAL}></div>
|
||||
</Card>
|
||||
<Card className="rounded-none h-1/3">
|
||||
<div className="w-full h-full" ref={viewportRef_SAGITTAL}></div>
|
||||
</Card>
|
||||
<Card className="rounded-none h-1/3">
|
||||
<div className="w-full h-full" ref={viewportRef_CORONAL}></div>
|
||||
</Card>
|
||||
<div className="w-full h-1/3" ref={viewportRef_AXIAL} />
|
||||
<div className="w-full h-1/3" ref={viewportRef_SAGITTAL} />
|
||||
<div className="w-full h-1/3" ref={viewportRef_CORONAL} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { MoonIcon, PersonStanding } from "lucide-react";
|
||||
|
||||
export const ToolBarMenu = () => {
|
||||
return (
|
||||
<div className="flex gap-x-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoonIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>窗宽/窗位</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<PersonStanding className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>MIP</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -2,7 +2,12 @@ import { useLocation } from "react-router-dom";
|
|||
import { initCornerstone } from "./MprViewer/CornerstoneDicomLoader/init";
|
||||
import { CrosshairMpr } from "./MprViewer/Crosshair";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import { ToolBarMenu } from "./MprViewer/ToolBarMenu";
|
||||
|
||||
export interface CurrentDicom {
|
||||
SeriesInstanceUID: string | null;
|
||||
|
@ -27,7 +32,7 @@ export const Viewer = () => {
|
|||
}, [currentDicom]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(window.location.href)
|
||||
console.log(window.location.href);
|
||||
initCornerstone(() => {
|
||||
setCornerstoneLoaded(true);
|
||||
});
|
||||
|
@ -37,19 +42,42 @@ export const Viewer = () => {
|
|||
console.log(cornerstoneLoaded);
|
||||
}, [cornerstoneLoaded]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex">
|
||||
<div className="w-2/3 h-full">vr</div>
|
||||
<div className="w-1/3 h-full">
|
||||
{cornerstoneLoaded &&
|
||||
currentDicom.StudyInstanceUID &&
|
||||
currentDicom.SeriesInstanceUID && (
|
||||
<CrosshairMpr
|
||||
StudyInstanceUID={currentDicom.StudyInstanceUID}
|
||||
SeriesInstanceUID={currentDicom.SeriesInstanceUID}
|
||||
/>
|
||||
)}</div>
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<div className="flex-shrink-0 border-b border-secondary">
|
||||
<ToolBarMenu />
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
|
||||
<ResizablePanel defaultSize={61.8}>
|
||||
<div className="h-full">
|
||||
<ResizablePanelGroup
|
||||
direction="vertical"
|
||||
className="w-full h-full"
|
||||
>
|
||||
<ResizablePanel defaultSize={50}>
|
||||
<div>top</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={50}>
|
||||
<div>bototm</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={38.2}>
|
||||
{cornerstoneLoaded &&
|
||||
currentDicom.StudyInstanceUID &&
|
||||
currentDicom.SeriesInstanceUID && (
|
||||
<CrosshairMpr
|
||||
StudyInstanceUID={currentDicom.StudyInstanceUID}
|
||||
SeriesInstanceUID={currentDicom.SeriesInstanceUID}
|
||||
/>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user