diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6bcd8dae..43ebf4c5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,7 +27,9 @@ "@monaco-editor/react": "^4.6.0", "@radix-ui/react-avatar": "^1.1.2", "@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-navigation-menu": "^1.2.3", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-select": "^2.1.4", "@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": { "version": "1.1.1", "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": { "version": "1.2.1", "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", "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" }, @@ -4196,7 +4299,6 @@ "version": "2.1.1", "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", "engines": { "node": ">=6" } @@ -8392,7 +8494,6 @@ "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz", "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", "dev": true, - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" diff --git a/frontend/package.json b/frontend/package.json index 8a1c8f33..b65f46aa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,9 @@ "@monaco-editor/react": "^4.6.0", "@radix-ui/react-avatar": "^1.1.2", "@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-navigation-menu": "^1.2.3", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", diff --git a/frontend/src/components/AppMenu.tsx b/frontend/src/components/AppMenu.tsx new file mode 100644 index 00000000..cecb9fab --- /dev/null +++ b/frontend/src/components/AppMenu.tsx @@ -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 ( + + +
Deploy Ease
+
+ + + {menus?.filter(menu => menu.type !== MenuTypeEnum.BUTTON && !menu.hidden) + .map(menu => ( + { + 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 => ( + { + if (child.path) { + navigate(child.path); + } + }} + /> + ))} + + ))} + + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/ui/sidebar-menu.tsx b/frontend/src/components/ui/sidebar-menu.tsx new file mode 100644 index 00000000..7816d460 --- /dev/null +++ b/frontend/src/components/ui/sidebar-menu.tsx @@ -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 { + icon?: React.ReactNode; + title: string; + active?: boolean; + expanded?: boolean; + disabled?: boolean; + children?: React.ReactNode; + onClick?: () => void; +} + +const MenuItem = React.forwardRef( + ({ className, icon, title, active, expanded, disabled, children, onClick, ...props }, ref) => { + return ( +
+
+ {icon && ( + + {icon} + + )} + {title} + {children && ( + + )} +
+ {expanded && children && ( +
+ {children} +
+ )} +
+ ); + } +); +MenuItem.displayName = "MenuItem"; + +const MenuGroup = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +MenuGroup.displayName = "MenuGroup"; + +export { MenuItem, MenuGroup }; \ No newline at end of file diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx new file mode 100644 index 00000000..0f988e7c --- /dev/null +++ b/frontend/src/components/ui/sidebar.tsx @@ -0,0 +1,67 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface SidebarProps extends React.HTMLAttributes { + className?: string; + children?: React.ReactNode; +} + +const Sidebar = React.forwardRef( + ({ className, children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); +Sidebar.displayName = "Sidebar"; + +const SidebarHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +SidebarHeader.displayName = "SidebarHeader"; + +const SidebarContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +SidebarContent.displayName = "SidebarContent"; + +export { + Sidebar, + SidebarHeader, + SidebarContent, +}; \ No newline at end of file diff --git a/frontend/src/layouts/BasicLayout.tsx b/frontend/src/layouts/BasicLayout.tsx index ce34b7d8..044dce4d 100644 --- a/frontend/src/layouts/BasicLayout.tsx +++ b/frontend/src/layouts/BasicLayout.tsx @@ -1,11 +1,10 @@ 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 {useDispatch, useSelector} from 'react-redux'; import { UserOutlined, LogoutOutlined, - DownOutlined, ExclamationCircleOutlined, ClockCircleOutlined, CloudOutlined @@ -17,10 +16,9 @@ import {getWeather} from '../services/weather'; import type {RootState} from '../store'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; -import {MenuResponse, MenuTypeEnum} from "@/pages/System/Menu/types"; -import { getIconComponent } from '@/config/icons.tsx'; +import { AppMenu } from '@/components/AppMenu'; -const {Header, Content, Sider} = Layout; +const {Header, Content} = Layout; const {confirm} = Modal; // 设置中文语言 @@ -31,7 +29,6 @@ const BasicLayout: React.FC = () => { const location = useLocation(); const dispatch = useDispatch(); const userInfo = useSelector((state: RootState) => state.user.userInfo); - const menus = useSelector((state: RootState) => state.user.menus); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(dayjs()); 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[]) => { - console.log('Menu open change:', keys); setOpenKeys(keys); }; @@ -197,25 +176,10 @@ const BasicLayout: React.FC = () => { ); } - return ( - - -
- { - console.log('Menu click:', key); - navigate(key); - }} - /> - - + + +
{ }}> - - - {currentTime.format('YYYY年MM月DD日 HH:mm:ss')} 星期{currentTime.format('dd')} - + + + {currentTime.format('YYYY年MM月DD日 HH:mm:ss')} 星期{currentTime.format('dd')} + - - - {weather.weather} {weather.temp}℃ - + + + {weather.weather} {weather.temp}℃ + - { - e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.025)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = 'transparent'; - }} - > - + + {userInfo?.nickname || userInfo?.username} - - +
- - + +
); -}; +} export default BasicLayout; \ No newline at end of file diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index bd0c391d..d084ccad 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { clsx, type ClassValue } from "clsx" +import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) {