feat: boot分析进度界面
This commit is contained in:
parent
fc26f12051
commit
3540413340
|
@ -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<ExtractMetadata> & {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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",
|
||||
|
|
36
apps/desktop/src/components/ui/badge.tsx
Normal file
36
apps/desktop/src/components/ui/badge.tsx
Normal file
|
@ -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<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
|
@ -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: {
|
||||
|
|
262
apps/desktop/src/components/ui/carousel.tsx
Normal file
262
apps/desktop/src/components/ui/carousel.tsx
Normal file
|
@ -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<typeof useEmblaCarousel>
|
||||
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<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & 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<HTMLDivElement>) => {
|
||||
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 (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
Carousel.displayName = "Carousel"
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
CarouselContent.displayName = "CarouselContent"
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
CarouselItem.displayName = "CarouselItem"
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselPrevious.displayName = "CarouselPrevious"
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselNext.displayName = "CarouselNext"
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
}
|
|
@ -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} 年前`;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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<ScanProgress>(defaultProgress);
|
||||
const { toast } = useToast();
|
||||
const [, setResult] = useState<[]>([]);
|
||||
|
||||
const [api, setApi] = useState<CarouselApi>();
|
||||
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: <ToastAction altText="重试">重试</ToastAction>,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "完成",
|
||||
description: `本次操作共导入${structDicom.length}组序列数据,耗时:${(
|
||||
scanDuration / 1000
|
||||
).toFixed(2)} s`,
|
||||
action: <ToastAction altText="启动AI测量">启动AI测量</ToastAction>,
|
||||
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"
|
||||
>
|
||||
<div className="p-4 h-full flex flex-col">
|
||||
{/* */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-2/3 mt-8">
|
||||
<Alert>
|
||||
<Terminal className="h-4 w-4" />
|
||||
<AlertTitle>Heads up!</AlertTitle>
|
||||
<AlertDescription>海量dicom,一键自动测量</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
<div className="w-2/3 mt-4">
|
||||
<Button size="sm" className="rounded-full mr-2">
|
||||
DICOM
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="rounded-full">
|
||||
MRI
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-2/3 mt-8 flex flex-col items-center justify-center">
|
||||
<Carousel setApi={setApi} className="w-full max-w-xs">
|
||||
<CarouselContent>
|
||||
{selectDicoms.map((item: Series, index: number) => (
|
||||
<CarouselItem key={index}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.PatientName}</CardTitle>
|
||||
<CardDescription>
|
||||
{item.PatientAge}, {item.PatientSex},{" "}
|
||||
{item.filePaths.length} 张
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center ">
|
||||
<span className="text-4xl font-semibold">
|
||||
{index + 1}
|
||||
</span>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>扔掉</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
<div className="py-2 text-center text-sm text-muted-foreground">
|
||||
{current} / {count}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-2/3 mt-4">进度</div>
|
||||
</div>
|
||||
{/* card */}
|
||||
<div>{openFolder}</div>
|
||||
<main className="flex-1 flex flex-col">
|
||||
<div className="mt-4 flex-1">
|
||||
<pre className="text-xs">{flaskInfo}</pre>
|
||||
|
@ -71,6 +251,26 @@ const Boot = () => {
|
|||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
{/* 导入数据dialog */}
|
||||
<Dialog open={importDialogVisible} onOpenChange={setImportDialogVisible}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>导入数据</DialogTitle>
|
||||
<DialogDescription>
|
||||
如果扫描速度很慢,请取消本次扫描,并缩小导入数据的体量
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Alert>
|
||||
<RocketIcon className="h-4 w-4" />
|
||||
<AlertTitle>扫描进度</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Progress value={progress?.percentage} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<Series> as string]: string } = {
|
|||
filePaths: "层数",
|
||||
createTime: "创建时间",
|
||||
updateTime: "修改时间",
|
||||
AcquisitionDate: "采集日期",
|
||||
};
|
||||
|
||||
export const columns: ColumnDef<Series>[] = [
|
||||
|
@ -123,23 +136,57 @@ export const columns: ColumnDef<Series>[] = [
|
|||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "AcquisitionDate",
|
||||
accessorKey: "AcquisitionDate",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{columnsAlias["AcquisitionDate"]}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-7 w-7 ml-2"
|
||||
size="icon"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
<ArrowUpDown className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) =>
|
||||
row.getValue("AcquisitionDate") && (
|
||||
<div className="capitalize">
|
||||
<span>{row.getValue("AcquisitionDate")}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: "createTime",
|
||||
accessorKey: "createTime",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{columnsAlias["createTime"]}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-7 w-7 ml-2"
|
||||
size="icon"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
{columnsAlias["createTime"]}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
<ArrowUpDown className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
cell: ({ row }) =>
|
||||
row.getValue("createTime") && (
|
||||
<div className="capitalize">
|
||||
{new Date(row.getValue("createTime")).toLocaleString()}
|
||||
<span>{dayjs(row.getValue("createTime")).format("YYYYMMDD")}</span>
|
||||
<Badge className="ml-2 text-xs font-normal">
|
||||
{timeAgo(row.getValue("createTime"))}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
@ -148,18 +195,26 @@ export const columns: ColumnDef<Series>[] = [
|
|||
accessorKey: "updateTime",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{columnsAlias["updateTime"]}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-7 w-7 ml-2"
|
||||
size="icon"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
{columnsAlias["updateTime"]}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
<ArrowUpDown className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
cell: ({ row }) =>
|
||||
row.getValue("updateTime") && (
|
||||
<div className="capitalize">
|
||||
{new Date(row.getValue("updateTime")).toLocaleString()}
|
||||
<span>{dayjs(row.getValue("updateTime")).format("YYYYMMDD")}</span>
|
||||
<Badge className="ml-2 text-xs font-normal">
|
||||
{timeAgo(row.getValue("updateTime"))}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
@ -204,8 +259,11 @@ export function SeriesTable(props: SeriesTableProps) {
|
|||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
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,9 +283,22 @@ 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 (
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<div className="flex-shrink-0 flex items-center p-4">
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
placeholder="筛选姓名"
|
||||
value={
|
||||
|
@ -241,8 +311,8 @@ export function SeriesTable(props: SeriesTableProps) {
|
|||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="ml-auto">
|
||||
显示 <ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||
<Button variant="ghost" size="icon" className="ml-2">
|
||||
<FilterIcon className="h-3.5 w-3.5 " />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
|
@ -266,6 +336,45 @@ export function SeriesTable(props: SeriesTableProps) {
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{table.getFilteredSelectedRowModel().rows.length > 0 && (
|
||||
<div className="flex-grow justify-end inline-flex items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="mr-4">
|
||||
操作 <ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={handleSelectExport2Excel}>
|
||||
<FileDownIcon className="mr-2 h-4 w-4" />
|
||||
<span>导出到excel</span>
|
||||
<DropdownMenuShortcut>⌘+T</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={handleSelectAI}>
|
||||
<Brain className="mr-2 h-4 w-4" />
|
||||
<span>自动测量</span>
|
||||
<DropdownMenuShortcut>⌘+A</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<DeleteIcon className="mr-2 h-4 w-4" />
|
||||
<span>删除</span>
|
||||
<DropdownMenuShortcut>⌘+D</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="space-x-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
选中了 {table.getFilteredSelectedRowModel().rows.length} 项
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ScrollArea className="h-full w-full flex-grow pl-4 pr-4">
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
|
|
|
@ -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<ScanProgress>(defaultProgress);
|
||||
const { toast } = useToast();
|
||||
const [, setResult] = useState<[]>([]);
|
||||
|
||||
const [seriesData, setSeriesData] = useState<Series[]>([]);
|
||||
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: <ToastAction altText="重试">重试</ToastAction>,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "完成",
|
||||
description: `本次操作共导入${structDicom.length}组序列数据,耗时:${(
|
||||
scanDuration / 1000
|
||||
).toFixed(2)} s`,
|
||||
action: <ToastAction altText="启动AI测量">启动AI测量</ToastAction>,
|
||||
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 (
|
||||
<motion.div
|
||||
|
@ -112,26 +34,6 @@ export const Datasource = () => {
|
|||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<SeriesTable data={seriesData} />
|
||||
{/* 导入数据dialog */}
|
||||
<Dialog open={importDialogVisible} onOpenChange={setImportDialogVisible}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>导入数据</DialogTitle>
|
||||
<DialogDescription>
|
||||
如果扫描速度很慢,请取消本次扫描,并缩小导入数据的体量
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Alert>
|
||||
<RocketIcon className="h-4 w-4" />
|
||||
<AlertTitle>扫描进度</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Progress value={progress?.percentage} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ type MenuItem = {
|
|||
};
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ to: "/", name: "一键启动", icon: <IoPlayOutline size={24} /> },
|
||||
{ to: "/", name: "自动分析", icon: <IoPlayOutline size={24} /> },
|
||||
{ to: "/datasource", name: "数据列表", icon: <IoListOutline size={24} /> },
|
||||
{ to: "/models", name: "模型管理", icon: <IoCubeOutline size={24} /> },
|
||||
{ to: "/tools", name: "小工具", icon: <IoHammerOutline size={24} /> },
|
||||
|
|
|
@ -19,7 +19,7 @@ export const MenuBar = () => {
|
|||
const navigate = useNavigate();
|
||||
|
||||
const handleImportDicom = () => {
|
||||
navigate("datasource");
|
||||
navigate("/");
|
||||
window.ipcRenderer.send("import-dicom-dialog-visible");
|
||||
};
|
||||
|
||||
|
|
|
@ -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,7 +256,13 @@ export function ModelTable() {
|
|||
}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow justify-end inline-flex items-center">
|
||||
<div className="text-right space-x-2">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
选中了 {table.getFilteredSelectedRowModel().rows.length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
<Button className="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 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2">
|
||||
<IoRefreshCircleOutline className="mr-2 h-4 w-4" /> 刷新列表
|
||||
</Button>
|
||||
|
@ -267,6 +271,7 @@ export function ModelTable() {
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="h-full w-full flex-grow pl-4 pr-4">
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
|
@ -318,11 +323,6 @@ export function ModelTable() {
|
|||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="text-right space-x-2 py-4">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
选中了 {table.getFilteredSelectedRowModel().rows.length}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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: {}
|
||||
|
|
Loading…
Reference in New Issue
Block a user