feat: 上传dicom
This commit is contained in:
parent
348c9df6bc
commit
34c826353b
|
@ -1,3 +0,0 @@
|
|||
export const useDicomHandler = () => {
|
||||
|
||||
}
|
12
apps/desktop/electron/ipcEvent/dicom/handler.ts
Normal file
12
apps/desktop/electron/ipcEvent/dicom/handler.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { dialog, ipcMain } from "electron";
|
||||
import { filterDicoms, uploadFilesInBatches } from "./util";
|
||||
|
||||
export const registerDicomHandler = () => {
|
||||
ipcMain.handle("dicom:upload", async () => {
|
||||
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];
|
||||
});
|
||||
};
|
101
apps/desktop/electron/ipcEvent/dicom/util.ts
Normal file
101
apps/desktop/electron/ipcEvent/dicom/util.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import axios from "axios";
|
||||
import FormData from "form-data";
|
||||
import log from "electron-log";
|
||||
|
||||
/**
|
||||
* 检查文件是否为 DICOM 文件(通过 Magic Number 判断)
|
||||
* @param filePath 文件路径
|
||||
* @returns 是否为 DICOM 文件
|
||||
*/
|
||||
const isDICOMFile = async (filePath: string) => {
|
||||
try {
|
||||
// 打开文件以进行读取
|
||||
const fileHandle = await fs.promises.open(filePath, "r");
|
||||
const buffer = Buffer.alloc(132); // 创建一个 132 字节的缓冲区
|
||||
|
||||
// 从文件中读取前 132 个字节
|
||||
await fileHandle.read(buffer, 0, 132, 0);
|
||||
await fileHandle.close(); // 关闭文件
|
||||
|
||||
// 检查 "DICM" 标识 (偏移 128-131 字节)
|
||||
const magicNumber = buffer.toString("utf-8", 128, 132);
|
||||
return magicNumber === "DICM";
|
||||
} catch (error) {
|
||||
console.error(`Error reading file ${filePath}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 定义一个异步函数来递归地查找.dcm文件
|
||||
* @param dir
|
||||
* @param fileList
|
||||
* @returns
|
||||
*/
|
||||
export const filterDicoms = async (
|
||||
dir: string,
|
||||
fileList: string[] = []
|
||||
): Promise<string[]> => {
|
||||
const files = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filePath = path.join(dir, file.name);
|
||||
if (file.isDirectory()) {
|
||||
await filterDicoms(filePath, fileList); // 递归调用以遍历子目录
|
||||
} else {
|
||||
// if (await isDICOMFile(filePath))
|
||||
fileList.push(filePath);
|
||||
}
|
||||
})
|
||||
);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
const batch = filePaths.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.allSettled(
|
||||
batch.map((filePath) => uploadDicomFile(filePath, orthancUrl))
|
||||
);
|
||||
// 提取状态为 'fulfilled' 的结果的 value
|
||||
const fulfilledResults = batchResults
|
||||
.filter((result) => result.status === "fulfilled")
|
||||
.map((result) => (result as PromiseFulfilledResult<boolean>).value);
|
||||
results.push(...fulfilledResults);
|
||||
}
|
||||
const totalEndTime = Date.now(); // 记录总体结束时间
|
||||
const totalSuccess = results.filter(Boolean).length;
|
||||
const totalFailed = results.length - totalSuccess;
|
||||
log.info(
|
||||
`[上传序列] Success: ${totalSuccess}, Failed: ${totalFailed}, Total upload time: ${
|
||||
totalEndTime - totalStartTime
|
||||
} ms`
|
||||
);
|
||||
return results;
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import { ipcMain } from "electron";
|
||||
import { useDicomHandler } from "./dicom";
|
||||
import { registerDicomHandler } from "./dicom/handler";
|
||||
|
||||
export const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow) => {
|
||||
ipcMain.removeAllListeners();
|
||||
|
@ -9,5 +9,5 @@ export const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow) => {
|
|||
*/
|
||||
ipcMain.on("ipc-loaded", () => mainWindow.show());
|
||||
|
||||
useDicomHandler()
|
||||
}
|
||||
registerDicomHandler();
|
||||
};
|
||||
|
|
|
@ -11,9 +11,8 @@ import { fileURLToPath } from "node:url";
|
|||
import path from "node:path";
|
||||
import { createDatabase } from "./core/db";
|
||||
import { getMachineId } from "./core/auth";
|
||||
import registerIpcMainHandlers from "./ipcMainHandlers";
|
||||
import { getPacsPath, runOrthancServer } from "./core/pacs";
|
||||
|
||||
import { registerIpcMainHandlers } from "./ipcEvent";
|
||||
|
||||
// const require = createRequire(import.meta.url);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
@ -68,7 +67,7 @@ function createWindow() {
|
|||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL);
|
||||
registerIpcMainHandlers(win);
|
||||
runOrthancServer(getPacsPath(platform, true))
|
||||
runOrthancServer(getPacsPath(platform, true));
|
||||
|
||||
// if (platform !== "macos") {
|
||||
// python_process = spawn(path.join(process.env.VITE_PUBLIC!, "main.exe"));
|
||||
|
@ -78,16 +77,14 @@ function createWindow() {
|
|||
// python_process = spawn(path.join(process.env.VITE_PUBLIC!, "main.exe"));
|
||||
// }
|
||||
|
||||
|
||||
win.loadFile(path.join(RENDERER_DIST, "index.html")).then(() => {
|
||||
registerIpcMainHandlers(win);
|
||||
runOrthancServer(getPacsPath(platform, false))
|
||||
registerIpcMainHandlers(win!);
|
||||
runOrthancServer(getPacsPath(platform, false));
|
||||
|
||||
// windows右键打开的目录路径
|
||||
if (process.argv.length >= 2) {
|
||||
const folderPath = process.argv[2];
|
||||
win?.webContents.send("context-menu-launch", folderPath);
|
||||
|
||||
win!.webContents.send("context-menu-launch", folderPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -64,7 +64,8 @@
|
|||
"zod": "3.23.8",
|
||||
"axios": "1.7.7",
|
||||
"lodash": "4.17.21",
|
||||
"electron-log": "5.2.0"
|
||||
"electron-log": "5.2.0",
|
||||
"form-data": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
|
@ -88,4 +89,4 @@
|
|||
"vite-plugin-electron-renderer": "^0.14.5",
|
||||
"@types/lodash": "4.17.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
/**
|
||||
* ipc 通信的type、interface
|
||||
*/
|
||||
export namespace IPCEvents {
|
||||
export enum Dicom {
|
||||
Upload = 'dicom:upload',
|
||||
Remove = 'dicom:remove',
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import {
|
|||
MenubarSubTrigger,
|
||||
MenubarTrigger,
|
||||
} from "@/components/ui/menubar";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { inferDeviceType } from "./type";
|
||||
import { inferDevices } from "./constant";
|
||||
|
@ -29,6 +29,7 @@ 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,10 +46,10 @@ export const MenuBar = () => {
|
|||
const [inferOption, setInferOption] =
|
||||
useState<inferDeviceType[]>(inferDevices);
|
||||
|
||||
const [, setResult] = useState<[]>([]);
|
||||
|
||||
const [importDialogVisible, setImportDialogVisible] = useState(false);
|
||||
|
||||
const [, setResult] = useState<[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScanProgress = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
|
@ -63,12 +64,6 @@ export const MenuBar = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.ipcRenderer.once("scan-start", () => {
|
||||
setImportDialogVisible(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScanFinished = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
|
@ -77,7 +72,6 @@ export const MenuBar = () => {
|
|||
const { scanDuration, structDicom } = data;
|
||||
console.log(data);
|
||||
setResult(structDicom);
|
||||
setImportDialogVisible(false);
|
||||
|
||||
if (data.error) {
|
||||
return toast({
|
||||
|
@ -112,15 +106,12 @@ export const MenuBar = () => {
|
|||
};
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!importDialogVisible) {
|
||||
setProgress(defaultProgress);
|
||||
}
|
||||
}, [importDialogVisible]);
|
||||
|
||||
const handleImportDicom = () => {
|
||||
navigate("datasource");
|
||||
window.ipcRenderer.send("import-dicom-dialog-visible");
|
||||
// window.ipcRenderer.send("import-dicom-dialog-visible");
|
||||
window.ipcRenderer.invoke("dicom:upload").then((value) => {
|
||||
console.log(value);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
|
|
@ -3,7 +3,6 @@ import path from "node:path";
|
|||
import electron from "vite-plugin-electron/simple";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
|
|
@ -112,6 +112,9 @@ importers:
|
|||
flexlayout-react:
|
||||
specifier: ^0.7.15
|
||||
version: 0.7.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
form-data:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
framer-motion:
|
||||
specifier: ^11.3.24
|
||||
version: 11.3.30(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
@ -5802,7 +5805,7 @@ snapshots:
|
|||
|
||||
app-builder-bin@4.0.0: {}
|
||||
|
||||
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||
dependencies:
|
||||
'@develar/schema-utils': 2.6.5
|
||||
'@electron/notarize': 2.2.1
|
||||
|
@ -5816,7 +5819,7 @@ snapshots:
|
|||
builder-util-runtime: 9.2.4
|
||||
chromium-pickle-js: 0.2.0
|
||||
debug: 4.3.6
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
ejs: 3.1.10
|
||||
electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3)
|
||||
electron-publish: 24.13.1
|
||||
|
@ -6305,9 +6308,9 @@ snapshots:
|
|||
|
||||
dlv@1.1.3: {}
|
||||
|
||||
dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3):
|
||||
dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||
dependencies:
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
builder-util: 24.13.1
|
||||
builder-util-runtime: 9.2.4
|
||||
fs-extra: 10.1.0
|
||||
|
@ -6375,7 +6378,7 @@ snapshots:
|
|||
|
||||
electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3):
|
||||
dependencies:
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
archiver: 5.3.2
|
||||
builder-util: 24.13.1
|
||||
fs-extra: 10.1.0
|
||||
|
@ -6385,11 +6388,11 @@ snapshots:
|
|||
|
||||
electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||
dependencies:
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
builder-util: 24.13.1
|
||||
builder-util-runtime: 9.2.4
|
||||
chalk: 4.1.2
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||
fs-extra: 10.1.0
|
||||
is-ci: 3.0.1
|
||||
lazy-val: 1.0.5
|
||||
|
|
Loading…
Reference in New Issue
Block a user