feat:重构UI

This commit is contained in:
2025-12-26 16:03:12 +08:00
parent 1429e0e66a
commit abcbe3cddc
67 changed files with 12170 additions and 515 deletions

View File

@@ -0,0 +1,226 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { KeyRound, Shield, Terminal } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { AdminUnauthorizedError, API_BASE, adminFetchJson, clearAdminToken } from '@/lib/admin-api'
export default function SettingsPage() {
const router = useRouter()
const [username, setUsername] = useState<string>('')
const [tokenExp, setTokenExp] = useState<number | null>(null)
const [healthOk, setHealthOk] = useState<boolean | null>(null)
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const fetchMe = async () => {
setError('')
try {
const data = await adminFetchJson<{ username?: string; exp?: number }>('/api/v1/admin/me')
setUsername(data.username || '')
setTokenExp(typeof data.exp === 'number' ? data.exp : null)
} catch (err: any) {
if (err instanceof AdminUnauthorizedError) {
router.replace('/login')
return
}
setError(err?.message || '加载失败')
}
}
const fetchHealth = async () => {
try {
const res = await fetch(`${API_BASE}/health`)
setHealthOk(res.ok)
} catch {
setHealthOk(false)
}
}
useEffect(() => {
fetchMe()
fetchHealth()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setSuccess('')
if (!oldPassword.trim() || !newPassword.trim()) {
setError('请输入原密码和新密码')
return
}
if (newPassword.trim().length < 8) {
setError('新密码至少 8 位')
return
}
if (newPassword !== confirmPassword) {
setError('两次输入的新密码不一致')
return
}
setIsSaving(true)
try {
await adminFetchJson('/api/v1/admin/change-password', {
method: 'POST',
body: JSON.stringify({
old_password: oldPassword,
new_password: newPassword,
}),
})
setSuccess('密码已更新,请重新登录')
clearAdminToken()
router.replace('/login')
} catch (err: any) {
if (err instanceof AdminUnauthorizedError) {
router.replace('/login')
return
}
setError(err?.message || '更新失败')
} finally {
setIsSaving(false)
}
}
const expText = tokenExp ? new Date(tokenExp * 1000).toLocaleString() : '未知'
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
{(error || success) && (
<div
className={
success
? 'rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300'
: 'rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive'
}
>
{success || error}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="border-muted bg-background/50 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-md border bg-background p-4 space-y-2">
<div className="text-sm text-muted-foreground"></div>
<div className="flex items-center gap-2">
<div className="text-lg font-semibold">{username || '—'}</div>
{username && <Badge variant="secondary"></Badge>}
</div>
<div className="text-xs text-muted-foreground">Token {expText}</div>
</div>
<form onSubmit={handleChangePassword} className="space-y-4">
<div className="space-y-1.5">
<Label></Label>
<Input
type="password"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
autoComplete="current-password"
/>
</div>
<div className="space-y-1.5">
<Label></Label>
<Input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
autoComplete="new-password"
/>
<div className="text-xs text-muted-foreground"> 8 </div>
</div>
<div className="space-y-1.5">
<Label></Label>
<Input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
autoComplete="new-password"
/>
</div>
<Button type="submit" disabled={isSaving} className="w-full">
<KeyRound className="mr-2 h-4 w-4" />
{isSaving ? '更新中...' : '更新密码'}
</Button>
</form>
</CardContent>
</Card>
<Card className="border-muted bg-background/50 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" />
</CardTitle>
<CardDescription> API Base </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-md border bg-background p-4 space-y-2">
<div className="text-sm text-muted-foreground">API Base URL</div>
<div className="font-mono text-xs break-all">{API_BASE}</div>
</div>
<div className="rounded-md border bg-background p-4 space-y-2">
<div className="text-sm text-muted-foreground">API </div>
<div className="flex items-center gap-2">
{healthOk === null ? (
<Badge variant="outline"></Badge>
) : healthOk ? (
<Badge variant="success">OK</Badge>
) : (
<Badge variant="destructive"></Badge>
)}
<Button variant="outline" size="sm" onClick={fetchHealth}>
</Button>
</div>
</div>
<div className="text-xs text-muted-foreground">
访/Redis
</div>
<Button
variant="destructive"
onClick={() => {
clearAdminToken()
router.replace('/login')
}}
>
退
</Button>
</CardContent>
</Card>
</div>
</div>
)
}