feat: polish user auth entrance

This commit is contained in:
boos
2026-06-06 20:38:18 +08:00
parent 6cd0a8caca
commit 4855db3fc8
4 changed files with 223 additions and 69 deletions
+129 -13
View File
@@ -12505,6 +12505,29 @@ textarea:focus {
box-shadow: 0 9px 18px rgba(32, 46, 73, 0.08);
}
.login-mode-context {
display: grid;
gap: 3px;
margin: -6px 0 16px;
padding: 12px 13px;
border: 1px solid color-mix(in srgb, var(--color-primary) 14%, var(--console-line));
border-radius: 14px;
background: color-mix(in srgb, var(--color-primary) 6%, var(--color-bg-card));
}
.login-mode-context span {
color: var(--console-ink);
font-size: 12px;
font-weight: 820;
}
.login-mode-context p {
margin: 0;
color: var(--color-text-secondary);
font-size: 11.5px;
line-height: 1.55;
}
.login-auth-input {
min-height: 46px;
border-radius: 12px;
@@ -12520,20 +12543,14 @@ textarea:focus {
}
.login-shell[data-variant="user"] .login-surface {
width: min(590px, 100%);
grid-template-columns: 1fr;
width: min(1080px, 100%);
grid-template-columns: minmax(440px, 0.95fr) minmax(380px, 1.05fr);
align-items: stretch;
}
.login-shell[data-variant="user"] .login-brand-panel {
min-height: 150px;
padding: 24px 24px 18px !important;
}
.login-shell[data-variant="user"] .login-metric-grid,
.login-shell[data-variant="user"] .login-capability-list,
.login-shell[data-variant="user"] .login-compat-line,
.login-shell[data-variant="user"] .login-brand-copy-block {
display: none;
min-height: 580px;
padding: clamp(26px, 3.3vw, 42px) !important;
}
.login-shell[data-variant="user"] .login-brand-header {
@@ -12547,7 +12564,26 @@ textarea:focus {
.login-shell[data-variant="user"] .login-auth-panel {
width: 100%;
padding: 30px;
max-width: 480px;
padding: clamp(28px, 3vw, 38px);
}
.login-shell[data-variant="user"] .login-brand-copy {
max-width: 38ch;
font-size: clamp(15px, 1.4vw, 18px);
}
.login-shell[data-variant="user"] .login-compat-line {
max-width: 40rem;
border-radius: 18px;
}
.login-shell[data-variant="user"] .login-metric-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.login-shell[data-variant="user"] .login-capability-list {
max-width: 38rem;
}
.mobile-drawer-header {
@@ -12756,6 +12792,15 @@ textarea:focus {
.login-brand-panel {
min-height: auto;
}
.login-shell[data-variant="user"] .login-surface {
grid-template-columns: 1fr;
width: min(620px, 100%);
}
.login-shell[data-variant="user"] .login-brand-panel {
min-height: auto;
}
}
@media (max-width: 768px) {
@@ -12864,6 +12909,15 @@ textarea:focus {
.login-metric-grid {
grid-template-columns: 1fr;
}
.login-shell[data-variant="user"] .login-brand-copy-block,
.login-shell[data-variant="user"] .login-compat-line {
display: none;
}
.login-shell[data-variant="user"] .login-metric-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* ===== Routes Performance + Register OAuth Polish ===== */
@@ -13106,8 +13160,61 @@ textarea:focus {
font-size: 12px;
}
.login-password-meter {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)) auto;
gap: 5px;
align-items: center;
min-height: 14px;
}
.login-password-meter span {
height: 4px;
border-radius: 999px;
background: var(--console-line);
}
.login-password-meter strong {
color: var(--color-text-muted);
font-size: 10.5px;
font-weight: 760;
white-space: nowrap;
}
.login-password-meter[data-tone="weak"] span:first-child,
.login-password-meter[data-tone="ok"] span:nth-child(-n + 2),
.login-password-meter[data-tone="strong"] span:nth-child(-n + 3) {
background: var(--color-primary);
}
.login-password-meter[data-tone="strong"] span:nth-child(-n + 3) {
background: var(--color-success);
}
.login-password-meter[data-tone="ok"] strong {
color: var(--color-primary);
}
.login-password-meter[data-tone="strong"] strong,
.login-field-hint.is-ok {
color: var(--color-success);
}
.login-field-hint {
margin-top: -3px;
color: var(--color-text-muted);
font-size: 11px;
font-weight: 680;
}
.login-field-hint.is-error {
color: var(--color-danger);
}
.login-register-hint {
padding: 10px 12px;
display: grid;
gap: 4px;
padding: 11px 12px;
border: 1px solid color-mix(in srgb, var(--color-primary) 16%, var(--console-line));
border-radius: 12px;
background: color-mix(in srgb, var(--color-primary) 7%, var(--color-bg-card));
@@ -13116,6 +13223,15 @@ textarea:focus {
line-height: 1.55;
}
.login-register-hint strong {
color: var(--console-ink);
font-size: 12px;
}
.login-register-hint span {
color: var(--color-text-secondary);
}
.login-auth-footer-links {
display: flex;
flex-direction: column;
+24 -2
View File
@@ -16,6 +16,16 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
const [captchaLoading, setCaptchaLoading] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const passwordScore = [
password.length >= 6,
/[A-Za-z]/.test(password) && /\d/.test(password),
password.length >= 10,
].filter(Boolean).length;
const passwordTone = passwordScore >= 3 ? 'strong' : passwordScore >= 2 ? 'ok' : 'weak';
const passwordLabel = password
? (passwordScore >= 3 ? '强' : passwordScore >= 2 ? '可用' : '偏弱')
: '等待输入';
const passwordsMatch = Boolean(confirmPassword) && password === confirmPassword;
const loadCaptcha = useCallback(async () => {
setCaptchaLoading(true);
@@ -97,7 +107,7 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
variant="user"
eyebrow="用户注册"
title="创建 BossAI 账号"
description="完成验证后会自动进入个人工作台,并生成你的默认 API Key。"
description="注册后自动进入个人控制台,并创建默认 API Key。"
error={error}
footer={(
<div className="login-auth-footer-links">
@@ -153,6 +163,12 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
autoComplete="new-password"
required
/>
<div className="login-password-meter" data-tone={passwordTone}>
<span />
<span />
<span />
<strong>{passwordLabel}</strong>
</div>
</div>
<div className="login-field-group">
<label className="login-auth-label" htmlFor="reg-confirm-password-input">确认密码</label>
@@ -166,6 +182,11 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
autoComplete="new-password"
required
/>
{confirmPassword ? (
<div className={`login-field-hint ${passwordsMatch ? 'is-ok' : 'is-error'}`}>
{passwordsMatch ? '两次密码一致' : '两次密码不一致'}
</div>
) : null}
</div>
</div>
<div className="login-captcha-panel">
@@ -204,7 +225,8 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
</button>
</div>
<div className="login-register-hint">
注册会受到验证码和频率限制保护,避免批量注册影响服务稳定性。
<strong>注册保护已启用</strong>
<span>图像验证码和频率限制会拦截批量注册,默认 API Key 会在账号创建后自动生成。</span>
</div>
<button
type="submit"
+28 -22
View File
@@ -28,6 +28,18 @@ const LOGIN_METRICS: Array<{ valueKey: string; labelKey: string }> = [
{ valueKey: '/v1', labelKey: '统一兼容入口' },
];
const USER_METRICS: Array<{ valueKey: string; labelKey: string }> = [
{ valueKey: 'Key', labelKey: '注册后默认创建' },
{ valueKey: '日志', labelKey: '调用内容与费用可查' },
{ valueKey: '空间', labelKey: '产出自动归档' },
];
const USER_CAPABILITY_KEYS: Array<{ titleKey: string; descKey: string }> = [
{ titleKey: '模型创作入口', descKey: '对话、生图、视频助手与数字人任务统一进入个人控制台' },
{ titleKey: 'API Key 与计费', descKey: '默认 Key 可直接调用兼容接口,每次请求都有日志和费用记录' },
{ titleKey: '工作空间归档', descKey: '只保存最终产出的图片、视频和音频,素材上传不会混进资产库' },
];
export default function LoginShell({
t,
eyebrow,
@@ -38,45 +50,41 @@ export default function LoginShell({
footer,
children,
}: LoginShellProps) {
const metrics = variant === 'user' ? USER_METRICS : LOGIN_METRICS;
const capabilities = variant === 'user' ? USER_CAPABILITY_KEYS : ADMIN_CAPABILITY_KEYS;
return (
<div className="login-shell" data-variant={variant}>
<MotionDiv className="login-surface animate-scale-in" motionVariant="login" motionKey={variant}>
<section
className="login-brand-panel login-brand-panel-light"
style={variant === 'user' ? { padding: '28px 24px' } : undefined}
>
<div
className="login-brand-header"
style={variant === 'user' ? { justifyContent: 'center' } : undefined}
>
<section className="login-brand-panel login-brand-panel-light">
<div className="login-brand-header">
<div className="brand-mark-frame brand-mark-frame-hero">
<div className="brand-mark-canvas">
<img src="/logo.png" alt="BossAI" className="login-brand-logo" />
</div>
</div>
<div
className="login-brand-summary"
style={variant === 'user' ? { textAlign: 'center' } : undefined}
>
<div className="login-brand-summary">
<div className="login-brand-name">BossAI</div>
<div className="login-brand-kicker">
{variant === 'user' ? t('创作者工作台') : t('多模型多模态 AI 应用平台')}
{variant === 'user' ? t('用户创作控制台') : t('多模型多模态 AI 应用平台')}
</div>
</div>
</div>
{variant === 'admin' && (
<>
<div className="login-brand-copy-block">
<p className="login-brand-copy">
{t('聚合对话、生图、视频、数字人与工作流编排,把 API 中转、模型路由和内容生产放进一套可运营的平台。')}
{variant === 'user'
? t('登录后即可使用模型广场、视频助手、生图助手、工作流和工作空间,把创意任务沉淀成可追踪的产出。')
: t('聚合对话、生图、视频、数字人与工作流编排,把 API 中转、模型路由和内容生产放进一套可运营的平台。')}
</p>
</div>
<div className="login-compat-line">
{t('兼容 OpenAI / Claude Messages / Comfy Cloud / RunningHub / 火山方舟等多类上游')}
{variant === 'user'
? t('支持 OpenAI / Claude Messages / 图像 / 视频 / Comfy Cloud 工作流')
: t('兼容 OpenAI / Claude Messages / Comfy Cloud / RunningHub / 火山方舟等多类上游')}
</div>
<div className="login-metric-grid" aria-label={t('平台能力指标')}>
{LOGIN_METRICS.map((metric) => (
<div className="login-metric-grid" aria-label={t(variant === 'user' ? '用户能力概览' : '平台能力指标')}>
{metrics.map((metric) => (
<div key={metric.labelKey} className="login-metric-card">
<strong>{t(metric.valueKey)}</strong>
<span>{t(metric.labelKey)}</span>
@@ -84,7 +92,7 @@ export default function LoginShell({
))}
</div>
<div className="login-capability-list">
{ADMIN_CAPABILITY_KEYS.map((feature, index) => (
{capabilities.map((feature, index) => (
<div key={feature.titleKey} className="login-capability-row">
<div className="login-capability-index">
{String(index + 1).padStart(2, '0')}
@@ -96,8 +104,6 @@ export default function LoginShell({
</div>
))}
</div>
</>
)}
</section>
<section className="login-auth-stage">
<div className="login-auth-panel">
+20 -10
View File
@@ -95,22 +95,28 @@ export default function UserLoginPage({ t, onLogin }: UserLoginPageProps) {
};
const description = mode === 'account'
? t('使用邮箱和密码登录你的账号。')
: t('使用账户中已签发的 API Key 直接登录。');
? t('进入个人控制台,继续使用模型、工作流和工作空间。')
: t('适合开发者和自动化脚本,使用账户中已签发的 Key 进入。');
const modeContext = mode === 'account'
? {
title: t('推荐方式'),
desc: t('邮箱密码会保留完整的个人中心、工作空间和使用日志权限。'),
}
: {
title: t('开发者入口'),
desc: t('仅识别已绑定到当前账号的 API Key,登录后仍会进入同一个用户控制台。'),
};
const footer = (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center' }}>
<div className="login-auth-footer-links">
<span>
{t('还没有账号?')}
<Link
to="/user/register"
style={{ color: 'var(--color-primary)', textDecoration: 'none', marginLeft: 4 }}
>
<Link to="/user/register">
{t('注册')}
</Link>
</span>
<span style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
<Link to="/boos" style={{ color: 'var(--color-text-muted)' }}>
← {t('管理员登录')}
<span>
<Link to="/boos">
{t('管理员登录')}
</Link>
</span>
</div>
@@ -152,6 +158,10 @@ export default function UserLoginPage({ t, onLogin }: UserLoginPageProps) {
{t('API Key')}
</button>
</div>
<div className="login-mode-context">
<span>{modeContext.title}</span>
<p>{modeContext.desc}</p>
</div>
{mode === 'account' ? (
<form onSubmit={handleAccountSubmit}>
<label className="login-auth-label" htmlFor="user-email-input">