chore: directory migration - gateway→server, web→client/electron

This commit is contained in:
2026-09-08 19:16:35 +08:00
parent fd9f99a3f9
commit f9d757b5e5
243 changed files with 5095 additions and 228 deletions

View File

@ -0,0 +1,114 @@
import { useEffect, useRef, useState } from 'react';
import { useAuthStore } from '../stores/authStore';
import { MailboxIcon, SpinnerIcon } from './icons';
export default function LoginPage() {
const login = useAuthStore(s => s.login);
const error = useAuthStore(s => s.error);
const retryAfter = useAuthStore(s => s.retryAfter);
const submitting = useAuthStore(s => s.submitting);
const clearError = useAuthStore(s => s.clearError);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [countdown, setCountdown] = useState(0);
const userRef = useRef<HTMLInputElement>(null);
useEffect(() => {
userRef.current?.focus();
}, []);
// 被限速时倒计时
useEffect(() => {
if (!retryAfter) return;
setCountdown(retryAfter);
const t = setInterval(() => {
setCountdown(c => {
if (c <= 1) {
clearInterval(t);
clearError();
return 0;
}
return c - 1;
});
}, 1000);
return () => clearInterval(t);
}, [retryAfter]);
const locked = countdown > 0;
const canSubmit = username.trim() !== '' && password !== '' && !submitting && !locked;
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!canSubmit) return;
const ok = await login(username, password);
if (!ok) setPassword('');
};
// 卡片高约 371px比横屏手机或软键盘弹出后的可视高度还高。
//
// 居中用卡片自己的 `my-auto` 而**不是**容器的 `items-center`:后者在内容超高时
// 会让卡片上下同时溢出而溢出到顶部那段滚不到scrollTop 最小是 0——
// 实测 568x280 下「登录」按钮完全在视口外,光加 overflow-y-auto 也够不着。
// auto margin 在空间不足时自动退化为 0于是矮屏变成正常的顶对齐可滚布局。
return (
<div className="h-full overflow-y-auto flex justify-center bg-slate-100">
<div className="w-[380px] max-w-[92vw] shrink-0 my-auto bg-white rounded-xl shadow-sm border border-gray-200 p-6 sm:p-8">
<div className="flex flex-col items-center mb-6">
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center">
<MailboxIcon className="w-6 h-6" />
</div>
<h1 className="mt-3 text-base font-semibold text-gray-900">AgentMail</h1>
<p className="mt-1 text-xs text-gray-500"></p>
</div>
<form onSubmit={submit} className="space-y-3">
<div>
<label className="block text-[11px] font-medium text-gray-500 mb-1"></label>
<input
ref={userRef}
value={username}
onChange={e => {
setUsername(e.target.value);
if (error) clearError();
}}
autoComplete="username"
spellCheck={false}
className="w-full text-sm border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400"
/>
</div>
<div>
<label className="block text-[11px] font-medium text-gray-500 mb-1"></label>
<input
type="password"
value={password}
onChange={e => {
setPassword(e.target.value);
if (error) clearError();
}}
autoComplete="current-password"
className="w-full text-sm border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400"
/>
</div>
{error && (
<p className="text-xs text-red-600 bg-red-50 border border-red-100 rounded-md px-2.5 py-1.5">
{error}
{locked && `${countdown} 秒后可重试)`}
</p>
)}
<button
type="submit"
disabled={!canSubmit}
className="w-full inline-flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
{submitting && <SpinnerIcon className="w-3.5 h-3.5" />}
{submitting ? '登录中' : locked ? `已锁定 ${countdown}s` : '登录'}
</button>
</form>
</div>
</div>
);
}