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); 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 { .login-auth-input {
min-height: 46px; min-height: 46px;
border-radius: 12px; border-radius: 12px;
@@ -12520,20 +12543,14 @@ textarea:focus {
} }
.login-shell[data-variant="user"] .login-surface { .login-shell[data-variant="user"] .login-surface {
width: min(590px, 100%); width: min(1080px, 100%);
grid-template-columns: 1fr; grid-template-columns: minmax(440px, 0.95fr) minmax(380px, 1.05fr);
align-items: stretch;
} }
.login-shell[data-variant="user"] .login-brand-panel { .login-shell[data-variant="user"] .login-brand-panel {
min-height: 150px; min-height: 580px;
padding: 24px 24px 18px !important; padding: clamp(26px, 3.3vw, 42px) !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;
} }
.login-shell[data-variant="user"] .login-brand-header { .login-shell[data-variant="user"] .login-brand-header {
@@ -12547,7 +12564,26 @@ textarea:focus {
.login-shell[data-variant="user"] .login-auth-panel { .login-shell[data-variant="user"] .login-auth-panel {
width: 100%; 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 { .mobile-drawer-header {
@@ -12756,6 +12792,15 @@ textarea:focus {
.login-brand-panel { .login-brand-panel {
min-height: auto; 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) { @media (max-width: 768px) {
@@ -12864,6 +12909,15 @@ textarea:focus {
.login-metric-grid { .login-metric-grid {
grid-template-columns: 1fr; 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 ===== */ /* ===== Routes Performance + Register OAuth Polish ===== */
@@ -13106,8 +13160,61 @@ textarea:focus {
font-size: 12px; 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 { .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: 1px solid color-mix(in srgb, var(--color-primary) 16%, var(--console-line));
border-radius: 12px; border-radius: 12px;
background: color-mix(in srgb, var(--color-primary) 7%, var(--color-bg-card)); background: color-mix(in srgb, var(--color-primary) 7%, var(--color-bg-card));
@@ -13116,6 +13223,15 @@ textarea:focus {
line-height: 1.55; 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 { .login-auth-footer-links {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+24 -2
View File
@@ -16,6 +16,16 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
const [captchaLoading, setCaptchaLoading] = useState(false); const [captchaLoading, setCaptchaLoading] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); 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 () => { const loadCaptcha = useCallback(async () => {
setCaptchaLoading(true); setCaptchaLoading(true);
@@ -97,7 +107,7 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
variant="user" variant="user"
eyebrow="用户注册" eyebrow="用户注册"
title="创建 BossAI 账号" title="创建 BossAI 账号"
description="完成验证后会自动进入个人工作台,并生成你的默认 API Key。" description="注册后自动进入个人控制台,并创建默认 API Key。"
error={error} error={error}
footer={( footer={(
<div className="login-auth-footer-links"> <div className="login-auth-footer-links">
@@ -153,6 +163,12 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
autoComplete="new-password" autoComplete="new-password"
required required
/> />
<div className="login-password-meter" data-tone={passwordTone}>
<span />
<span />
<span />
<strong>{passwordLabel}</strong>
</div>
</div> </div>
<div className="login-field-group"> <div className="login-field-group">
<label className="login-auth-label" htmlFor="reg-confirm-password-input">确认密码</label> <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" autoComplete="new-password"
required required
/> />
{confirmPassword ? (
<div className={`login-field-hint ${passwordsMatch ? 'is-ok' : 'is-error'}`}>
{passwordsMatch ? '两次密码一致' : '两次密码不一致'}
</div>
) : null}
</div> </div>
</div> </div>
<div className="login-captcha-panel"> <div className="login-captcha-panel">
@@ -204,7 +225,8 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
</button> </button>
</div> </div>
<div className="login-register-hint"> <div className="login-register-hint">
注册会受到验证码和频率限制保护,避免批量注册影响服务稳定性。 <strong>注册保护已启用</strong>
<span>图像验证码和频率限制会拦截批量注册,默认 API Key 会在账号创建后自动生成。</span>
</div> </div>
<button <button
type="submit" type="submit"
+28 -22
View File
@@ -28,6 +28,18 @@ const LOGIN_METRICS: Array<{ valueKey: string; labelKey: string }> = [
{ valueKey: '/v1', labelKey: '统一兼容入口' }, { 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({ export default function LoginShell({
t, t,
eyebrow, eyebrow,
@@ -38,45 +50,41 @@ export default function LoginShell({
footer, footer,
children, children,
}: LoginShellProps) { }: LoginShellProps) {
const metrics = variant === 'user' ? USER_METRICS : LOGIN_METRICS;
const capabilities = variant === 'user' ? USER_CAPABILITY_KEYS : ADMIN_CAPABILITY_KEYS;
return ( return (
<div className="login-shell" data-variant={variant}> <div className="login-shell" data-variant={variant}>
<MotionDiv className="login-surface animate-scale-in" motionVariant="login" motionKey={variant}> <MotionDiv className="login-surface animate-scale-in" motionVariant="login" motionKey={variant}>
<section <section className="login-brand-panel login-brand-panel-light">
className="login-brand-panel login-brand-panel-light" <div className="login-brand-header">
style={variant === 'user' ? { padding: '28px 24px' } : undefined}
>
<div
className="login-brand-header"
style={variant === 'user' ? { justifyContent: 'center' } : undefined}
>
<div className="brand-mark-frame brand-mark-frame-hero"> <div className="brand-mark-frame brand-mark-frame-hero">
<div className="brand-mark-canvas"> <div className="brand-mark-canvas">
<img src="/logo.png" alt="BossAI" className="login-brand-logo" /> <img src="/logo.png" alt="BossAI" className="login-brand-logo" />
</div> </div>
</div> </div>
<div <div className="login-brand-summary">
className="login-brand-summary"
style={variant === 'user' ? { textAlign: 'center' } : undefined}
>
<div className="login-brand-name">BossAI</div> <div className="login-brand-name">BossAI</div>
<div className="login-brand-kicker"> <div className="login-brand-kicker">
{variant === 'user' ? t('创作者工作台') : t('多模型多模态 AI 应用平台')} {variant === 'user' ? t('用户创作控制台') : t('多模型多模态 AI 应用平台')}
</div> </div>
</div> </div>
</div> </div>
{variant === 'admin' && (
<>
<div className="login-brand-copy-block"> <div className="login-brand-copy-block">
<p className="login-brand-copy"> <p className="login-brand-copy">
{t('聚合对话、生图、视频、数字人与工作流编排,把 API 中转、模型路由和内容生产放进一套可运营的平台。')} {variant === 'user'
? t('登录后即可使用模型广场、视频助手、生图助手、工作流和工作空间,把创意任务沉淀成可追踪的产出。')
: t('聚合对话、生图、视频、数字人与工作流编排,把 API 中转、模型路由和内容生产放进一套可运营的平台。')}
</p> </p>
</div> </div>
<div className="login-compat-line"> <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>
<div className="login-metric-grid" aria-label={t('平台能力指标')}> <div className="login-metric-grid" aria-label={t(variant === 'user' ? '用户能力概览' : '平台能力指标')}>
{LOGIN_METRICS.map((metric) => ( {metrics.map((metric) => (
<div key={metric.labelKey} className="login-metric-card"> <div key={metric.labelKey} className="login-metric-card">
<strong>{t(metric.valueKey)}</strong> <strong>{t(metric.valueKey)}</strong>
<span>{t(metric.labelKey)}</span> <span>{t(metric.labelKey)}</span>
@@ -84,7 +92,7 @@ export default function LoginShell({
))} ))}
</div> </div>
<div className="login-capability-list"> <div className="login-capability-list">
{ADMIN_CAPABILITY_KEYS.map((feature, index) => ( {capabilities.map((feature, index) => (
<div key={feature.titleKey} className="login-capability-row"> <div key={feature.titleKey} className="login-capability-row">
<div className="login-capability-index"> <div className="login-capability-index">
{String(index + 1).padStart(2, '0')} {String(index + 1).padStart(2, '0')}
@@ -96,8 +104,6 @@ export default function LoginShell({
</div> </div>
))} ))}
</div> </div>
</>
)}
</section> </section>
<section className="login-auth-stage"> <section className="login-auth-stage">
<div className="login-auth-panel"> <div className="login-auth-panel">
+20 -10
View File
@@ -95,22 +95,28 @@ export default function UserLoginPage({ t, onLogin }: UserLoginPageProps) {
}; };
const description = mode === 'account' const description = mode === 'account'
? t('使用邮箱和密码登录你的账号。') ? t('进入个人控制台,继续使用模型、工作流和工作空间。')
: t('使用账户中已签发的 API Key 直接登录。'); : t('适合开发者和自动化脚本,使用账户中已签发的 Key 进入。');
const modeContext = mode === 'account'
? {
title: t('推荐方式'),
desc: t('邮箱密码会保留完整的个人中心、工作空间和使用日志权限。'),
}
: {
title: t('开发者入口'),
desc: t('仅识别已绑定到当前账号的 API Key,登录后仍会进入同一个用户控制台。'),
};
const footer = ( const footer = (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center' }}> <div className="login-auth-footer-links">
<span> <span>
{t('还没有账号?')} {t('还没有账号?')}
<Link <Link to="/user/register">
to="/user/register"
style={{ color: 'var(--color-primary)', textDecoration: 'none', marginLeft: 4 }}
>
{t('注册')} {t('注册')}
</Link> </Link>
</span> </span>
<span style={{ fontSize: 12, color: 'var(--color-text-muted)' }}> <span>
<Link to="/boos" style={{ color: 'var(--color-text-muted)' }}> <Link to="/boos">
← {t('管理员登录')} {t('管理员登录')}
</Link> </Link>
</span> </span>
</div> </div>
@@ -152,6 +158,10 @@ export default function UserLoginPage({ t, onLogin }: UserLoginPageProps) {
{t('API Key')} {t('API Key')}
</button> </button>
</div> </div>
<div className="login-mode-context">
<span>{modeContext.title}</span>
<p>{modeContext.desc}</p>
</div>
{mode === 'account' ? ( {mode === 'account' ? (
<form onSubmit={handleAccountSubmit}> <form onSubmit={handleAccountSubmit}>
<label className="login-auth-label" htmlFor="user-email-input"> <label className="login-auth-label" htmlFor="user-email-input">