Files
erp/apps/web/src/layouts/MainLayout.tsx
iven e16c1a85d7 feat(web): comprehensive frontend performance and UI/UX optimization
Performance improvements:
- Vite build: manual chunks, terser minification, optimizeDeps
- API response caching with 5s TTL via axios interceptors
- React.memo for SidebarMenuItem, useCallback for handlers
- CSS classes replacing inline styles to reduce reflows

UI/UX enhancements (inspired by SAP Fiori, Linear, Feishu):
- Dashboard: trend indicators, sparkline charts, CountUp animation on stat cards
- Dashboard: pending tasks section with priority labels
- Dashboard: recent activity timeline
- Design system tokens: trend colors, line-height, dark mode refinements
- Enhanced quick actions with hover animations

Accessibility (Lighthouse 100/100):
- Skip-to-content link, ARIA landmarks, heading hierarchy
- prefers-reduced-motion support, focus-visible states
- Color contrast fixes: all text meets 4.5:1 ratio
- Keyboard navigation for stat cards and task items

SEO: meta theme-color, format-detection, robots.txt
2026-04-13 01:37:55 +08:00

243 lines
7.8 KiB
TypeScript

import { useCallback, memo } 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,
} from '@ant-design/icons';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAppStore } from '../stores/app';
import { useAuthStore } from '../stores/auth';
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: '系统设置' },
];
const routeTitleMap: Record<string, string> = {
'/': '工作台',
'/users': '用户管理',
'/roles': '权限管理',
'/organizations': '组织架构',
'/workflow': '工作流',
'/messages': '消息中心',
'/settings': '系统设置',
};
// 侧边栏菜单项 - 提取为独立组件避免重复渲染
const SidebarMenuItem = memo(function SidebarMenuItem({
item,
isActive,
collapsed,
onClick,
}: {
item: MenuItem;
isActive: boolean;
collapsed: boolean;
onClick: () => void;
}) {
return (
<Tooltip title={collapsed ? item.label : ''} placement="right">
<div
onClick={onClick}
className={`erp-sidebar-item ${isActive ? 'erp-sidebar-item-active' : ''}`}
>
<span className="erp-sidebar-item-icon">{item.icon}</span>
{!collapsed && <span className="erp-sidebar-item-label">{item.label}</span>}
</div>
</Tooltip>
);
});
export default function MainLayout({ children }: { children: React.ReactNode }) {
const { sidebarCollapsed, toggleSidebar, theme: themeMode, setTheme } = useAppStore();
const { user, logout } = useAuthStore();
const { token } = theme.useToken();
const navigate = useNavigate();
const location = useLocation();
const currentPath = location.pathname || '/';
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>
{/* 菜单组:系统 */}
{!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] || '页面'}
</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>
);
}