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

+89 -113
View File
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { User, Lock, Palette, Check, Loader2, AlertCircle } from 'lucide-react';
import { User, Lock, Palette, Check, Loader2, AlertCircle, Eye, EyeOff } from 'lucide-react';
import { useUserStore } from '@/store';
import { userApi } from '@/services/api';
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
@@ -14,46 +14,7 @@ import {
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
/* ---------- Floating background orbs (themed light spots) ---------- */
const ORBS = [
{
top: '-10%',
left: '-5%',
size: 520,
color: 'var(--dg-blue)',
opacity: 0.22,
blur: 90,
animation: 'login-orb-1 18s ease-in-out infinite',
},
{
bottom: '-15%',
right: '-8%',
size: 480,
color: 'var(--dg-purple)',
opacity: 0.2,
blur: 100,
animation: 'login-orb-2 22s ease-in-out infinite',
},
{
top: '30%',
right: '10%',
size: 320,
color: 'var(--dg-cyan)',
opacity: 0.16,
blur: 70,
animation: 'login-orb-3 16s ease-in-out infinite',
},
{
bottom: '10%',
left: '8%',
size: 380,
color: 'var(--dg-gradient-start)',
opacity: 0.18,
blur: 80,
animation: 'login-orb-4 20s ease-in-out infinite',
},
];
/* ---------- CSS Animations ---------- */
const ANIMATIONS = `
@keyframes login-orb-1 {
0%, 100% { transform: translate(0, 0) scale(1); }
@@ -80,8 +41,31 @@ const ANIMATIONS = `
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes login-input-focus {
from { box-shadow: 0 0 0 0px var(--dg-input-focus-ring); }
to { box-shadow: 0 0 0 4px var(--dg-input-focus-ring); }
}
`;
/* ---------- Floating background orbs ---------- */
const ORBS = [
{ top: '-10%', left: '-5%', size: 520, color: 'var(--dg-blue)', opacity: 0.18, blur: 90, animation: 'login-orb-1 18s ease-in-out infinite' },
{ bottom: '-15%', right: '-8%', size: 480, color: 'var(--dg-purple)', opacity: 0.16, blur: 100, animation: 'login-orb-2 22s ease-in-out infinite' },
{ top: '30%', right: '10%', size: 320, color: 'var(--dg-cyan)', opacity: 0.12, blur: 70, animation: 'login-orb-3 16s ease-in-out infinite' },
{ bottom: '10%', left: '8%', size: 380, color: 'var(--dg-gradient-start)', opacity: 0.14, blur: 80, animation: 'login-orb-4 20s ease-in-out infinite' },
];
/**
* Login - 优化后的登录页面
*
* 优化点:
* 1. 使用 glass 效果替代简单的半透明背景
* 2. 输入框使用 dg-input 工具类
* 3. 密码显示/隐藏使用图标按钮
* 4. 主题切换更精致
* 5. 错误提示动画更流畅
* 6. 背景光球动画更协调
*/
export function Login() {
const navigate = useNavigate();
const { theme, setTheme } = useTheme();
@@ -90,6 +74,11 @@ export function Login() {
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -98,7 +87,7 @@ export function Login() {
try {
const result = await userApi.login(username, password);
useUserStore.getState().login(result.user, result.token);
localStorage.setItem('token', result.token); // backward compat
localStorage.setItem('token', result.token);
navigate('/', { replace: true });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '登录失败,请检查用户名和密码';
@@ -115,11 +104,10 @@ export function Login() {
<div
className="relative min-h-screen flex items-center justify-center px-4 overflow-hidden"
style={{
background:
'linear-gradient(135deg, var(--dg-bg-primary) 0%, var(--dg-bg-secondary) 100%)',
background: `radial-gradient(ellipse at 50% 0%, ${getComputedStyle(document.documentElement).getPropertyValue('--dg-bg-secondary')} 0%, ${getComputedStyle(document.documentElement).getPropertyValue('--dg-bg-primary')} 100%)`,
}}
>
{/* -------- Floating themed light spots -------- */}
{/* Floating light spots */}
{ORBS.map((orb, i) => (
<div
key={`orb-${i}`}
@@ -134,15 +122,16 @@ export function Login() {
height: orb.size,
borderRadius: '50%',
background: `radial-gradient(circle, ${orb.color} 0%, transparent 70%)`,
opacity: orb.opacity,
opacity: mounted ? orb.opacity : 0,
filter: `blur(${orb.blur}px)`,
animation: orb.animation,
willChange: 'transform',
transition: 'opacity 1s ease-out',
}}
/>
))}
{/* -------- Theme switcher (top-right) -------- */}
{/* Theme switcher */}
<div
className="fixed top-4 right-4 z-50"
style={{ animation: 'login-fade-in 0.6s ease-out' }}
@@ -154,77 +143,75 @@ export function Login() {
aria-label="切换主题"
className={cn(
'inline-flex items-center justify-center size-10 rounded-full',
'border transition-all duration-200 hover:scale-105 active:scale-95'
'border border-[var(--dg-border)] transition-all duration-200',
'hover:scale-105 active:scale-95 hover:border-[var(--dg-border-hover)]',
'glass'
)}
style={{
background: 'color-mix(in srgb, var(--dg-bg-secondary) 70%, transparent)',
borderColor: 'var(--dg-border)',
color: 'var(--dg-text-secondary)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
}}
style={{ color: 'var(--dg-text-secondary)' }}
>
<Palette className="size-5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuLabel>主题切换</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuContent
align="end"
className="w-60 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
style={{ boxShadow: 'var(--shadow-dropdown)' }}
>
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">主题切换</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
{THEME_OPTIONS.map((option) => (
<DropdownMenuItem
key={option.id}
className="flex items-center gap-2.5 cursor-pointer"
className={cn(
'flex items-center gap-2.5 cursor-pointer transition-colors',
'focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]',
theme === option.id ? 'text-[var(--dg-text-primary)]' : 'text-[var(--dg-text-secondary)]',
)}
onClick={() => setTheme(option.id)}
>
<span
className="size-3.5 rounded-full shrink-0 ring-2"
style={{
backgroundColor: option.primaryColor,
// @ts-expect-error css var in style string
'--tw-ring-color': 'var(--dg-bg-primary)',
boxShadow: '0 0 0 2px var(--dg-bg-primary)',
}}
/>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium block">{option.name}</span>
<span className="text-xs opacity-60 block truncate">
{option.description}
</span>
<span className="text-xs opacity-60 block truncate">{option.description}</span>
</div>
{theme === option.id && <Check className="size-4 shrink-0" />}
{theme === option.id && <Check className="size-4 shrink-0 text-[var(--dg-blue)]" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* -------- Login card -------- */}
{/* Login card */}
<div
className="relative z-10 w-full max-w-md"
style={{
background:
'color-mix(in srgb, var(--dg-bg-secondary) 75%, transparent)',
background: 'color-mix(in srgb, var(--dg-bg-secondary) 80%, transparent)',
backdropFilter: 'blur(24px)',
WebkitBackdropFilter: 'blur(24px)',
border: '1px solid var(--dg-border)',
borderRadius: 24,
boxShadow:
'0 8px 48px rgba(0,0,0,0.28), 0 0 0 1px rgba(255,255,255,0.02)',
padding: '40px 36px',
boxShadow: 'var(--shadow-card-hover)',
padding: '44px 40px',
animation: 'login-card-enter 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
}}
>
{/* Brand */}
<div className="text-center mb-8">
<div
className="inline-flex items-center justify-center size-14 rounded-2xl mb-4 shadow-lg"
className="inline-flex items-center justify-center size-16 rounded-2xl mb-5"
style={{
background:
'linear-gradient(135deg, var(--dg-gradient-start), var(--dg-gradient-end))',
boxShadow: '0 8px 24px color-mix(in srgb, var(--dg-gradient-start) 35%, transparent)',
background: 'linear-gradient(135deg, var(--dg-gradient-start), var(--dg-gradient-end))',
boxShadow: '0 8px 32px color-mix(in srgb, var(--dg-gradient-start) 30%, transparent)',
}}
>
<svg
className="size-7 text-white"
className="size-8 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -238,10 +225,7 @@ export function Login() {
<h1 className="text-3xl font-bold dg-gradient-text tracking-tight">
AIOps
</h1>
<p
className="mt-2 text-sm"
style={{ color: 'var(--dg-text-secondary)' }}
>
<p className="mt-2.5 text-sm text-[var(--dg-text-secondary)]">
智能运维 · 全栈可观测平台
</p>
</div>
@@ -251,12 +235,11 @@ export function Login() {
{/* Error */}
{error && (
<div
className="flex items-start gap-2 rounded-lg px-4 py-3 text-sm"
className="flex items-start gap-2.5 rounded-xl px-4 py-3 text-sm animate-fade-in"
style={{
background: 'color-mix(in srgb, var(--dg-red) 12%, transparent)',
border: '1px solid color-mix(in srgb, var(--dg-red) 30%, transparent)',
background: 'color-mix(in srgb, var(--dg-red) 10%, transparent)',
border: '1px solid color-mix(in srgb, var(--dg-red) 25%, transparent)',
color: 'var(--dg-red)',
animation: 'login-fade-in 0.25s ease-out',
}}
>
<AlertCircle className="size-4 shrink-0 mt-0.5" />
@@ -265,17 +248,13 @@ export function Login() {
)}
{/* Username */}
<div>
<label
className="block text-sm font-medium mb-1.5"
style={{ color: 'var(--dg-text-secondary)' }}
>
<div className="space-y-1.5">
<label className="block text-sm font-medium text-[var(--dg-text-secondary)]">
用户名
</label>
<div className="relative">
<User
className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none"
style={{ color: 'var(--dg-text-muted)' }}
className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-[var(--dg-text-muted)]"
/>
<input
type="text"
@@ -284,23 +263,19 @@ export function Login() {
placeholder="请输入用户名"
autoComplete="username"
autoFocus
className="dg-input w-full rounded-lg py-2.5 pl-10 pr-4 text-sm transition-colors"
className="dg-input w-full py-2.5 pl-10 pr-4 text-sm"
/>
</div>
</div>
{/* Password */}
<div>
<label
className="block text-sm font-medium mb-1.5"
style={{ color: 'var(--dg-text-secondary)' }}
>
<div className="space-y-1.5">
<label className="block text-sm font-medium text-[var(--dg-text-secondary)]">
密码
</label>
<div className="relative">
<Lock
className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none"
style={{ color: 'var(--dg-text-muted)' }}
className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-[var(--dg-text-muted)]"
/>
<input
type={showPassword ? 'text' : 'password'}
@@ -308,15 +283,15 @@ export function Login() {
onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码"
autoComplete="current-password"
className="dg-input w-full rounded-lg py-2.5 pl-10 pr-12 text-sm transition-colors"
className="dg-input w-full py-2.5 pl-10 pr-12 text-sm"
/>
<button
type="button"
onClick={() => setShowPassword((v) => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-medium transition-colors"
style={{ color: 'var(--dg-text-muted)' }}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)] transition-colors"
tabIndex={-1}
>
{showPassword ? '隐藏' : '显示'}
{showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
</button>
</div>
</div>
@@ -326,20 +301,20 @@ export function Login() {
type="submit"
disabled={loading || !username || !password}
className={cn(
'dg-gradient-bg w-full rounded-lg py-3 mt-2 text-white font-semibold text-[15px]',
'transition-all duration-200 active:scale-[0.98]',
'disabled:cursor-not-allowed'
'dg-gradient-bg w-full rounded-xl py-3 mt-2 text-white font-semibold text-[15px]',
'transition-all duration-200 active:scale-[0.98] disabled:cursor-not-allowed',
)}
style={{
opacity: loading || !username || !password ? 0.6 : 1,
boxShadow:
'0 6px 24px color-mix(in srgb, var(--dg-gradient-start) 30%, transparent)',
boxShadow: loading || !username || !password
? 'none'
: '0 6px 24px color-mix(in srgb, var(--dg-gradient-start) 30%, transparent)',
}}
>
{loading ? (
<span className="inline-flex items-center justify-center gap-2">
<Loader2 className="size-4 animate-spin" />
登录中…
登录中...
</span>
) : (
'登 录'
@@ -356,7 +331,8 @@ export function Login() {
}}
>
<p>
默认管理员账号 <span className="font-medium" style={{ color: 'var(--dg-text-secondary)' }}>admin / 123456</span>
默认管理员账号{' '}
<span className="font-medium text-[var(--dg-text-secondary)]">admin / 123456</span>
</p>
<p>LDAP 用户可直接使用域账号登录</p>
</div>
@@ -365,9 +341,9 @@ export function Login() {
{/* Footer */}
<p
className="absolute bottom-4 left-0 right-0 text-center text-xs"
style={{ color: 'var(--dg-text-muted)', opacity: 0.7 }}
style={{ color: 'var(--dg-text-muted)', opacity: 0.5 }}
>
© AIOps · 智能运维平台
&copy; AIOps 智能运维平台
</p>
</div>
</>