304 lines
10 KiB
TypeScript
304 lines
10 KiB
TypeScript
import React from 'react';
|
|
import { useLogin } from '@/hooks/useLogin';
|
|
|
|
const KEYFRAMES = `
|
|
@keyframes float-slow {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
25% { transform: translate(20px, -30px) scale(1.03); }
|
|
50% { transform: translate(-10px, -50px) scale(1.01); }
|
|
75% { transform: translate(15px, -20px) scale(1.04); }
|
|
}
|
|
|
|
@keyframes float-medium {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
25% { transform: translate(-25px, -40px) scale(1.02); }
|
|
50% { transform: translate(20px, -60px) scale(0.98); }
|
|
75% { transform: translate(-15px, -25px) scale(1.03); }
|
|
}
|
|
|
|
@keyframes float-fast {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
25% { transform: translate(15px, -20px) scale(1.01); }
|
|
50% { transform: translate(-20px, -35px) scale(1.02); }
|
|
75% { transform: translate(10px, -15px) scale(0.99); }
|
|
}
|
|
`;
|
|
|
|
// Floating shapes: [width, height, top, left, color, opacity, animation, duration]
|
|
const FLOATING_SHAPES: [number, number, string, string, string, number, string, string][] = [
|
|
[340, 280, '8%', '5%', 'linear-gradient(135deg, #e0f2fe 0%, #f3e8ff 100%)', 0.5, 'float-slow', '20s'],
|
|
[240, 200, '55%', '2%', 'linear-gradient(135deg, #f3e8ff 0%, #ede9fe 100%)', 0.4, 'float-medium', '25s'],
|
|
[180, 160, '70%', '15%', 'linear-gradient(135deg, #dbeafe 0%, #e0f2fe 100%)', 0.35, 'float-fast', '18s'],
|
|
[280, 240, '20%', '18%', 'linear-gradient(135deg, #ede9fe 0%, #e0f2fe 100%)', 0.3, 'float-slow', '22s'],
|
|
];
|
|
|
|
export default function NeonCityView(): JSX.Element {
|
|
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
|
|
|
const isDisabled = !username || !password || loading;
|
|
|
|
return (
|
|
<>
|
|
<style>{KEYFRAMES}</style>
|
|
<div
|
|
style={{
|
|
minHeight: '100vh',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'linear-gradient(180deg, #fafbfc 0%, #f0f2f5 100%)',
|
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
}}
|
|
>
|
|
{/* Floating soft geometric shapes */}
|
|
{FLOATING_SHAPES.map(([w, h, top, left, bg, opacity, anim, dur], i) => (
|
|
<div
|
|
key={`shape-${i}`}
|
|
style={{
|
|
position: 'absolute',
|
|
top,
|
|
left,
|
|
width: w,
|
|
height: h,
|
|
background: bg,
|
|
opacity,
|
|
borderRadius: '50%',
|
|
filter: 'blur(40px)',
|
|
animation: `${anim} ${dur} ease-in-out infinite`,
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{/* Login card */}
|
|
<div
|
|
style={{
|
|
position: 'relative',
|
|
zIndex: 10,
|
|
width: '100%',
|
|
maxWidth: 400,
|
|
margin: '0 20px',
|
|
background: 'rgba(255, 255, 255, 0.72)',
|
|
backdropFilter: 'blur(24px)',
|
|
WebkitBackdropFilter: 'blur(24px)',
|
|
borderRadius: 20,
|
|
padding: '48px 40px',
|
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.06)',
|
|
}}
|
|
>
|
|
{/* Brand area */}
|
|
<div style={{ textAlign: 'center', marginBottom: 36 }}>
|
|
{/* Logo */}
|
|
<div
|
|
style={{
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 14,
|
|
background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: 20,
|
|
boxShadow: '0 4px 16px rgba(59, 130, 246, 0.25)',
|
|
}}
|
|
>
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L20 6V11C20 16 16 19.5 12 22C8 19.5 4 16 4 11V6L12 2Z" /><path d="M9 12L11 14L15 10" /></svg>
|
|
</div>
|
|
|
|
{/* Title */}
|
|
<h1
|
|
style={{
|
|
fontSize: 26,
|
|
fontWeight: 700,
|
|
color: '#1a1a2e',
|
|
margin: '0 0 8px',
|
|
letterSpacing: '0.3px',
|
|
}}
|
|
>
|
|
OpsxSQL
|
|
</h1>
|
|
|
|
{/* Subtitle */}
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
color: '#64748b',
|
|
margin: 0,
|
|
letterSpacing: '0.2px',
|
|
}}
|
|
>
|
|
企业级数据库审核与合规管理
|
|
</p>
|
|
</div>
|
|
|
|
{/* Login form */}
|
|
<form onSubmit={handleLogin}>
|
|
{/* Username input */}
|
|
<div style={{ marginBottom: 20 }}>
|
|
<label
|
|
htmlFor="username"
|
|
style={{
|
|
display: 'block',
|
|
fontSize: 13,
|
|
fontWeight: 500,
|
|
color: '#64748b',
|
|
marginBottom: 8,
|
|
letterSpacing: '0.2px',
|
|
}}
|
|
>
|
|
用户名
|
|
</label>
|
|
<input
|
|
id="username"
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder="请输入用户名"
|
|
autoComplete="username"
|
|
style={{
|
|
width: '100%',
|
|
height: 46,
|
|
padding: '0 16px',
|
|
fontSize: 14,
|
|
color: '#1a1a2e',
|
|
background: '#f5f5f5',
|
|
border: '1.5px solid transparent',
|
|
borderRadius: 10,
|
|
outline: 'none',
|
|
boxSizing: 'border-box',
|
|
transition: 'border-color 0.25s ease, background 0.25s ease',
|
|
}}
|
|
onFocus={(e) => {
|
|
e.currentTarget.style.borderColor = '#93c5fd';
|
|
e.currentTarget.style.background = '#fff';
|
|
}}
|
|
onBlur={(e) => {
|
|
e.currentTarget.style.borderColor = 'transparent';
|
|
e.currentTarget.style.background = '#f5f5f5';
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Password input */}
|
|
<div style={{ marginBottom: 28 }}>
|
|
<label
|
|
htmlFor="password"
|
|
style={{
|
|
display: 'block',
|
|
fontSize: 13,
|
|
fontWeight: 500,
|
|
color: '#64748b',
|
|
marginBottom: 8,
|
|
letterSpacing: '0.2px',
|
|
}}
|
|
>
|
|
密码
|
|
</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="请输入密码"
|
|
autoComplete="current-password"
|
|
style={{
|
|
width: '100%',
|
|
height: 46,
|
|
padding: '0 16px',
|
|
fontSize: 14,
|
|
color: '#1a1a2e',
|
|
background: '#f5f5f5',
|
|
border: '1.5px solid transparent',
|
|
borderRadius: 10,
|
|
outline: 'none',
|
|
boxSizing: 'border-box',
|
|
transition: 'border-color 0.25s ease, background 0.25s ease',
|
|
}}
|
|
onFocus={(e) => {
|
|
e.currentTarget.style.borderColor = '#93c5fd';
|
|
e.currentTarget.style.background = '#fff';
|
|
}}
|
|
onBlur={(e) => {
|
|
e.currentTarget.style.borderColor = 'transparent';
|
|
e.currentTarget.style.background = '#f5f5f5';
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Error message */}
|
|
{error && (
|
|
<div
|
|
style={{
|
|
marginBottom: 16,
|
|
padding: '10px 14px',
|
|
borderRadius: 10,
|
|
background: '#fef2f2',
|
|
color: '#dc2626',
|
|
fontSize: 13,
|
|
}}
|
|
>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Login button */}
|
|
<button
|
|
type="submit"
|
|
disabled={isDisabled}
|
|
style={{
|
|
width: '100%',
|
|
height: 48,
|
|
fontSize: 15,
|
|
fontWeight: 600,
|
|
color: '#fff',
|
|
background: isDisabled
|
|
? '#cbd5e1'
|
|
: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
|
|
border: 'none',
|
|
borderRadius: 10,
|
|
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
transition: 'all 0.25s ease',
|
|
letterSpacing: '0.5px',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (!isDisabled) {
|
|
e.currentTarget.style.transform = 'scale(1.02)';
|
|
e.currentTarget.style.boxShadow = '0 6px 20px rgba(59, 130, 246, 0.3)';
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.transform = 'scale(1)';
|
|
e.currentTarget.style.boxShadow = 'none';
|
|
}}
|
|
>
|
|
{loading ? (
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" /><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite" /></svg>
|
|
登录中...
|
|
</span>
|
|
) : (
|
|
'登 录'
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Copyright */}
|
|
<p
|
|
style={{
|
|
textAlign: 'center',
|
|
fontSize: 12,
|
|
color: '#94a3b8',
|
|
marginTop: 32,
|
|
marginBottom: 0,
|
|
}}
|
|
>
|
|
© OpsxSQL · 安全合规的数据库管理
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|