'use client'; // Login.tsx — 登录(凭证 → 可选 TOTP 二段式)。承袭 ucapp.jsx UCLogin 视觉。 import React, { useEffect, useState } from 'react'; import { Icon, Mark } from './icons'; import { card, input, LangSeg } from './shared'; import { useUI } from '../lib/theme'; import { makeT } from '../lib/i18n'; import { getClient } from '../lib/api/client'; import { ApiError } from '../lib/api/types'; import { bilingual } from '../lib/api/errors'; export default function Login({ onDone }: { onDone: () => void }) { const { lang, setLang } = useUI(); const t = makeT(lang); const api = getClient(); const [email, setEmail] = useState(''); const [pw, setPw] = useState(''); const [step, setStep] = useState<'cred' | 'totp'>('cred'); const [otp, setOtp] = useState(''); const [pending, setPending] = useState(''); const [busy, setBusy] = useState(false); const [err, setErr] = useState(''); const [lockLeft, setLockLeft] = useState(0); const ok = /\S+@\S+\.\S+/.test(email) && pw.length > 0; // 锁定倒计时(限流在服务端,前端只做失败提示与倒计时) useEffect(() => { if (lockLeft <= 0) return; const id = setInterval(() => setLockLeft((s) => Math.max(0, s - 1)), 1000); return () => clearInterval(id); }, [lockLeft]); async function submitCred() { if (!ok || busy || lockLeft > 0) return; setBusy(true); setErr(''); try { const r = await api.login(email.trim(), pw); if (r.kind === 'totp_required') { setPending(r.pendingToken); setStep('totp'); } else { onDone(); } } catch (e) { setErr(bilingual(e, lang)); if (e instanceof ApiError && e.code === 'account_locked' && e.retryAfter) setLockLeft(e.retryAfter); } finally { setBusy(false); } } async function submitOtp() { if (otp.length !== 6 || busy) return; setBusy(true); setErr(''); try { await api.loginTotp(pending, otp); onDone(); } catch (e) { setErr(bilingual(e, lang)); } finally { setBusy(false); } } const credDisabled = !ok || busy || lockLeft > 0; return (