feat: polish user auth entrance
This commit is contained in:
+129
-13
@@ -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;
|
||||||
|
|||||||
@@ -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,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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user