5个登录组件全面升级为动态视觉效果: 1. SplitView — 星河流转: 深靛蓝渐变(#0f0c29→#302b63) + 3条CSS流星动画 + 7个漂浮星辰光点 + 脉动光晕,底线输入框聚焦靛蓝 2. CenteredCard — 极光幻境: 深蓝黑底(#0a0a2e) + 3条流动极光色带 (翠绿/靛蓝/紫) filter:blur(80px) 缓慢流动,毛玻璃居中卡片 3. SidePanel — 日月交替(重点重做): 暮光紫→夜空蓝渐变 + 200px天体球(太阳金→月亮银蓝 8s循环) + 300px旋转轨道环 + 6个轨道光点 + 3条流星 + 10个闪烁星点 4. FullImage — 星辰连线: 深海军蓝(#0a1628) + 33个随机星点 + 6条星座连线 + 右上角蓝紫发光球 + 左下角暖色光球 5. MinimalDark — 流星雨: 深紫黑(#0c0a1a) + 6条流星动画 + 10个静态星点 + 渐变菱形logo + 极简布局
335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
import { useLogin } from '@/hooks/useLogin';
|
|
|
|
export default function SidePanel() {
|
|
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
|
|
|
/* 6 orbit light-points evenly spaced at 60° intervals on a 300px ring (radius 150px) */
|
|
const orbitDots = [
|
|
{ angle: 0, size: 8, opacity: 0.9 },
|
|
{ angle: 60, size: 6, opacity: 0.6 },
|
|
{ angle: 120, size: 7, opacity: 0.75 },
|
|
{ angle: 180, size: 6, opacity: 0.5 },
|
|
{ angle: 240, size: 8, opacity: 0.8 },
|
|
{ angle: 300, size: 6, opacity: 0.55 },
|
|
];
|
|
|
|
/* Scattered twinkling stars */
|
|
const stars = [
|
|
{ top: '6%', left: '12%', size: 3, delay: '0s', dur: '3s' },
|
|
{ top: '14%', left: '72%', size: 2, delay: '1.2s', dur: '4s' },
|
|
{ top: '25%', left: '38%', size: 4, delay: '0.5s', dur: '3.5s' },
|
|
{ top: '18%', left: '55%', size: 2, delay: '2s', dur: '5s' },
|
|
{ top: '60%', left: '15%', size: 3, delay: '1s', dur: '4.5s' },
|
|
{ top: '70%', left: '80%', size: 2, delay: '0.3s', dur: '3.2s' },
|
|
{ top: '82%', left: '30%', size: 4, delay: '1.8s', dur: '3.8s' },
|
|
{ top: '45%', left: '8%', size: 2, delay: '2.5s', dur: '4.2s' },
|
|
{ top: '35%', left: '88%', size: 3, delay: '0.8s', dur: '3.6s' },
|
|
{ top: '90%', left: '65%', size: 2, delay: '1.5s', dur: '5.2s' },
|
|
];
|
|
|
|
return (
|
|
<div className="flex min-h-screen" style={{ background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
|
|
{/* ========== Left Panel — 40% Celestial Animation ========== */}
|
|
<div
|
|
style={{
|
|
position: 'relative',
|
|
width: '40%',
|
|
background: 'linear-gradient(180deg, #1a0a2e 0%, #0f172a 100%)',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
{/* --- Core Celestial Orb (200px, centered-upper) --- */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '25%',
|
|
left: '50%',
|
|
transform: 'translate(-50%, -50%)',
|
|
width: 200,
|
|
height: 200,
|
|
zIndex: 2,
|
|
}}
|
|
>
|
|
{/* Main orb */}
|
|
<div
|
|
style={{
|
|
width: 200,
|
|
height: 200,
|
|
borderRadius: '50%',
|
|
animation: 'celestial-orb-color 8s ease-in-out infinite',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* --- Orbit Ring (300px, dashed) with 6 light dots --- */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '25%',
|
|
left: '50%',
|
|
width: 300,
|
|
height: 300,
|
|
marginTop: -150,
|
|
marginLeft: -150,
|
|
borderRadius: '50%',
|
|
border: '1px dashed rgba(255,255,255,0.1)',
|
|
animation: 'celestial-orbit-rotate 20s linear infinite',
|
|
zIndex: 3,
|
|
}}
|
|
>
|
|
{orbitDots.map((dot, i) => {
|
|
const rad = (dot.angle * Math.PI) / 180;
|
|
const r = 150; // radius of the 300px ring
|
|
const x = 150 + r * Math.cos(rad) - dot.size / 2;
|
|
const y = 150 + r * Math.sin(rad) - dot.size / 2;
|
|
return (
|
|
<div
|
|
key={i}
|
|
style={{
|
|
position: 'absolute',
|
|
left: x,
|
|
top: y,
|
|
width: dot.size,
|
|
height: dot.size,
|
|
borderRadius: '50%',
|
|
background: `rgba(255,255,255,${dot.opacity})`,
|
|
boxShadow: `0 0 6px rgba(255,255,255,${dot.opacity * 0.5})`,
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* --- 3 Meteors --- */}
|
|
{/* Meteor 1 */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '8%',
|
|
left: '15%',
|
|
width: 2,
|
|
height: 60,
|
|
background: 'linear-gradient(to bottom right, rgba(255,255,255,0.9), transparent)',
|
|
borderRadius: 2,
|
|
transform: 'rotate(-45deg)',
|
|
animation: 'celestial-meteor-1 1.5s ease-in 3s infinite',
|
|
zIndex: 1,
|
|
}}
|
|
/>
|
|
{/* Meteor 2 */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '5%',
|
|
left: '55%',
|
|
width: 1.5,
|
|
height: 50,
|
|
background: 'linear-gradient(to bottom right, rgba(255,255,255,0.7), transparent)',
|
|
borderRadius: 2,
|
|
transform: 'rotate(-40deg)',
|
|
animation: 'celestial-meteor-2 2s ease-in 7s infinite',
|
|
zIndex: 1,
|
|
}}
|
|
/>
|
|
{/* Meteor 3 */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '20%',
|
|
left: '35%',
|
|
width: 1.5,
|
|
height: 55,
|
|
background: 'linear-gradient(to bottom right, rgba(255,255,255,0.8), transparent)',
|
|
borderRadius: 2,
|
|
transform: 'rotate(-50deg)',
|
|
animation: 'celestial-meteor-3 1.8s ease-in 11s infinite',
|
|
zIndex: 1,
|
|
}}
|
|
/>
|
|
|
|
{/* --- Scattered Twinkling Stars --- */}
|
|
{stars.map((s, i) => (
|
|
<div
|
|
key={`star-${i}`}
|
|
style={{
|
|
position: 'absolute',
|
|
top: s.top,
|
|
left: s.left,
|
|
width: s.size,
|
|
height: s.size,
|
|
borderRadius: '50%',
|
|
background: 'rgba(255,255,255,0.7)',
|
|
animation: `celestial-twinkle ${s.dur} ease-in-out ${s.delay} infinite`,
|
|
zIndex: 1,
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{/* --- Bottom-left brand text --- */}
|
|
<div style={{ position: 'absolute', bottom: 48, left: 44, zIndex: 4 }}>
|
|
<div style={{ fontSize: 20, fontWeight: 600, color: 'rgba(255,255,255,0.7)', marginBottom: 6 }}>
|
|
SQL审核平台
|
|
</div>
|
|
<div style={{ fontSize: 13, color: 'rgba(255,255,255,0.3)' }}>
|
|
企业级数据库审核与合规管理
|
|
</div>
|
|
</div>
|
|
|
|
{/* --- Keyframes --- */}
|
|
<style>{`
|
|
/* Celestial orb: sun ↔ moon color cycle */
|
|
@keyframes celestial-orb-color {
|
|
0%, 100% {
|
|
background: radial-gradient(circle, #fbbf24, #f97316);
|
|
box-shadow: 0 0 60px rgba(251,191,36,0.4);
|
|
}
|
|
50% {
|
|
background: radial-gradient(circle, #e2e8f0, #93c5fd);
|
|
box-shadow: 0 0 60px rgba(147,197,253,0.4);
|
|
}
|
|
}
|
|
|
|
/* Orbit ring rotation */
|
|
@keyframes celestial-orbit-rotate {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Meteor 1 */
|
|
@keyframes celestial-meteor-1 {
|
|
0% { opacity: 0; transform: rotate(-45deg) translate(0, 0); }
|
|
10% { opacity: 1; }
|
|
70% { opacity: 1; }
|
|
100% { opacity: 0; transform: rotate(-45deg) translate(180px, 180px); }
|
|
}
|
|
|
|
/* Meteor 2 */
|
|
@keyframes celestial-meteor-2 {
|
|
0% { opacity: 0; transform: rotate(-40deg) translate(0, 0); }
|
|
10% { opacity: 1; }
|
|
70% { opacity: 1; }
|
|
100% { opacity: 0; transform: rotate(-40deg) translate(160px, 160px); }
|
|
}
|
|
|
|
/* Meteor 3 */
|
|
@keyframes celestial-meteor-3 {
|
|
0% { opacity: 0; transform: rotate(-50deg) translate(0, 0); }
|
|
10% { opacity: 1; }
|
|
70% { opacity: 1; }
|
|
100% { opacity: 0; transform: rotate(-50deg) translate(170px, 170px); }
|
|
}
|
|
|
|
/* Star twinkle */
|
|
@keyframes celestial-twinkle {
|
|
0%, 100% { opacity: 0.2; transform: scale(1); }
|
|
50% { opacity: 1; transform: scale(1.4); }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
|
|
{/* ========== Right Panel — 60% Login Form ========== */}
|
|
<div
|
|
className="flex items-center justify-center"
|
|
style={{ width: '60%', background: 'var(--dg-bg-primary)' }}
|
|
>
|
|
<form
|
|
onSubmit={handleLogin}
|
|
style={{ width: '100%', maxWidth: 400, display: 'flex', flexDirection: 'column' }}
|
|
>
|
|
<h2 style={{ fontSize: 30, fontWeight: 700, marginBottom: 8, color: 'var(--dg-text-primary)' }}>
|
|
欢迎回来
|
|
</h2>
|
|
<p style={{ color: 'var(--dg-text-muted)', fontSize: 15, marginBottom: 40 }}>
|
|
登录以继续
|
|
</p>
|
|
|
|
{error && (
|
|
<div style={{
|
|
color: '#f87171',
|
|
fontSize: 13,
|
|
marginBottom: 16,
|
|
padding: '10px 14px',
|
|
background: 'rgba(248,113,113,0.08)',
|
|
borderRadius: 10,
|
|
}}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Username */}
|
|
<div style={{ marginBottom: 24 }}>
|
|
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: 'var(--dg-text-secondary)', marginBottom: 8 }}>
|
|
用户名
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="请输入用户名"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
className="w-full"
|
|
style={{
|
|
padding: '13px 16px',
|
|
borderRadius: 10,
|
|
background: 'var(--dg-bg-secondary)',
|
|
border: 'none',
|
|
color: 'var(--dg-text-primary)',
|
|
fontSize: 14,
|
|
outline: 'none',
|
|
transition: 'background 0.3s',
|
|
}}
|
|
onFocus={(e) => { e.target.style.background = 'var(--dg-bg-tertiary)'; }}
|
|
onBlur={(e) => { e.target.style.background = 'var(--dg-bg-secondary)'; }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Password */}
|
|
<div style={{ marginBottom: 32 }}>
|
|
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, color: 'var(--dg-text-secondary)', marginBottom: 8 }}>
|
|
密码
|
|
</label>
|
|
<input
|
|
type="password"
|
|
placeholder="请输入密码"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="w-full"
|
|
style={{
|
|
padding: '13px 16px',
|
|
borderRadius: 10,
|
|
background: 'var(--dg-bg-secondary)',
|
|
border: 'none',
|
|
color: 'var(--dg-text-primary)',
|
|
fontSize: 14,
|
|
outline: 'none',
|
|
transition: 'background 0.3s',
|
|
}}
|
|
onFocus={(e) => { e.target.style.background = 'var(--dg-bg-tertiary)'; }}
|
|
onBlur={(e) => { e.target.style.background = 'var(--dg-bg-secondary)'; }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Submit */}
|
|
<button
|
|
type="submit"
|
|
disabled={loading || !username || !password}
|
|
className="dg-gradient-bg w-full"
|
|
style={{
|
|
padding: '13px 20px',
|
|
borderRadius: 10,
|
|
border: 'none',
|
|
color: '#fff',
|
|
fontWeight: 600,
|
|
fontSize: 15,
|
|
cursor: loading || !username || !password ? 'not-allowed' : 'pointer',
|
|
opacity: loading || !username || !password ? 0.5 : 1,
|
|
transition: 'opacity 0.2s, transform 0.1s',
|
|
}}
|
|
>
|
|
{loading ? '登录中…' : '登录'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|