1
This commit is contained in:
parent
02e795fead
commit
72de966d0c
107
frontend/package-lock.json
generated
107
frontend/package-lock.json
generated
@ -27,7 +27,9 @@
|
|||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
"@radix-ui/react-label": "^2.1.1",
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||||
"@radix-ui/react-progress": "^1.1.1",
|
"@radix-ui/react-progress": "^1.1.1",
|
||||||
"@radix-ui/react-select": "^2.1.4",
|
"@radix-ui/react-select": "^2.1.4",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
@ -2137,6 +2139,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-menu": "2.1.4",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-focus-guards": {
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
|
||||||
@ -2218,6 +2248,80 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-collection": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.3",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.1",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.1",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-popper": "1.2.1",
|
||||||
|
"@radix-ui/react-portal": "1.1.3",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.1",
|
||||||
|
"@radix-ui/react-slot": "1.1.1",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "^2.6.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-navigation-menu": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-IQWAsQ7dsLIYDrn0WqPU+cdM7MONTv9nqrLVYoie3BPiabSfUVDe6Fr+oEt0Cofsr9ONDcDe9xhmJbL1Uq1yKg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-collection": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.3",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.0",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
|
||||||
@ -4173,7 +4277,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
"resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.1.1"
|
"clsx": "^2.1.1"
|
||||||
},
|
},
|
||||||
@ -4196,7 +4299,6 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
|
||||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -8392,7 +8494,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
"resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
||||||
"integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
|
"integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/dcastil"
|
"url": "https://github.com/sponsors/dcastil"
|
||||||
|
|||||||
@ -29,7 +29,9 @@
|
|||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
"@radix-ui/react-label": "^2.1.1",
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||||
"@radix-ui/react-progress": "^1.1.1",
|
"@radix-ui/react-progress": "^1.1.1",
|
||||||
"@radix-ui/react-select": "^2.1.4",
|
"@radix-ui/react-select": "^2.1.4",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
|
|||||||
66
frontend/src/components/AppMenu.tsx
Normal file
66
frontend/src/components/AppMenu.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { Sidebar, SidebarHeader, SidebarContent } from './ui/sidebar';
|
||||||
|
import { MenuItem, MenuGroup } from './ui/sidebar-menu';
|
||||||
|
import type { RootState } from '@/store';
|
||||||
|
import { MenuResponse, MenuTypeEnum } from '@/pages/System/Menu/types';
|
||||||
|
import { getIconComponent } from '@/config/icons';
|
||||||
|
|
||||||
|
interface AppMenuProps {
|
||||||
|
openKeys: string[];
|
||||||
|
onOpenChange: (keys: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppMenu({ openKeys, onOpenChange }: AppMenuProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const menus = useSelector((state: RootState) => state.user.menus);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar>
|
||||||
|
<SidebarHeader>
|
||||||
|
<div className="text-lg font-semibold">Deploy Ease</div>
|
||||||
|
</SidebarHeader>
|
||||||
|
<SidebarContent>
|
||||||
|
<MenuGroup>
|
||||||
|
{menus?.filter(menu => menu.type !== MenuTypeEnum.BUTTON && !menu.hidden)
|
||||||
|
.map(menu => (
|
||||||
|
<MenuItem
|
||||||
|
key={menu.path || String(menu.id)}
|
||||||
|
icon={getIconComponent(menu.icon)}
|
||||||
|
title={menu.name}
|
||||||
|
active={location.pathname === menu.path}
|
||||||
|
expanded={openKeys.includes(menu.path || String(menu.id))}
|
||||||
|
onClick={() => {
|
||||||
|
if (menu.children?.length) {
|
||||||
|
onOpenChange(
|
||||||
|
openKeys.includes(menu.path || String(menu.id))
|
||||||
|
? openKeys.filter(key => key !== (menu.path || String(menu.id)))
|
||||||
|
: [...openKeys, menu.path || String(menu.id)]
|
||||||
|
);
|
||||||
|
} else if (menu.path) {
|
||||||
|
navigate(menu.path);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menu.children?.map(child => (
|
||||||
|
<MenuItem
|
||||||
|
key={child.path || String(child.id)}
|
||||||
|
icon={getIconComponent(child.icon)}
|
||||||
|
title={child.name}
|
||||||
|
active={location.pathname === child.path}
|
||||||
|
onClick={() => {
|
||||||
|
if (child.path) {
|
||||||
|
navigate(child.path);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
frontend/src/components/ui/sidebar-menu.tsx
Normal file
81
frontend/src/components/ui/sidebar-menu.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
active?: boolean;
|
||||||
|
expanded?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
||||||
|
({ className, icon, title, active, expanded, disabled, children, onClick, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={cn("relative", className)} {...props}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm font-medium",
|
||||||
|
"transition-all duration-200 ease-in-out",
|
||||||
|
"hover:bg-accent/50 hover:text-accent-foreground",
|
||||||
|
"dark:hover:bg-slate-800 dark:hover:text-slate-100",
|
||||||
|
active && "bg-accent text-accent-foreground dark:bg-slate-800 dark:text-slate-100",
|
||||||
|
disabled && "pointer-events-none opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<span className={cn(
|
||||||
|
"mr-2 transition-colors",
|
||||||
|
"text-muted-foreground group-hover:text-current",
|
||||||
|
active && "text-current"
|
||||||
|
)}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="flex-1">{title}</span>
|
||||||
|
{children && (
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
"ml-1 h-4 w-4 transition-transform duration-200",
|
||||||
|
"text-muted-foreground group-hover:text-current",
|
||||||
|
active && "text-current",
|
||||||
|
expanded && "rotate-180"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{expanded && children && (
|
||||||
|
<div className={cn(
|
||||||
|
"mt-1 space-y-1 pl-4",
|
||||||
|
"animate-in slide-in-from-left-5 duration-200"
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
MenuItem.displayName = "MenuItem";
|
||||||
|
|
||||||
|
const MenuGroup = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"space-y-1.5 px-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
MenuGroup.displayName = "MenuGroup";
|
||||||
|
|
||||||
|
export { MenuItem, MenuGroup };
|
||||||
67
frontend/src/components/ui/sidebar.tsx
Normal file
67
frontend/src/components/ui/sidebar.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full flex-col border-r bg-background transition-all duration-300",
|
||||||
|
"min-w-[240px] w-60",
|
||||||
|
"dark:border-slate-700 dark:bg-slate-900",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Sidebar.displayName = "Sidebar";
|
||||||
|
|
||||||
|
const SidebarHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-14 items-center border-b px-4",
|
||||||
|
"dark:border-slate-700 dark:bg-slate-900/50",
|
||||||
|
"bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SidebarHeader.displayName = "SidebarHeader";
|
||||||
|
|
||||||
|
const SidebarContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 overflow-auto py-2",
|
||||||
|
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-200",
|
||||||
|
"dark:scrollbar-thumb-slate-700",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SidebarContent.displayName = "SidebarContent";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sidebar,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarContent,
|
||||||
|
};
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import React, {useEffect, useState, useCallback} from 'react';
|
import React, {useEffect, useState, useCallback} from 'react';
|
||||||
import {Layout, Menu, Dropdown, Modal, message, Spin, Space, Tooltip} from 'antd';
|
import {Layout, Dropdown, Modal, message, Spin, Space, Tooltip} from 'antd';
|
||||||
import {useNavigate, useLocation, Outlet} from 'react-router-dom';
|
import {useNavigate, useLocation, Outlet} from 'react-router-dom';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
DownOutlined,
|
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
CloudOutlined
|
CloudOutlined
|
||||||
@ -17,10 +16,9 @@ import {getWeather} from '../services/weather';
|
|||||||
import type {RootState} from '../store';
|
import type {RootState} from '../store';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
import {MenuResponse, MenuTypeEnum} from "@/pages/System/Menu/types";
|
import { AppMenu } from '@/components/AppMenu';
|
||||||
import { getIconComponent } from '@/config/icons.tsx';
|
|
||||||
|
|
||||||
const {Header, Content, Sider} = Layout;
|
const {Header, Content} = Layout;
|
||||||
const {confirm} = Modal;
|
const {confirm} = Modal;
|
||||||
|
|
||||||
// 设置中文语言
|
// 设置中文语言
|
||||||
@ -31,7 +29,6 @@ const BasicLayout: React.FC = () => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const userInfo = useSelector((state: RootState) => state.user.userInfo);
|
const userInfo = useSelector((state: RootState) => state.user.userInfo);
|
||||||
const menus = useSelector((state: RootState) => state.user.menus);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [currentTime, setCurrentTime] = useState(dayjs());
|
const [currentTime, setCurrentTime] = useState(dayjs());
|
||||||
const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
|
const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
|
||||||
@ -149,26 +146,8 @@ const BasicLayout: React.FC = () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// 获取图标组件
|
|
||||||
const getIcon = getIconComponent;
|
|
||||||
|
|
||||||
// 将菜单数据转换为antd Menu需要的格式
|
|
||||||
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
|
|
||||||
return menuList
|
|
||||||
?.filter(menu => menu.type !== MenuTypeEnum.BUTTON && !menu.hidden)
|
|
||||||
.map(menu => {
|
|
||||||
return {
|
|
||||||
key: menu.path || String(menu.id),
|
|
||||||
icon: getIconComponent(menu.icon),
|
|
||||||
label: menu.name,
|
|
||||||
children: menu.children ? getMenuItems(menu.children) : undefined
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理菜单展开/收起
|
// 处理菜单展开/收起
|
||||||
const handleOpenChange = (keys: string[]) => {
|
const handleOpenChange = (keys: string[]) => {
|
||||||
console.log('Menu open change:', keys);
|
|
||||||
setOpenKeys(keys);
|
setOpenKeys(keys);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -197,25 +176,10 @@ const BasicLayout: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{minHeight: '100vh'}}>
|
<Layout style={{minHeight: '100vh', display: 'flex', flexDirection: 'row'}}>
|
||||||
<Sider>
|
<AppMenu openKeys={openKeys} onOpenChange={handleOpenChange} />
|
||||||
<div style={{height: 32, margin: 16, background: 'rgba(255, 255, 255, 0.2)'}}/>
|
<Layout style={{flex: 1, overflow: 'hidden'}}>
|
||||||
<Menu
|
|
||||||
theme="dark"
|
|
||||||
mode="inline"
|
|
||||||
selectedKeys={[location.pathname]}
|
|
||||||
openKeys={openKeys}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
items={getMenuItems(menus)}
|
|
||||||
onClick={({key}) => {
|
|
||||||
console.log('Menu click:', key);
|
|
||||||
navigate(key);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Sider>
|
|
||||||
<Layout>
|
|
||||||
<Header style={{
|
<Header style={{
|
||||||
padding: '0 24px',
|
padding: '0 24px',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
@ -248,35 +212,25 @@ const BasicLayout: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
<Dropdown menu={{items: userMenuItems}} trigger={['hover']}>
|
<Dropdown menu={{items: userMenuItems}} trigger={['hover']}>
|
||||||
<span style={{
|
<Space style={{cursor: 'pointer'}}>
|
||||||
cursor: 'pointer',
|
<UserOutlined/>
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '100%',
|
|
||||||
transition: 'all 0.3s',
|
|
||||||
padding: '0 4px',
|
|
||||||
borderRadius: 4
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.025)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = 'transparent';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UserOutlined style={{fontSize: 16, marginRight: 8}}/>
|
|
||||||
<span>{userInfo?.nickname || userInfo?.username}</span>
|
<span>{userInfo?.nickname || userInfo?.username}</span>
|
||||||
<DownOutlined style={{fontSize: 12, marginLeft: 6}}/>
|
</Space>
|
||||||
</span>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Space>
|
</Space>
|
||||||
</Header>
|
</Header>
|
||||||
<Content style={{ margin: '24px 16px', padding: 24, background: '#fff', minHeight: 280 }}>
|
<Content style={{
|
||||||
<Outlet />
|
margin: '24px 16px',
|
||||||
|
padding: 24,
|
||||||
|
background: '#fff',
|
||||||
|
minHeight: 280,
|
||||||
|
overflow: 'auto'
|
||||||
|
}}>
|
||||||
|
<Outlet/>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default BasicLayout;
|
export default BasicLayout;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { type ClassValue, clsx } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user