Files
RiskAgent/web/src/pages/Login.tsx
T
freedakgmail 6562208b13 多用户与角色:新增系统管理员 + 用户管理
- 新增 users 表(scrypt 口令哈希)与持久化层,启动兜底种子账号
- 登录改为后端用户表校验账号密码;JWT 带角色;保留无DB演示回退
- 新增系统管理员角色 + 用户管理页(增删改/改角色/启停/重置密码)
- 用户管理端点按 系统管理员 角色强制校验(RBAC)
- 各角色可建任意多个账号(多销售/多风控/多管理)
- 更新登录页快速登录与首屏快照
2026-06-13 17:35:52 +08:00

190 lines
6.3 KiB
TypeScript

/**
* 登录页面 — 3 个测试角色账号。
*/
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
colorVar,
FONT_FAMILY,
RADIUS,
SHADOW,
space,
typographyStyle,
} from '../design-system/components/styles.js';
import { useAuthStore, TEST_ACCOUNTS } from '../stores/authStore.js';
export function Login(): JSX.Element {
const navigate = useNavigate();
const { login, error, clearError } = useAuthStore();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
clearError();
const ok = await login(username, password);
if (ok) {
navigate('/');
}
};
const pageStyle: React.CSSProperties = {
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: `${space(5)}px`,
padding: `${space(6)}px ${space(4)}px`,
backgroundColor: colorVar('color.bg.canvas'),
fontFamily: FONT_FAMILY,
};
const cardStyle: React.CSSProperties = {
fontFamily: FONT_FAMILY,
width: '100%',
maxWidth: 400,
padding: `${space(7)}px ${space(6)}px`,
backgroundColor: colorVar('color.bg.elevated'),
borderRadius: `${RADIUS.lg}px`,
border: `1px solid ${colorVar('color.border.default')}`,
boxShadow: SHADOW.md,
};
const inputStyle: React.CSSProperties = {
width: '100%',
padding: `${space(2)}px ${space(3)}px`,
border: `1px solid ${colorVar('color.border.default')}`,
borderRadius: `${RADIUS.md}px`,
fontFamily: FONT_FAMILY,
...typographyStyle('body'),
backgroundColor: colorVar('color.bg.canvas'),
color: colorVar('color.text.primary'),
boxSizing: 'border-box',
};
const buttonStyle: React.CSSProperties = {
width: '100%',
padding: `${space(3)}px`,
backgroundColor: colorVar('color.brand.primary'),
color: colorVar('color.text.onAccent'),
border: 'none',
borderRadius: `${RADIUS.md}px`,
cursor: 'pointer',
...typographyStyle('body'),
fontWeight: 600,
letterSpacing: '-0.01em',
};
return (
<div style={pageStyle}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: `${space(3)}px` }}>
<div
aria-hidden="true"
style={{
width: 52,
height: 52,
borderRadius: `${RADIUS.lg}px`,
background: `linear-gradient(135deg, ${colorVar('color.brand.primary')}, #7C83F0)`,
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
fontSize: '24px',
boxShadow: SHADOW.sm,
}}
>
</div>
<div style={{ textAlign: 'center', display: 'flex', flexDirection: 'column', gap: `${space(1)}px` }}>
<h1 style={{ ...typographyStyle('heading'), margin: 0, letterSpacing: '-0.02em', color: colorVar('color.text.primary') }}>
</h1>
<span style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary') }}>
</span>
</div>
</div>
<div style={cardStyle}>
<form onSubmit={(e) => { void handleSubmit(e); }} style={{ display: 'flex', flexDirection: 'column', gap: `${space(3)}px` }}>
<div>
<label style={{ display: 'block', marginBottom: `${space(1)}px`, color: colorVar('color.text.secondary'), ...typographyStyle('caption') }}>
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="请输入用户名"
style={inputStyle}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: `${space(1)}px`, color: colorVar('color.text.secondary'), ...typographyStyle('caption') }}>
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码"
style={inputStyle}
/>
</div>
{error !== null && (
<div style={{ color: colorVar('color.risk.critical'), ...typographyStyle('caption'), textAlign: 'center' }}>
{error}
</div>
)}
<button type="submit" style={buttonStyle}>
</button>
</form>
<div style={{ marginTop: `${space(5)}px`, paddingTop: `${space(4)}px`, borderTop: `1px solid ${colorVar('color.border.default')}` }}>
<p style={{ margin: `0 0 ${space(3)}px`, color: colorVar('color.text.secondary'), ...typographyStyle('caption'), textAlign: 'center' }}>
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: `${space(2)}px` }}>
{TEST_ACCOUNTS.map((a) => (
<button
key={a.username}
type="button"
onClick={() => {
setUsername(a.username);
setPassword(a.password);
}}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: `${space(2)}px ${space(3)}px`,
backgroundColor: colorVar('color.bg.surface'),
border: `1px solid ${colorVar('color.border.default')}`,
borderRadius: `${RADIUS.md}px`,
cursor: 'pointer',
fontFamily: FONT_FAMILY,
textAlign: 'left',
width: '100%',
color: colorVar('color.text.primary'),
}}
>
<span style={{ fontWeight: 600 }}>{a.role}</span>
<span style={{ color: colorVar('color.text.secondary'), ...typographyStyle('caption') }}>
{a.username} / {a.password}
</span>
</button>
))}
</div>
</div>
</div>
</div>
);
}