332 lines
20 KiB
TypeScript
332 lines
20 KiB
TypeScript
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>
|
||
);
|
||
}
|