Archived
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
/**
|
|
* 登录页(ADR-010)—— 两份原型都没有这一屏,是新造的。
|
|
*
|
|
* 它在两个台面**之前**渲染,拿不到任一台面的皮肤(工作台补充样式通篇作用域 `.proto-root`,
|
|
* 治理台压根不加载它),所以视觉全部由 `login.css` 自带 —— 类名一律 `jc-login-*`,
|
|
* 不蹭原型的类,免得在另一台面上落空。
|
|
*/
|
|
import { useState } from 'react';
|
|
import type { Session } from '@contracts';
|
|
import { loginRoute } from '@contracts';
|
|
import { Ico } from '@web-shared/Ico.tsx';
|
|
import { ApiCallError, call } from '@web-shared/typed-fetch.ts';
|
|
|
|
export function LoginPage({ onLogin }: { onLogin(session: Session): void }) {
|
|
const [account, setAccount] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState<string>();
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
const ready = account.trim() !== '' && password !== '';
|
|
|
|
const submit = async () => {
|
|
if (!ready || busy) return;
|
|
setBusy(true);
|
|
setError(undefined);
|
|
try {
|
|
onLogin(await call<Session>(loginRoute, { body: { account: account.trim(), password } }));
|
|
} catch (e) {
|
|
// 后端只回一句话(不区分账号不存在 / 已停用 / 口令错),这里照实呈现,不替它猜
|
|
setError(e instanceof ApiCallError ? e.message : '登录失败,请稍后再试');
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="jc-login">
|
|
<form
|
|
className="jc-login-card"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
void submit();
|
|
}}
|
|
>
|
|
<span className="jc-login-mark">
|
|
<Ico name="bot" s={24} />
|
|
</span>
|
|
<h1 className="jc-login-title">JCAI 任务操作系统</h1>
|
|
<p className="jc-login-sub">登录后进入你的工位</p>
|
|
|
|
<label className="jc-login-field">
|
|
<span>账号</span>
|
|
<input
|
|
value={account}
|
|
autoFocus
|
|
autoComplete="username"
|
|
placeholder="如 fengbing"
|
|
onChange={(e) => setAccount(e.target.value)}
|
|
/>
|
|
</label>
|
|
<label className="jc-login-field">
|
|
<span>口令</span>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
autoComplete="current-password"
|
|
placeholder="口令"
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
/>
|
|
</label>
|
|
|
|
{error !== undefined && (
|
|
<div className="jc-login-err" role="alert">
|
|
<Ico name="alert-circle" s={15} />
|
|
<p>{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
<button className="jc-login-go" type="submit" disabled={busy || !ready}>
|
|
{busy ? '登录中…' : '登录'}
|
|
</button>
|
|
<p className="jc-login-hint">初始口令由部署时的 JCAI_SEED_PASSWORD 决定</p>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|