From 40717541d44945d5abfbdda777b0f0d1236908ab Mon Sep 17 00:00:00 2001 From: mozzie Date: Mon, 2 Sep 2024 14:18:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=BC=E5=85=A5=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=88=97=E8=A1=A8=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/electron/core/db.ts | 48 ++- apps/desktop/electron/core/dicom.ts | 33 +- apps/desktop/electron/ipcMainHandlers.ts | 20 ++ apps/desktop/electron/main.ts | 3 + .../src/pages/Datasource/SeriesTable.tsx | 329 ++++++++---------- apps/desktop/src/pages/Datasource/index.tsx | 19 +- apps/desktop/src/pages/Models/ModelTable.tsx | 6 +- apps/desktop/src/pages/Models/index.tsx | 1 + 8 files changed, 232 insertions(+), 227 deletions(-) diff --git a/apps/desktop/electron/core/db.ts b/apps/desktop/electron/core/db.ts index 918d3a8..91e775f 100644 --- a/apps/desktop/electron/core/db.ts +++ b/apps/desktop/electron/core/db.ts @@ -1,24 +1,38 @@ import path from "node:path"; import { JSONFilePreset } from "lowdb/node"; import { app } from "electron"; +import { Low } from "node_modules/lowdb/lib/core/Low"; +import { StructuredMetadata } from "./dicom"; -const initDb = async () => { - // Read or create db.json - const defaultData = { posts: [] }; - const db = await JSONFilePreset( - path.join(app.getPath("userData"), "db.json"), - defaultData - ); +interface ICreateDatabase { + name: string; +} - // Update db.json - await db.update(({ posts }) => posts.push("hello world")); - - // Alternatively you can call db.write() explicitely later - // to write to db.json - db.data.posts.push("hello world"); - await db.write(); - - console.log(db); +type SeriesTableType = StructuredMetadata & { + createTime?: number; + updateTime?: number; }; -initDb(); +interface DbTable { + series: SeriesTableType[]; +} + +const defaultData: DbTable = { series: [] }; +export let db: Low<{ series: SeriesTableType[] }>; + +export const createDatabase = async (config: ICreateDatabase) => { + const { name } = config; + db = await JSONFilePreset( + path.join(app.getPath("userData"), name), + defaultData + ); +}; + +// Update db.json +// await db.update(({ posts }) => posts.push("hello world")); + +// Alternatively you can call db.write() explicitely later to write to db.json +// db.data.posts.push("hello world"); +// await db.write(); + +// console.log(db); diff --git a/apps/desktop/electron/core/dicom.ts b/apps/desktop/electron/core/dicom.ts index 188a129..c47a4c1 100644 --- a/apps/desktop/electron/core/dicom.ts +++ b/apps/desktop/electron/core/dicom.ts @@ -1,7 +1,7 @@ import path from "path"; import * as dicomParser from "dicom-parser"; import fs from "fs"; -import crypto from 'crypto'; +import crypto from "crypto"; export interface StructuredData { [SeriesInstanceUID: string]: ExtractMetadata[]; @@ -13,6 +13,8 @@ export interface ExtractMetadata { SeriesInstanceUID?: string; PatientName?: string; pixelData?: Uint16Array; + PatientSex: string; + PatientAge: string; } /** @@ -77,6 +79,8 @@ export const parseDICOMFile = async ( const StudyInstanceUID = dataSet.string("x0020000d"); const SeriesInstanceUID = dataSet.string("x0020000e"); const PatientName = dataSet.string("x00100030"); + const PatientSex = dataSet.string("x00100040") ?? ""; + const PatientAge = dataSet.string("x00101010") ?? ""; const pixelDataElement = dataSet.elements.x7fe00010; const pixelData = new Uint16Array( dataSet.byteArray.buffer, @@ -89,6 +93,8 @@ export const parseDICOMFile = async ( StudyInstanceUID, SeriesInstanceUID, PatientName, + PatientSex, + PatientAge, // pixelData, }; } catch (error) { @@ -129,34 +135,37 @@ export interface StructuredMetadata { StudyInstanceUID?: string; SeriesInstanceUID?: string; PatientName?: string; - fileHash: string[] + fileHash: string[]; + PatientSex: string; + PatientAge: string; } export interface ScanProgress { - percentage: number + percentage: number; } // 计算文件的哈希值 async function calculateFileHash(filePath: string): Promise { return new Promise((resolve, reject) => { - const hash = crypto.createHash('sha256'); + const hash = crypto.createHash("sha256"); const stream = fs.createReadStream(filePath); - stream.on('error', reject); - stream.pipe(hash).on('finish', () => resolve(hash.digest('hex'))); + stream.on("error", reject); + stream.pipe(hash).on("finish", () => resolve(hash.digest("hex"))); }); } +// 序列级别 +export const keyProp = "SeriesInstanceUID"; + export const structureMetadata = async ( data: ExtractMetadata[], progressCallback?: (progress: ScanProgress) => void ): Promise => { const result: StructuredMetadata[] = []; - // 序列级别 - const keyProp = 'SeriesInstanceUID' for (let i = 0; i < data.length; i++) { - const item = data[i] + const item = data[i]; const existItem = result.find((i) => i[keyProp] === item[keyProp]); const hash = await calculateFileHash(item.filePath); @@ -164,7 +173,7 @@ export const structureMetadata = async ( // 如果找到了相同的条目,合并 filePath if (!existItem.fileHash.includes(hash)) { existItem.filePaths.push(item.filePath); - existItem.fileHash.push(hash) + existItem.fileHash.push(hash); } } else { // 如果没有找到,创建一个新的条目 @@ -174,10 +183,12 @@ export const structureMetadata = async ( StudyInstanceUID: item.StudyInstanceUID, SeriesInstanceUID: item.SeriesInstanceUID, PatientName: item.PatientName, + PatientAge: item.PatientAge, + PatientSex: item.PatientSex, }); } const progress: ScanProgress = { - percentage: ((i + 1) / data.length) * 100 + percentage: ((i + 1) / data.length) * 100, }; progressCallback?.(progress); } diff --git a/apps/desktop/electron/ipcMainHandlers.ts b/apps/desktop/electron/ipcMainHandlers.ts index 0af9433..4171f62 100644 --- a/apps/desktop/electron/ipcMainHandlers.ts +++ b/apps/desktop/electron/ipcMainHandlers.ts @@ -2,11 +2,14 @@ import { dialog, ipcMain } from "electron"; import os from "os"; import { findDcmFiles, + keyProp, processFilesInBatches, + StructuredMetadata, structureMetadata, } from "./core/dicom"; import { EVENT_PARSE_DICOM } from "./ipcEvent"; import PythonManager from "./core/PythonManager"; +import { db } from "./core/db"; /** * 渲染进程和主进程的事件调度 @@ -60,8 +63,25 @@ const registerIpcMainHandlers = ( const structDicom = await structureMetadata(items, (progress) => { event.reply("scan-progress", progress); }); + // 存数据库 + const changeTime = Date.now(); + for (const item of structDicom) { + const existSeries = db.data.series.find( + (i) => i[keyProp] === item[keyProp] + ); + existSeries + ? Object.assign(existSeries, item, { updateTime: changeTime }) + : db.data.series.push({ ...item, createTime: changeTime }); + await db.write(); + } event.reply("scan-progress-done", structDicom); }); + + ipcMain.on("db:series:select", async (event, data) => { + await db.read(); + const seriesList = db.data.series; + event.reply("db:series:select:response", seriesList); + }); }; export default registerIpcMainHandlers; diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index f43954d..fa6d4c0 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url"; import path from "node:path"; import registerIpcMainHandlers from "./ipcMainHandlers"; import PythonManager from "./core/PythonManager"; +import { createDatabase } from "./core/db"; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -133,6 +134,8 @@ app.whenReady().then(() => { createWindow(); createTray(); registerGlobalShortcuts(); + createDatabase({ name: "cvpilot.json" }); + console.log(path.join(app.getPath("userData"))); // 设置 Dock 图标 if (process.platform === "darwin") { diff --git a/apps/desktop/src/pages/Datasource/SeriesTable.tsx b/apps/desktop/src/pages/Datasource/SeriesTable.tsx index bf36d88..ea68155 100644 --- a/apps/desktop/src/pages/Datasource/SeriesTable.tsx +++ b/apps/desktop/src/pages/Datasource/SeriesTable.tsx @@ -1,11 +1,4 @@ -"use client"; - -import * as React from "react"; -import { - CaretSortIcon, - ChevronDownIcon, - DotsHorizontalIcon, -} from "@radix-ui/react-icons"; +import { ChevronDownIcon } from "@radix-ui/react-icons"; import { ColumnDef, ColumnFiltersState, @@ -25,9 +18,6 @@ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; @@ -39,48 +29,22 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { useEffect, useState } from "react"; +import { ScrollArea } from "@/components/ui/scroll-area"; -const data: Payment[] = [ - { - id: "m5gr84i9", - amount: 316, - status: "success", - email: "ken99@yahoo.com", - }, - { - id: "3u1reuv4", - amount: 242, - status: "success", - email: "Abe45@gmail.com", - }, - { - id: "derv1ws0", - amount: 837, - status: "processing", - email: "Monserrat44@gmail.com", - }, - { - id: "5kma53ae", - amount: 874, - status: "success", - email: "Silas22@gmail.com", - }, - { - id: "bhqecj4p", - amount: 721, - status: "failed", - email: "carmella@hotmail.com", - }, -]; - -export type Payment = { +export type Series = { id: string; - amount: number; - status: "pending" | "processing" | "success" | "failed"; - email: string; + StudyInstanceUID?: string; + SeriesInstanceUID?: string; + PatientName?: string; + PatientAge: string; + PatientSex: string; + createTime?: number; + updateTime?: number; + filePaths: string[]; }; -export const columns: ColumnDef[] = [ +export const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -104,81 +68,82 @@ export const columns: ColumnDef[] = [ enableHiding: false, }, { - accessorKey: "status", - header: "Status", + accessorKey: "PatientName", + header: "姓名", cell: ({ row }) => ( -
{row.getValue("status")}
+
{row.getValue("PatientName")}
), }, { - accessorKey: "email", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) =>
{row.getValue("email")}
, + accessorKey: "PatientAge", + header: "年龄", + cell: ({ row }) => ( +
{row.getValue("PatientAge")}
+ ), }, { - accessorKey: "amount", - header: () =>
Amount
, - cell: ({ row }) => { - const amount = parseFloat(row.getValue("amount")); - - // Format the amount as a dollar amount - const formatted = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(amount); - - return
{formatted}
; - }, + accessorKey: "PatientSex", + header: "性别", + cell: ({ row }) => ( +
{row.getValue("PatientSex")}
+ ), }, { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - const payment = row.original; - - return ( - - - - - - Actions - navigator.clipboard.writeText(payment.id)} - > - Copy payment ID - - - View customer - View payment details - - - ); - }, + accessorKey: "SeriesInstanceUID", + header: "序列UID", + cell: ({ row }) => ( +
{row.getValue("SeriesInstanceUID")}
+ ), + }, + { + accessorKey: "filePaths", + header: "层数", + cell: ({ row }) => ( +
+ {(row.getValue("filePaths") as string[]).length} +
+ ), + }, + { + accessorKey: "createTime", + header: "创建时间", + cell: ({ row }) => ( +
+ {new Date(row.getValue("createTime")).toLocaleString()} +
+ ), + }, + { + accessorKey: "updateTime", + header: "修改时间", + cell: ({ row }) => ( +
+ {new Date(row.getValue("updateTime")).toLocaleString()} +
+ ), }, ]; export function SeriesTable() { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [] - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); + const [sorting, setSorting] = useState([]); + const [data, setData] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({ + SeriesInstanceUID: false, + updateTime: false, + }); + const [rowSelection, setRowSelection] = useState({}); + + useEffect(() => { + window.ipcRenderer.send("db:series:select"); + }, []); + + useEffect(() => { + window.ipcRenderer.on("db:series:select:response", (event, data) => { + console.log(data); + setData(data); + }); + }, []); const table = useReactTable({ data, @@ -200,20 +165,22 @@ export function SeriesTable() { }); return ( -
-
+
+
- table.getColumn("email")?.setFilterValue(event.target.value) + table.getColumn("PatientName")?.setFilterValue(event.target.value) } className="max-w-sm" /> - @@ -230,87 +197,65 @@ export function SeriesTable() { column.toggleVisibility(!!value) } > - {column.id} + {column.columnDef.header} ); })}
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} + +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} - )) - ) : ( - - - No results. - - - )} - -
-
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
-
- - -
-
+
); } diff --git a/apps/desktop/src/pages/Datasource/index.tsx b/apps/desktop/src/pages/Datasource/index.tsx index 5b438d9..f73d575 100644 --- a/apps/desktop/src/pages/Datasource/index.tsx +++ b/apps/desktop/src/pages/Datasource/index.tsx @@ -11,6 +11,8 @@ import { ToastAction } from "@/components/ui/toast"; import { useToast } from "@/components/ui/use-toast"; import { RocketIcon } from "@radix-ui/react-icons"; import { useEffect, useState } from "react"; +import { SeriesTable } from "./SeriesTable"; +import { motion } from "framer-motion"; interface ScanProgress { percentage: number; @@ -43,7 +45,7 @@ export const Datasource = () => { setImportDialogVisible(true); setStartTime(Date.now()); }); - }, [progress?.percentage, toast]); + }, []); useEffect(() => { const handleScanFinished = (event, data) => { @@ -63,8 +65,9 @@ export const Datasource = () => { toast({ variant: "default", title: "完成", - description: `本次操作共导入${data.length}组序列数据,耗时:${timeDuration } s`, + description: `本次操作共导入${data.length}组序列数据,耗时:${timeDuration} s`, action: 启动AI测量, + duration: 30 * 1000, }); } }; @@ -86,7 +89,15 @@ export const Datasource = () => { }, [importDialogVisible]); return ( -
+ + + {/* 导入数据dialog */} @@ -106,6 +117,6 @@ export const Datasource = () => {
-
+ ); }; diff --git a/apps/desktop/src/pages/Models/ModelTable.tsx b/apps/desktop/src/pages/Models/ModelTable.tsx index 1f2e5e8..395335c 100644 --- a/apps/desktop/src/pages/Models/ModelTable.tsx +++ b/apps/desktop/src/pages/Models/ModelTable.tsx @@ -251,9 +251,9 @@ export function ModelTable() {
- table.getColumn("email")?.setFilterValue(event.target.value) + table.getColumn("modelname")?.setFilterValue(event.target.value) } className="max-w-sm" /> @@ -266,7 +266,7 @@ export function ModelTable() {
- +
diff --git a/apps/desktop/src/pages/Models/index.tsx b/apps/desktop/src/pages/Models/index.tsx index 35ac5fb..565fcbb 100644 --- a/apps/desktop/src/pages/Models/index.tsx +++ b/apps/desktop/src/pages/Models/index.tsx @@ -4,6 +4,7 @@ import { ModelTable } from "./ModelTable"; export const Models = () => { return (