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