polish auth entry surfaces

This commit is contained in:
boos
2026-06-06 17:42:14 +08:00
parent 188e457e7f
commit 1b2880739a
6 changed files with 154 additions and 56 deletions
+27 -4
View File
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
import { create, type ReactTestInstance } from 'react-test-renderer'; import { create, type ReactTestInstance } from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import AdminLoginPage from './pages/auth/AdminLoginPage.js'; import AdminLoginPage from './pages/auth/AdminLoginPage.js';
import UserLoginPage from './pages/auth/UserLoginPage.js';
function collectText(node: ReactTestInstance): string { function collectText(node: ReactTestInstance): string {
return (node.children || []).map((child) => { return (node.children || []).map((child) => {
@@ -36,13 +37,13 @@ describe('Login surface', () => {
&& node.props.className.includes('brand-mark-canvas') && node.props.className.includes('brand-mark-canvas')
)); ));
expect(pageText).toContain('BoosAPI'); expect(pageText).toContain('BossAI');
expect(pageText).toContain('AI 应用平台'); expect(pageText).toContain('AI 应用平台');
expect(pageText).not.toContain('一个 API Key,一个入口'); expect(pageText).not.toContain('一个 API Key,一个入口');
expect(pageText).toContain('兼容 New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API'); expect(pageText).toContain('兼容 OpenAI / Claude Messages / Comfy Cloud / RunningHub / 火山方舟等多类上游');
expect(pageText).toContain('统一代理网关'); expect(pageText).toContain('统一 API 网关');
expect(pageText).toContain('智能路由引擎'); expect(pageText).toContain('智能路由引擎');
expect(pageText).toContain('自动模型发现'); expect(pageText).toContain('工作流生产台');
expect(lightBrandPanel).toBeTruthy(); expect(lightBrandPanel).toBeTruthy();
expect(authStage).toBeTruthy(); expect(authStage).toBeTruthy();
expect(brandMarkCanvas).toBeTruthy(); expect(brandMarkCanvas).toBeTruthy();
@@ -61,4 +62,26 @@ describe('Login surface', () => {
root?.unmount(); root?.unmount();
} }
}); });
it('renders OAuth provider entries as configured placeholders on user login', () => {
const root = create(
<MemoryRouter>
<UserLoginPage t={(text) => text} />
</MemoryRouter>,
);
try {
const providers = root.root.findAll((node) => (
node.type === 'button'
&& typeof node.props.className === 'string'
&& node.props.className.includes('login-oauth-provider')
));
expect(providers.map((node) => node.props['data-provider'])).toEqual(['google', 'github', 'wechat']);
expect(providers.every((node) => node.props.disabled === true)).toBe(true);
expect(collectText(root.root)).toContain('待配置');
} finally {
root?.unmount();
}
});
}); });
+6
View File
@@ -22,6 +22,12 @@ const zhToEn: Record<string, string> = {
'仅校验本地服务访问权限,不会把令牌发送到第三方。': 'Only checks local service access and never sends the token to a third party.', '仅校验本地服务访问权限,不会把令牌发送到第三方。': 'Only checks local service access and never sends the token to a third party.',
'验证中...': 'Verifying...', '验证中...': 'Verifying...',
'登录': 'Sign In', '登录': 'Sign In',
'第三方登录': 'Third-party sign-in',
'第三方账号快速注册': 'Quick sign-up with third-party accounts',
'接入后可自动创建账号,并绑定同一用户身份。': 'Once connected, accounts can be created automatically and bound to the same user identity.',
'管理员配置 OAuth 后即可启用这些入口。': 'These entries become available after an admin configures OAuth.',
'等待管理员配置 OAuth 后启用': 'Waiting for an admin to configure OAuth',
'待配置': 'Not configured',
'AI 应用平台': 'AI Application Platform', 'AI 应用平台': 'AI Application Platform',
'聚合多模型、多模态、工作流与智能路由,把对话、生图、视频和数字人生产放进一个统一平台。': 'Unify multi-model routing, multimodal creation, workflows, chat, image generation, video, and digital human production in one platform.', '聚合多模型、多模态、工作流与智能路由,把对话、生图、视频和数字人生产放进一个统一平台。': 'Unify multi-model routing, multimodal creation, workflows, chat, image generation, video, and digital human production in one platform.',
'中转站的中转站': 'The Hub of Hubs', '中转站的中转站': 'The Hub of Hubs',
+99 -42
View File
@@ -12153,8 +12153,8 @@ textarea:focus {
min-height: 100dvh; min-height: 100dvh;
padding: clamp(18px, 3vw, 34px); padding: clamp(18px, 3vw, 34px);
background: background:
radial-gradient(circle at 18% 10%, color-mix(in srgb, var(--color-primary) 18%, transparent), transparent 32%), radial-gradient(circle at 18% 10%, color-mix(in srgb, var(--color-primary) 14%, transparent), transparent 32%),
radial-gradient(circle at 90% 18%, rgba(20, 184, 166, 0.12), transparent 28%), radial-gradient(circle at 90% 18%, rgba(20, 184, 166, 0.1), transparent 28%),
linear-gradient(180deg, rgb(var(--console-bg-rgb)), color-mix(in srgb, rgb(var(--console-bg-rgb)) 92%, var(--color-bg-card))); linear-gradient(180deg, rgb(var(--console-bg-rgb)), color-mix(in srgb, rgb(var(--console-bg-rgb)) 92%, var(--color-bg-card)));
} }
@@ -12163,12 +12163,12 @@ textarea:focus {
grid-template-columns: minmax(520px, 1.08fr) minmax(380px, 0.92fr); grid-template-columns: minmax(520px, 1.08fr) minmax(380px, 0.92fr);
gap: 14px; gap: 14px;
padding: 14px; padding: 14px;
border: 1px solid var(--console-line-strong); border: 1px solid color-mix(in srgb, var(--console-line-strong) 86%, transparent);
border-radius: 28px; border-radius: 26px;
background: color-mix(in srgb, var(--console-panel-solid) 84%, transparent); background: color-mix(in srgb, var(--console-panel-solid) 90%, transparent);
box-shadow: 0 36px 92px rgba(20, 38, 68, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.78); box-shadow: 0 30px 78px rgba(20, 38, 68, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.74);
backdrop-filter: blur(22px) saturate(1.12); backdrop-filter: blur(18px) saturate(1.08);
-webkit-backdrop-filter: blur(22px) saturate(1.12); -webkit-backdrop-filter: blur(18px) saturate(1.08);
} }
[data-theme="dark"] .login-surface { [data-theme="dark"] .login-surface {
@@ -12177,11 +12177,11 @@ textarea:focus {
.login-brand-panel { .login-brand-panel {
min-height: 650px; min-height: 650px;
padding: clamp(30px, 4vw, 48px); padding: clamp(28px, 3.8vw, 46px);
border: 1px solid var(--console-line); border: 1px solid var(--console-line);
border-radius: 22px; border-radius: 20px;
background: background:
linear-gradient(140deg, color-mix(in srgb, var(--color-primary) 13%, var(--color-bg-card)) 0%, color-mix(in srgb, var(--color-bg-card) 80%, transparent) 54%, color-mix(in srgb, #14b8a6 9%, var(--color-bg-card)) 100%); linear-gradient(140deg, color-mix(in srgb, var(--color-primary) 10%, var(--color-bg-card)) 0%, color-mix(in srgb, var(--color-bg-card) 82%, transparent) 56%, color-mix(in srgb, #14b8a6 7%, var(--color-bg-card)) 100%);
} }
.login-brand-panel::before, .login-brand-panel::before,
@@ -12216,13 +12216,13 @@ textarea:focus {
} }
.brand-mark-frame-hero { .brand-mark-frame-hero {
width: 94px; width: 88px;
height: 94px; height: 88px;
border-radius: 25px; border-radius: 23px;
} }
.brand-mark-canvas { .brand-mark-canvas {
border-radius: 19px; border-radius: 18px;
} }
.login-brand-name { .login-brand-name {
@@ -12314,20 +12314,24 @@ textarea:focus {
} }
.login-auth-panel { .login-auth-panel {
width: min(100%, 410px); width: min(100%, 420px);
padding: 34px; padding: 32px;
border: 1px solid var(--console-line-strong); border: 1px solid var(--console-line-strong);
border-radius: 20px; border-radius: 18px;
background: color-mix(in srgb, var(--color-bg-card) 88%, transparent); background: color-mix(in srgb, var(--color-bg-card) 92%, transparent);
box-shadow: var(--console-shadow-panel); box-shadow: var(--console-shadow-panel);
} }
.login-auth-title { .login-auth-title {
color: var(--console-ink); color: var(--console-ink);
font-size: clamp(30px, 3vw, 38px); font-size: clamp(29px, 3vw, 36px);
letter-spacing: 0; letter-spacing: 0;
} }
.login-auth-copy {
margin-bottom: 20px;
}
.login-mode-tabs { .login-mode-tabs {
border-color: var(--console-line); border-color: var(--console-line);
background: color-mix(in srgb, var(--color-bg) 72%, transparent); background: color-mix(in srgb, var(--color-bg) 72%, transparent);
@@ -12358,12 +12362,13 @@ textarea:focus {
} }
.login-shell[data-variant="user"] .login-surface { .login-shell[data-variant="user"] .login-surface {
width: min(560px, 100%); width: min(590px, 100%);
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.login-shell[data-variant="user"] .login-brand-panel { .login-shell[data-variant="user"] .login-brand-panel {
min-height: 168px; min-height: 150px;
padding: 24px 24px 18px !important;
} }
.login-shell[data-variant="user"] .login-metric-grid, .login-shell[data-variant="user"] .login-metric-grid,
@@ -12375,12 +12380,18 @@ textarea:focus {
.login-shell[data-variant="user"] .login-brand-header { .login-shell[data-variant="user"] .login-brand-header {
justify-content: flex-start !important; justify-content: flex-start !important;
gap: 16px;
} }
.login-shell[data-variant="user"] .login-brand-summary { .login-shell[data-variant="user"] .login-brand-summary {
text-align: left !important; text-align: left !important;
} }
.login-shell[data-variant="user"] .login-auth-panel {
width: 100%;
padding: 30px;
}
.mobile-drawer-header { .mobile-drawer-header {
border-bottom: 1px solid var(--console-line); border-bottom: 1px solid var(--console-line);
} }
@@ -12609,19 +12620,20 @@ textarea:focus {
.login-oauth-slot { .login-oauth-slot {
display: grid; display: grid;
gap: 10px; gap: 12px;
margin: 0 0 18px; margin: 18px 0 0;
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.login-oauth-divider { .login-oauth-divider {
display: grid; display: grid;
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
gap: 10px; gap: 12px;
align-items: center; align-items: center;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 11px; font-size: 11px;
font-weight: 760; font-weight: 760;
line-height: 1.3;
} }
.login-oauth-divider::before, .login-oauth-divider::before,
@@ -12634,61 +12646,94 @@ textarea:focus {
.login-oauth-provider-row { .login-oauth-provider-row {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px; gap: 9px;
} }
.login-oauth-provider { .login-oauth-provider {
min-width: 0; min-width: 0;
min-height: 42px; min-height: 54px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
gap: 7px; gap: 9px;
padding: 0 10px; padding: 8px 10px;
border: 1px solid var(--console-line); border: 1px solid var(--console-line-strong);
border-radius: 12px; border-radius: 12px;
background: color-mix(in srgb, var(--color-bg-card) 80%, var(--color-bg)); background:
linear-gradient(180deg, color-mix(in srgb, var(--color-bg-card) 94%, transparent), color-mix(in srgb, var(--color-bg) 74%, var(--color-bg-card)));
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-family: var(--font-sans);
font-size: 12px; font-size: 12px;
font-weight: 720; font-weight: 720;
text-align: left;
transition: border-color 0.16s ease, background-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
} }
.login-oauth-provider:disabled { .login-oauth-provider:disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.78; opacity: 0.86;
} }
.login-oauth-provider-mark { .login-oauth-provider-mark {
width: 20px; width: 28px;
height: 20px; height: 28px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
border-radius: 999px; border: 1px solid var(--console-line);
background: color-mix(in srgb, var(--color-primary) 10%, var(--color-bg-card)); border-radius: 10px;
background: var(--color-bg-card);
color: var(--color-primary); color: var(--color-primary);
font-size: 11px; font-size: 11px;
font-weight: 840; font-weight: 840;
} }
.login-oauth-provider-copy {
min-width: 0;
display: grid;
gap: 2px;
line-height: 1.15;
}
.login-oauth-provider-name {
overflow: hidden;
color: var(--console-ink);
text-overflow: ellipsis;
white-space: nowrap;
}
.login-oauth-provider-state {
overflow: hidden;
color: var(--color-text-muted);
font-size: 10.5px;
font-weight: 680;
text-overflow: ellipsis;
white-space: nowrap;
}
.login-oauth-provider[data-provider="wechat"] .login-oauth-provider-mark {
background: color-mix(in srgb, #07c160 10%, var(--color-bg-card));
color: #07a854;
}
.login-oauth-desc { .login-oauth-desc {
margin: 0; margin: 0;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 11.5px; font-size: 11.5px;
line-height: 1.55; line-height: 1.6;
text-align: center; text-align: center;
} }
.login-register-form { .login-register-form {
display: grid; display: grid;
gap: 12px; gap: 13px;
} }
.login-register-grid { .login-register-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px; gap: 11px;
} }
.login-field-group { .login-field-group {
@@ -12705,7 +12750,7 @@ textarea:focus {
.login-captcha-panel { .login-captcha-panel {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) 138px; grid-template-columns: minmax(0, 1fr) 148px;
gap: 10px; gap: 10px;
align-items: end; align-items: end;
} }
@@ -12722,6 +12767,13 @@ textarea:focus {
linear-gradient(180deg, color-mix(in srgb, var(--color-bg-card) 92%, transparent), color-mix(in srgb, var(--color-bg-card) 76%, var(--color-bg))); linear-gradient(180deg, color-mix(in srgb, var(--color-bg-card) 92%, transparent), color-mix(in srgb, var(--color-bg-card) 76%, var(--color-bg)));
color: var(--color-text-muted); color: var(--color-text-muted);
cursor: pointer; cursor: pointer;
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
.login-captcha-card:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--color-primary) 28%, var(--console-line-strong));
box-shadow: 0 10px 18px rgba(32, 46, 73, 0.08);
transform: translateY(-1px);
} }
.login-captcha-card:disabled { .login-captcha-card:disabled {
@@ -12754,7 +12806,7 @@ textarea:focus {
.login-auth-footer-links { .login-auth-footer-links {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 7px;
align-items: center; align-items: center;
} }
@@ -12779,6 +12831,11 @@ textarea:focus {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.login-oauth-provider {
min-height: 50px;
justify-content: center;
}
.login-captcha-card { .login-captcha-card {
width: 100%; width: 100%;
} }
+1 -1
View File
@@ -113,7 +113,6 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
</div> </div>
)} )}
> >
<OAuthSlot t={(text) => text} variant="user" mode="register" />
<form onSubmit={handleSubmit} className="login-register-form"> <form onSubmit={handleSubmit} className="login-register-form">
<div className="login-register-grid"> <div className="login-register-grid">
<div className="login-field-group"> <div className="login-field-group">
@@ -234,6 +233,7 @@ export default function UserRegister({ onLogin }: { onLogin?: () => void }) {
)} )}
</button> </button>
</form> </form>
<OAuthSlot t={(text) => text} variant="user" mode="register" />
</LoginShell> </LoginShell>
); );
} }
+2 -2
View File
@@ -51,7 +51,7 @@ export default function LoginShell({
> >
<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="BoosAPI" className="login-brand-logo" /> <img src="/logo.png" alt="BossAI" className="login-brand-logo" />
</div> </div>
</div> </div>
<div <div
@@ -104,12 +104,12 @@ export default function LoginShell({
<div className="login-auth-eyebrow">{eyebrow}</div> <div className="login-auth-eyebrow">{eyebrow}</div>
<h2 className="login-auth-title">{title}</h2> <h2 className="login-auth-title">{title}</h2>
<p className="login-auth-copy">{description}</p> <p className="login-auth-copy">{description}</p>
{children}
{error && ( {error && (
<div className="alert alert-error animate-shake" style={{ marginBottom: 12 }}> <div className="alert alert-error animate-shake" style={{ marginBottom: 12 }}>
{error} {error}
</div> </div>
)} )}
{children}
{footer && <div className="login-auth-footer">{footer}</div>} {footer && <div className="login-auth-footer">{footer}</div>}
</div> </div>
</section> </section>
+19 -7
View File
@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { BrandGlyph } from '../../components/BrandIcon.js';
export interface OAuthSlotProps { export interface OAuthSlotProps {
t: (text: string) => string; t: (text: string) => string;
@@ -17,16 +18,17 @@ export interface OAuthSlotProps {
* does not touch `authSession`. Tests assert that property. * does not touch `authSession`. Tests assert that property.
*/ */
const OAUTH_PROVIDER_KEYS = [ const OAUTH_PROVIDER_KEYS = [
{ key: 'google', label: 'Google' }, { key: 'google', label: 'Google', icon: 'google-color' },
{ key: 'github', label: 'GitHub' }, { key: 'github', label: 'GitHub', icon: 'github' },
{ key: 'wechat', label: '微信' }, { key: 'wechat', label: '微信', icon: 'wechat-color' },
]; ];
export default function OAuthSlot({ t, mode = 'login' }: OAuthSlotProps) { export default function OAuthSlot({ t, mode = 'login' }: OAuthSlotProps) {
const title = mode === 'register' ? t('第三方账号快速注册') : t('第三方登录'); const title = mode === 'register' ? t('第三方账号快速注册') : t('第三方登录');
const desc = mode === 'register' const desc = mode === 'register'
? t('OAuth 接入后可自动创建账号并绑定同一用户身份。') ? t('接入后可自动创建账号,并绑定同一用户身份。')
: t('即将支持配置后的 OAuth 提供商。'); : t('管理员配置 OAuth 后即可启用这些入口。');
const pendingLabel = t('待配置');
return ( return (
<div <div
@@ -48,8 +50,18 @@ export default function OAuthSlot({ t, mode = 'login' }: OAuthSlotProps) {
data-provider={provider.key} data-provider={provider.key}
title={t('等待管理员配置 OAuth 后启用')} title={t('等待管理员配置 OAuth 后启用')}
> >
<span className="login-oauth-provider-mark">{provider.label.slice(0, 1)}</span> <span className="login-oauth-provider-mark" aria-hidden="true">
<span>{provider.label}</span> <BrandGlyph
icon={provider.icon}
alt=""
size={18}
fallbackText={provider.label}
/>
</span>
<span className="login-oauth-provider-copy">
<span className="login-oauth-provider-name">{provider.label}</span>
<span className="login-oauth-provider-state">{pendingLabel}</span>
</span>
</button> </button>
))} ))}
</div> </div>