Files
AI_Translator/apps/web/app/admin/settings/page.tsx
2025-12-26 16:03:12 +08:00

227 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}