Files
sundynix-radio-admin/src/pages/Login/index.tsx
T
2026-03-06 16:53:48 +08:00

332 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../../store/authStore';
import { loginApi, getCaptchaApi } from '../../api/auth';
import { toast } from 'sonner';
import { Button } from '../../components/ui/button';
import { Input } from '../../components/ui/input';
import { Label } from '../../components/ui/label';
import { ShieldCheck, Disc3, ArrowRight, Heart, Sparkles, Waves } from 'lucide-react';
import { motion } from 'framer-motion';
export default function Login() {
const navigate = useNavigate();
const setToken = useAuthStore((state) => state.setToken);
const setUserInfo = useAuthStore((state) => state.setUserInfo);
const [account, setAccount] = useState('');
const [password, setPassword] = useState('');
const [captcha, setCaptcha] = useState('');
const [captchaId, setCaptchaId] = useState('');
const [captchaImage, setCaptchaImage] = useState('');
const [loading, setLoading] = useState(false);
const mounted = useRef(false);
const fetchCaptcha = async () => {
try {
const res: any = await getCaptchaApi();
const b64 = res.captcha;
if (b64 && !b64.startsWith('data:')) {
setCaptchaImage(`data:image/png;base64,${b64}`);
} else {
setCaptchaImage(b64);
}
setCaptchaId(res.captchaId);
} catch (error) {
console.error('获取验证码失败', error);
}
};
useEffect(() => {
if (!mounted.current) {
fetchCaptcha();
mounted.current = true;
}
}, []);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!account || !password) return toast.error('请输入账号和密码');
if (!captcha) return toast.error('请输入验证码');
try {
setLoading(true);
const res: any = await loginApi({
account,
password,
captcha,
captchaId
});
toast.success('欢迎回来,首席播音官');
setToken(res.token);
setUserInfo(res.user);
navigate('/');
} catch (error: any) {
fetchCaptcha();
setCaptcha('');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-[#FAF5E6] relative overflow-hidden font-sans warm-noise selection:bg-[#D28F4F]/30 selection:text-[#4A3A2C]">
{/* Ambient Background Elements */}
<div className="absolute top-[-20%] left-[-10%] w-[60%] h-[60%] bg-gradient-to-br from-[#D28F4F]/15 via-transparent to-transparent rounded-full blur-[160px] animate-pulse" />
<div className="absolute bottom-[-15%] right-[-5%] w-[55%] h-[55%] bg-gradient-to-tr from-[#A64452]/10 via-transparent to-transparent rounded-full blur-[140px] animate-pulse transition-duration-[4s]" />
{/* Animated Floating Particles */}
<div className="absolute inset-0 pointer-events-none opacity-20">
<motion.div
animate={{ y: [0, -40, 0], opacity: [0.1, 0.4, 0.1] }}
transition={{ duration: 10, repeat: Infinity }}
className="absolute top-1/4 left-1/4 w-32 h-32 bg-[#D28F4F] rounded-full blur-[80px]"
/>
<motion.div
animate={{ y: [0, 50, 0], opacity: [0.1, 0.3, 0.1] }}
transition={{ duration: 12, repeat: Infinity, delay: 1 }}
className="absolute bottom-1/4 right-1/3 w-40 h-40 bg-[#A64452] rounded-full blur-[90px]"
/>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.98, y: 15 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
className="w-full max-w-[1240px] grid grid-cols-1 lg:grid-cols-2 bg-white/40 backdrop-blur-3xl rounded-[4.5rem] shadow-[0_40px_100px_-20px_rgba(74,58,44,0.12)] border border-white/60 overflow-hidden relative z-10 m-6 ring-1 ring-[#D28F4F]/5"
>
{/* Visual Side (Japanese Zen/Retro Radio Vibe) */}
<div className="hidden lg:flex flex-col justify-between p-20 sidebar-noise text-white relative overflow-hidden group">
<div className="absolute inset-0 bg-[#4A3A2C]/10 mix-blend-overlay pointer-events-none" />
<div className="absolute top-[-30%] right-[-20%] w-[120%] h-[120%] bg-gradient-to-bl from-[#D28F4F]/40 via-transparent to-transparent rounded-full blur-[120px] group-hover:scale-110 transition-transform duration-[4s]" />
<div className="relative z-10">
<motion.div
initial={{ x: -30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.4, duration: 0.8 }}
className="flex items-center gap-6"
>
<div className="w-20 h-20 bg-white rounded-[2.5rem] flex items-center justify-center shadow-2xl shadow-[#D28F4F]/30 ring-1 ring-white/40 overflow-hidden relative group p-2.5">
<img src="/favicon.jpg" alt="logo" className="w-full h-full object-cover rounded-[1.8rem]" />
<div className="absolute inset-0 bg-[#D28F4F] opacity-0 group-hover:opacity-20 transition-opacity" />
</div>
<div className="flex flex-col">
<span className="text-3xl font-black tracking-tightest leading-none drop-shadow-sm"></span>
<div className="flex items-center gap-3 mt-2">
<span className="text-[10px] uppercase font-black tracking-[0.4em] opacity-50 px-2 py-0.5 rounded-full border border-white/20">QUAN SHENG HUI</span>
</div>
</div>
</motion.div>
<div className="mt-28 space-y-10">
<motion.h2
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 1 }}
className="text-6xl font-black text-white leading-[1.05] tracking-tighter"
>
<span className="text-white/40 italic font-serif mx-1"></span><br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-[#D28F4F] to-[#E29A66] italic px-2"></span>
</motion.h2>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="flex flex-col gap-6"
>
<p className="text-white/60 text-xl font-medium max-w-sm leading-relaxed border-l-3 border-[#D28F4F] pl-8">
<br />
</p>
<div className="flex gap-4 items-center">
<div className="h-px w-12 bg-white/20" />
<span className="text-[10px] font-black uppercase tracking-[0.4em] text-white/30">System v2.5.0 Premium</span>
</div>
</motion.div>
</div>
</div>
<div className="relative z-10 flex items-end justify-between">
<div className="flex flex-col">
<div className="flex items-center gap-3 mb-2">
<Waves className="w-5 h-5 text-[#D28F4F]" />
<span className="text-[10px] uppercase font-black tracking-widest text-[#D28F4F]">Live Interaction Index</span>
</div>
<div className="text-4xl font-black flex items-baseline gap-2">
99.9%
<span className="text-xs text-white/40 font-bold uppercase">Up-time</span>
</div>
</div>
<div className="bg-white/10 backdrop-blur-xl rounded-[2rem] px-6 py-4 border border-white/10 flex items-center gap-4 group cursor-help hover:bg-white/20 transition-all">
<div className="w-3 h-3 rounded-full bg-emerald-500 shadow-[0_0_12px_#10b981]" />
<span className="text-sm font-black tracking-wide text-white/80 flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-[#D28F4F]" />
</span>
</div>
</div>
</div>
{/* Form Side (Clean Japanese Creamy Minimalist) */}
<div className="p-12 lg:p-24 flex flex-col justify-center bg-[#FAF5E6]/30 backdrop-blur-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-64 bg-[#D28F4F]/5 rounded-full blur-[100px] pointer-events-none" />
<div className="max-w-md mx-auto w-full relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mb-16"
>
<div className="flex items-center gap-4 mb-3">
<Sparkles className="w-5 h-5 text-[#D28F4F]" />
<span className="text-[11px] font-black text-[#D28F4F] uppercase tracking-[0.5em] opacity-70">Identity Authentication</span>
</div>
<h3 className="text-5xl font-black text-[#4A3A2C] tracking-tight leading-tight"></h3>
<div className="h-1.5 w-24 bg-gradient-to-r from-[#D28F4F] to-transparent rounded-full mt-6" />
</motion.div>
<form onSubmit={handleLogin} className="space-y-12">
<div className="space-y-10">
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.7 }}
className="space-y-4"
>
<Label className="text-[12px] uppercase font-black tracking-[0.2em] text-[#8C7E6C] ml-2 flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-[#D28F4F]" />
</Label>
<Input
placeholder="Admin / Operator ID"
className="h-20 rounded-[2rem] border-none bg-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/15 transition-all pl-10 placeholder:text-[#8C7E6C]/30 text-lg group-hover:shadow-[0_8px_30px_rgba(210,143,79,0.08)]"
value={account}
onChange={(e) => setAccount(e.target.value)}
disabled={loading}
/>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8 }}
className="space-y-4"
>
<Label className="text-[12px] uppercase font-black tracking-[0.2em] text-[#8C7E6C] ml-2 flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-[#D28F4F]" />
</Label>
<Input
type="password"
placeholder="Secure Passphrase"
className="h-20 rounded-[2rem] border-none bg-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/15 transition-all pl-10 placeholder:text-[#8C7E6C]/30 text-lg group-hover:shadow-[0_8px_30px_rgba(210,143,79,0.08)]"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.9 }}
className="space-y-4"
>
<Label className="text-[12px] uppercase font-black tracking-[0.2em] text-[#8C7E6C] ml-2 flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-[#D28F4F]" />
</Label>
<div className="flex gap-5">
<Input
placeholder="Code"
className="h-20 rounded-[2rem] border-none bg-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/15 transition-all pl-10 flex-1 placeholder:text-[#8C7E6C]/30 text-lg"
value={captcha}
onChange={(e) => setCaptcha(e.target.value)}
disabled={loading}
/>
<div
className="h-20 w-44 bg-white rounded-[2rem] p-3 shadow-[0_8px_30px_rgb(0,0,0,0.04)] border border-[#D28F4F]/10 cursor-pointer hover:ring-4 ring-[#D28F4F]/20 transition-all flex items-center justify-center overflow-hidden group/captcha"
onClick={fetchCaptcha}
>
{captchaImage ? (
<img src={captchaImage} alt="captcha" className="h-full w-full object-contain filter group-hover/captcha:scale-110 transition-transform duration-500 contrast-125" />
) : (
<Disc3 className="w-6 h-6 text-[#D28F4F]/40 animate-spin" />
)}
</div>
</div>
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.1 }}
>
<Button
type="submit"
disabled={loading}
className="w-full h-24 rounded-[2.5rem] bg-gradient-to-r from-[#D28F4F] to-[#A64452] hover:scale-[1.02] active:scale-[0.98] transition-all shadow-2xl shadow-[#D28F4F]/30 border-none group overflow-hidden relative"
>
<div className="relative z-10 flex items-center justify-center gap-6 text-2xl font-black tracking-tight text-white">
{loading ? (
<>
<Disc3 className="w-8 h-8 animate-spin" />
<span>...</span>
</>
) : (
<>
<span></span>
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center group-hover:bg-white/30 transition-colors">
<ArrowRight className="w-6 h-6 group-hover:translate-x-1.5 transition-transform" />
</div>
</>
)}
</div>
<div className="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity" />
{/* Liquid gradient animation effect */}
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full"
animate={{ translateX: ['-100%', '200%'] }}
transition={{ duration: 2.5, repeat: Infinity, ease: "linear" }}
/>
</Button>
</motion.div>
</form>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.3 }}
className="mt-20 pt-10 border-t border-[#4A3A2C]/5 flex items-center justify-between"
>
<div className="flex items-center gap-3">
<span className="w-2 h-2 rounded-full bg-[#A64452]/20 border border-[#A64452]/40" />
<span className="text-[10px] font-black text-[#8C7E6C]/50 uppercase tracking-[0.25em]">Crafted For Visual Harmony</span>
</div>
<div className="flex items-center gap-2">
<Heart className="w-3.5 h-3.5 text-[#A64452] fill-[#A64452]/30 animate-pulse" />
</div>
</motion.div>
</div>
</div>
</motion.div>
{/* Decorative Kanji/Background elements for Japanese Zen Vibe */}
<div className="absolute top-20 left-20 pointer-events-none select-none opacity-[0.03] flex flex-col items-center">
<span className="text-[200px] font-serif leading-none"></span>
<span className="text-[100px] font-serif leading-none mt-[-40px]"></span>
</div>
<div className="absolute bottom-12 right-12 flex gap-4 opacity-[0.05] pointer-events-none">
<div className="w-4 h-32 bg-[#D28F4F] rounded-full blur-[1px]" />
<div className="w-4 h-48 bg-[#D28F4F] rounded-full mt-10 blur-[1px]" />
<div className="w-4 h-32 bg-[#D28F4F] rounded-full mt-4 blur-[1px]" />
</div>
</div>
);
}