166 lines
6.2 KiB
TypeScript
166 lines
6.2 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { motion } from 'framer-motion'
|
|
import { Shield, Lock, ArrowRight, AlertCircle, Loader2 } from 'lucide-react'
|
|
import { api } from '@/lib/api'
|
|
import { Logo } from '@/components/Logo'
|
|
|
|
const BRAND = '#8B5CF6'
|
|
|
|
export function Verify() {
|
|
const navigate = useNavigate()
|
|
const [uuid, setUuid] = useState('')
|
|
const [error, setError] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const { data: status, isLoading: statusLoading } = useQuery({
|
|
queryKey: ['access-auth-status'],
|
|
queryFn: () => api.accessAuthStatus(),
|
|
})
|
|
|
|
// 已验证 或 未启用门控,直接进首页
|
|
useEffect(() => {
|
|
if (status && (!status.enabled || status.verified)) {
|
|
navigate('/', { replace: true })
|
|
}
|
|
}, [status, navigate])
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
const value = uuid.trim()
|
|
if (!value) {
|
|
setError('请输入访问密钥')
|
|
return
|
|
}
|
|
setLoading(true)
|
|
setError('')
|
|
try {
|
|
const res = await api.verifyAccessUuid(value)
|
|
if (res.valid && res.token) {
|
|
localStorage.setItem('access_token', res.token)
|
|
localStorage.setItem('access_role', res.role || 'user')
|
|
if (res.role === 'admin') {
|
|
navigate('/admin/uuids', { replace: true })
|
|
} else {
|
|
navigate('/', { replace: true })
|
|
}
|
|
} else {
|
|
setError('访问密钥无效,请检查后重试')
|
|
}
|
|
} catch (err: any) {
|
|
setError(err?.message || '验证失败,请稍后重试')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="relative min-h-screen bg-base overflow-hidden flex flex-col">
|
|
{/* 背景光晕 */}
|
|
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
|
<div
|
|
className="absolute -top-40 -left-40 h-[28rem] w-[28rem] rounded-full blur-[120px] opacity-20"
|
|
style={{ background: `radial-gradient(circle, ${BRAND}, transparent 70%)` }}
|
|
/>
|
|
<div
|
|
className="absolute -bottom-40 -right-32 h-[26rem] w-[26rem] rounded-full blur-[120px] opacity-15"
|
|
style={{ background: 'radial-gradient(circle, hsl(var(--accent)), transparent 70%)' }}
|
|
/>
|
|
<div
|
|
className="absolute inset-0 opacity-[0.025]"
|
|
style={{
|
|
backgroundImage:
|
|
'linear-gradient(hsl(var(--fg-primary)) 1px, transparent 1px), linear-gradient(90deg, hsl(var(--fg-primary)) 1px, transparent 1px)',
|
|
backgroundSize: '40px 40px',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* 顶栏 */}
|
|
<header className="relative z-10 flex items-center justify-between px-6 py-4 border-b border-border">
|
|
<div className="flex items-center gap-2.5 text-foreground">
|
|
<Logo
|
|
size={24}
|
|
className="shrink-0"
|
|
style={{ color: BRAND, filter: `drop-shadow(0 0 8px ${BRAND}55)` }}
|
|
/>
|
|
<span className="text-sm font-semibold tracking-tight">Stock Panel</span>
|
|
</div>
|
|
</header>
|
|
|
|
{/* 验证卡片 */}
|
|
<main className="relative z-10 flex-1 flex items-center justify-center px-6 py-10">
|
|
{statusLoading ? (
|
|
<div className="flex items-center justify-center">
|
|
<Loader2 className="h-8 w-8 animate-spin text-accent" />
|
|
</div>
|
|
) : (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 16 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
|
|
className="w-full max-w-md"
|
|
>
|
|
<div className="rounded-card border border-border bg-surface/80 backdrop-blur-sm p-6">
|
|
<div className="flex flex-col items-center text-center">
|
|
<div
|
|
className="rounded-2xl p-4 border border-border"
|
|
style={{ background: `linear-gradient(135deg, ${BRAND}22, transparent)` }}
|
|
>
|
|
<Shield className="h-8 w-8" style={{ color: BRAND }} />
|
|
</div>
|
|
<h1 className="mt-5 text-2xl font-bold text-foreground tracking-tight">
|
|
请输入访问密钥
|
|
</h1>
|
|
<p className="mt-2 text-sm text-secondary leading-relaxed">
|
|
当前环境已启用访问门控,请输入管理员令牌或访问 UUID 后继续。
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="mt-6 space-y-4">
|
|
<div className="relative">
|
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted">
|
|
<Lock className="h-4 w-4" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
autoComplete="off"
|
|
placeholder="输入管理员令牌或访问 UUID"
|
|
value={uuid}
|
|
onChange={(e) => {
|
|
setUuid(e.target.value)
|
|
if (error) setError('')
|
|
}}
|
|
className="w-full pl-9 pr-3 py-2.5 rounded-input bg-base border border-border text-sm font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="flex items-start gap-2 rounded-btn border border-danger/30 bg-danger/10 px-3 py-2.5 text-xs text-danger">
|
|
<AlertCircle className="h-3.5 w-3.5 mt-px shrink-0" />
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full inline-flex items-center justify-center gap-2 px-5 h-11 rounded-xl bg-accent text-white text-sm font-semibold shadow-lg shadow-accent/20 hover:bg-accent/90 hover:shadow-accent/30 disabled:opacity-60 transition-all"
|
|
>
|
|
{loading ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<ArrowRight className="h-4 w-4" />
|
|
)}
|
|
{loading ? '验证中…' : '进入系统'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|