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 win: BrowserWindow | null;
|
||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
const theme: "dark" | "light" = "light";
|
const theme: "dark" | "light" = "dark";
|
||||||
|
|
||||||
const themeTitleBarStyles = {
|
const themeTitleBarStyles = {
|
||||||
dark: { color: "rgb(32,32,32)", symbolColor: "#fff" },
|
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 { motion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable";
|
} 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 { Input } from "@/components/ui/input";
|
||||||
import { Search } from "lucide-react";
|
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";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { PatientInfo, SeriesInfo } from "./type";
|
||||||
interface SeriesClickItem {
|
import { PatientList } from "./PatientList";
|
||||||
action: "viewMpr" | "view3D" | "exportMeaurement" | "viewReport";
|
import { StudyList } from "./StudyList";
|
||||||
series: SeriesInfo;
|
import { SeriesList } from "./SeriesList";
|
||||||
}
|
|
||||||
|
|
||||||
export const Datasource = () => {
|
export const Datasource = () => {
|
||||||
const rawPatientsRef = useRef<PatientInfo[]>([]);
|
const rawPatientsRef = useRef<PatientInfo[]>([]);
|
||||||
const [patients, setPatients] = useState<PatientInfo[]>([]);
|
const [patients, setPatients] = useState<PatientInfo[]>([]);
|
||||||
|
const [selectedPatientId, setSelectedPatientId] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -44,43 +29,47 @@ export const Datasource = () => {
|
||||||
.then((patients: PatientInfo[]) => {
|
.then((patients: PatientInfo[]) => {
|
||||||
console.log(patients);
|
console.log(patients);
|
||||||
rawPatientsRef.current = patients;
|
rawPatientsRef.current = patients;
|
||||||
setPatients(
|
setPatients(patients);
|
||||||
patients.map((p) => ({
|
|
||||||
...p,
|
|
||||||
active: false,
|
|
||||||
children: p.children.map((s) => ({ ...s, active: false })),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onClickItem = (p: SeriesClickItem) => {
|
const handlePatientSearch = (filterValue: string) => {
|
||||||
const activeStudy = patients
|
const raw = rawPatientsRef.current;
|
||||||
.find((p) => p.active)
|
const filteredPatients = raw.filter((p) =>
|
||||||
?.children.find((s) => s.active);
|
p.MainDicomTags.PatientName.toUpperCase().includes(
|
||||||
const StudyInstanceUID = activeStudy?.MainDicomTags.StudyInstanceUID;
|
filterValue.toUpperCase()
|
||||||
const SeriesInstanceUID = p.series.MainDicomTags.SeriesInstanceUID;
|
)
|
||||||
|
);
|
||||||
|
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}`;
|
const query = `StudyInstanceUID=${StudyInstanceUID}&SeriesInstanceUID=${SeriesInstanceUID}`;
|
||||||
switch (p.action) {
|
switch (action) {
|
||||||
case "viewMpr":
|
case "viewMpr":
|
||||||
navigate(`/viewer?${query}`, { replace: true });
|
navigate(`/viewer?${query}`, { replace: true });
|
||||||
break;
|
break;
|
||||||
|
// 其他操作逻辑
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
console.log(StudyInstanceUID, p.action, p.series);
|
console.log(StudyInstanceUID, action, series);
|
||||||
};
|
|
||||||
|
|
||||||
const handlePatientSearch = (filterValue: string) => {
|
|
||||||
const raw = rawPatientsRef.current;
|
|
||||||
setPatients(
|
|
||||||
raw.filter((p) =>
|
|
||||||
p.MainDicomTags.PatientName.toUpperCase().includes(
|
|
||||||
filterValue.toUpperCase()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -93,7 +82,7 @@ export const Datasource = () => {
|
||||||
>
|
>
|
||||||
<div className="p-4 h-full flex flex-col">
|
<div className="p-4 h-full flex flex-col">
|
||||||
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
|
<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="flex flex-col h-full pt-2">
|
||||||
<div className="px-4 pb-2">
|
<div className="px-4 pb-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
@ -105,178 +94,37 @@ export const Datasource = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
<PatientList
|
||||||
<div className="flex flex-col gap-y-2">
|
patients={patients}
|
||||||
{patients.map((patient) => (
|
selectedPatientId={selectedPatientId}
|
||||||
<Card
|
onSelectPatient={(id) => {
|
||||||
key={patient.ID}
|
setSelectedPatientId(id);
|
||||||
onClick={() =>
|
setSelectedStudyId(null);
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
<ResizablePanel defaultSize={24}>
|
<ResizablePanel defaultSize={100 / 3}>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
{selectedPatient && (
|
||||||
<div className="flex flex-col gap-y-2">
|
<StudyList
|
||||||
{patients.find((p) => p.active) &&
|
studies={selectedPatient.children}
|
||||||
patients
|
selectedStudyId={selectedStudyId}
|
||||||
.find((p) => p.active)
|
onSelectStudy={(id) => setSelectedStudyId(id)}
|
||||||
?.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>
|
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
<ResizablePanel defaultSize={52}>
|
<ResizablePanel defaultSize={100 / 3}>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
{selectedStudy && (
|
||||||
<div className="flex flex-col gap-y-2">
|
<SeriesList
|
||||||
{patients.find((p) => p.active) &&
|
seriesList={selectedStudy.children}
|
||||||
patients
|
onAction={handleSeriesAction}
|
||||||
.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>
|
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import * as cornerstoneTools from "@cornerstonejs/tools";
|
import * as cornerstoneTools from "@cornerstonejs/tools";
|
||||||
import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js";
|
import { PublicViewportInput } from "@cornerstonejs/core/dist/types/types/IViewport.js";
|
||||||
import { createImageIdsAndCacheMetaData } from "./CornerstoneDicomLoader/createImageIdsAndCacheMetaData";
|
import { createImageIdsAndCacheMetaData } from "./CornerstoneDicomLoader/createImageIdsAndCacheMetaData";
|
||||||
|
@ -20,17 +20,12 @@ import {
|
||||||
ViewportId,
|
ViewportId,
|
||||||
} from "./Crosshair.config";
|
} 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 {
|
const {
|
||||||
ToolGroupManager,
|
ToolGroupManager,
|
||||||
CrosshairsTool,
|
CrosshairsTool,
|
||||||
StackScrollMouseWheelTool,
|
StackScrollMouseWheelTool,
|
||||||
WindowLevelTool,
|
WindowLevelTool,
|
||||||
|
ZoomTool,
|
||||||
Enums: csToolsEnums,
|
Enums: csToolsEnums,
|
||||||
} = cornerstoneTools;
|
} = cornerstoneTools;
|
||||||
|
|
||||||
|
@ -72,14 +67,22 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
const viewportRef_SAGITTAL = useRef<HTMLDivElement | null>(null);
|
const viewportRef_SAGITTAL = useRef<HTMLDivElement | null>(null);
|
||||||
const viewportRef_CORONAL = useRef<HTMLDivElement | null>(null);
|
const viewportRef_CORONAL = useRef<HTMLDivElement | null>(null);
|
||||||
const renderingEngine = useRef<RenderingEngine>();
|
const renderingEngine = useRef<RenderingEngine>();
|
||||||
|
const toolGroupIdRef = useRef<string>("");
|
||||||
|
const toolGroupRef = useRef<cornerstoneTools.Types.IToolGroup | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
const imageIds = useRef<string[]>();
|
const imageIds = useRef<string[]>();
|
||||||
const ts = "-" + Date.now();
|
const ts = "-" + Date.now();
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cornerstoneTools.addTool(StackScrollMouseWheelTool);
|
cornerstoneTools.addTool(StackScrollMouseWheelTool);
|
||||||
cornerstoneTools.addTool(CrosshairsTool);
|
cornerstoneTools.addTool(CrosshairsTool);
|
||||||
cornerstoneTools.addTool(WindowLevelTool);
|
cornerstoneTools.addTool(WindowLevelTool);
|
||||||
|
cornerstoneTools.addTool(ZoomTool);
|
||||||
|
|
||||||
|
const toolGroupId = props.SeriesInstanceUID + ts;
|
||||||
|
toolGroupIdRef.current = toolGroupId;
|
||||||
|
toolGroupRef.current = ToolGroupManager.createToolGroup(toolGroupId);
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
if (
|
if (
|
||||||
|
@ -92,9 +95,6 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
renderingEngine.current = new RenderingEngine(
|
renderingEngine.current = new RenderingEngine(
|
||||||
props.SeriesInstanceUID + ts
|
props.SeriesInstanceUID + ts
|
||||||
);
|
);
|
||||||
const toolGroup = ToolGroupManager.createToolGroup(
|
|
||||||
props.SeriesInstanceUID + ts
|
|
||||||
);
|
|
||||||
|
|
||||||
imageIds.current = await createImageIdsAndCacheMetaData({
|
imageIds.current = await createImageIdsAndCacheMetaData({
|
||||||
StudyInstanceUID: props.StudyInstanceUID,
|
StudyInstanceUID: props.StudyInstanceUID,
|
||||||
|
@ -124,7 +124,7 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
element: viewportRef_AXIAL.current,
|
element: viewportRef_AXIAL.current,
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
orientation: CoreEnums.OrientationAxis.AXIAL,
|
orientation: CoreEnums.OrientationAxis.AXIAL,
|
||||||
background: [0, 0, 0],
|
background: [0, 0, 0],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -169,38 +169,59 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
[viewportId1, viewportId2, viewportId3]
|
[viewportId1, viewportId2, viewportId3]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (toolGroup) {
|
if (toolGroupRef.current) {
|
||||||
// For the crosshairs to operate, the viewports must currently be
|
// For the crosshairs to operate, the viewports must currently be
|
||||||
// added ahead of setting the tool active. This will be improved in the future.
|
// added ahead of setting the tool active. This will be improved in the future.
|
||||||
toolGroup.addViewport(viewportId1, props.SeriesInstanceUID + ts);
|
toolGroupRef.current.addViewport(
|
||||||
toolGroup.addViewport(viewportId2, props.SeriesInstanceUID + ts);
|
viewportId1,
|
||||||
toolGroup.addViewport(viewportId3, props.SeriesInstanceUID + ts);
|
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
|
// Manipulation Tools
|
||||||
toolGroup.addTool(StackScrollMouseWheelTool.toolName);
|
toolGroupRef.current.addTool(StackScrollMouseWheelTool.toolName);
|
||||||
// Add Crosshairs tool and configure it to link the three viewports
|
// Add Crosshairs tool and configure it to link the three viewports
|
||||||
// These viewports could use different tool groups. See the PET-CT example
|
// These viewports could use different tool groups. See the PET-CT example
|
||||||
// for a more complicated used case.
|
// for a more complicated used case.
|
||||||
|
|
||||||
toolGroup.addTool(CrosshairsTool.toolName, {
|
toolGroupRef.current.addTool(CrosshairsTool.toolName, {
|
||||||
getReferenceLineColor,
|
getReferenceLineColor,
|
||||||
getReferenceLineControllable,
|
getReferenceLineControllable,
|
||||||
getReferenceLineDraggableRotatable,
|
getReferenceLineDraggableRotatable,
|
||||||
getReferenceLineSlabThicknessControlsOn,
|
getReferenceLineSlabThicknessControlsOn,
|
||||||
});
|
});
|
||||||
|
|
||||||
toolGroup.setToolActive(CrosshairsTool.toolName, {
|
toolGroupRef.current.setToolActive(CrosshairsTool.toolName, {
|
||||||
bindings: [{ mouseButton: 1 }],
|
bindings: [{ mouseButton: 1 }],
|
||||||
});
|
});
|
||||||
// As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback`
|
// 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.
|
// 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);
|
toolGroupRef.current.addTool(WindowLevelTool.toolName);
|
||||||
toolGroup.setToolActive(WindowLevelTool.toolName, {
|
toolGroupRef.current.setToolActive(WindowLevelTool.toolName, {
|
||||||
bindings: [
|
bindings: [
|
||||||
{
|
{
|
||||||
mouseButton: MouseBindings.Secondary,
|
mouseButton: MouseBindings.Auxiliary,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -218,37 +239,50 @@ export const CrosshairMpr = (props: CrosshairMprProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
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(StackScrollMouseWheelTool);
|
||||||
cornerstoneTools.removeTool(CrosshairsTool);
|
cornerstoneTools.removeTool(CrosshairsTool);
|
||||||
cornerstoneTools.removeTool(WindowLevelTool);
|
cornerstoneTools.removeTool(WindowLevelTool);
|
||||||
renderingEngine.current?.destroy();
|
cornerstoneTools.removeTool(ZoomTool);
|
||||||
ToolGroupManager.destroyToolGroup(props.SeriesInstanceUID + ts);
|
|
||||||
};
|
};
|
||||||
}, [props, ts]);
|
}, [props, ts]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mpr resize
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resize = () => renderingEngine.current?.resize()
|
const container = containerRef.current;
|
||||||
window.addEventListener("resize", resize);
|
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 () => {
|
return () => {
|
||||||
window.removeEventListener("resize", resize);
|
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||||
|
resizeObserver.unobserve(container);
|
||||||
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onDBClickViewport = (viewportRef) => {
|
|
||||||
console.log("dblcik", viewportRef);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="w-full h-full flex flex-col">
|
<div ref={containerRef} className="w-full h-full flex flex-col">
|
||||||
<Card className="rounded-none h-1/3">
|
<div className="w-full h-1/3" ref={viewportRef_AXIAL} />
|
||||||
<div className="w-full h-full" onDoubleClick={() => onDBClickViewport(viewportRef_AXIAL)} ref={viewportRef_AXIAL}></div>
|
<div className="w-full h-1/3" ref={viewportRef_SAGITTAL} />
|
||||||
</Card>
|
<div className="w-full h-1/3" ref={viewportRef_CORONAL} />
|
||||||
<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>
|
</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 { initCornerstone } from "./MprViewer/CornerstoneDicomLoader/init";
|
||||||
import { CrosshairMpr } from "./MprViewer/Crosshair";
|
import { CrosshairMpr } from "./MprViewer/Crosshair";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
} from "@/components/ui/resizable";
|
||||||
|
import { ToolBarMenu } from "./MprViewer/ToolBarMenu";
|
||||||
|
|
||||||
export interface CurrentDicom {
|
export interface CurrentDicom {
|
||||||
SeriesInstanceUID: string | null;
|
SeriesInstanceUID: string | null;
|
||||||
|
@ -27,7 +32,7 @@ export const Viewer = () => {
|
||||||
}, [currentDicom]);
|
}, [currentDicom]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(window.location.href)
|
console.log(window.location.href);
|
||||||
initCornerstone(() => {
|
initCornerstone(() => {
|
||||||
setCornerstoneLoaded(true);
|
setCornerstoneLoaded(true);
|
||||||
});
|
});
|
||||||
|
@ -37,19 +42,42 @@ export const Viewer = () => {
|
||||||
console.log(cornerstoneLoaded);
|
console.log(cornerstoneLoaded);
|
||||||
}, [cornerstoneLoaded]);
|
}, [cornerstoneLoaded]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex">
|
<div className="w-full h-full flex flex-col">
|
||||||
<div className="w-2/3 h-full">vr</div>
|
<div className="flex-shrink-0 border-b border-secondary">
|
||||||
<div className="w-1/3 h-full">
|
<ToolBarMenu />
|
||||||
{cornerstoneLoaded &&
|
</div>
|
||||||
currentDicom.StudyInstanceUID &&
|
<div className="flex-grow">
|
||||||
currentDicom.SeriesInstanceUID && (
|
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
|
||||||
<CrosshairMpr
|
<ResizablePanel defaultSize={61.8}>
|
||||||
StudyInstanceUID={currentDicom.StudyInstanceUID}
|
<div className="h-full">
|
||||||
SeriesInstanceUID={currentDicom.SeriesInstanceUID}
|
<ResizablePanelGroup
|
||||||
/>
|
direction="vertical"
|
||||||
)}</div>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user