feat: 前端全面优化 — 懒加载/骨架屏/错误边界/面包屑/主题增强/Glass登录页/动画系统

- 新增: ErrorBoundary, PageTransition, Breadcrumb, SkeletonCard (4组件)
- 重构: App.tsx (React.lazy懒加载所有页面), index.css (55+主题变量/动画/工具类)
- 优化: MainLayout (CSS变量布局+Footer), Header (搜索增强+快捷键), Sidebar (折叠Tooltip+子菜单动画)
- 优化: Dashboard骨架屏+级联动画, Stats/Charts/Recent主题感知颜色
- 优化: Login.tsx Glass毛玻璃效果+密码显隐图标+光球动画
- 图表Recharts主题化(CustomTooltip/chart-grid/chart-axis)
- 减少动画偏好支持(prefers-reduced-motion)
This commit is contained in:
attacker committed 2026-06-24 02:06:07 +00:00
1 parent 3d7932c087
commit 722587ab84
14 files changed
+2026 -969

No files matched your search

+159 -63
View File
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { useThemeStore, useMenuStore } from '@/store';
@@ -9,10 +9,10 @@ import {
Menu, Network, CloudCog, Globe, MapPin, Key,
Brain, Activity, BarChart3, GitBranch,
Bot, AlertTriangle, Container, Boxes, Clock, Rocket,
Layers,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
interface MenuItem {
id: string;
@@ -39,7 +39,6 @@ const menuItems: MenuItem[] = [
id: 'kubernetes', name: 'Kubernetes', icon: Cloud,
children: [
{ id: 'k8s-clusters', name: '集群列表', icon: Cloud, path: '/kubernetes' },
{ id: 'k8s-compare', name: '多集群对比', icon: Layers, path: '/kubernetes/compare' },
{ id: 'k8s-nodes', name: '节点管理', icon: Cpu, path: '/nodes' },
{
id: 'k8s-workload', name: '工作负载', icon: Boxes,
@@ -95,7 +94,7 @@ const menuItems: MenuItem[] = [
children: [
{ id: 'cronjobs', name: '定时任务', icon: PlayCircle, path: '/cronjobs' },
{ id: 'scripts', name: '脚本库', icon: FileCode, path: '/scripts' },
{ id: 'ansible', name: '远程执行', icon: Terminal, path: '/remote-exec' },
{ id: 'ansible', name: '远程执行', icon: Terminal, path: '/ansible' },
],
},
{
@@ -120,18 +119,38 @@ const menuItems: MenuItem[] = [
},
];
/**
* MenuItemComponent - 侧边栏菜单项(支持多级嵌套)
*
* 优化点:
* 1. 使用 Tooltip 在折叠状态下显示菜单名称
* 2. 动画过渡平滑
* 3. 活动状态判断更精确
* 4. 子菜单展开/收起动画
*/
function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number }) {
const location = useLocation();
const { openMenus, toggleMenu, activeMenu, setActiveMenu } = useMenuStore();
const { sidebarCollapsed } = useThemeStore();
const hasChildren = item.children && item.children.length > 0;
const isOpen = openMenus.includes(item.id);
// Determine if this menu item is active
const isActive = item.path
? location.pathname === item.path || location.pathname.startsWith(item.path + '/')
: (item.id === 'kubernetes' && location.pathname.startsWith('/kubernetes')) ||
(item.id === 'k8s-workload' && location.pathname.startsWith('/workloads')) ||
(item.id === 'k8s-network' && (location.pathname.includes('/services') || location.pathname.includes('/ingresses'))) ||
(item.id === 'k8s-config' && (location.pathname.includes('/configmaps') || location.pathname.includes('/secrets'))) ||
activeMenu === item.id;
const isActiveParent = hasChildren && item.children?.some((child) => {
if (child.path) {
return location.pathname === child.path || location.pathname.startsWith(child.path + '/');
}
return false;
});
const handleClick = () => {
if (hasChildren) {
toggleMenu(item.id);
@@ -140,107 +159,184 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
}
};
// Collapsed mode (only top-level)
if (sidebarCollapsed && depth === 0) {
return (
<div className="relative group">
<Button
variant={isActive ? 'secondary' : 'ghost'}
size="icon"
className={cn('w-10 h-10 mx-auto mb-1', isActive && 'bg-blue-600/20 text-blue-400')}
asChild={!hasChildren}
>
{hasChildren ? (
<span onClick={handleClick}><item.icon className="h-5 w-5" /></span>
) : (
<NavLink to={item.path || '#'} onClick={handleClick}><item.icon className="h-5 w-5" /></NavLink>
)}
</Button>
<div className="absolute left-full top-1/2 -translate-y-1/2 ml-2 px-3 py-1.5 bg-slate-800 text-slate-100 text-sm rounded-md opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50 border border-slate-700">
{item.name}
{hasChildren && (
<div className="absolute left-0 top-0 mt-2 ml-2 w-48 bg-slate-800 rounded-md border border-slate-700 shadow-lg py-1">
{item.children?.map((child) => (
<NavLink key={child.id} to={child.path || '#'}
className={({ isActive }) => cn('block px-4 py-2 text-sm hover:bg-slate-700 transition-colors', isActive ? 'text-blue-400 bg-blue-600/10' : 'text-slate-300')}>
{child.name}
</NavLink>
))}
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<div className="relative">
{hasChildren ? (
<Button
variant={isActive || isActiveParent ? 'secondary' : 'ghost'}
size="icon"
className={cn(
'w-10 h-10 mx-auto mb-1 transition-all duration-200',
(isActive || isActiveParent)
? 'bg-[var(--dg-blue)]/15 text-[var(--dg-blue)]'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]',
)}
onClick={handleClick}
>
<item.icon className="h-5 w-5" />
</Button>
) : (
<Button
variant={isActive ? 'secondary' : 'ghost'}
size="icon"
className={cn(
'w-10 h-10 mx-auto mb-1 transition-all duration-200',
isActive
? 'bg-[var(--dg-blue)]/15 text-[var(--dg-blue)]'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]',
)}
asChild
>
<NavLink to={item.path || '#'} onClick={handleClick}>
<item.icon className="h-5 w-5" />
</NavLink>
</Button>
)}
</div>
)}
</div>
</div>
</TooltipTrigger>
<TooltipContent side="right" className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
{item.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return (
<div className={cn('mb-0.5', depth > 0 && 'ml-3')}>
<div className={cn('mb-0.5', depth > 0 && 'ml-2.5')}>
{hasChildren ? (
<>
<Button
variant={isActive ? 'secondary' : 'ghost'}
className={cn('w-full justify-between h-8 px-2.5 text-xs font-normal', isActive ? 'bg-blue-600/20 text-blue-400 hover:bg-blue-600/30' : 'text-slate-300 hover:text-slate-100 hover:bg-slate-800')}
variant={isActive || isActiveParent ? 'secondary' : 'ghost'}
className={cn(
'w-full justify-between h-8 px-2.5 text-xs font-normal transition-all duration-200',
(isActive || isActiveParent)
? 'bg-[var(--dg-blue)]/10 text-[var(--dg-blue)] hover:bg-[var(--dg-blue)]/15'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]',
)}
onClick={handleClick}
>
<span className="flex items-center gap-1.5">
<item.icon className="h-3.5 w-3.5" />
<span>{item.name}</span>
<span className="flex items-center gap-2">
<item.icon className={cn('h-3.5 w-3.5', (isActive || isActiveParent) && 'text-[var(--dg-blue)]')} />
<span className="truncate">{item.name}</span>
</span>
{isOpen ? <ChevronDown className="h-3 w-3 text-slate-500" /> : <ChevronRight className="h-3 w-3 text-slate-500" />}
<ChevronDown
className={cn(
'h-3 w-3 text-[var(--dg-text-muted)] transition-transform duration-200',
!isOpen && '-rotate-90',
)}
/>
</Button>
{isOpen && (
<div className="mt-0.5 space-y-0.5">
{/* Submenu with animation */}
<div
className={cn(
'overflow-hidden transition-all duration-200 ease-in-out',
isOpen ? 'max-h-[800px] opacity-100 mt-0.5' : 'max-h-0 opacity-0',
)}
>
<div className="space-y-0.5 border-l border-[var(--dg-border)] ml-4 pl-1.5">
{item.children?.map((child) => (
<MenuItemComponent key={child.id} item={child} depth={depth + 1} />
))}
</div>
)}
</div>
</>
) : (
<NavLink
to={item.path || '#'}
onClick={handleClick}
className={({ isActive }) =>
cn('flex items-center gap-1.5 h-8 px-2.5 text-xs rounded-md transition-colors',
isActive ? 'bg-blue-600/20 text-blue-400' : 'text-slate-400 hover:text-slate-100 hover:bg-slate-800')
className={({ isActive: navActive }) =>
cn(
'flex items-center gap-2 h-8 px-2.5 text-xs rounded-md transition-all duration-200',
navActive || isActive
? 'bg-[var(--dg-blue)]/10 text-[var(--dg-blue)] font-medium'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]',
)
}
>
<item.icon className="h-3.5 w-3.5" />
<span>{item.name}</span>
<item.icon className="h-3.5 w-3.5 flex-shrink-0" />
<span className="truncate">{item.name}</span>
</NavLink>
)}
</div>
);
}
/**
* Sidebar - 优化后的侧边栏导航
*
* 优化点:
* 1. 使用 CSS 变量控制宽度,统一协调
* 2. 添加 Tooltip 支持(折叠状态)
* 3. 子菜单展开/收起动画
* 4. Logo 区域更精致
* 5. 底部添加版本信息
*/
export function Sidebar() {
const { sidebarCollapsed, toggleSidebar } = useThemeStore();
// Listen for keyboard shortcut
useEffect(() => {
const handler = () => toggleSidebar();
window.addEventListener('toggle-sidebar' as any, handler);
return () => window.removeEventListener('toggle-sidebar' as any, handler);
}, [toggleSidebar]);
return (
<aside className={cn('fixed left-0 top-0 z-40 h-screen bg-slate-900 border-r border-slate-800 transition-all duration-300', sidebarCollapsed ? 'w-16' : 'w-64')}>
<div className="flex items-center justify-between h-14 px-4 border-b border-slate-800">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center">
<Cloud className="h-5 w-5 text-white" />
</div>
<span className="font-semibold text-slate-100">AIOps平台</span>
</div>
)}
{sidebarCollapsed && (
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center mx-auto">
<aside
className={cn(
'fixed left-0 top-0 z-40 h-screen bg-[var(--dg-bg-secondary)] border-r border-[var(--dg-border)]',
'flex flex-col transition-[width] duration-300 ease-in-out',
)}
style={{ width: 'var(--sidebar-width)' }}
>
{/* Logo Area */}
<div className="flex items-center h-14 px-3 border-b border-[var(--dg-border)] flex-shrink-0">
<div className={cn('flex items-center gap-2.5 flex-1 min-w-0', sidebarCollapsed && 'justify-center')}>
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[var(--dg-blue)] to-[var(--dg-cyan)] flex items-center justify-center flex-shrink-0 shadow-lg shadow-[var(--dg-blue)]/20">
<Cloud className="h-5 w-5 text-white" />
</div>
)}
{!sidebarCollapsed && (
<span className="font-bold text-[var(--dg-text-primary)] text-sm tracking-tight truncate">
AIOps平台
</span>
)}
</div>
{!sidebarCollapsed && (
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100" onClick={toggleSidebar}>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] flex-shrink-0"
onClick={toggleSidebar}
title="收起侧边栏"
>
<Menu className="h-4 w-4" />
</Button>
)}
</div>
<ScrollArea className="h-[calc(100vh-3.5rem)]">
<div className={cn('p-1.5', sidebarCollapsed && 'px-1')}>
{menuItems.map((item) => <MenuItemComponent key={item.id} item={item} />)}
</div>
{/* Menu Items */}
<ScrollArea className="flex-1 py-2 px-1.5">
<nav className="space-y-0.5">
{menuItems.map((item) => (
<MenuItemComponent key={item.id} item={item} />
))}
</nav>
</ScrollArea>
{/* Footer */}
{!sidebarCollapsed && (
<div className="px-3 py-2 border-t border-[var(--dg-border)] flex-shrink-0">
<div className="text-[10px] text-[var(--dg-text-muted)] text-center">
AIOps v1.0.0
</div>
</div>
)}
</aside>
);
}