From 35404133408c0b7c23502e132e35d6215d012a22 Mon Sep 17 00:00:00 2001 From: mozzie Date: Wed, 4 Sep 2024 17:00:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20boot=E5=88=86=E6=9E=90=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/electron/core/dicom.ts | 13 +- apps/desktop/electron/main.ts | 3 +- apps/desktop/package.json | 8 +- apps/desktop/src/components/ui/badge.tsx | 36 +++ apps/desktop/src/components/ui/button.tsx | 19 +- apps/desktop/src/components/ui/carousel.tsx | 262 ++++++++++++++++++ apps/desktop/src/lib/utils.ts | 26 ++ apps/desktop/src/pages/Boot/index.tsx | 208 +++++++++++++- .../src/pages/Datasource/SeriesTable.tsx | 231 +++++++++++---- apps/desktop/src/pages/Datasource/index.tsx | 124 +-------- apps/desktop/src/pages/LeftDocker.tsx | 2 +- apps/desktop/src/pages/MenuBar.tsx | 2 +- apps/desktop/src/pages/Models/ModelTable.tsx | 28 +- pnpm-lock.yaml | 45 ++- 14 files changed, 787 insertions(+), 220 deletions(-) create mode 100644 apps/desktop/src/components/ui/badge.tsx create mode 100644 apps/desktop/src/components/ui/carousel.tsx diff --git a/apps/desktop/electron/core/dicom.ts b/apps/desktop/electron/core/dicom.ts index 53b0835..e16111c 100644 --- a/apps/desktop/electron/core/dicom.ts +++ b/apps/desktop/electron/core/dicom.ts @@ -15,6 +15,7 @@ export interface ExtractMetadata { pixelData?: Uint16Array; PatientSex: string; PatientAge: string; + AcquisitionDate: string; } /** @@ -81,6 +82,7 @@ export const parseDICOMFile = async ( const PatientName = dataSet.string("x00100030"); const PatientSex = dataSet.string("x00100040") ?? ""; const PatientAge = dataSet.string("x00101010") ?? ""; + const AcquisitionDate = dataSet.string("x00080022") ?? ""; // const pixelDataElement = dataSet.elements.x7fe00010; // const pixelData = new Uint16Array( // dataSet.byteArray.buffer, @@ -95,6 +97,7 @@ export const parseDICOMFile = async ( PatientName, PatientSex, PatientAge, + AcquisitionDate, // pixelData, }; } catch (error) { @@ -130,15 +133,10 @@ export const processFilesInBatches = async ( return results; }; -export interface StructuredMetadata { +export type StructuredMetadata = Partial & { filePaths: string[]; - StudyInstanceUID?: string; - SeriesInstanceUID?: string; - PatientName?: string; fileHash: string[]; - PatientSex: string; - PatientAge: string; -} +}; export interface ScanProgress { percentage: number; @@ -184,6 +182,7 @@ export const structureMetadata = async ( SeriesInstanceUID: item.SeriesInstanceUID, PatientName: item.PatientName, PatientAge: item.PatientAge, + AcquisitionDate: item.AcquisitionDate, PatientSex: item.PatientSex, }); } diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 2dcbff0..80fc8f0 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -63,6 +63,7 @@ function createWindow() { if (VITE_DEV_SERVER_URL) { win.loadURL(VITE_DEV_SERVER_URL); + registerIpcMainHandlers(win); } else { win.loadFile(path.join(RENDERER_DIST, "index.html")).then(() => { if (process.argv.length >= 2) { @@ -71,11 +72,9 @@ function createWindow() { registerIpcMainHandlers(win); } }); - } // pythonManager = new PythonManager(win, "http://127.0.0.1:15001", 3000); - registerIpcMainHandlers(win); } function createTray() { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d812c78..36bb7b5 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -36,14 +36,17 @@ "cmdk": "^1.0.0", "custom-electron-titlebar": "^4.2.8", "date-fns": "^3.6.0", + "dayjs": "1.11.13", "dexie": "^4.0.8", "dicom-parser": "1.8.21", "dockview": "^1.15.2", "electron-store": "^10.0.0", + "embla-carousel-react": "^8.2.0", "flexlayout-react": "^0.7.15", "framer-motion": "^11.3.24", "lowdb": "^7.0.1", "lucide-react": "^0.408.0", + "node-machine-id": "1.1.12", "object-hash": "^3.0.0", "onnxruntime-node": "^1.18.0", "openvino-node": "2024.3.0", @@ -58,12 +61,11 @@ "react-router-dom": "^6.26.0", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", - "zod": "3.23.8", - "node-machine-id": "1.1.12" + "zod": "3.23.8" }, "devDependencies": { - "@types/node": "22.5.2", "@radix-ui/react-icons": "^1.3.0", + "@types/node": "22.5.2", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", "@typescript-eslint/eslint-plugin": "^7.1.1", diff --git a/apps/desktop/src/components/ui/badge.tsx b/apps/desktop/src/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/apps/desktop/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/apps/desktop/src/components/ui/button.tsx b/apps/desktop/src/components/ui/button.tsx index 0ba4277..0270f64 100644 --- a/apps/desktop/src/components/ui/button.tsx +++ b/apps/desktop/src/components/ui/button.tsx @@ -5,25 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", }, }, defaultVariants: { diff --git a/apps/desktop/src/components/ui/carousel.tsx b/apps/desktop/src/components/ui/carousel.tsx new file mode 100644 index 0000000..f9b6840 --- /dev/null +++ b/apps/desktop/src/components/ui/carousel.tsx @@ -0,0 +1,262 @@ +"use client" + +import * as React from "react" +import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons" +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on("reInit", onSelect) + api.on("select", onSelect) + + return () => { + api?.off("select", onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) + } +) +Carousel.displayName = "Carousel" + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) +}) +CarouselContent.displayName = "CarouselContent" + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( +
+ ) +}) +CarouselItem.displayName = "CarouselItem" + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +}) +CarouselPrevious.displayName = "CarouselPrevious" + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +}) +CarouselNext.displayName = "CarouselNext" + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +} diff --git a/apps/desktop/src/lib/utils.ts b/apps/desktop/src/lib/utils.ts index 365058c..7f5d3ee 100644 --- a/apps/desktop/src/lib/utils.ts +++ b/apps/desktop/src/lib/utils.ts @@ -1,6 +1,32 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; +import dayjs from "dayjs"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export const timeAgo = (inputDate: string | Date): string => { + const now = dayjs(); + const date = dayjs(inputDate); + const diffMinutes = now.diff(date, "minute"); + const diffHours = now.diff(date, "hour"); + const diffDays = now.diff(date, "day"); + + if (diffMinutes < 60) { + return `${diffMinutes} 分钟前`; + } else if (diffHours < 24) { + return `${diffHours} 小时前`; + } else if (diffDays < 7) { + return `${diffDays} 天前`; + } else if (diffDays < 30) { + const weeks = Math.floor(diffDays / 7); + return `${weeks} 周前`; + } else if (diffDays < 365) { + const months = Math.floor(diffDays / 30); + return `${months} 个月前`; + } else { + const years = Math.floor(diffDays / 365); + return `${years} 年前`; + } +}; diff --git a/apps/desktop/src/pages/Boot/index.tsx b/apps/desktop/src/pages/Boot/index.tsx index e5043e6..09fa6dc 100644 --- a/apps/desktop/src/pages/Boot/index.tsx +++ b/apps/desktop/src/pages/Boot/index.tsx @@ -2,12 +2,141 @@ import { Button } from "@/components/ui/button"; import { motion } from "framer-motion"; import { useEffect, useState } from "react"; import { IoCheckmarkCircleSharp } from "react-icons/io5"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Progress } from "@/components/ui/progress"; +import { ToastAction } from "@/components/ui/toast"; +import { useToast } from "@/components/ui/use-toast"; +import { RocketIcon } from "@radix-ui/react-icons"; +import { useLocation } from "react-router-dom"; +import { Terminal } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Carousel, + CarouselApi, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from "@/components/ui/carousel"; +import { Series } from "../Datasource/SeriesTable"; + +interface ScanProgress { + percentage: number; +} + +const defaultProgress = { + percentage: 0, +}; const Boot = () => { const [flaskWaiting, setFlaskWaiting] = useState(false); const [flaskRunning, setflaskRunning] = useState(false); const [flaskInfo, setFlaskInfo] = useState(""); - const [openFolder, setOpenFolder] = useState(""); + const location = useLocation(); + const selectDicoms = + location.state?.selectDicoms ?? + JSON.parse(localStorage.getItem("selectDicoms") ?? "{}"); + + /** + * windows系统右键启动应用菜单的入口路径 + */ + const [bootDirectoryPath, setBootDirectoryPath] = useState(""); + + const [progress, setProgress] = useState(defaultProgress); + const { toast } = useToast(); + const [, setResult] = useState<[]>([]); + + const [api, setApi] = useState(); + const [current, setCurrent] = useState(0); + const [count, setCount] = useState(0); + + const [importDialogVisible, setImportDialogVisible] = useState(false); + + useEffect(() => { + if (!api) return; + setCount(api.scrollSnapList().length); + setCurrent(api.selectedScrollSnap() + 1); + api.on("select", () => { + setCurrent(api.selectedScrollSnap() + 1); + }); + }, [api]); + + useEffect(() => { + const handleScanProgress = ( + _event: Electron.IpcRendererEvent, + data: any + ) => { + setProgress(data); + if (data.error) return; + }; + window.ipcRenderer.on("scan-progress", handleScanProgress); + return () => { + window.ipcRenderer.off("scan-progress", handleScanProgress); + }; + }, []); + + useEffect(() => { + window.ipcRenderer.once("scan-start", () => { + setImportDialogVisible(true); + }); + }, []); + + useEffect(() => { + const handleScanFinished = ( + _event: Electron.IpcRendererEvent, + data: any + ) => { + const { scanDuration, structDicom } = data; + console.log(structDicom); + setResult(structDicom); + setImportDialogVisible(false); + + if (data.error) { + return toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: "There was a problem with your request.", + action: 重试, + }); + } else { + toast({ + variant: "default", + title: "完成", + description: `本次操作共导入${structDicom.length}组序列数据,耗时:${( + scanDuration / 1000 + ).toFixed(2)} s`, + action: 启动AI测量, + 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]); + + useEffect(() => { + if (!importDialogVisible) { + setProgress(defaultProgress); + } + }, [importDialogVisible]); const handleBootPythonServer = () => { if (!flaskRunning) setFlaskWaiting(true); @@ -35,8 +164,8 @@ const Boot = () => { useEffect(() => { window.ipcRenderer.on("open-folder", (_, data) => { - console.log(data); - setOpenFolder(data); + console.log("右键菜单拉起启动文件夹路径", data); + setBootDirectoryPath(data); }); }, []); @@ -49,8 +178,59 @@ const Boot = () => { className="h-full" >
+ {/* */} +
+
+ + + Heads up! + 海量dicom,一键自动测量 + +
+
+ + +
+
+ + + {selectDicoms.map((item: Series, index: number) => ( + + + + {item.PatientName} + + {item.PatientAge}, {item.PatientSex},{" "} + {item.filePaths.length} 张 + + + + + {index + 1} + + + + + + + + + ))} + + + + +
+ {current} / {count} +
+
+
进度
+
{/* card */} -
{openFolder}
{flaskInfo}
@@ -71,6 +251,26 @@ const Boot = () => {
+ {/* 导入数据dialog */} + + + + 导入数据 + + 如果扫描速度很慢,请取消本次扫描,并缩小导入数据的体量 + + +
+ + + 扫描进度 + + + + +
+
+
); }; diff --git a/apps/desktop/src/pages/Datasource/SeriesTable.tsx b/apps/desktop/src/pages/Datasource/SeriesTable.tsx index 83ce7cf..7198d45 100644 --- a/apps/desktop/src/pages/Datasource/SeriesTable.tsx +++ b/apps/desktop/src/pages/Datasource/SeriesTable.tsx @@ -1,4 +1,5 @@ import { ChevronDownIcon } from "@radix-ui/react-icons"; +import dayjs from "dayjs"; import { ColumnDef, ColumnFiltersState, @@ -7,11 +8,17 @@ import { flexRender, getCoreRowModel, getFilteredRowModel, - getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; -import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import { + ArrowUpDown, + Brain, + DeleteIcon, + FileDownIcon, + FilterIcon, + MoreHorizontal, +} from "lucide-react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -19,9 +26,11 @@ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, + DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, + DropdownMenuShortcut, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; @@ -35,6 +44,9 @@ import { } from "@/components/ui/table"; import { useState } from "react"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { timeAgo } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { useNavigate } from "react-router-dom"; export type Series = { id: string; @@ -56,6 +68,7 @@ const columnsAlias: { [K in keyof Partial as string]: string } = { filePaths: "层数", createTime: "创建时间", updateTime: "修改时间", + AcquisitionDate: "采集日期", }; export const columns: ColumnDef[] = [ @@ -123,45 +136,87 @@ export const columns: ColumnDef[] = [
), }, + { + id: "AcquisitionDate", + accessorKey: "AcquisitionDate", + header: ({ column }) => { + return ( +
+ {columnsAlias["AcquisitionDate"]} + +
+ ); + }, + cell: ({ row }) => + row.getValue("AcquisitionDate") && ( +
+ {row.getValue("AcquisitionDate")} +
+ ), + }, + { id: "createTime", accessorKey: "createTime", header: ({ column }) => { return ( - + +
); }, - cell: ({ row }) => ( -
- {new Date(row.getValue("createTime")).toLocaleString()} -
- ), + cell: ({ row }) => + row.getValue("createTime") && ( +
+ {dayjs(row.getValue("createTime")).format("YYYYMMDD")} + + {timeAgo(row.getValue("createTime"))} + +
+ ), }, { id: "updateTime", accessorKey: "updateTime", header: ({ column }) => { return ( - + +
); }, - cell: ({ row }) => ( -
- {new Date(row.getValue("updateTime")).toLocaleString()} -
- ), + cell: ({ row }) => + row.getValue("updateTime") && ( +
+ {dayjs(row.getValue("updateTime")).format("YYYYMMDD")} + + {timeAgo(row.getValue("updateTime"))} + +
+ ), }, { id: "actions", @@ -204,8 +259,11 @@ export function SeriesTable(props: SeriesTableProps) { const [columnFilters, setColumnFilters] = useState([]); const [columnVisibility, setColumnVisibility] = useState({ SeriesInstanceUID: false, + updateTime: false, + createTime: false, }); const [rowSelection, setRowSelection] = useState({}); + const navigate = useNavigate(); const table = useReactTable({ data: props.data, @@ -213,7 +271,6 @@ export function SeriesTable(props: SeriesTableProps) { onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, @@ -226,45 +283,97 @@ export function SeriesTable(props: SeriesTableProps) { }, }); + const handleSelectExport2Excel = () => { + console.log("导出到excel"); + }; + + const handleSelectAI = () => { + const selectDicoms = table + .getSelectedRowModel() + .rows.map((r) => r.original); + localStorage.setItem("selectDicoms", JSON.stringify(selectDicoms)); + navigate("/", { state: { selectDicoms } }); + }; + return (
- - table.getColumn("PatientName")?.setFilterValue(event.target.value) - } - className="max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {columnsAlias[column.id]} - - ); - })} - - +
+ + table.getColumn("PatientName")?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {columnsAlias[column.id]} + + ); + })} + + +
+ {table.getFilteredSelectedRowModel().rows.length > 0 && ( +
+ + + + + + + + + 导出到excel + ⌘+T + + + + 自动测量 + ⌘+A + + + + + + + 删除 + ⌘+D + + + + +
+
+ 选中了 {table.getFilteredSelectedRowModel().rows.length} 项 +
+
+
+ )}
diff --git a/apps/desktop/src/pages/Datasource/index.tsx b/apps/desktop/src/pages/Datasource/index.tsx index 9d81a2f..9b6af04 100644 --- a/apps/desktop/src/pages/Datasource/index.tsx +++ b/apps/desktop/src/pages/Datasource/index.tsx @@ -1,107 +1,29 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Progress } from "@/components/ui/progress"; -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 { useCallback, useEffect, useState } from "react"; import { Series, SeriesTable } from "./SeriesTable"; import { motion } from "framer-motion"; -interface ScanProgress { - percentage: number; -} - -const defaultProgress = { - percentage: 0, -}; - export const Datasource = () => { - const [progress, setProgress] = useState(defaultProgress); - const { toast } = useToast(); - const [, setResult] = useState<[]>([]); - const [seriesData, setSeriesData] = useState([]); - const [importDialogVisible, setImportDialogVisible] = useState(false); - - useEffect(() => { - const handleScanProgress = ( - _event: Electron.IpcRendererEvent, - data: any - ) => { - setProgress(data); - if (data.error) return; - }; - window.ipcRenderer.on("scan-progress", handleScanProgress); - return () => { - window.ipcRenderer.off("scan-progress", handleScanProgress); - }; - }, []); - - useEffect(() => { - window.ipcRenderer.once("scan-start", () => { - setImportDialogVisible(true); - }); - }, []); - - useEffect(() => { - const handleScanFinished = ( - _event: Electron.IpcRendererEvent, - data: any - ) => { - const { scanDuration, structDicom } = data; - setResult(structDicom); - setImportDialogVisible(false); - - if (data.error) { - return toast({ - variant: "destructive", - title: "Uh oh! Something went wrong.", - description: "There was a problem with your request.", - action: 重试, - }); - } else { - toast({ - variant: "default", - title: "完成", - description: `本次操作共导入${structDicom.length}组序列数据,耗时:${( - scanDuration / 1000 - ).toFixed(2)} s`, - action: 启动AI测量, - 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]); - - useEffect(() => { - if (!importDialogVisible) { - setProgress(defaultProgress); - } - }, [importDialogVisible]); useEffect(() => { window.ipcRenderer.send("db:series:select"); }, []); - useEffect(() => { - window.ipcRenderer.on("db:series:select:response", (_event, data) => { + 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 ( { transition={{ duration: 0.25 }} > - {/* 导入数据dialog */} - - - - 导入数据 - - 如果扫描速度很慢,请取消本次扫描,并缩小导入数据的体量 - - -
- - - 扫描进度 - - - - -
-
-
); }; diff --git a/apps/desktop/src/pages/LeftDocker.tsx b/apps/desktop/src/pages/LeftDocker.tsx index 96c4ae5..7918df8 100644 --- a/apps/desktop/src/pages/LeftDocker.tsx +++ b/apps/desktop/src/pages/LeftDocker.tsx @@ -17,7 +17,7 @@ type MenuItem = { }; const menuItems: MenuItem[] = [ - { to: "/", name: "一键启动", icon: }, + { to: "/", name: "自动分析", icon: }, { to: "/datasource", name: "数据列表", icon: }, { to: "/models", name: "模型管理", icon: }, { to: "/tools", name: "小工具", icon: }, diff --git a/apps/desktop/src/pages/MenuBar.tsx b/apps/desktop/src/pages/MenuBar.tsx index 7c4d0bd..6e1f85d 100644 --- a/apps/desktop/src/pages/MenuBar.tsx +++ b/apps/desktop/src/pages/MenuBar.tsx @@ -19,7 +19,7 @@ export const MenuBar = () => { const navigate = useNavigate(); const handleImportDicom = () => { - navigate("datasource"); + navigate("/"); window.ipcRenderer.send("import-dicom-dialog-visible"); }; diff --git a/apps/desktop/src/pages/Models/ModelTable.tsx b/apps/desktop/src/pages/Models/ModelTable.tsx index d4698b4..cc24caa 100644 --- a/apps/desktop/src/pages/Models/ModelTable.tsx +++ b/apps/desktop/src/pages/Models/ModelTable.tsx @@ -7,7 +7,6 @@ import { flexRender, getCoreRowModel, getFilteredRowModel, - getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; @@ -232,7 +231,6 @@ export function ModelTable() { onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, @@ -258,13 +256,20 @@ export function ModelTable() { } className="max-w-sm" /> -
- - +
+
+
+ 选中了 {table.getFilteredSelectedRowModel().rows.length} +
+
+
+ + +
@@ -318,11 +323,6 @@ export function ModelTable() {
-
-
- 选中了 {table.getFilteredSelectedRowModel().rows.length} -
-
); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edf193a..8bae5e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,9 @@ importers: date-fns: specifier: ^3.6.0 version: 3.6.0 + dayjs: + specifier: 1.11.13 + version: 1.11.13 dexie: specifier: ^4.0.8 version: 4.0.8 @@ -97,6 +100,9 @@ importers: electron-store: specifier: ^10.0.0 version: 10.0.0 + embla-carousel-react: + specifier: ^8.2.0 + version: 8.2.0(react@18.3.1) flexlayout-react: specifier: ^0.7.15 version: 0.7.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2191,6 +2197,19 @@ packages: engines: {node: '>= 12.20.55'} hasBin: true + embla-carousel-react@8.2.0: + resolution: {integrity: sha512-dWqbmaEBQjeAcy/EKrcAX37beVr0ubXuHPuLZkx27z58V1FIvRbbMb4/c3cLZx0PAv/ofngX2QFrwUB+62SPnw==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 + + embla-carousel-reactive-utils@8.2.0: + resolution: {integrity: sha512-ZdaPNgMydkPBiDRUv+wRIz3hpZJ3LKrTyz+XWi286qlwPyZFJDjbzPBiXnC3czF9N/nsabSc7LTRvGauUzwKEg==} + peerDependencies: + embla-carousel: 8.2.0 + + embla-carousel@8.2.0: + resolution: {integrity: sha512-rf2GIX8rab9E6ZZN0Uhz05746qu2KrDje9IfFyHzjwxLwhvGjUt6y9+uaY1Sf+B0OPSa3sgas7BE2hWZCtopTA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -5730,7 +5749,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 @@ -5744,7 +5763,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 @@ -6222,9 +6241,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 @@ -6292,7 +6311,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 @@ -6302,11 +6321,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 @@ -6344,6 +6363,18 @@ snapshots: transitivePeerDependencies: - supports-color + embla-carousel-react@8.2.0(react@18.3.1): + dependencies: + embla-carousel: 8.2.0 + embla-carousel-reactive-utils: 8.2.0(embla-carousel@8.2.0) + react: 18.3.1 + + embla-carousel-reactive-utils@8.2.0(embla-carousel@8.2.0): + dependencies: + embla-carousel: 8.2.0 + + embla-carousel@8.2.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {}