插件菜单从扁平列表改为三级结构: 插件(分组)→ 插件名(可折叠子标题)→ 页面列表 - store 新增 PluginMenuGroup 类型和 pluginMenuGroups getter - MainLayout 新增 SidebarSubMenu 组件,支持展开/收起 - 折叠侧边栏时子菜单显示插件图标 + tooltip - 子菜单项增加缩进样式区分层级 - CRM 插件 name 改为 "CRM" 避免与页面标题重名
365 lines
12 KiB
TypeScript
365 lines
12 KiB
TypeScript
import { useCallback, useState, memo, useEffect } from 'react';
|
||
import { Layout, Avatar, Space, Dropdown, Tooltip, theme } from 'antd';
|
||
import {
|
||
HomeOutlined,
|
||
UserOutlined,
|
||
SafetyOutlined,
|
||
ApartmentOutlined,
|
||
SettingOutlined,
|
||
MenuFoldOutlined,
|
||
MenuUnfoldOutlined,
|
||
PartitionOutlined,
|
||
LogoutOutlined,
|
||
MessageOutlined,
|
||
SearchOutlined,
|
||
BulbOutlined,
|
||
BulbFilled,
|
||
AppstoreOutlined,
|
||
TeamOutlined,
|
||
TableOutlined,
|
||
TagsOutlined,
|
||
RightOutlined,
|
||
} from '@ant-design/icons';
|
||
import { useNavigate, useLocation } from 'react-router-dom';
|
||
import { useAppStore } from '../stores/app';
|
||
import { useAuthStore } from '../stores/auth';
|
||
import { usePluginStore } from '../stores/plugin';
|
||
import type { PluginMenuGroup } from '../stores/plugin';
|
||
import NotificationPanel from '../components/NotificationPanel';
|
||
|
||
const { Header, Sider, Content, Footer } = Layout;
|
||
|
||
interface MenuItem {
|
||
key: string;
|
||
icon: React.ReactNode;
|
||
label: string;
|
||
}
|
||
|
||
const mainMenuItems: MenuItem[] = [
|
||
{ key: '/', icon: <HomeOutlined />, label: '工作台' },
|
||
{ key: '/users', icon: <UserOutlined />, label: '用户管理' },
|
||
{ key: '/roles', icon: <SafetyOutlined />, label: '权限管理' },
|
||
{ key: '/organizations', icon: <ApartmentOutlined />, label: '组织架构' },
|
||
];
|
||
|
||
const bizMenuItems: MenuItem[] = [
|
||
{ key: '/workflow', icon: <PartitionOutlined />, label: '工作流' },
|
||
{ key: '/messages', icon: <MessageOutlined />, label: '消息中心' },
|
||
];
|
||
|
||
const sysMenuItems: MenuItem[] = [
|
||
{ key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
|
||
{ key: '/plugins/admin', icon: <AppstoreOutlined />, label: '插件管理' },
|
||
];
|
||
|
||
const routeTitleMap: Record<string, string> = {
|
||
'/': '工作台',
|
||
'/users': '用户管理',
|
||
'/roles': '权限管理',
|
||
'/organizations': '组织架构',
|
||
'/workflow': '工作流',
|
||
'/messages': '消息中心',
|
||
'/settings': '系统设置',
|
||
'/plugins/admin': '插件管理',
|
||
};
|
||
|
||
// 侧边栏菜单项 - 提取为独立组件避免重复渲染
|
||
const SidebarMenuItem = memo(function SidebarMenuItem({
|
||
item,
|
||
isActive,
|
||
collapsed,
|
||
onClick,
|
||
indented,
|
||
}: {
|
||
item: MenuItem;
|
||
isActive: boolean;
|
||
collapsed: boolean;
|
||
onClick: () => void;
|
||
indented?: boolean;
|
||
}) {
|
||
return (
|
||
<Tooltip title={collapsed ? item.label : ''} placement="right">
|
||
<div
|
||
onClick={onClick}
|
||
className={`erp-sidebar-item ${isActive ? 'erp-sidebar-item-active' : ''} ${indented ? 'erp-sidebar-item-indented' : ''}`}
|
||
>
|
||
<span className="erp-sidebar-item-icon">{item.icon}</span>
|
||
{!collapsed && <span className="erp-sidebar-item-label">{item.label}</span>}
|
||
</div>
|
||
</Tooltip>
|
||
);
|
||
});
|
||
|
||
// 动态图标映射
|
||
const pluginIconMap: Record<string, React.ReactNode> = {
|
||
AppstoreOutlined: <AppstoreOutlined />,
|
||
team: <TeamOutlined />,
|
||
TeamOutlined: <TeamOutlined />,
|
||
user: <UserOutlined />,
|
||
UserOutlined: <UserOutlined />,
|
||
message: <MessageOutlined />,
|
||
MessageOutlined: <MessageOutlined />,
|
||
tags: <TagsOutlined />,
|
||
TagsOutlined: <TagsOutlined />,
|
||
apartment: <ApartmentOutlined />,
|
||
ApartmentOutlined: <ApartmentOutlined />,
|
||
TableOutlined: <TableOutlined />,
|
||
DashboardOutlined: <AppstoreOutlined />,
|
||
};
|
||
|
||
function getPluginIcon(iconName: string): React.ReactNode {
|
||
return pluginIconMap[iconName] || <AppstoreOutlined />;
|
||
}
|
||
|
||
// 插件子菜单组 — 可折叠二级标题 + 三级菜单项
|
||
const SidebarSubMenu = memo(function SidebarSubMenu({
|
||
group,
|
||
collapsed,
|
||
currentPath,
|
||
onNavigate,
|
||
}: {
|
||
group: PluginMenuGroup;
|
||
collapsed: boolean;
|
||
currentPath: string;
|
||
onNavigate: (key: string) => void;
|
||
}) {
|
||
const [expanded, setExpanded] = useState(true);
|
||
const hasActive = group.items.some((item) => currentPath === item.key);
|
||
|
||
if (collapsed) {
|
||
// 折叠模式:显示插件图标,Tooltip 列出所有子项
|
||
const tooltipContent = group.items.map((item) => item.label).join(' / ');
|
||
return (
|
||
<Tooltip title={tooltipContent} placement="right">
|
||
<div
|
||
onClick={() => {
|
||
const first = group.items[0];
|
||
if (first) onNavigate(first.key);
|
||
}}
|
||
className={`erp-sidebar-item ${hasActive ? 'erp-sidebar-item-active' : ''}`}
|
||
>
|
||
<span className="erp-sidebar-item-icon"><AppstoreOutlined /></span>
|
||
</div>
|
||
</Tooltip>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<div
|
||
className={`erp-sidebar-submenu-title ${hasActive ? 'erp-sidebar-submenu-title-active' : ''}`}
|
||
onClick={() => setExpanded((e) => !e)}
|
||
>
|
||
<span className="erp-sidebar-submenu-arrow">
|
||
<RightOutlined
|
||
style={{ fontSize: 10, transition: 'transform 0.2s', transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
||
/>
|
||
</span>
|
||
<span className="erp-sidebar-submenu-label">{group.pluginName}</span>
|
||
</div>
|
||
{expanded && group.items.map((item) => (
|
||
<SidebarMenuItem
|
||
key={item.key}
|
||
item={{
|
||
key: item.key,
|
||
icon: getPluginIcon(item.icon),
|
||
label: item.label,
|
||
}}
|
||
isActive={currentPath === item.key}
|
||
collapsed={collapsed}
|
||
onClick={() => onNavigate(item.key)}
|
||
indented
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
});
|
||
|
||
export default function MainLayout({ children }: { children: React.ReactNode }) {
|
||
const { sidebarCollapsed, toggleSidebar, theme: themeMode, setTheme } = useAppStore();
|
||
const { user, logout } = useAuthStore();
|
||
const { pluginMenuItems, pluginMenuGroups, fetchPlugins } = usePluginStore();
|
||
theme.useToken();
|
||
const navigate = useNavigate();
|
||
const location = useLocation();
|
||
const currentPath = location.pathname || '/';
|
||
|
||
// 加载插件菜单
|
||
useEffect(() => {
|
||
fetchPlugins(1, 'running');
|
||
}, [fetchPlugins]);
|
||
|
||
const handleLogout = useCallback(async () => {
|
||
await logout();
|
||
navigate('/login');
|
||
}, [logout, navigate]);
|
||
|
||
const userMenuItems = [
|
||
{
|
||
key: 'profile',
|
||
icon: <UserOutlined />,
|
||
label: user?.display_name || user?.username || '用户',
|
||
disabled: true,
|
||
},
|
||
{ type: 'divider' as const },
|
||
{
|
||
key: 'logout',
|
||
icon: <LogoutOutlined />,
|
||
label: '退出登录',
|
||
danger: true,
|
||
onClick: handleLogout,
|
||
},
|
||
];
|
||
|
||
const sidebarWidth = sidebarCollapsed ? 72 : 240;
|
||
const isDark = themeMode === 'dark';
|
||
|
||
return (
|
||
<Layout style={{ minHeight: '100vh' }}>
|
||
{/* 现代深色侧边栏 */}
|
||
<Sider
|
||
trigger={null}
|
||
collapsible
|
||
collapsed={sidebarCollapsed}
|
||
width={240}
|
||
collapsedWidth={72}
|
||
className={isDark ? 'erp-sider-dark' : 'erp-sider-dark erp-sider-default'}
|
||
>
|
||
{/* Logo 区域 */}
|
||
<div className="erp-sidebar-logo" onClick={() => navigate('/')}>
|
||
<div className="erp-sidebar-logo-icon">E</div>
|
||
{!sidebarCollapsed && (
|
||
<span className="erp-sidebar-logo-text">ERP Platform</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 菜单组:基础模块 */}
|
||
{!sidebarCollapsed && <div className="erp-sidebar-group">基础模块</div>}
|
||
<div className="erp-sidebar-menu">
|
||
{mainMenuItems.map((item) => (
|
||
<SidebarMenuItem
|
||
key={item.key}
|
||
item={item}
|
||
isActive={currentPath === item.key}
|
||
collapsed={sidebarCollapsed}
|
||
onClick={() => navigate(item.key)}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* 菜单组:业务模块 */}
|
||
{!sidebarCollapsed && <div className="erp-sidebar-group">业务模块</div>}
|
||
<div className="erp-sidebar-menu">
|
||
{bizMenuItems.map((item) => (
|
||
<SidebarMenuItem
|
||
key={item.key}
|
||
item={item}
|
||
isActive={currentPath === item.key}
|
||
collapsed={sidebarCollapsed}
|
||
onClick={() => navigate(item.key)}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* 菜单组:插件 */}
|
||
{pluginMenuGroups.length > 0 && (
|
||
<>
|
||
{!sidebarCollapsed && <div className="erp-sidebar-group">插件</div>}
|
||
<div className="erp-sidebar-menu">
|
||
{pluginMenuGroups.map((group) => (
|
||
<SidebarSubMenu
|
||
key={group.pluginId}
|
||
group={group}
|
||
collapsed={sidebarCollapsed}
|
||
currentPath={currentPath}
|
||
onNavigate={(key) => navigate(key)}
|
||
/>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* 菜单组:系统 */}
|
||
{!sidebarCollapsed && <div className="erp-sidebar-group">系统</div>}
|
||
<div className="erp-sidebar-menu">
|
||
{sysMenuItems.map((item) => (
|
||
<SidebarMenuItem
|
||
key={item.key}
|
||
item={item}
|
||
isActive={currentPath === item.key}
|
||
collapsed={sidebarCollapsed}
|
||
onClick={() => navigate(item.key)}
|
||
/>
|
||
))}
|
||
</div>
|
||
</Sider>
|
||
|
||
{/* 右侧主区域 */}
|
||
<Layout
|
||
className={`erp-main-layout ${isDark ? 'erp-main-layout-dark' : 'erp-main-layout-light'}`}
|
||
style={{ marginLeft: sidebarWidth }}
|
||
>
|
||
{/* 顶部导航栏 */}
|
||
<Header className={`erp-header ${isDark ? 'erp-header-dark' : 'erp-header-light'}`}>
|
||
{/* 左侧:折叠按钮 + 标题 */}
|
||
<Space size="middle" style={{ alignItems: 'center' }}>
|
||
<div className="erp-header-btn" onClick={toggleSidebar}>
|
||
{sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||
</div>
|
||
<span className={`erp-header-title ${isDark ? 'erp-text-dark' : 'erp-text-light'}`}>
|
||
{routeTitleMap[currentPath] ||
|
||
pluginMenuItems.find((p) => p.key === currentPath)?.label ||
|
||
'页面'}
|
||
</span>
|
||
</Space>
|
||
|
||
{/* 右侧:搜索 + 通知 + 主题切换 + 用户 */}
|
||
<Space size={4} style={{ alignItems: 'center' }}>
|
||
<Tooltip title="搜索">
|
||
<div className="erp-header-btn">
|
||
<SearchOutlined style={{ fontSize: 16 }} />
|
||
</div>
|
||
</Tooltip>
|
||
|
||
<Tooltip title={isDark ? '切换亮色模式' : '切换暗色模式'}>
|
||
<div className="erp-header-btn" onClick={() => setTheme(isDark ? 'light' : 'dark')}>
|
||
{isDark ? <BulbFilled style={{ fontSize: 16 }} /> : <BulbOutlined style={{ fontSize: 16 }} />}
|
||
</div>
|
||
</Tooltip>
|
||
|
||
<NotificationPanel />
|
||
|
||
<div className={`erp-header-divider ${isDark ? 'erp-header-divider-dark' : 'erp-header-divider-light'}`} />
|
||
|
||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight" trigger={['click']}>
|
||
<div className="erp-header-user">
|
||
<Avatar
|
||
size={30}
|
||
className="erp-user-avatar"
|
||
>
|
||
{(user?.display_name?.[0] || user?.username?.[0] || 'U').toUpperCase()}
|
||
</Avatar>
|
||
{!sidebarCollapsed && (
|
||
<span className={`erp-user-name ${isDark ? 'erp-text-dark-secondary' : 'erp-text-light-secondary'}`}>
|
||
{user?.display_name || user?.username || 'User'}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</Dropdown>
|
||
</Space>
|
||
</Header>
|
||
|
||
{/* 内容区域 */}
|
||
<Content style={{ padding: 20, minHeight: 'calc(100vh - 56px - 48px)' }}>
|
||
{children}
|
||
</Content>
|
||
|
||
{/* 底部 */}
|
||
<Footer className={`erp-footer ${isDark ? 'erp-footer-dark' : 'erp-footer-light'}`}>
|
||
ERP Platform v0.1.0
|
||
</Footer>
|
||
</Layout>
|
||
</Layout>
|
||
);
|
||
}
|