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 { ipcMain } from "electron";
|
||||||
import { useDicomHandler } from "./dicom";
|
import { registerDicomHandler } from "./dicom/handler";
|
||||||
|
|
||||||
export const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow) => {
|
export const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow) => {
|
||||||
ipcMain.removeAllListeners();
|
ipcMain.removeAllListeners();
|
||||||
|
@ -9,5 +9,5 @@ export const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow) => {
|
||||||
*/
|
*/
|
||||||
ipcMain.on("ipc-loaded", () => mainWindow.show());
|
ipcMain.on("ipc-loaded", () => mainWindow.show());
|
||||||
|
|
||||||
useDicomHandler()
|
registerDicomHandler();
|
||||||
}
|
};
|
||||||
|
|
|
@ -11,9 +11,8 @@ import { fileURLToPath } from "node:url";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { createDatabase } from "./core/db";
|
import { createDatabase } from "./core/db";
|
||||||
import { getMachineId } from "./core/auth";
|
import { getMachineId } from "./core/auth";
|
||||||
import registerIpcMainHandlers from "./ipcMainHandlers";
|
|
||||||
import { getPacsPath, runOrthancServer } from "./core/pacs";
|
import { getPacsPath, runOrthancServer } from "./core/pacs";
|
||||||
|
import { registerIpcMainHandlers } from "./ipcEvent";
|
||||||
|
|
||||||
// const require = createRequire(import.meta.url);
|
// const require = createRequire(import.meta.url);
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
@ -68,7 +67,7 @@ function createWindow() {
|
||||||
if (VITE_DEV_SERVER_URL) {
|
if (VITE_DEV_SERVER_URL) {
|
||||||
win.loadURL(VITE_DEV_SERVER_URL);
|
win.loadURL(VITE_DEV_SERVER_URL);
|
||||||
registerIpcMainHandlers(win);
|
registerIpcMainHandlers(win);
|
||||||
runOrthancServer(getPacsPath(platform, true))
|
runOrthancServer(getPacsPath(platform, true));
|
||||||
|
|
||||||
// if (platform !== "macos") {
|
// if (platform !== "macos") {
|
||||||
// python_process = spawn(path.join(process.env.VITE_PUBLIC!, "main.exe"));
|
// 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"));
|
// python_process = spawn(path.join(process.env.VITE_PUBLIC!, "main.exe"));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
win.loadFile(path.join(RENDERER_DIST, "index.html")).then(() => {
|
win.loadFile(path.join(RENDERER_DIST, "index.html")).then(() => {
|
||||||
registerIpcMainHandlers(win);
|
registerIpcMainHandlers(win!);
|
||||||
runOrthancServer(getPacsPath(platform, false))
|
runOrthancServer(getPacsPath(platform, false));
|
||||||
|
|
||||||
// windows右键打开的目录路径
|
// windows右键打开的目录路径
|
||||||
if (process.argv.length >= 2) {
|
if (process.argv.length >= 2) {
|
||||||
const folderPath = process.argv[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",
|
"zod": "3.23.8",
|
||||||
"axios": "1.7.7",
|
"axios": "1.7.7",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"electron-log": "5.2.0"
|
"electron-log": "5.2.0",
|
||||||
|
"form-data": "4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
|
|
@ -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,
|
MenubarSubTrigger,
|
||||||
MenubarTrigger,
|
MenubarTrigger,
|
||||||
} from "@/components/ui/menubar";
|
} from "@/components/ui/menubar";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { inferDeviceType } from "./type";
|
import { inferDeviceType } from "./type";
|
||||||
import { inferDevices } from "./constant";
|
import { inferDevices } from "./constant";
|
||||||
|
@ -29,6 +29,7 @@ import {
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { RocketIcon } from "@radix-ui/react-icons";
|
import { RocketIcon } from "@radix-ui/react-icons";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
interface ScanProgress {
|
interface ScanProgress {
|
||||||
percentage: number;
|
percentage: number;
|
||||||
|
@ -45,10 +46,10 @@ export const MenuBar = () => {
|
||||||
const [inferOption, setInferOption] =
|
const [inferOption, setInferOption] =
|
||||||
useState<inferDeviceType[]>(inferDevices);
|
useState<inferDeviceType[]>(inferDevices);
|
||||||
|
|
||||||
const [, setResult] = useState<[]>([]);
|
|
||||||
|
|
||||||
const [importDialogVisible, setImportDialogVisible] = useState(false);
|
const [importDialogVisible, setImportDialogVisible] = useState(false);
|
||||||
|
|
||||||
|
const [, setResult] = useState<[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScanProgress = (
|
const handleScanProgress = (
|
||||||
_event: Electron.IpcRendererEvent,
|
_event: Electron.IpcRendererEvent,
|
||||||
|
@ -63,12 +64,6 @@ export const MenuBar = () => {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.ipcRenderer.once("scan-start", () => {
|
|
||||||
setImportDialogVisible(true);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScanFinished = (
|
const handleScanFinished = (
|
||||||
_event: Electron.IpcRendererEvent,
|
_event: Electron.IpcRendererEvent,
|
||||||
|
@ -77,7 +72,6 @@ export const MenuBar = () => {
|
||||||
const { scanDuration, structDicom } = data;
|
const { scanDuration, structDicom } = data;
|
||||||
console.log(data);
|
console.log(data);
|
||||||
setResult(structDicom);
|
setResult(structDicom);
|
||||||
setImportDialogVisible(false);
|
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
return toast({
|
return toast({
|
||||||
|
@ -112,15 +106,12 @@ export const MenuBar = () => {
|
||||||
};
|
};
|
||||||
}, [toast]);
|
}, [toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!importDialogVisible) {
|
|
||||||
setProgress(defaultProgress);
|
|
||||||
}
|
|
||||||
}, [importDialogVisible]);
|
|
||||||
|
|
||||||
const handleImportDicom = () => {
|
const handleImportDicom = () => {
|
||||||
navigate("datasource");
|
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": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|
|
@ -3,7 +3,6 @@ import path from "node:path";
|
||||||
import electron from "vite-plugin-electron/simple";
|
import electron from "vite-plugin-electron/simple";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
|
@ -112,6 +112,9 @@ importers:
|
||||||
flexlayout-react:
|
flexlayout-react:
|
||||||
specifier: ^0.7.15
|
specifier: ^0.7.15
|
||||||
version: 0.7.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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:
|
framer-motion:
|
||||||
specifier: ^11.3.24
|
specifier: ^11.3.24
|
||||||
version: 11.3.30(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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-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:
|
dependencies:
|
||||||
'@develar/schema-utils': 2.6.5
|
'@develar/schema-utils': 2.6.5
|
||||||
'@electron/notarize': 2.2.1
|
'@electron/notarize': 2.2.1
|
||||||
|
@ -5816,7 +5819,7 @@ snapshots:
|
||||||
builder-util-runtime: 9.2.4
|
builder-util-runtime: 9.2.4
|
||||||
chromium-pickle-js: 0.2.0
|
chromium-pickle-js: 0.2.0
|
||||||
debug: 4.3.6
|
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
|
ejs: 3.1.10
|
||||||
electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3)
|
electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3)
|
||||||
electron-publish: 24.13.1
|
electron-publish: 24.13.1
|
||||||
|
@ -6305,9 +6308,9 @@ snapshots:
|
||||||
|
|
||||||
dlv@1.1.3: {}
|
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:
|
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: 24.13.1
|
||||||
builder-util-runtime: 9.2.4
|
builder-util-runtime: 9.2.4
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
|
@ -6375,7 +6378,7 @@ snapshots:
|
||||||
|
|
||||||
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:
|
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
|
archiver: 5.3.2
|
||||||
builder-util: 24.13.1
|
builder-util: 24.13.1
|
||||||
fs-extra: 10.1.0
|
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)):
|
electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||||
dependencies:
|
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: 24.13.1
|
||||||
builder-util-runtime: 9.2.4
|
builder-util-runtime: 9.2.4
|
||||||
chalk: 4.1.2
|
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
|
fs-extra: 10.1.0
|
||||||
is-ci: 3.0.1
|
is-ci: 3.0.1
|
||||||
lazy-val: 1.0.5
|
lazy-val: 1.0.5
|
||||||
|
|
Loading…
Reference in New Issue
Block a user