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:
1 parent
3d7932c087
commit
722587ab84
14 files changed
+2026
-969
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user