diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index 29b14322c..42c214b0a 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -1,15 +1,28 @@ -import { useState, useRef, useEffect, useCallback } from 'react'; +/** + * @file pages/auth/LoginPage.tsx + * @description 登录页 — V3 Design System 重构 + * + * 设计语言:深色太空主题 + 品牌渐变 + 雷达扫描动画 + 玻璃拟态 + * 功能:账号密码登录、记住用户名、MaxKey SSO、快捷演示账号 + */ + +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useMutation } from '@tanstack/react-query'; -import { Eye, EyeOff, Shield, User, Lock, ShieldCheck, Package, Building2, Wrench, BarChart3, LogIn } from 'lucide-react'; +import { + Eye, EyeOff, Shield, User, Lock, ShieldCheck, Package, + Building2, Wrench, BarChart3, LogIn, CheckCircle2, + Radar, Fingerprint, Activity, +} from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/Button'; -import { Input } from '@/components/ui/Input'; import { login } from '@/api/auth'; +// ─── Schema ────────────────────────────────────────────────────────────────── + const loginSchema = z.object({ username: z.string().min(1, '请输入用户名'), password: z.string().min(1, '请输入密码'), @@ -17,165 +30,126 @@ const loginSchema = z.object({ type LoginForm = z.infer; -interface Particle { - x: number; - y: number; - vx: number; - vy: number; - size: number; - opacity: number; - hue: number; - life: number; - maxLife: number; -} +// ─── Demo Accounts ────────────────────────────────────────────────────────── -// 开发环境使用演示账号,生产环境通过环境变量注入(为空则隐藏) const isDev = import.meta.env.DEV; const envDemoUser = import.meta.env.VITE_DEMO_USERNAME; const envDemoPass = import.meta.env.VITE_DEMO_PASSWORD; -const DEMO_ACCOUNTS: Array<{ label: string; desc: string; username: string; password: string; Icon: typeof ShieldCheck }> = isDev +const DEMO_ACCOUNTS: Array<{ + label: string; + desc: string; + username: string; + password: string; + Icon: typeof ShieldCheck; + gradient: string; +}> = isDev ? [ - { label: '系统管理员', desc: '全域权限', username: envDemoUser || 'admin', password: envDemoPass || 'admin123', Icon: ShieldCheck }, - { label: '资产管理员', desc: '全生命周期', username: 'asset', password: 'asset123', Icon: Package }, - { label: '部门负责人', desc: '资源审批', username: 'manager', password: 'manager123', Icon: Building2 }, - { label: '运维人员', desc: '巡检维修', username: 'staff', password: 'staff123', Icon: Wrench }, + { label: '系统管理员', desc: '全域权限', username: envDemoUser || 'admin', password: envDemoPass || 'admin123', Icon: ShieldCheck, gradient: 'from-blue-500 to-cyan-400' }, + { label: '资产管理员', desc: '全生命周期', username: 'asset', password: 'asset123', Icon: Package, gradient: 'from-violet-500 to-purple-400' }, + { label: '部门负责人', desc: '资源审批', username: 'manager', password: 'manager123', Icon: Building2, gradient: 'from-emerald-500 to-teal-400' }, + { label: '运维人员', desc: '巡检维修', username: 'staff', password: 'staff123', Icon: Wrench, gradient: 'from-amber-500 to-orange-400' }, ] : envDemoUser && envDemoPass - ? [{ label: '演示账号', desc: '只读权限', username: envDemoUser, password: envDemoPass, Icon: ShieldCheck }] + ? [{ label: '演示账号', desc: '只读权限', username: envDemoUser, password: envDemoPass, Icon: ShieldCheck, gradient: 'from-blue-500 to-cyan-400' }] : []; -function useParticleCanvas(canvasRef: React.RefObject) { - const particlesRef = useRef([]); - const animRef = useRef(0); - const mouseRef = useRef({ x: -1000, y: -1000 }); - const timeRef = useRef(0); - - const initParticles = useCallback((width: number, height: number) => { - const count = Math.min(Math.floor((width * height) / 6000), 120); - particlesRef.current = Array.from({ length: count }, () => ({ - x: Math.random() * width, - y: Math.random() * height, - vx: (Math.random() - 0.5) * 0.25, - vy: (Math.random() - 0.5) * 0.25, - size: Math.random() * 2 + 0.5, - opacity: Math.random() * 0.4 + 0.15, - hue: Math.random() * 60 + 200, - life: 0, - maxLife: Math.random() * 500 + 300, - })); - }, []); +// ─── Radar Animation ──────────────────────────────────────────────────────── - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - const resize = () => { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - if (particlesRef.current.length === 0) initParticles(canvas.width, canvas.height); - }; - resize(); - window.addEventListener('resize', resize); - window.addEventListener('mousemove', (e) => { - mouseRef.current = { x: e.clientX, y: e.clientY }; - }); - - const CONNECTION_DIST = 120; - const MOUSE_DIST = 200; - - const animate = () => { - timeRef.current++; - ctx.fillStyle = 'rgba(11, 19, 38, 0.15)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - const particles = particlesRef.current; - const mouse = mouseRef.current; - - for (let i = 0; i < particles.length; i++) { - const p = particles[i]; - p.life++; - const lifeRatio = p.life / p.maxLife; - const fadeIn = Math.min(p.life / 60, 1); - const fadeOut = lifeRatio > 0.8 ? 1 - (lifeRatio - 0.8) / 0.2 : 1; - const alpha = p.opacity * fadeIn * fadeOut; - - if (p.life >= p.maxLife) { - p.x = Math.random() * canvas.width; - p.y = Math.random() * canvas.height; - p.life = 0; - p.maxLife = Math.random() * 500 + 300; - } +function RadarAnimation() { + return ( +
+
+
+
+
+
+
+
+
+
+
+ ); +} - const dx = mouse.x - p.x; - const dy = mouse.y - p.y; - const dist = Math.sqrt(dx * dx + dy * dy); - if (dist < MOUSE_DIST) { - const force = (MOUSE_DIST - dist) / MOUSE_DIST * 0.006; - p.vx += dx * force; - p.vy += dy * force; - } +// ─── Floating Orbs Background ─────────────────────────────────────────────── - p.vx *= 0.988; - p.vy *= 0.988; - p.x += p.vx; - p.y += p.vy; - - if (p.x < 0) { p.x = 0; p.vx *= -1; } - if (p.x > canvas.width) { p.x = canvas.width; p.vx *= -1; } - if (p.y < 0) { p.y = 0; p.vy *= -1; } - if (p.y > canvas.height) { p.y = canvas.height; p.vy *= -1; } - - ctx.beginPath(); - ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); - ctx.fillStyle = `hsla(${p.hue}, 80%, 75%, ${alpha})`; - ctx.fill(); - - for (let j = i + 1; j < particles.length; j++) { - const p2 = particles[j]; - const ddx = p.x - p2.x; - const ddy = p.y - p2.y; - const d = Math.sqrt(ddx * ddx + ddy * ddy); - if (d < CONNECTION_DIST) { - const lineAlpha = (1 - d / CONNECTION_DIST) * 0.15 * fadeIn * fadeOut; - ctx.beginPath(); - ctx.moveTo(p.x, p.y); - ctx.lineTo(p2.x, p2.y); - ctx.strokeStyle = `hsla(${p.hue}, 70%, 65%, ${lineAlpha})`; - ctx.lineWidth = 0.6; - ctx.stroke(); - } - } - } +function FloatingOrbs() { + return ( +
+
+
+
+
+
+
+ ); +} - animRef.current = requestAnimationFrame(animate); - }; +// ─── Feature Card ─────────────────────────────────────────────────────────── - animate(); - return () => { - window.removeEventListener('resize', resize); - cancelAnimationFrame(animRef.current); - }; - }, [canvasRef, initParticles]); +interface FeatureCardProps { + icon: React.ComponentType<{ className?: string }>; + title: string; + subtitle: string; + gradient: string; } +function FeatureCard({ icon: Icon, title, subtitle, gradient }: FeatureCardProps) { + return ( +
+
+ +
+

{title}

+

{subtitle}

+
+
+ ); +} + +// ─── Trust Badge ──────────────────────────────────────────────────────────── + +function TrustBadge({ icon: Icon, text }: { icon: React.ComponentType<{ className?: string }>; text: string }) { + return ( +
+ + {text} +
+ ); +} + +// ─── Main Component ───────────────────────────────────────────────────────── + export default function LoginPage() { const navigate = useNavigate(); - const canvasRef = useRef(null); const [showPassword, setShowPassword] = useState(false); const [errorMsg, setErrorMsg] = useState(null); const [rememberMe, setRememberMe] = useState(false); - useParticleCanvas(canvasRef); - const { register, handleSubmit, setValue, formState: { errors } } = useForm({ resolver: zodResolver(loginSchema), defaultValues: { username: '', password: '' }, }); - // 页面加载时从 localStorage 恢复记住的用户名 useEffect(() => { const saved = localStorage.getItem('remembered_username'); if (saved) { @@ -194,7 +168,6 @@ export default function LoginPage() { } sessionStorage.setItem('auth_token', token); sessionStorage.setItem('user_info', JSON.stringify({ userId, username, realName })); - // 记住我:保存或清除用户名 if (rememberMe) { localStorage.setItem('remembered_username', username); } else { @@ -224,291 +197,276 @@ export default function LoginPage() { }; return ( -
+
+ {/* ── CSS Animations ── */} - - - {DEMO_ACCOUNTS.length > 0 && ( -
-
-
-

快捷账号

-

测试期间保留,后续可按指令移除

-
-
-
- {DEMO_ACCOUNTS.map(({ label, desc, username, password, Icon }) => ( - - ))} -
-
- )} -
+ + +
+ {/* ── 左侧品牌面板 ─────────────────────────────────────────── */} +
+ {/* 顶部标签 */} +
+ + forthAMS · Enterprise Asset Management +
-
-
-
-
+ + {/* 主标题区 */}
-
- - 星空无界 · UNI 有方 -
-

- 仰望星空,
驾驭资产万象 +

+ 统一资产、流程与审计的运营入口

-

- 以 Universe 之广,纳资产之全。
- 设备、流程、数据,尽在 UNI 统一视界。 +

+ 为资产台账、盘点、审批和审计提供一致的登录起点,帮助运营团队稳定进入核心工作台。

+ + {/* 功能卡片 */} +
+ + + +
-
-
-
-
- - - - - -
- - - - - - -
- - UNI - - - UNI - + {/* 底部信任标签 */} +
+ + + + +
+
+ + {/* ── 右侧登录面板 ─────────────────────────────────────────── */} +
+
+
+ {/* 顶部渐变光条 */} +
+ + {/* 装饰环 */} +
+
+ + {/* 头部 */} +
+
+
-
-
-
- {[ - { icon: Shield, label: '全生命周期追踪' }, - { icon: BarChart3, label: '实时数据看板' }, - ].map(({ icon: Icon, label }) => ( -
-
- +

Secure Sign In

+

登录资产管理系统

+

使用组织账号进入 forthAMS 工作台

+ + + {/* 表单 */} +
+
+ +
+ +
- {label} + {errors.username && ( +

{errors.username.message}

+ )}
- ))} -
-
-
-
-
-
-
- -
-

- 资产管理系统 -

-

- 安全资产网关 -

-
- - -
- -
- - -
- {errors.username &&

{errors.username.message}

} -
+
+ +
+ + + +
+ {errors.password && ( +

{errors.password.message}

+ )} +
-
- -
- - - +
+ + {errorMsg && ( +
+ {errorMsg} +
+ )} + + + {loginMutation.isPending ? '登录中...' : '登录系统'} + + + + {/* SSO */} +
+ + + MaxKey 单点登录 + +

使用组织统一身份认证登录

- {errors.password &&

{errors.password.message}

} -
-
- - + {/* 快捷账号 */} + {DEMO_ACCOUNTS.length > 0 && ( +
+
+ +

快捷账号

+ + 演示环境 + +
+
+ {DEMO_ACCOUNTS.map(({ label, desc, username, password, Icon, gradient }) => ( + + ))} +
+
+ )}
- {errorMsg && ( -
- {errorMsg} + {/* 底部版权 */} + -
+
);