原因:登录时缺少密码可见性切换,长密码输入难以核对。 影响:新增可复用 PasswordInput,在登录表单提供眼睛按钮,并补充禁用态、焦点样式和无障碍标签。 风险:密码仅在用户主动点击时于当前输入框显示,不改变提交、存储或日志行为。 验证:前端 112 项测试通过;pnpm lint 通过;@easyai-ai-gateway/web 生产构建通过。
142 lines
5.3 KiB
TypeScript
142 lines
5.3 KiB
TypeScript
import type { FormEvent } from 'react';
|
|
import { LogIn, UserPlus } from 'lucide-react';
|
|
import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, PasswordInput, Tabs } from './ui';
|
|
import type { AuthMode, LoadState, LoginForm, RegisterForm } from '../types';
|
|
|
|
const tabs = [
|
|
{ value: 'login', label: '账号登录' },
|
|
{ value: 'register', label: '注册账号' },
|
|
{ value: 'external', label: '外部 Token' },
|
|
] satisfies Array<{ value: AuthMode; label: string }>;
|
|
|
|
export function AuthPanel(props: {
|
|
authMode: AuthMode;
|
|
externalToken: string;
|
|
loginForm: LoginForm;
|
|
registerForm: RegisterForm;
|
|
state: LoadState;
|
|
onAuthModeChange: (value: AuthMode) => void;
|
|
onExternalTokenChange: (value: string) => void;
|
|
onLoginChange: (value: LoginForm) => void;
|
|
onRegisterChange: (value: RegisterForm) => void;
|
|
onSubmitExternalToken: (event: FormEvent<HTMLFormElement>) => void;
|
|
onSubmitLogin: (event: FormEvent<HTMLFormElement>) => void;
|
|
onSubmitRegister: (event: FormEvent<HTMLFormElement>) => void;
|
|
oidcEnabled: boolean;
|
|
onOIDCLogin: () => void;
|
|
}) {
|
|
return (
|
|
<section className="authShell" aria-label="登录">
|
|
<Card className="authCard">
|
|
<CardHeader>
|
|
<div>
|
|
<p className="eyebrow">Gateway Identity</p>
|
|
<CardTitle>登录 AI Gateway</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="authContent">
|
|
{props.oidcEnabled && (
|
|
<Button type="button" disabled={props.state === 'loading'} onClick={props.onOIDCLogin}>
|
|
<LogIn size={15} />
|
|
使用统一认证中心登录
|
|
</Button>
|
|
)}
|
|
<Tabs value={props.authMode} tabs={tabs} onValueChange={props.onAuthModeChange} />
|
|
{props.authMode === 'login' && <LoginFormView {...props} />}
|
|
{props.authMode === 'register' && <RegisterFormView {...props} />}
|
|
{props.authMode === 'external' && <ExternalTokenForm {...props} />}
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function LoginFormView(props: {
|
|
loginForm: LoginForm;
|
|
state: LoadState;
|
|
onLoginChange: (value: LoginForm) => void;
|
|
onSubmitLogin: (event: FormEvent<HTMLFormElement>) => void;
|
|
}) {
|
|
return (
|
|
<form className="formGrid" onSubmit={props.onSubmitLogin}>
|
|
<Label>
|
|
账号
|
|
<Input
|
|
autoComplete="username"
|
|
value={props.loginForm.account}
|
|
onChange={(event) => props.onLoginChange({ ...props.loginForm, account: event.target.value })}
|
|
placeholder="用户名或邮箱"
|
|
/>
|
|
</Label>
|
|
<Label>
|
|
密码
|
|
<PasswordInput
|
|
autoComplete="current-password"
|
|
value={props.loginForm.password}
|
|
onChange={(event) => props.onLoginChange({ ...props.loginForm, password: event.target.value })}
|
|
placeholder="至少 8 位"
|
|
/>
|
|
</Label>
|
|
<Button type="submit" disabled={props.state === 'loading'}>
|
|
<LogIn size={15} />
|
|
{props.state === 'loading' ? '登录中' : '登录'}
|
|
</Button>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function RegisterFormView(props: {
|
|
registerForm: RegisterForm;
|
|
state: LoadState;
|
|
onRegisterChange: (value: RegisterForm) => void;
|
|
onSubmitRegister: (event: FormEvent<HTMLFormElement>) => void;
|
|
}) {
|
|
return (
|
|
<form className="formGrid two" onSubmit={props.onSubmitRegister}>
|
|
<Label>
|
|
用户名
|
|
<Input autoComplete="username" value={props.registerForm.username} onChange={(event) => props.onRegisterChange({ ...props.registerForm, username: event.target.value })} />
|
|
</Label>
|
|
<Label>
|
|
邮箱
|
|
<Input autoComplete="email" type="email" value={props.registerForm.email} onChange={(event) => props.onRegisterChange({ ...props.registerForm, email: event.target.value })} />
|
|
</Label>
|
|
<Label>
|
|
显示名
|
|
<Input value={props.registerForm.displayName} onChange={(event) => props.onRegisterChange({ ...props.registerForm, displayName: event.target.value })} />
|
|
</Label>
|
|
<Label>
|
|
密码
|
|
<Input autoComplete="new-password" type="password" value={props.registerForm.password} onChange={(event) => props.onRegisterChange({ ...props.registerForm, password: event.target.value })} />
|
|
</Label>
|
|
<Label>
|
|
邀请码
|
|
<Input value={props.registerForm.invitationCode} onChange={(event) => props.onRegisterChange({ ...props.registerForm, invitationCode: event.target.value })} placeholder="可选" />
|
|
</Label>
|
|
<Button type="submit" disabled={props.state === 'loading'} className="spanTwo">
|
|
<UserPlus size={15} />
|
|
{props.state === 'loading' ? '注册中' : '注册并登录'}
|
|
</Button>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function ExternalTokenForm(props: {
|
|
externalToken: string;
|
|
state: LoadState;
|
|
onExternalTokenChange: (value: string) => void;
|
|
onSubmitExternalToken: (event: FormEvent<HTMLFormElement>) => void;
|
|
}) {
|
|
return (
|
|
<form className="formGrid" onSubmit={props.onSubmitExternalToken}>
|
|
<Label>
|
|
Access Token
|
|
<Input value={props.externalToken} onChange={(event) => props.onExternalTokenChange(event.target.value)} placeholder="粘贴 server-main access token" />
|
|
</Label>
|
|
<Button type="submit" disabled={props.state === 'loading'}>
|
|
{props.state === 'loading' ? '验证中' : '进入控制台'}
|
|
</Button>
|
|
</form>
|
|
);
|
|
}
|