From 34c826353b9d59add21f96bc130856dc7b451f37 Mon Sep 17 00:00:00 2001 From: mozzie Date: Wed, 11 Sep 2024 12:58:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=8A=E4=BC=A0dicom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/electron/ipcEvent/dicom.ts | 3 - .../electron/ipcEvent/dicom/handler.ts | 12 +++ apps/desktop/electron/ipcEvent/dicom/util.ts | 101 ++++++++++++++++++ apps/desktop/electron/ipcEvent/index.ts | 6 +- apps/desktop/electron/main.ts | 13 +-- apps/desktop/package.json | 5 +- apps/desktop/shared/ipc.types.ts | 9 -- .../src/components/base/MenuBar/index.tsx | 25 ++--- apps/desktop/tsconfig.json | 2 +- apps/desktop/vite.config.ts | 1 - pnpm-lock.yaml | 17 +-- 11 files changed, 143 insertions(+), 51 deletions(-) delete mode 100644 apps/desktop/electron/ipcEvent/dicom.ts create mode 100644 apps/desktop/electron/ipcEvent/dicom/handler.ts create mode 100644 apps/desktop/electron/ipcEvent/dicom/util.ts delete mode 100644 apps/desktop/shared/ipc.types.ts diff --git a/apps/desktop/electron/ipcEvent/dicom.ts b/apps/desktop/electron/ipcEvent/dicom.ts deleted file mode 100644 index e6fb7a6..0000000 --- a/apps/desktop/electron/ipcEvent/dicom.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const useDicomHandler = () => { - -} \ No newline at end of file diff --git a/apps/desktop/electron/ipcEvent/dicom/handler.ts b/apps/desktop/electron/ipcEvent/dicom/handler.ts new file mode 100644 index 0000000..0053c35 --- /dev/null +++ b/apps/desktop/electron/ipcEvent/dicom/handler.ts @@ -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]; + }); +}; diff --git a/apps/desktop/electron/ipcEvent/dicom/util.ts b/apps/desktop/electron/ipcEvent/dicom/util.ts new file mode 100644 index 0000000..e4194d8 --- /dev/null +++ b/apps/desktop/electron/ipcEvent/dicom/util.ts @@ -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 => { + 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).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; +}; diff --git a/apps/desktop/electron/ipcEvent/index.ts b/apps/desktop/electron/ipcEvent/index.ts index 5446510..5d1a2ce 100644 --- a/apps/desktop/electron/ipcEvent/index.ts +++ b/apps/desktop/electron/ipcEvent/index.ts @@ -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() -} \ No newline at end of file + registerDicomHandler(); +}; diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index d6cc17d..92985cc 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -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); } }); } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 7b40a10..f409980 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/apps/desktop/shared/ipc.types.ts b/apps/desktop/shared/ipc.types.ts deleted file mode 100644 index 90de541..0000000 --- a/apps/desktop/shared/ipc.types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * ipc 通信的type、interface - */ -export namespace IPCEvents { - export enum Dicom { - Upload = 'dicom:upload', - Remove = 'dicom:remove', - } -} \ No newline at end of file diff --git a/apps/desktop/src/components/base/MenuBar/index.tsx b/apps/desktop/src/components/base/MenuBar/index.tsx index b0a1e79..addb1dd 100644 --- a/apps/desktop/src/components/base/MenuBar/index.tsx +++ b/apps/desktop/src/components/base/MenuBar/index.tsx @@ -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(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); + }); }; /** diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index f45f246..936e744 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -27,7 +27,7 @@ "paths": { "@/*": [ "./src/*" - ] + ], } }, "include": [ diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index dd8de09..11fed98 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -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: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea46776..b8ae8e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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