chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -2,7 +2,7 @@
import { useState, type FormEvent } from 'react'
import { useRouter } from 'next/navigation'
import { Lock, User, Loader2, Eye, EyeOff } from 'lucide-react'
import { Lock, User, Loader2, Eye, EyeOff, ShieldCheck } from 'lucide-react'
import { api } from '@/lib/api-client'
import { login } from '@/lib/auth'
import { ApiRequestError } from '@/lib/api-client'
@@ -11,7 +11,9 @@ export default function LoginPage() {
const router = useRouter()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [totpCode, setTotpCode] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [needTotp, setNeedTotp] = useState(false)
const [remember, setRemember] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
@@ -31,12 +33,23 @@ export default function LoginPage() {
setLoading(true)
try {
const res = await api.auth.login({ username: username.trim(), password })
const res = await api.auth.login({
username: username.trim(),
password,
totp_code: totpCode.trim() || undefined,
})
login(res.token, res.account)
router.replace('/')
} catch (err) {
if (err instanceof ApiRequestError) {
setError(err.body.message || '登录失败,请检查用户名和密码')
const msg = err.body.message || ''
// 后端返回 "需要 TOTP" 时显示 TOTP 输入框
if (msg.includes('TOTP') || msg.includes('totp') || msg.includes('2FA') || msg.includes('验证码') || err.status === 403) {
setNeedTotp(true)
setError(msg || '请输入两步验证码')
} else {
setError(msg || '登录失败,请检查用户名和密码')
}
} else {
setError('网络错误,请稍后重试')
}
@@ -152,6 +165,35 @@ export default function LoginPage() {
</div>
</div>
{/* TOTP 验证码 */}
{needTotp && (
<div className="space-y-2">
<label
htmlFor="totp"
className="text-sm font-medium text-foreground"
>
</label>
<div className="relative">
<ShieldCheck className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
id="totp"
type="text"
placeholder="请输入 6 位验证码"
value={totpCode}
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
maxLength={6}
className="flex h-10 w-full rounded-md border border-input bg-transparent pl-10 pr-3 py-2 text-sm shadow-sm transition-colors duration-200 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring tracking-widest"
autoComplete="one-time-code"
inputMode="numeric"
/>
</div>
<p className="text-xs text-muted-foreground">
使 App Google Authenticator
</p>
</div>
)}
{/* 记住我 */}
<div className="flex items-center gap-2">
<input