feat: boot分析进度界面
This commit is contained in:
parent
fc26f12051
commit
3540413340
|
@ -15,6 +15,7 @@ export interface ExtractMetadata {
|
||||||
pixelData?: Uint16Array;
|
pixelData?: Uint16Array;
|
||||||
PatientSex: string;
|
PatientSex: string;
|
||||||
PatientAge: string;
|
PatientAge: string;
|
||||||
|
AcquisitionDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,6 +82,7 @@ export const parseDICOMFile = async (
|
||||||
const PatientName = dataSet.string("x00100030");
|
const PatientName = dataSet.string("x00100030");
|
||||||
const PatientSex = dataSet.string("x00100040") ?? "";
|
const PatientSex = dataSet.string("x00100040") ?? "";
|
||||||
const PatientAge = dataSet.string("x00101010") ?? "";
|
const PatientAge = dataSet.string("x00101010") ?? "";
|
||||||
|
const AcquisitionDate = dataSet.string("x00080022") ?? "";
|
||||||
// const pixelDataElement = dataSet.elements.x7fe00010;
|
// const pixelDataElement = dataSet.elements.x7fe00010;
|
||||||
// const pixelData = new Uint16Array(
|
// const pixelData = new Uint16Array(
|
||||||
// dataSet.byteArray.buffer,
|
// dataSet.byteArray.buffer,
|
||||||
|
@ -95,6 +97,7 @@ export const parseDICOMFile = async (
|
||||||
PatientName,
|
PatientName,
|
||||||
PatientSex,
|
PatientSex,
|
||||||
PatientAge,
|
PatientAge,
|
||||||
|
AcquisitionDate,
|
||||||
// pixelData,
|
// pixelData,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -130,15 +133,10 @@ export const processFilesInBatches = async (
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StructuredMetadata {
|
export type StructuredMetadata = Partial<ExtractMetadata> & {
|
||||||
filePaths: string[];
|
filePaths: string[];
|
||||||
StudyInstanceUID?: string;
|
|
||||||
SeriesInstanceUID?: string;
|
|
||||||
PatientName?: string;
|
|
||||||
fileHash: string[];
|
fileHash: string[];
|
||||||
PatientSex: string;
|
};
|
||||||
PatientAge: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScanProgress {
|
export interface ScanProgress {
|
||||||
percentage: number;
|
percentage: number;
|
||||||
|
@ -184,6 +182,7 @@ export const structureMetadata = async (
|
||||||
SeriesInstanceUID: item.SeriesInstanceUID,
|
SeriesInstanceUID: item.SeriesInstanceUID,
|
||||||
PatientName: item.PatientName,
|
PatientName: item.PatientName,
|
||||||
PatientAge: item.PatientAge,
|
PatientAge: item.PatientAge,
|
||||||
|
AcquisitionDate: item.AcquisitionDate,
|
||||||
PatientSex: item.PatientSex,
|
PatientSex: item.PatientSex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ function createWindow() {
|
||||||
|
|
||||||
if (VITE_DEV_SERVER_URL) {
|
if (VITE_DEV_SERVER_URL) {
|
||||||
win.loadURL(VITE_DEV_SERVER_URL);
|
win.loadURL(VITE_DEV_SERVER_URL);
|
||||||
|
registerIpcMainHandlers(win);
|
||||||
} else {
|
} else {
|
||||||
win.loadFile(path.join(RENDERER_DIST, "index.html")).then(() => {
|
win.loadFile(path.join(RENDERER_DIST, "index.html")).then(() => {
|
||||||
if (process.argv.length >= 2) {
|
if (process.argv.length >= 2) {
|
||||||
|
@ -71,11 +72,9 @@ function createWindow() {
|
||||||
registerIpcMainHandlers(win);
|
registerIpcMainHandlers(win);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pythonManager = new PythonManager(win, "http://127.0.0.1:15001", 3000);
|
// pythonManager = new PythonManager(win, "http://127.0.0.1:15001", 3000);
|
||||||
registerIpcMainHandlers(win);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTray() {
|
function createTray() {
|
||||||
|
|
|
@ -36,14 +36,17 @@
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"custom-electron-titlebar": "^4.2.8",
|
"custom-electron-titlebar": "^4.2.8",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"dayjs": "1.11.13",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"dicom-parser": "1.8.21",
|
"dicom-parser": "1.8.21",
|
||||||
"dockview": "^1.15.2",
|
"dockview": "^1.15.2",
|
||||||
"electron-store": "^10.0.0",
|
"electron-store": "^10.0.0",
|
||||||
|
"embla-carousel-react": "^8.2.0",
|
||||||
"flexlayout-react": "^0.7.15",
|
"flexlayout-react": "^0.7.15",
|
||||||
"framer-motion": "^11.3.24",
|
"framer-motion": "^11.3.24",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
"lucide-react": "^0.408.0",
|
"lucide-react": "^0.408.0",
|
||||||
|
"node-machine-id": "1.1.12",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"onnxruntime-node": "^1.18.0",
|
"onnxruntime-node": "^1.18.0",
|
||||||
"openvino-node": "2024.3.0",
|
"openvino-node": "2024.3.0",
|
||||||
|
@ -58,12 +61,11 @@
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "3.23.8",
|
"zod": "3.23.8"
|
||||||
"node-machine-id": "1.1.12"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "22.5.2",
|
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@types/node": "22.5.2",
|
||||||
"@types/react": "^18.2.64",
|
"@types/react": "^18.2.64",
|
||||||
"@types/react-dom": "^18.2.21",
|
"@types/react-dom": "^18.2.21",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
"@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"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
outline:
|
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:
|
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",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-9 px-4 py-2",
|
||||||
sm: "h-9 rounded-md px-3",
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
lg: "h-11 rounded-md px-8",
|
lg: "h-10 rounded-md px-8",
|
||||||
icon: "h-10 w-10",
|
icon: "h-9 w-9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
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 { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
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 { motion } from "framer-motion";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { IoCheckmarkCircleSharp } from "react-icons/io5";
|
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 Boot = () => {
|
||||||
const [flaskWaiting, setFlaskWaiting] = useState(false);
|
const [flaskWaiting, setFlaskWaiting] = useState(false);
|
||||||
const [flaskRunning, setflaskRunning] = useState(false);
|
const [flaskRunning, setflaskRunning] = useState(false);
|
||||||
const [flaskInfo, setFlaskInfo] = useState("");
|
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 = () => {
|
const handleBootPythonServer = () => {
|
||||||
if (!flaskRunning) setFlaskWaiting(true);
|
if (!flaskRunning) setFlaskWaiting(true);
|
||||||
|
@ -35,8 +164,8 @@ const Boot = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.ipcRenderer.on("open-folder", (_, data) => {
|
window.ipcRenderer.on("open-folder", (_, data) => {
|
||||||
console.log(data);
|
console.log("右键菜单拉起启动文件夹路径", data);
|
||||||
setOpenFolder(data);
|
setBootDirectoryPath(data);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -49,8 +178,59 @@ const Boot = () => {
|
||||||
className="h-full"
|
className="h-full"
|
||||||
>
|
>
|
||||||
<div className="p-4 h-full flex flex-col">
|
<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 */}
|
{/* card */}
|
||||||
<div>{openFolder}</div>
|
|
||||||
<main className="flex-1 flex flex-col">
|
<main className="flex-1 flex flex-col">
|
||||||
<div className="mt-4 flex-1">
|
<div className="mt-4 flex-1">
|
||||||
<pre className="text-xs">{flaskInfo}</pre>
|
<pre className="text-xs">{flaskInfo}</pre>
|
||||||
|
@ -71,6 +251,26 @@ const Boot = () => {
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
ColumnFiltersState,
|
ColumnFiltersState,
|
||||||
|
@ -7,11 +8,17 @@ import {
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} 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 { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
@ -19,9 +26,11 @@ import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
@ -35,6 +44,9 @@ import {
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
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 = {
|
export type Series = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -56,6 +68,7 @@ const columnsAlias: { [K in keyof Partial<Series> as string]: string } = {
|
||||||
filePaths: "层数",
|
filePaths: "层数",
|
||||||
createTime: "创建时间",
|
createTime: "创建时间",
|
||||||
updateTime: "修改时间",
|
updateTime: "修改时间",
|
||||||
|
AcquisitionDate: "采集日期",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const columns: ColumnDef<Series>[] = [
|
export const columns: ColumnDef<Series>[] = [
|
||||||
|
@ -123,45 +136,87 @@ export const columns: ColumnDef<Series>[] = [
|
||||||
</div>
|
</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",
|
id: "createTime",
|
||||||
accessorKey: "createTime",
|
accessorKey: "createTime",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<div className="flex items-center">
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
{columnsAlias["createTime"]}
|
{columnsAlias["createTime"]}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<Button
|
||||||
</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 }) => (
|
cell: ({ row }) =>
|
||||||
<div className="capitalize">
|
row.getValue("createTime") && (
|
||||||
{new Date(row.getValue("createTime")).toLocaleString()}
|
<div className="capitalize">
|
||||||
</div>
|
<span>{dayjs(row.getValue("createTime")).format("YYYYMMDD")}</span>
|
||||||
),
|
<Badge className="ml-2 text-xs font-normal">
|
||||||
|
{timeAgo(row.getValue("createTime"))}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "updateTime",
|
id: "updateTime",
|
||||||
accessorKey: "updateTime",
|
accessorKey: "updateTime",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<div className="flex items-center">
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
{columnsAlias["updateTime"]}
|
{columnsAlias["updateTime"]}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<Button
|
||||||
</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 }) => (
|
cell: ({ row }) =>
|
||||||
<div className="capitalize">
|
row.getValue("updateTime") && (
|
||||||
{new Date(row.getValue("updateTime")).toLocaleString()}
|
<div className="capitalize">
|
||||||
</div>
|
<span>{dayjs(row.getValue("updateTime")).format("YYYYMMDD")}</span>
|
||||||
),
|
<Badge className="ml-2 text-xs font-normal">
|
||||||
|
{timeAgo(row.getValue("updateTime"))}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
|
@ -204,8 +259,11 @@ export function SeriesTable(props: SeriesTableProps) {
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||||
SeriesInstanceUID: false,
|
SeriesInstanceUID: false,
|
||||||
|
updateTime: false,
|
||||||
|
createTime: false,
|
||||||
});
|
});
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: props.data,
|
data: props.data,
|
||||||
|
@ -213,7 +271,6 @@ export function SeriesTable(props: SeriesTableProps) {
|
||||||
onSortingChange: setSorting,
|
onSortingChange: setSorting,
|
||||||
onColumnFiltersChange: setColumnFilters,
|
onColumnFiltersChange: setColumnFilters,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
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 (
|
return (
|
||||||
<div className="w-full h-full flex flex-col">
|
<div className="w-full h-full flex flex-col">
|
||||||
<div className="flex-shrink-0 flex items-center p-4">
|
<div className="flex-shrink-0 flex items-center p-4">
|
||||||
<Input
|
<div className="flex items-center">
|
||||||
placeholder="筛选姓名"
|
<Input
|
||||||
value={
|
placeholder="筛选姓名"
|
||||||
(table.getColumn("PatientName")?.getFilterValue() as string) ?? ""
|
value={
|
||||||
}
|
(table.getColumn("PatientName")?.getFilterValue() as string) ?? ""
|
||||||
onChange={(event) =>
|
}
|
||||||
table.getColumn("PatientName")?.setFilterValue(event.target.value)
|
onChange={(event) =>
|
||||||
}
|
table.getColumn("PatientName")?.setFilterValue(event.target.value)
|
||||||
className="max-w-sm"
|
}
|
||||||
/>
|
className="max-w-sm"
|
||||||
<DropdownMenu>
|
/>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<Button variant="outline" size="sm" className="ml-auto">
|
<DropdownMenuTrigger asChild>
|
||||||
显示 <ChevronDownIcon className="ml-2 h-4 w-4" />
|
<Button variant="ghost" size="icon" className="ml-2">
|
||||||
</Button>
|
<FilterIcon className="h-3.5 w-3.5 " />
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
<DropdownMenuContent align="end">
|
</DropdownMenuTrigger>
|
||||||
{table
|
<DropdownMenuContent align="end">
|
||||||
.getAllColumns()
|
{table
|
||||||
.filter((column) => column.getCanHide())
|
.getAllColumns()
|
||||||
.map((column) => {
|
.filter((column) => column.getCanHide())
|
||||||
return (
|
.map((column) => {
|
||||||
<DropdownMenuCheckboxItem
|
return (
|
||||||
key={column.id}
|
<DropdownMenuCheckboxItem
|
||||||
className="capitalize"
|
key={column.id}
|
||||||
checked={column.getIsVisible()}
|
className="capitalize"
|
||||||
onCheckedChange={(value) =>
|
checked={column.getIsVisible()}
|
||||||
column.toggleVisibility(!!value)
|
onCheckedChange={(value) =>
|
||||||
}
|
column.toggleVisibility(!!value)
|
||||||
>
|
}
|
||||||
{columnsAlias[column.id]}
|
>
|
||||||
</DropdownMenuCheckboxItem>
|
{columnsAlias[column.id]}
|
||||||
);
|
</DropdownMenuCheckboxItem>
|
||||||
})}
|
);
|
||||||
</DropdownMenuContent>
|
})}
|
||||||
</DropdownMenu>
|
</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>
|
</div>
|
||||||
<ScrollArea className="h-full w-full flex-grow pl-4 pr-4">
|
<ScrollArea className="h-full w-full flex-grow pl-4 pr-4">
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
|
|
|
@ -1,107 +1,29 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
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 { Series, SeriesTable } from "./SeriesTable";
|
import { Series, SeriesTable } from "./SeriesTable";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
interface ScanProgress {
|
|
||||||
percentage: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProgress = {
|
|
||||||
percentage: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Datasource = () => {
|
export const Datasource = () => {
|
||||||
const [progress, setProgress] = useState<ScanProgress>(defaultProgress);
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [, setResult] = useState<[]>([]);
|
|
||||||
|
|
||||||
const [seriesData, setSeriesData] = useState<Series[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
window.ipcRenderer.send("db:series:select");
|
window.ipcRenderer.send("db:series:select");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchSeriesData = useCallback(
|
||||||
window.ipcRenderer.on("db:series:select:response", (_event, data) => {
|
(_event: Electron.IpcRendererEvent, data: any) => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
setSeriesData(data);
|
setSeriesData(data);
|
||||||
});
|
},
|
||||||
}, []);
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.ipcRenderer.once("db:series:select:response", fetchSeriesData);
|
||||||
|
return () => {
|
||||||
|
window.ipcRenderer.off("db:series:select:response", fetchSeriesData);
|
||||||
|
};
|
||||||
|
}, [fetchSeriesData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
@ -112,26 +34,6 @@ export const Datasource = () => {
|
||||||
transition={{ duration: 0.25 }}
|
transition={{ duration: 0.25 }}
|
||||||
>
|
>
|
||||||
<SeriesTable data={seriesData} />
|
<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>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ type MenuItem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
{ to: "/", name: "一键启动", icon: <IoPlayOutline size={24} /> },
|
{ to: "/", name: "自动分析", icon: <IoPlayOutline size={24} /> },
|
||||||
{ to: "/datasource", name: "数据列表", icon: <IoListOutline size={24} /> },
|
{ to: "/datasource", name: "数据列表", icon: <IoListOutline size={24} /> },
|
||||||
{ to: "/models", name: "模型管理", icon: <IoCubeOutline size={24} /> },
|
{ to: "/models", name: "模型管理", icon: <IoCubeOutline size={24} /> },
|
||||||
{ to: "/tools", name: "小工具", icon: <IoHammerOutline size={24} /> },
|
{ to: "/tools", name: "小工具", icon: <IoHammerOutline size={24} /> },
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const MenuBar = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleImportDicom = () => {
|
const handleImportDicom = () => {
|
||||||
navigate("datasource");
|
navigate("/");
|
||||||
window.ipcRenderer.send("import-dicom-dialog-visible");
|
window.ipcRenderer.send("import-dicom-dialog-visible");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
|
@ -232,7 +231,6 @@ export function ModelTable() {
|
||||||
onSortingChange: setSorting,
|
onSortingChange: setSorting,
|
||||||
onColumnFiltersChange: setColumnFilters,
|
onColumnFiltersChange: setColumnFilters,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
@ -258,13 +256,20 @@ export function ModelTable() {
|
||||||
}
|
}
|
||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex-grow justify-end inline-flex items-center">
|
||||||
<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">
|
<div className="text-right space-x-2">
|
||||||
<IoRefreshCircleOutline className="mr-2 h-4 w-4" /> 刷新列表
|
<div className="flex-1 text-sm text-muted-foreground">
|
||||||
</Button>
|
选中了 {table.getFilteredSelectedRowModel().rows.length}
|
||||||
<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">
|
</div>
|
||||||
<IoAdd className="mr-2 h-4 w-4" /> 添加模型
|
</div>
|
||||||
</Button>
|
<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>
|
||||||
|
<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">
|
||||||
|
<IoAdd className="mr-2 h-4 w-4" /> 添加模型
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="h-full w-full flex-grow pl-4 pr-4">
|
<ScrollArea className="h-full w-full flex-grow pl-4 pr-4">
|
||||||
|
@ -318,11 +323,6 @@ export function ModelTable() {
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</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>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -85,6 +85,9 @@ importers:
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
|
dayjs:
|
||||||
|
specifier: 1.11.13
|
||||||
|
version: 1.11.13
|
||||||
dexie:
|
dexie:
|
||||||
specifier: ^4.0.8
|
specifier: ^4.0.8
|
||||||
version: 4.0.8
|
version: 4.0.8
|
||||||
|
@ -97,6 +100,9 @@ importers:
|
||||||
electron-store:
|
electron-store:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.0.0
|
version: 10.0.0
|
||||||
|
embla-carousel-react:
|
||||||
|
specifier: ^8.2.0
|
||||||
|
version: 8.2.0(react@18.3.1)
|
||||||
flexlayout-react:
|
flexlayout-react:
|
||||||
specifier: ^0.7.15
|
specifier: ^0.7.15
|
||||||
version: 0.7.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 0.7.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
@ -2191,6 +2197,19 @@ packages:
|
||||||
engines: {node: '>= 12.20.55'}
|
engines: {node: '>= 12.20.55'}
|
||||||
hasBin: true
|
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:
|
emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
@ -5730,7 +5749,7 @@ snapshots:
|
||||||
|
|
||||||
app-builder-bin@4.0.0: {}
|
app-builder-bin@4.0.0: {}
|
||||||
|
|
||||||
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@develar/schema-utils': 2.6.5
|
'@develar/schema-utils': 2.6.5
|
||||||
'@electron/notarize': 2.2.1
|
'@electron/notarize': 2.2.1
|
||||||
|
@ -5744,7 +5763,7 @@ snapshots:
|
||||||
builder-util-runtime: 9.2.4
|
builder-util-runtime: 9.2.4
|
||||||
chromium-pickle-js: 0.2.0
|
chromium-pickle-js: 0.2.0
|
||||||
debug: 4.3.6
|
debug: 4.3.6
|
||||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||||
ejs: 3.1.10
|
ejs: 3.1.10
|
||||||
electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3)
|
electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3)
|
||||||
electron-publish: 24.13.1
|
electron-publish: 24.13.1
|
||||||
|
@ -6222,9 +6241,9 @@ snapshots:
|
||||||
|
|
||||||
dlv@1.1.3: {}
|
dlv@1.1.3: {}
|
||||||
|
|
||||||
dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3):
|
dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||||
builder-util: 24.13.1
|
builder-util: 24.13.1
|
||||||
builder-util-runtime: 9.2.4
|
builder-util-runtime: 9.2.4
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
|
@ -6292,7 +6311,7 @@ snapshots:
|
||||||
|
|
||||||
electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3):
|
electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||||
archiver: 5.3.2
|
archiver: 5.3.2
|
||||||
builder-util: 24.13.1
|
builder-util: 24.13.1
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
|
@ -6302,11 +6321,11 @@ snapshots:
|
||||||
|
|
||||||
electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||||
builder-util: 24.13.1
|
builder-util: 24.13.1
|
||||||
builder-util-runtime: 9.2.4
|
builder-util-runtime: 9.2.4
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
is-ci: 3.0.1
|
is-ci: 3.0.1
|
||||||
lazy-val: 1.0.5
|
lazy-val: 1.0.5
|
||||||
|
@ -6344,6 +6363,18 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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@8.0.0: {}
|
||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user