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,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 · 智能运维平台
|
||||
© AIOps 智能运维平台
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in new issue
Block a user