- Login: 400px card with 24px radius, white 28px logo, password eye toggle, 42px inputs, 48px honey button, placeholder var(--text-muted) - Sidebar: NEW badges golden, count badges red, hover bg-hover, active honey-glow - Header: search with search icon, status dots, inline hover dropdown - Dashboard: 4-column stat grid, horizontal stacked health bar (8px height), 48-bar gateway trend (no dupes), tables with height 44px and padding 10px 14px - Gateway: "Gateway 健康监控" title, stat summary, honey filter buttons, honey-outline action buttons, search + type filter, last-update timestamp All colors from var(--*), spacing in 4px multiples, animations ≤300ms, table rows 44px, shadows only 3 layers. Co-Authored-By: Claude <noreply@anthropic.com>
212 lines
6.0 KiB
TypeScript
212 lines
6.0 KiB
TypeScript
import { type FormEvent, useState } from 'react'
|
|
import { useNavigate, Link } from 'react-router-dom'
|
|
import { Eye, EyeOff } from 'lucide-react'
|
|
import { useAuth } from '../hooks/useAuth'
|
|
import toast from '../components/ui/Toast'
|
|
|
|
export default function LoginPage() {
|
|
const { login } = useAuth()
|
|
const navigate = useNavigate()
|
|
const [username, setUsername] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [showPwd, setShowPwd] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
setLoading(true)
|
|
try {
|
|
await login(username, password)
|
|
navigate('/', { replace: true })
|
|
} catch (err: any) {
|
|
const status = err?.response?.status
|
|
const detail = err?.response?.data?.detail ?? '登录失败'
|
|
if (status === 423) {
|
|
setError('账户已锁定,请 15 分钟后再试')
|
|
} else if (status === 401) {
|
|
setError('用户名或密码错误')
|
|
} else {
|
|
setError(detail)
|
|
}
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'var(--bg-root)',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 400,
|
|
padding: 32,
|
|
borderRadius: 24,
|
|
background: 'var(--bg-card)',
|
|
boxShadow: 'var(--shadow-lg)',
|
|
}}
|
|
>
|
|
{/* Logo */}
|
|
<div style={{ textAlign: 'center', marginBottom: 4 }}>
|
|
<div style={{ fontSize: 28, fontWeight: 700, color: '#fff' }}>
|
|
🐝 知微蜂巢
|
|
</div>
|
|
<div style={{ fontSize: 14, color: 'var(--text-dim)', marginTop: 4 }}>
|
|
Hive Manage v3
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div
|
|
style={{
|
|
marginTop: 16,
|
|
padding: '8px 12px',
|
|
borderRadius: 'var(--radius)',
|
|
background: 'var(--bg-error)',
|
|
color: 'var(--red)',
|
|
fontSize: 13,
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Form */}
|
|
<form onSubmit={handleSubmit} style={{ marginTop: 24, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<div>
|
|
<label
|
|
style={{
|
|
display: 'block',
|
|
fontSize: 12,
|
|
fontWeight: 600,
|
|
color: '#fff',
|
|
marginBottom: 6,
|
|
}}
|
|
>
|
|
用户名
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder="请输入用户名"
|
|
required
|
|
autoFocus
|
|
style={{
|
|
width: '100%',
|
|
height: 42,
|
|
padding: '0 12px',
|
|
borderRadius: 8,
|
|
border: '1px solid var(--border)',
|
|
background: 'var(--bg-root)',
|
|
color: 'var(--text)',
|
|
fontSize: 13,
|
|
outline: 'none',
|
|
fontFamily: 'inherit',
|
|
}}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
style={{
|
|
display: 'block',
|
|
fontSize: 12,
|
|
fontWeight: 600,
|
|
color: '#fff',
|
|
marginBottom: 6,
|
|
}}
|
|
>
|
|
密码
|
|
</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<input
|
|
type={showPwd ? 'text' : 'password'}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="请输入密码"
|
|
required
|
|
style={{
|
|
width: '100%',
|
|
height: 42,
|
|
padding: '0 36px 0 12px',
|
|
borderRadius: 8,
|
|
border: '1px solid var(--border)',
|
|
background: 'var(--bg-root)',
|
|
color: 'var(--text)',
|
|
fontSize: 13,
|
|
outline: 'none',
|
|
fontFamily: 'inherit',
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPwd(!showPwd)}
|
|
style={{
|
|
position: 'absolute',
|
|
right: 10,
|
|
top: '50%',
|
|
transform: 'translateY(-50%)',
|
|
background: 'transparent',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
color: 'var(--text-muted)',
|
|
padding: 4,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
}}
|
|
tabIndex={-1}
|
|
>
|
|
{showPwd ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
style={{
|
|
width: '100%',
|
|
height: 48,
|
|
borderRadius: 8,
|
|
border: 'none',
|
|
background: 'var(--honey)',
|
|
color: '#000',
|
|
fontSize: 14,
|
|
fontWeight: 700,
|
|
cursor: loading ? 'not-allowed' : 'pointer',
|
|
opacity: loading ? 0.6 : 1,
|
|
fontFamily: 'inherit',
|
|
}}
|
|
>
|
|
{loading ? '登录中...' : '登入'}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Register link */}
|
|
<p
|
|
style={{
|
|
textAlign: 'center',
|
|
marginTop: 20,
|
|
fontSize: 13,
|
|
color: 'var(--text-dim)',
|
|
}}
|
|
>
|
|
还没有账号?{' '}
|
|
<Link to="/register" style={{ color: 'var(--honey)', fontWeight: 600 }}>
|
|
注册
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|