Files
MailUI4Agents/client/electron/src/components/SetupPage.tsx

141 lines
5.9 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.

import { useEffect, useRef, useState } from 'react';
import { useAuthStore } from '../stores/authStore';
import * as api from '../api/client';
import { MailboxIcon, SpinnerIcon } from './icons';
/** 首次初始化向导:系统无任何用户时展示,创建首个管理员 */
export default function SetupPage({ onDone }: { onDone: () => void }) {
const bootstrap = useAuthStore(s => s.bootstrap);
const [username, setUsername] = useState('admin');
const [displayName, setDisplayName] = useState('');
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const nameRef = useRef<HTMLInputElement>(null);
useEffect(() => {
nameRef.current?.focus();
}, []);
const mismatch = confirm !== '' && password !== confirm;
const ok =
username.trim().length >= 2 &&
password.length >= 8 &&
!mismatch &&
!busy;
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!ok) return;
setBusy(true);
setError(null);
try {
await api.setupAdmin({
username: username.trim().toLowerCase(),
password,
display_name: displayName.trim()
});
// 初始化后直接登录(后端已经种了 cookie拉取用户态
await bootstrap();
onDone();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setBusy(false);
}
};
// 卡片高约 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-[420px] 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"></h1>
<p className="mt-1 text-xs text-gray-500 text-center">
使 AgentMail
</p>
</div>
<form onSubmit={submit} className="space-y-3">
<div>
<label className="block text-[11px] font-medium text-gray-500 mb-1">
<span className="text-red-400"> name </span>
</label>
<input
ref={nameRef}
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="admin"
spellCheck={false}
className="w-full text-sm font-mono border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400"
/>
<p className="mt-1 text-[10px] text-gray-400">
. _ -2-64 `admin@.new`
</p>
</div>
<div>
<label className="block text-[11px] font-medium text-gray-500 mb-1"></label>
<input
value={displayName}
onChange={e => setDisplayName(e.target.value)}
placeholder="系统管理员"
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">
<span className="text-red-400"> 8 </span>
</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
autoComplete="new-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>
<div>
<label className="block text-[11px] font-medium text-gray-500 mb-1"></label>
<input
type="password"
value={confirm}
onChange={e => setConfirm(e.target.value)}
autoComplete="new-password"
className={`w-full text-sm border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-100 ${
mismatch ? 'border-red-300' : 'border-gray-300 focus:border-blue-400'
}`}
/>
{mismatch && <p className="mt-1 text-[10px] text-red-500"></p>}
</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}
</p>
)}
<button
type="submit"
disabled={!ok}
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"
>
{busy && <SpinnerIcon className="w-3.5 h-3.5" />}
{busy ? '初始化中' : '创建管理员并进入'}
</button>
</form>
</div>
</div>
);
}