(null)
+
+ const initChart = useCallback(
+ (container: HTMLDivElement) => {
+ if (chartRef.current) {
+ ;(chartRef.current as any).destroy?.()
+ }
+ containerRef.current = container
+ chartRef.current = new ChartClass(container, options)
+ },
+ [ChartClass, options]
+ )
+
+ const updateData = useCallback((data: any[]) => {
+ if (chartRef.current && typeof (chartRef.current as any).changeData === 'function') {
+ ;(chartRef.current as any).changeData(data)
+ }
+ }, [])
+
+ useEffect(() => {
+ return () => {
+ if (antvOptions?.autoDestroy !== false && chartRef.current) {
+ ;(chartRef.current as any).destroy?.()
+ chartRef.current = null
+ }
+ }
+ }, [antvOptions?.autoDestroy])
+
+ return { chartRef, containerRef, initChart, updateData }
+}
diff --git a/novalon-manage-web/src/hooks/usePermission.ts b/novalon-manage-web/src/hooks/usePermission.ts
new file mode 100644
index 0000000..d2d2794
--- /dev/null
+++ b/novalon-manage-web/src/hooks/usePermission.ts
@@ -0,0 +1,10 @@
+import { usePermissionStore } from '@/stores/usePermissionStore'
+
+export function usePermission() {
+ const hasPermission = usePermissionStore((s) => s.hasPermission)
+ const hasRole = usePermissionStore((s) => s.hasRole)
+ const permissions = usePermissionStore((s) => s.permissions)
+ const roles = usePermissionStore((s) => s.roles)
+
+ return { hasPermission, hasRole, permissions, roles }
+}
diff --git a/novalon-manage-web/src/layouts/DefaultLayout.tsx b/novalon-manage-web/src/layouts/DefaultLayout.tsx
deleted file mode 100644
index a1adf1d..0000000
--- a/novalon-manage-web/src/layouts/DefaultLayout.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Outlet } from 'react-router'
-
-export default function DefaultLayout() {
- return (
-
-
DefaultLayout (TODO)
-
-
- )
-}
diff --git a/novalon-manage-web/src/layouts/DefaultLayout/HeaderRight.tsx b/novalon-manage-web/src/layouts/DefaultLayout/HeaderRight.tsx
new file mode 100644
index 0000000..43470ad
--- /dev/null
+++ b/novalon-manage-web/src/layouts/DefaultLayout/HeaderRight.tsx
@@ -0,0 +1,53 @@
+import { useNavigate } from 'react-router'
+import { Dropdown, Avatar, Space } from 'antd'
+import { UserOutlined, LogoutOutlined, KeyOutlined } from '@ant-design/icons'
+import type { MenuProps } from 'antd'
+import { useAuthStore } from '@/stores/useAuthStore'
+
+export default function HeaderRight() {
+ const navigate = useNavigate()
+ const username = useAuthStore((s) => s.username)
+ const logout = useAuthStore((s) => s.logout)
+
+ const menuItems: MenuProps['items'] = [
+ {
+ key: 'profile',
+ icon: ,
+ label: '个人中心',
+ },
+ {
+ key: 'password',
+ icon: ,
+ label: '修改密码',
+ },
+ { type: 'divider' },
+ {
+ key: 'logout',
+ icon: ,
+ label: '退出登录',
+ danger: true,
+ },
+ ]
+
+ const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
+ switch (key) {
+ case 'profile':
+ break
+ case 'password':
+ break
+ case 'logout':
+ logout()
+ navigate('/login')
+ break
+ }
+ }
+
+ return (
+
+
+ } />
+ {username || '用户'}
+
+
+ )
+}
diff --git a/novalon-manage-web/src/layouts/DefaultLayout/SideMenu.tsx b/novalon-manage-web/src/layouts/DefaultLayout/SideMenu.tsx
new file mode 100644
index 0000000..60b7d3e
--- /dev/null
+++ b/novalon-manage-web/src/layouts/DefaultLayout/SideMenu.tsx
@@ -0,0 +1,104 @@
+import { useNavigate, useLocation } from 'react-router'
+import { Menu } from 'antd'
+import type { MenuProps } from 'antd'
+import {
+ DashboardOutlined,
+ UserOutlined,
+ TeamOutlined,
+ MenuOutlined,
+ SettingOutlined,
+ BookOutlined,
+ FileOutlined,
+ NotificationOutlined,
+ AuditOutlined,
+ FileSearchOutlined,
+ WarningOutlined,
+} from '@ant-design/icons'
+import { usePermissionStore } from '@/stores/usePermissionStore'
+import type { MenuItem } from '@/api/menu'
+
+type AntMenuItem = Required['items'][number]
+
+const iconMap: Record = {
+ dashboard: ,
+ user: ,
+ users: ,
+ role: ,
+ roles: ,
+ menu: ,
+ menus: ,
+ config: ,
+ dict: ,
+ file: ,
+ files: ,
+ notice: ,
+ loginlog: ,
+ oplog: ,
+ exceptionlog: ,
+}
+
+const pathMap: Record = {
+ dashboard: '/dashboard',
+ users: '/users',
+ roles: '/roles',
+ menus: '/menus',
+ 'sys/config': '/sys/config',
+ dict: '/dict',
+ files: '/files',
+ notice: '/notice',
+ loginlog: '/loginlog',
+ oplog: '/oplog',
+ exceptionlog: '/exceptionlog',
+}
+
+function convertMenus(items: MenuItem[]): AntMenuItem[] {
+ return items
+ .filter((item) => item.type !== 'button' && item.visible !== false)
+ .map((item) => {
+ const path = item.path || pathMap[item.permission] || `/custom/${item.id}`
+ const icon = iconMap[item.icon] || iconMap[item.permission]
+
+ if (item.children?.length) {
+ return {
+ key: path,
+ icon,
+ label: item.name,
+ children: convertMenus(item.children),
+ }
+ }
+
+ return {
+ key: path,
+ icon,
+ label: item.name,
+ }
+ })
+}
+
+export default function SideMenu() {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const menus = usePermissionStore((s) => s.menus)
+
+ const menuItems = convertMenus(menus)
+
+ const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
+ navigate(key)
+ }
+
+ return (
+
+ )
+}
+
+function getOpenKey(pathname: string): string {
+ const segments = pathname.split('/').filter(Boolean)
+ return segments.length > 1 ? `/${segments[0]}` : pathname
+}
diff --git a/novalon-manage-web/src/layouts/DefaultLayout/index.tsx b/novalon-manage-web/src/layouts/DefaultLayout/index.tsx
new file mode 100644
index 0000000..9289cc8
--- /dev/null
+++ b/novalon-manage-web/src/layouts/DefaultLayout/index.tsx
@@ -0,0 +1,39 @@
+import { Suspense } from 'react'
+import { Outlet } from 'react-router'
+import { ProLayout } from '@ant-design/pro-components'
+import { Spin } from 'antd'
+import { useAppStore } from '@/stores/useAppStore'
+import { useAuthStore } from '@/stores/useAuthStore'
+import HeaderRight from './HeaderRight'
+
+export default function DefaultLayout() {
+ const collapsed = useAppStore((s) => s.collapsed)
+ const toggleCollapsed = useAppStore((s) => s.toggleCollapsed)
+ const username = useAuthStore((s) => s.username)
+
+ return (
+ item.onClick?.()}>{dom}}
+ headerTitleRender={(logo, title) => (
+
+ {logo}{title}
+
+ )}
+ avatarProps={{
+ title: username || '用户',
+ render: () => ,
+ }}
+ >
+ }>
+
+
+
+ )
+}
diff --git a/novalon-manage-web/src/router/routes.ts b/novalon-manage-web/src/router/routes.ts
index a1ca388..e345bbc 100644
--- a/novalon-manage-web/src/router/routes.ts
+++ b/novalon-manage-web/src/router/routes.ts
@@ -13,7 +13,7 @@ const LoginLog = lazy(() => import('@/pages/audit/login-log'))
const OperationLog = lazy(() => import('@/pages/audit/operation-log'))
const ExceptionLog = lazy(() => import('@/pages/audit/exception-log'))
const Forbidden = lazy(() => import('@/pages/403'))
-const DefaultLayout = lazy(() => import('@/layouts/DefaultLayout'))
+const DefaultLayout = lazy(() => import('@/layouts/DefaultLayout/index'))
export { authLoader } from './guards'
export { DefaultLayout, Login, Dashboard, UserManagement, RoleManagement, MenuManagement, ConfigManagement, DictManagement, FileManagement, NoticeManagement, LoginLog, OperationLog, ExceptionLog, Forbidden }