feat: 更新了查询orthanc数据结构
This commit is contained in:
parent
9b905b7498
commit
5aa710431d
|
@ -1,26 +1,104 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import log from 'electron-log'
|
||||
import log from "electron-log";
|
||||
import path from "node:path";
|
||||
import FormData from "form-data";
|
||||
import { readFile } from "fs/promises";
|
||||
import axios from "axios";
|
||||
import { PatientInfo, SeriesInfo, StudyInfo } from "./pacs.type";
|
||||
|
||||
export const getPacsPath = (platform: "macos" | "windows", isDevelopment: boolean): string => {
|
||||
export const OrthancServerRoot = "http://localhost:8042";
|
||||
|
||||
export const getPacsPath = (
|
||||
platform: "macos" | "windows",
|
||||
isDevelopment: boolean
|
||||
): string => {
|
||||
const orthancExecFile = {
|
||||
macos: "orthanc-mac-24.8.1/Orthanc",
|
||||
windows: "orthanc-win64-1.12.4/Orthanc.exe"
|
||||
}
|
||||
windows: "orthanc-win64-1.12.4/Orthanc.exe",
|
||||
};
|
||||
const basePath = isDevelopment
|
||||
? path.join(process.env.VITE_PUBLIC, "../extraResources")
|
||||
: path.join(process.resourcesPath, 'lib');
|
||||
return path.join(basePath, orthancExecFile[platform])
|
||||
}
|
||||
: path.join(process.resourcesPath, "lib");
|
||||
return path.join(basePath, orthancExecFile[platform]);
|
||||
};
|
||||
|
||||
export const runOrthancServer = (pacsPath: string) => {
|
||||
if (existsSync(pacsPath)) {
|
||||
const child_process = spawn(pacsPath)
|
||||
child_process.stdout.on('data', data => log.info(data))
|
||||
const child_process = spawn(pacsPath);
|
||||
child_process.stdout.on("data", (data) => log.info(data));
|
||||
// child_process.stderr.on('data', data => log.error(data))
|
||||
} else {
|
||||
console.error('pacsPath is a not exist')
|
||||
log.error('pacsPath is a not exist')
|
||||
console.error("pacsPath is a not exist");
|
||||
log.error("pacsPath is a not exist");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传文件到到pacs
|
||||
* @param {string} filePath 文件地址
|
||||
* @param {string} orthancUrl orthanc的服务地址
|
||||
* @returns
|
||||
*/
|
||||
export const uploadDicomFile = async (
|
||||
filePath: string,
|
||||
orthancUrl: string = OrthancServerRoot
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const buffer = await readFile(filePath);
|
||||
const fd = new FormData();
|
||||
fd.append("files", buffer);
|
||||
const url = `${orthancUrl}/instances`;
|
||||
const headers = { "Content-Type": "multipart/form-data" };
|
||||
const { status } = await axios.post(url, fd, { headers });
|
||||
return status === 200;
|
||||
} catch (error) {
|
||||
log.error("Failed to upload DICOM file:", error);
|
||||
console.error("Failed to upload DICOM file:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const selectStructuredDicom = async (
|
||||
orthancUrl: string = OrthancServerRoot
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.get(`${orthancUrl}/patients`);
|
||||
const patientIds: string[] = response.data;
|
||||
const patients: PatientInfo[] = [];
|
||||
|
||||
for (const patientId of patientIds) {
|
||||
const patientDetailsResponse = await axios.get<PatientInfo>(
|
||||
`${orthancUrl}/patients/${patientId}`
|
||||
);
|
||||
const patientDetails = patientDetailsResponse.data;
|
||||
|
||||
const studyIds = patientDetails.Studies;
|
||||
const studies: StudyInfo[] = [];
|
||||
|
||||
for (const studyId of studyIds) {
|
||||
const studyDetailsResponse = await axios.get<StudyInfo>(
|
||||
`${orthancUrl}/studies/${studyId}`
|
||||
);
|
||||
const studyDetails = studyDetailsResponse.data;
|
||||
|
||||
const seriesIds = studyDetails.Series;
|
||||
const series: SeriesInfo[] = [];
|
||||
|
||||
for (const seriesId of seriesIds) {
|
||||
const seriesDetailsResponse = await axios.get<SeriesInfo>(
|
||||
`${orthancUrl}/series/${seriesId}`
|
||||
);
|
||||
const seriesDetails = seriesDetailsResponse.data;
|
||||
series.push({ ...seriesDetails });
|
||||
}
|
||||
studies.push({ ...studyDetails, children: series });
|
||||
}
|
||||
patients.push({ ...patientDetails, children: studies });
|
||||
}
|
||||
return patients;
|
||||
} catch (error) {
|
||||
console.error("Error fetching detailed patient information:", error);
|
||||
throw error; // or handle it accordingly
|
||||
}
|
||||
};
|
||||
|
|
68
apps/desktop/electron/core/pacs.type.ts
Normal file
68
apps/desktop/electron/core/pacs.type.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
export interface PatientInfo {
|
||||
ID: string;
|
||||
LastUpdate: string;
|
||||
MainDicomTags: {
|
||||
OtherPatientIDs: string;
|
||||
PatientBirthDate: string;
|
||||
PatientID: string;
|
||||
PatientName: string;
|
||||
PatientSex: string;
|
||||
};
|
||||
Studies: string[];
|
||||
Type: string;
|
||||
children: StudyInfo[];
|
||||
}
|
||||
|
||||
export interface StudyInfo {
|
||||
ID: string;
|
||||
MainDicomTags: {
|
||||
AccessionNumber: string;
|
||||
StudyDate: string;
|
||||
StudyDescription: string;
|
||||
StudyID: string;
|
||||
StudyInstanceUID: string;
|
||||
StudyTime: string;
|
||||
};
|
||||
ParentPatient: string;
|
||||
Series: string[];
|
||||
Type: string;
|
||||
children: SeriesInfo[];
|
||||
}
|
||||
|
||||
export interface SeriesInfo {
|
||||
ExpectedNumberOfInstances: number;
|
||||
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;
|
||||
};
|
||||
ParentStudy: string;
|
||||
Status: string;
|
||||
Type: string;
|
||||
children: [];
|
||||
}
|
||||
|
||||
export interface InstanceInfo {
|
||||
FileSize: number;
|
||||
FileUuid: string;
|
||||
ID: string;
|
||||
IndexInSeries: number;
|
||||
MainDicomTags: {
|
||||
ImageIndex: string;
|
||||
InstanceCreationDate: string;
|
||||
InstanceCreationTime: string;
|
||||
InstanceNumber: string;
|
||||
SOPInstanceUID: string;
|
||||
};
|
||||
ParentSeries: string;
|
||||
Type: string;
|
||||
}
|
33
apps/desktop/electron/ipcEvent/common/index.ts
Normal file
33
apps/desktop/electron/ipcEvent/common/index.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { ipcMain, shell } from "electron";
|
||||
import { mkdir, stat } from "fs/promises";
|
||||
import { db } from "../../core/db";
|
||||
import log from "electron-log";
|
||||
import path from "node:path";
|
||||
|
||||
export const registerCommonHandler = () => {
|
||||
ipcMain.handle("output:open", async () => {
|
||||
await db.read();
|
||||
const optPath = db.data.setting.outputPath;
|
||||
const resolvedPath = path.resolve(optPath);
|
||||
try {
|
||||
// 检查路径是否存在
|
||||
const stats = await stat(resolvedPath);
|
||||
if (stats.isDirectory()) shell.openPath(resolvedPath);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
await mkdir(resolvedPath, { recursive: true });
|
||||
shell.openPath(resolvedPath);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("device:infer:set", async (_event, inferDevice) => {
|
||||
try {
|
||||
await db.update(({ setting }) => ({ ...setting, inferDevice }));
|
||||
return { success: true, msg: `推理硬件修改为${inferDevice}` };
|
||||
} catch (error) {
|
||||
await db.update(({ setting }) => ({ ...setting, inferDevice }));
|
||||
log.error(error);
|
||||
return { success: false, msg: `操作失败` };
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,12 +1,32 @@
|
|||
import { dialog, ipcMain } from "electron";
|
||||
import { filterDicoms, uploadFilesInBatches } from "./util";
|
||||
import { selectStructuredDicom } from "../../core/pacs";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const registerDicomHandler = () => {
|
||||
ipcMain.handle("dicom:upload", async () => {
|
||||
ipcMain.on("dicom:upload", async (event) => {
|
||||
const dia = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||
if (dia.canceled) return null;
|
||||
const dcmPaths = await filterDicoms(dia.filePaths[0]);
|
||||
uploadFilesInBatches(dcmPaths, 5);
|
||||
// return dia.filePaths[0];
|
||||
uploadFilesInBatches({
|
||||
filePaths: dcmPaths,
|
||||
batchSize: 6,
|
||||
feedback: (d) => event.reply("dicom:upload:detail", d),
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle("dicom:select", async () => {
|
||||
const patients = await selectStructuredDicom();
|
||||
const sortedPatients = [...patients]
|
||||
.map((patient) => ({
|
||||
...patient,
|
||||
LastUpdate: dayjs(patient.LastUpdate).format("YYYY-MM-DD HH:mm:ss"), // 格式化日期
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
new Date(b.LastUpdate).getTime() - new Date(a.LastUpdate).getTime()
|
||||
);
|
||||
});
|
||||
return sortedPatients;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import axios from "axios";
|
||||
import FormData from "form-data";
|
||||
import log from "electron-log";
|
||||
import { uploadDicomFile } from "../../core/pacs";
|
||||
|
||||
/**
|
||||
* 检查文件是否为 DICOM 文件(通过 Magic Number 判断)
|
||||
|
@ -52,49 +50,69 @@ export const filterDicoms = async (
|
|||
return fileList;
|
||||
};
|
||||
|
||||
export const uploadDicomFile = async (
|
||||
filePath: string,
|
||||
orthancUrl: string = "http://localhost:8042"
|
||||
) => {
|
||||
try {
|
||||
const buffer = await fs.promises.readFile(filePath);
|
||||
const fd = new FormData();
|
||||
fd.append("files", buffer);
|
||||
const url = `${orthancUrl}/instances`;
|
||||
const headers = { "Content-Type": "multipart/form-data" };
|
||||
const { status } = await axios.post(url, fd, { headers });
|
||||
return status === 200;
|
||||
} catch (error) {
|
||||
log.error("Failed to upload DICOM file:", error);
|
||||
console.error("Failed to upload DICOM file:", error);
|
||||
}
|
||||
};
|
||||
interface UploadFilesInBatchesParams {
|
||||
filePaths: string[];
|
||||
batchSize: number;
|
||||
orthancUrl?: string;
|
||||
feedback?: (detail: {
|
||||
progress: number;
|
||||
totalSuccess: number;
|
||||
totalFailed: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const uploadFilesInBatches = async (
|
||||
filePaths: string[],
|
||||
batchSize: number,
|
||||
orthancUrl: string = "http://localhost:8042"
|
||||
) => {
|
||||
const results = [];
|
||||
const totalStartTime = Date.now(); // 记录总体开始时间
|
||||
for (let i = 0; i < filePaths.length; i += batchSize) {
|
||||
/**
|
||||
* 批量上传文件,并提供进度更新。
|
||||
* @param filePaths 文件路径数组。
|
||||
* @param batchSize 每批上传的文件数量。
|
||||
* @param orthancUrl Orthanc 服务器的 URL。
|
||||
* @param feedback 每批处理完成后调用的回调函数。
|
||||
*/
|
||||
export const uploadFilesInBatches = async ({
|
||||
filePaths,
|
||||
batchSize,
|
||||
feedback,
|
||||
}: UploadFilesInBatchesParams) => {
|
||||
const totalStartTime = Date.now();
|
||||
let totalSuccess = 0;
|
||||
let totalFailed = 0;
|
||||
const totalFiles = filePaths.length;
|
||||
|
||||
for (let i = 0; i < totalFiles; i += batchSize) {
|
||||
const batch = filePaths.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.allSettled(
|
||||
batch.map((filePath) => uploadDicomFile(filePath, orthancUrl))
|
||||
batch.map((filePath) => uploadDicomFile(filePath))
|
||||
);
|
||||
// 提取状态为 'fulfilled' 的结果的 value
|
||||
const fulfilledResults = batchResults
|
||||
.filter((result) => result.status === "fulfilled")
|
||||
.map((result) => (result as PromiseFulfilledResult<boolean>).value);
|
||||
results.push(...fulfilledResults);
|
||||
|
||||
batchResults.forEach((result) => {
|
||||
if (result.status === "fulfilled" && result.value) {
|
||||
totalSuccess++;
|
||||
} else {
|
||||
totalFailed++;
|
||||
}
|
||||
const totalEndTime = Date.now(); // 记录总体结束时间
|
||||
const totalSuccess = results.filter(Boolean).length;
|
||||
const totalFailed = results.length - totalSuccess;
|
||||
log.info(
|
||||
});
|
||||
|
||||
const actualProcessed = i + batch.length;
|
||||
const progress = Math.round((actualProcessed / totalFiles) * 100);
|
||||
const detail = {
|
||||
progress: Math.min(progress, 100),
|
||||
totalSuccess,
|
||||
totalFailed,
|
||||
};
|
||||
if (feedback) feedback(detail);
|
||||
}
|
||||
|
||||
const totalEndTime = Date.now();
|
||||
console.log(
|
||||
`[上传序列] Success: ${totalSuccess}, Failed: ${totalFailed}, Total upload time: ${
|
||||
totalEndTime - totalStartTime
|
||||
} ms`
|
||||
);
|
||||
return results;
|
||||
|
||||
return {
|
||||
totalSuccess,
|
||||
totalFailed,
|
||||
totalTime: totalEndTime - totalStartTime,
|
||||
progress: 100,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ipcMain } from "electron";
|
||||
import { registerDicomHandler } from "./dicom/handler";
|
||||
import { registerCommonHandler } from "./common";
|
||||
|
||||
export const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow) => {
|
||||
ipcMain.removeAllListeners();
|
||||
|
@ -9,5 +10,6 @@ export const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow) => {
|
|||
*/
|
||||
ipcMain.on("ipc-loaded", () => mainWindow.show());
|
||||
|
||||
registerCommonHandler();
|
||||
registerDicomHandler();
|
||||
};
|
||||
|
|
|
@ -13,12 +13,11 @@ import {
|
|||
MenubarSubTrigger,
|
||||
MenubarTrigger,
|
||||
} from "@/components/ui/menubar";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { inferDeviceType } from "./type";
|
||||
import { inferDevices } from "./constant";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { ToastAction } from "@/components/ui/toast";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import {
|
||||
Dialog,
|
||||
|
@ -29,7 +28,6 @@ import {
|
|||
} from "@/components/ui/dialog";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { RocketIcon } from "@radix-ui/react-icons";
|
||||
import axios from "axios";
|
||||
|
||||
interface ScanProgress {
|
||||
percentage: number;
|
||||
|
@ -45,73 +43,29 @@ export const MenuBar = () => {
|
|||
const [progress, setProgress] = useState<ScanProgress>(defaultProgress);
|
||||
const [inferOption, setInferOption] =
|
||||
useState<inferDeviceType[]>(inferDevices);
|
||||
|
||||
const [importDialogVisible, setImportDialogVisible] = useState(false);
|
||||
|
||||
const [, setResult] = useState<[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScanProgress = (
|
||||
const handleUploadFeedback = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: any
|
||||
data: { progress: number; totalSuccess: number; totalFailed: number }
|
||||
) => {
|
||||
setProgress(data);
|
||||
if (data.error) return;
|
||||
setProgress({ percentage: data.progress });
|
||||
};
|
||||
window.ipcRenderer.on("scan-progress", handleScanProgress);
|
||||
window.ipcRenderer.on("dicom:upload:detail", handleUploadFeedback);
|
||||
return () => {
|
||||
window.ipcRenderer.off("scan-progress", handleScanProgress);
|
||||
window.ipcRenderer.off("dicom:upload:detail", handleUploadFeedback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScanFinished = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: any
|
||||
) => {
|
||||
const { scanDuration, structDicom } = data;
|
||||
console.log(data);
|
||||
setResult(structDicom);
|
||||
|
||||
if (data.error) {
|
||||
return toast({
|
||||
variant: "destructive",
|
||||
title: "Uh oh! Something went wrong.",
|
||||
description: "There was a problem with your request.",
|
||||
action: <ToastAction altText="重试">重试</ToastAction>,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "完成",
|
||||
description: `导入${structDicom.length}组序列数据,耗时:${(
|
||||
scanDuration / 1000
|
||||
).toFixed(2)} s`,
|
||||
action: (
|
||||
<ToastAction
|
||||
altText="启动AI测量"
|
||||
onClick={() => handleTasks(structDicom)}
|
||||
>
|
||||
分析
|
||||
</ToastAction>
|
||||
),
|
||||
duration: 30 * 1000,
|
||||
});
|
||||
window.ipcRenderer.send("db:series:select");
|
||||
}
|
||||
};
|
||||
window.ipcRenderer.on("scan-progress-done", handleScanFinished);
|
||||
return () => {
|
||||
window.ipcRenderer.off("scan-progress-done", handleScanFinished);
|
||||
};
|
||||
}, [toast]);
|
||||
const visible = ![0, 100].includes(progress.percentage);
|
||||
setImportDialogVisible(visible);
|
||||
}, [progress.percentage]);
|
||||
|
||||
const handleImportDicom = () => {
|
||||
navigate("datasource");
|
||||
// window.ipcRenderer.send("import-dicom-dialog-visible");
|
||||
window.ipcRenderer.invoke("dicom:upload").then((value) => {
|
||||
console.log(value);
|
||||
});
|
||||
window.ipcRenderer.send("dicom:upload");
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -121,33 +75,13 @@ export const MenuBar = () => {
|
|||
setInferOption((p) =>
|
||||
p.map((i) => ({ ...i, checked: i.key === item.key }))
|
||||
);
|
||||
window.ipcRenderer.send("setInferDevice", item.key);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const actionToast = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
response: string
|
||||
) => {
|
||||
toast({
|
||||
title: "操作成功",
|
||||
description: response,
|
||||
window.ipcRenderer.invoke("device:infer:set", item.key).then((res) => {
|
||||
const { success, msg } = res;
|
||||
if (success) toast({ title: "操作成功", description: msg });
|
||||
});
|
||||
};
|
||||
window.ipcRenderer.on("setInferDevice:response", actionToast);
|
||||
return () => {
|
||||
window.ipcRenderer.off("setInferDevice:response", actionToast);
|
||||
};
|
||||
}, [toast]);
|
||||
|
||||
const handleOpenOutputPath = () => {
|
||||
window.ipcRenderer.send("openOutputPath");
|
||||
};
|
||||
|
||||
const handleTasks = (structDicoms: any) => {
|
||||
localStorage.setItem("selectDicoms", JSON.stringify(structDicoms));
|
||||
navigate("/", { state: { selectDicoms: structDicoms } });
|
||||
};
|
||||
const handleOpenOutputPath = () => window.ipcRenderer.invoke("output:open");
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -158,7 +92,7 @@ export const MenuBar = () => {
|
|||
<MenubarTrigger>文件</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem onSelect={handleImportDicom}>
|
||||
批量导入Dicom<MenubarShortcut>⌘T</MenubarShortcut>
|
||||
导入Dicom<MenubarShortcut>⌘T</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarItem onClick={handleOpenOutputPath}>
|
||||
打开输出文件夹<MenubarShortcut>⌘N</MenubarShortcut>
|
||||
|
|
|
@ -77,7 +77,7 @@ const Boot = () => {
|
|||
<div className="p-4 h-full flex flex-col">
|
||||
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
|
||||
<ResizablePanel defaultSize={38.2}>
|
||||
<div className="flex flex-col h-full pt-4">
|
||||
<div className="flex flex-col h-full">
|
||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
||||
<div className="w-full flex flex-col gap-y-2">
|
||||
{tasks.map((dicom: Series, index: number) => (
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Series, SeriesTable } from "./SeriesTable";
|
||||
import { useEffect, useState } 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 } from "./type";
|
||||
export const Datasource = () => {
|
||||
const [seriesData, setSeriesData] = useState<Series[]>([]);
|
||||
const [patients, setPatients] = useState<
|
||||
(PatientInfo & { active: boolean })[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
window.ipcRenderer.send("db:series:select");
|
||||
window.ipcRenderer
|
||||
.invoke("dicom:select")
|
||||
.then((patients: PatientInfo[]) => {
|
||||
console.log(patients);
|
||||
setPatients(patients.map((p) => ({ ...p, active: false })));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchSeriesData = useCallback(
|
||||
(_event: Electron.IpcRendererEvent, data: any) => {
|
||||
console.log(data);
|
||||
setSeriesData(data);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.ipcRenderer.once("db:series:select:response", fetchSeriesData);
|
||||
return () => {
|
||||
window.ipcRenderer.off("db:series:select:response", fetchSeriesData);
|
||||
};
|
||||
}, [fetchSeriesData]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="h-full"
|
||||
|
@ -33,7 +30,57 @@ export const Datasource = () => {
|
|||
exit={{ y: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<SeriesTable data={seriesData} />
|
||||
<div className="p-4 h-full flex flex-col">
|
||||
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
|
||||
<ResizablePanel defaultSize={24}>
|
||||
<div className="flex flex-col h-full">
|
||||
<ScrollArea className="flex-grow w-full h-full px-4 pb-2">
|
||||
<div className="w-full 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>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={24}>
|
||||
123
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={52}>33</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
68
apps/desktop/src/pages/Datasource/type.ts
Normal file
68
apps/desktop/src/pages/Datasource/type.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
export interface PatientInfo {
|
||||
ID: string;
|
||||
LastUpdate: string;
|
||||
MainDicomTags: {
|
||||
OtherPatientIDs: string;
|
||||
PatientBirthDate: string;
|
||||
PatientID: string;
|
||||
PatientName: string;
|
||||
PatientSex: string;
|
||||
};
|
||||
Studies: string[];
|
||||
Type: string;
|
||||
children: StudyInfo[];
|
||||
}
|
||||
|
||||
export interface StudyInfo {
|
||||
ID: string;
|
||||
MainDicomTags: {
|
||||
AccessionNumber: string;
|
||||
StudyDate: string;
|
||||
StudyDescription: string;
|
||||
StudyID: string;
|
||||
StudyInstanceUID: string;
|
||||
StudyTime: string;
|
||||
};
|
||||
ParentPatient: string;
|
||||
Series: string[];
|
||||
Type: string;
|
||||
children: SeriesInfo[];
|
||||
}
|
||||
|
||||
export interface SeriesInfo {
|
||||
ExpectedNumberOfInstances: number;
|
||||
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;
|
||||
};
|
||||
ParentStudy: string;
|
||||
Status: string;
|
||||
Type: string;
|
||||
children: [];
|
||||
}
|
||||
|
||||
export interface InstanceInfo {
|
||||
FileSize: number;
|
||||
FileUuid: string;
|
||||
ID: string;
|
||||
IndexInSeries: number;
|
||||
MainDicomTags: {
|
||||
ImageIndex: string;
|
||||
InstanceCreationDate: string;
|
||||
InstanceCreationTime: string;
|
||||
InstanceNumber: string;
|
||||
SOPInstanceUID: string;
|
||||
};
|
||||
ParentSeries: string;
|
||||
Type: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user