From 719e21cce71f347d10977b957321d14875ba1719 Mon Sep 17 00:00:00 2001 From: ciat-777 Date: Sat, 21 Mar 2026 17:12:11 +0800 Subject: [PATCH] feat(web): redesign login surface --- src/web/App.login-surface.test.tsx | 78 +++++ src/web/App.tsx | 141 ++++++-- src/web/docsLink.ts | 3 +- src/web/i18n.tsx | 7 + src/web/index.css | 494 ++++++++++++++++++++++++++++- 5 files changed, 680 insertions(+), 43 deletions(-) create mode 100644 src/web/App.login-surface.test.tsx diff --git a/src/web/App.login-surface.test.tsx b/src/web/App.login-surface.test.tsx new file mode 100644 index 0000000..00c21b1 --- /dev/null +++ b/src/web/App.login-surface.test.tsx @@ -0,0 +1,78 @@ +import { describe, expect, it, vi } from 'vitest'; +import { create, type ReactTestInstance } from 'react-test-renderer'; +import { Login } from './App.js'; +import { SITE_DOCS_URL, SITE_GITHUB_URL } from './docsLink.js'; + +function collectText(node: ReactTestInstance): string { + return (node.children || []).map((child) => { + if (typeof child === 'string') return child; + return collectText(child); + }).join(''); +} + +describe('Login surface', () => { + it('uses the site root as the documentation URL', () => { + expect(SITE_DOCS_URL).toBe('https://metapi.cita777.me'); + }); + + it('uses the author github profile for the login github shortcut', () => { + expect(SITE_GITHUB_URL).toBe('https://github.com/cita-777'); + }); + + it('renders a poster-style hero with a floating admin login panel', () => { + const root = create( + text} />, + ); + + try { + const pageText = collectText(root.root); + const lightBrandPanel = root.root.find((node) => ( + node.type === 'section' + && typeof node.props.className === 'string' + && node.props.className.includes('login-brand-panel-light') + )); + const authStage = root.root.find((node) => ( + node.type === 'section' + && typeof node.props.className === 'string' + && node.props.className.includes('login-auth-stage') + )); + const brandMarkCanvas = root.root.find((node) => ( + node.type === 'div' + && typeof node.props.className === 'string' + && node.props.className.includes('brand-mark-canvas') + )); + + expect(pageText).toContain('Metapi'); + expect(pageText).toContain('中转站的中转站'); + expect(pageText).not.toContain('一个 API Key,一个入口'); + expect(pageText).toContain('兼容 New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API'); + expect(pageText).toContain('统一代理网关'); + expect(pageText).toContain('智能路由引擎'); + expect(pageText).toContain('自动模型发现'); + expect(pageText).toContain('部署文档'); + expect(lightBrandPanel).toBeTruthy(); + expect(authStage).toBeTruthy(); + expect(brandMarkCanvas).toBeTruthy(); + + const docsLink = root.root.find((node) => ( + node.type === 'a' + && node.props.href === SITE_DOCS_URL + )); + const tokenInput = root.root.find((node) => ( + node.type === 'input' + && node.props.placeholder === '管理员令牌' + )); + const githubLink = root.root.find((node) => ( + node.type === 'a' + && node.props.href === SITE_GITHUB_URL + )); + + expect(docsLink.props.target).toBe('_blank'); + expect(githubLink.props['aria-label']).toBe('GitHub'); + expect(githubLink.props.target).toBe('_blank'); + expect(tokenInput.props.type).toBe('password'); + } finally { + root.unmount(); + } + }); +}); diff --git a/src/web/App.tsx b/src/web/App.tsx index 82cfa44..a2fc437 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -14,7 +14,7 @@ import { } from './appLocalState.js'; import { I18nProvider, useI18n } from './i18n.js'; import { resolveLoginErrorMessage } from './loginError.js'; -import { SITE_DOCS_URL } from './docsLink.js'; +import { SITE_DOCS_URL, SITE_GITHUB_URL } from './docsLink.js'; import { useAnimatedVisibility } from './components/useAnimatedVisibility.js'; import { useIsMobile } from './components/useIsMobile.js'; import { MobileDrawer } from './components/MobileDrawer.js'; @@ -127,10 +127,24 @@ function resolveStoredProfile(): UserProfile { } } -function Login({ onLogin, t }: { onLogin: (token: string) => void; t: (text: string) => string }) { +export function Login({ onLogin, t }: { onLogin: (token: string) => void; t: (text: string) => string }) { const [token, setToken] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); + const capabilityRows = [ + { + title: t('统一代理网关'), + description: t('一个 Key、一个入口,兼容 OpenAI / Claude 下游格式'), + }, + { + title: t('自动模型发现'), + description: t('上游新增模型自动出现在模型列表,零配置路由生成'), + }, + { + title: t('智能路由引擎'), + description: t('按成本、延迟、成功率自动选择最优通道,故障自动转移'), + }, + ]; const handleLogin = async () => { if (!token) return; @@ -167,37 +181,100 @@ function Login({ onLogin, t }: { onLogin: (token: string) => void; t: (text: str }; return ( -
-
-
- Metapi -

Metapi

-
-

{t('请输入管理员令牌后继续。')}

- { - setToken(e.target.value); - setError(''); - }} - onKeyDown={(e) => e.key === 'Enter' && handleLogin()} - style={{ width: '100%', padding: '10px 14px', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', fontSize: 14, outline: 'none', background: 'var(--color-bg)', color: 'var(--color-text-primary)', marginBottom: 12, transition: 'border-color 0.2s' }} - /> - {error && ( -
- {error} +
+
+
+
+
+
+ Metapi +
+
+
+
Metapi
+
{t('中转站的中转站')}
+
- )} - +
+

+ {t('把分散的 New API / One API / OneHub 等站点聚合成统一网关,自动发现模型、智能路由、成本更优。')} +

+
+
{t('兼容 New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API')}
+
+ {capabilityRows.map((feature, index) => ( +
+
{String(index + 1).padStart(2, '0')}
+
+
{feature.title}
+

{feature.description}

+
+
+ ))} +
+ +
+ +
+
+
{t('管理员入口')}
+

{t('登录')}

+

{t('请输入管理员令牌后继续。')}

+ + { + setToken(e.target.value); + setError(''); + }} + onKeyDown={(e) => e.key === 'Enter' && handleLogin()} + className="login-auth-input" + /> + {error && ( +
+ {error} +
+ )} + +
{t('仅校验本地服务访问权限,不会把令牌发送到第三方。')}
+
+ {t('管理员登录后继续。')} +
+
+
); diff --git a/src/web/docsLink.ts b/src/web/docsLink.ts index ed56ae3..08fa0d4 100644 --- a/src/web/docsLink.ts +++ b/src/web/docsLink.ts @@ -1 +1,2 @@ -export const SITE_DOCS_URL = 'https://metapi.cita777.me/faq'; +export const SITE_DOCS_URL = 'https://metapi.cita777.me'; +export const SITE_GITHUB_URL = 'https://github.com/cita-777'; diff --git a/src/web/i18n.tsx b/src/web/i18n.tsx index a9cee5f..bcf6be5 100644 --- a/src/web/i18n.tsx +++ b/src/web/i18n.tsx @@ -13,8 +13,15 @@ const zhToEn: Record = { '无法连接到服务器': 'Unable to connect to server', '请输入管理员令牌后继续。': 'Enter admin token to continue.', '管理员令牌': 'Admin Token', + '管理员入口': 'Admin Access', + '部署文档': 'Deployment Docs', + '管理员登录后继续。': 'Continue with admin sign-in.', + '仅校验本地服务访问权限,不会把令牌发送到第三方。': 'Only checks local service access and never sends the token to a third party.', '验证中...': 'Verifying...', '登录': 'Sign In', + '中转站的中转站': 'The Hub of Hubs', + '把分散的 New API / One API / OneHub 等站点聚合成统一网关,自动发现模型、智能路由、成本更优。': 'Turn fragmented New API / One API / OneHub sites into one unified gateway with auto model discovery, smart routing, and better cost control.', + '兼容 New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API': 'Compatible with New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API', '用户名不能为空': 'Username cannot be empty', '用户名最多 24 个字符': 'Username can be at most 24 characters', '个人信息': 'Profile', diff --git a/src/web/index.css b/src/web/index.css index b56201b..63c8f7f 100644 --- a/src/web/index.css +++ b/src/web/index.css @@ -2016,11 +2016,6 @@ body { color: var(--color-text-muted); } -.badge-purple { - background: #ede9fe; - color: #6d28d9; -} - .badge-link { display: inline-flex; text-decoration: none; @@ -2382,11 +2377,6 @@ body[data-tooltip-portal="true"] [data-tooltip]::before { color: var(--color-text-muted); } -[data-theme="dark"] .badge-purple { - background: #2e1065; - color: #c4b5fd; -} - [data-theme="dark"] .badge-link:hover .badge-muted, [data-theme="dark"] .badge-link:focus-visible .badge-muted { background: rgba(96, 165, 250, 0.2); @@ -2692,6 +2682,490 @@ textarea { padding: 20px; } +/* ===== Frontend Skill Surfaces ===== */ +.login-shell { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 32px; + background: + radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 34%), + radial-gradient(circle at right 18%, rgba(34, 211, 238, 0.16), transparent 30%), + linear-gradient(180deg, #eef4fb 0%, #e9eef7 100%); +} + +[data-theme="dark"] .login-shell { + background: + radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 34%), + radial-gradient(circle at right 18%, rgba(34, 211, 238, 0.12), transparent 30%), + linear-gradient(180deg, #0d1320 0%, #0a1018 100%); +} + +.login-surface { + width: min(1240px, 100%); + display: grid; + grid-template-columns: minmax(0, 1.16fr) minmax(360px, 440px); + gap: 24px; + padding: 20px; + background: + linear-gradient(145deg, rgba(255, 255, 255, 0.78), rgba(244, 248, 253, 0.88)); + border: 1px solid rgba(255, 255, 255, 0.58); + border-radius: 36px; + box-shadow: + 0 24px 60px rgba(148, 163, 184, 0.24), + inset 0 1px 0 rgba(255, 255, 255, 0.9); +} + +[data-theme="dark"] .login-surface { + background: + linear-gradient(145deg, rgba(15, 23, 42, 0.88), rgba(10, 18, 30, 0.94)); + border-color: rgba(148, 163, 184, 0.12); + box-shadow: + 0 24px 80px rgba(2, 6, 23, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +.login-brand-panel { + position: relative; + min-height: 760px; + padding: 56px 56px 44px; + display: flex; + flex-direction: column; + gap: 28px; + color: var(--color-text-primary); + border-radius: 30px; + overflow: hidden; +} + +.login-brand-panel-light { + background: + radial-gradient(circle at top left, rgba(96, 165, 250, 0.22), transparent 26%), + radial-gradient(circle at right 20%, rgba(34, 211, 238, 0.16), transparent 22%), + linear-gradient(160deg, #f8fbff 0%, #edf4ff 52%, #e9f0fa 100%); +} + +.login-brand-panel::before { + content: ''; + position: absolute; + inset: auto auto -12% -8%; + width: 280px; + height: 280px; + border-radius: 50%; + background: radial-gradient(circle, rgba(125, 211, 252, 0.2), transparent 68%); + pointer-events: none; +} + +.login-brand-panel::after { + content: ''; + position: absolute; + inset: 28px 26px auto auto; + width: 220px; + height: 220px; + border-radius: 50%; + background: radial-gradient(circle, rgba(59, 130, 246, 0.12), transparent 66%); + pointer-events: none; +} + +.login-brand-header { + display: flex; + align-items: center; + gap: 20px; +} + +.login-brand-summary { + display: flex; + flex-direction: column; + gap: 6px; +} + +.brand-mark-frame { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 10px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(227, 238, 252, 0.84)); + border: 1px solid rgba(191, 219, 254, 0.6); + box-shadow: + 0 24px 40px rgba(148, 163, 184, 0.22), + inset 0 1px 0 rgba(255, 255, 255, 0.94); +} + +.brand-mark-frame::after { + content: ''; + position: absolute; + inset: 6px; + border-radius: inherit; + border: 1px solid rgba(255, 255, 255, 0.62); + pointer-events: none; +} + +.brand-mark-frame-hero { + width: 118px; + height: 118px; + border-radius: 36px; +} + +.brand-mark-canvas { + width: 100%; + height: 100%; + padding: 10px; + border-radius: 28px; + overflow: hidden; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 250, 255, 0.92)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.96), + 0 12px 28px rgba(59, 130, 246, 0.1); +} + +.login-brand-logo { + width: 100%; + height: 100%; + object-fit: contain; + border-radius: 20px; + filter: drop-shadow(0 8px 18px rgba(37, 99, 235, 0.16)); +} + +.login-brand-name { + font-family: 'Avenir Next', 'Segoe UI Variable Display', 'PingFang SC', 'Noto Sans SC', sans-serif; + font-size: clamp(34px, 3.2vw, 46px); + font-weight: 800; + letter-spacing: -0.05em; +} + +.login-brand-kicker { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #64748b; +} + +.login-brand-copy-block { + display: grid; + max-width: 580px; +} + +.login-brand-copy { + max-width: 42ch; + margin: 0; + font-size: 18px; + line-height: 1.92; + color: var(--color-text-secondary); +} + +.login-compat-line { + max-width: 42ch; + font-size: 13px; + line-height: 1.8; + color: #475569; +} + +.login-capability-list { + display: grid; + gap: 0; + max-width: 44rem; + margin-top: auto; + border-top: 1px solid rgba(148, 163, 184, 0.18); +} + +.login-capability-row { + display: grid; + grid-template-columns: 36px minmax(0, 1fr); + gap: 16px; + align-items: start; + padding: 18px 0; + border-bottom: 1px solid rgba(148, 163, 184, 0.16); +} + +.login-capability-index { + padding-top: 3px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.18em; + color: #64748b; +} + +.login-capability-content { + display: grid; + gap: 6px; +} + +.login-capability-title { + font-family: 'Avenir Next', 'Segoe UI Variable Display', 'PingFang SC', 'Noto Sans SC', sans-serif; + font-size: 18px; + font-weight: 700; + letter-spacing: -0.03em; + color: #0f172a; +} + +.login-capability-desc { + margin: 0; + max-width: 34ch; + font-size: 14px; + line-height: 1.75; + color: var(--color-text-secondary); +} + +.login-brand-footer { + display: flex; + align-items: center; + gap: 12px; +} + +.login-icon-link { + display: inline-flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 999px; + border: 1px solid rgba(99, 102, 241, 0.12); + background: rgba(255, 255, 255, 0.76); + color: #0f172a; + text-decoration: none; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; +} + +.login-icon-link:hover { + transform: translateY(-1px); + border-color: rgba(99, 102, 241, 0.2); + box-shadow: 0 10px 18px rgba(37, 99, 235, 0.12); +} + +.login-icon-link-svg { + width: 18px; + height: 18px; +} + +.login-doc-link { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 10px 15px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.76); + color: var(--color-primary); + border: 1px solid rgba(99, 102, 241, 0.16); + font-size: 12px; + font-weight: 700; + text-decoration: none; + transition: transform 0.18s ease, box-shadow 0.18s ease; +} + +.login-doc-link:hover { + transform: translateY(-1px); + box-shadow: 0 10px 18px rgba(37, 99, 235, 0.12); +} + +.login-auth-stage { + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + border-radius: 30px; + background: + radial-gradient(circle at top, rgba(125, 211, 252, 0.16), transparent 34%), + linear-gradient(180deg, rgba(255, 255, 255, 0.44), rgba(241, 245, 249, 0.58)); +} + +.login-auth-panel { + width: min(100%, 392px); + padding: 40px 34px 28px; + border-radius: 28px; + border: 1px solid rgba(226, 232, 240, 0.92); + background: rgba(255, 255, 255, 0.92); + box-shadow: + 0 28px 50px rgba(148, 163, 184, 0.22), + inset 0 1px 0 rgba(255, 255, 255, 0.94); + backdrop-filter: blur(10px); +} + +.login-auth-eyebrow { + margin-bottom: 12px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--color-primary); +} + +.login-auth-title { + margin: 0 0 10px; + font-family: 'Avenir Next', 'Segoe UI Variable Display', 'PingFang SC', 'Noto Sans SC', sans-serif; + font-size: 42px; + font-weight: 800; + line-height: 0.96; + letter-spacing: -0.05em; +} + +.login-auth-copy { + margin: 0 0 24px; + font-size: 14px; + line-height: 1.7; + color: var(--color-text-secondary); +} + +.login-auth-label { + margin-bottom: 8px; + font-size: 12px; + font-weight: 700; + color: var(--color-text-secondary); +} + +.login-auth-input { + width: 100%; + padding: 14px 16px; + border: 1px solid color-mix(in srgb, var(--color-primary) 18%, var(--color-border)); + border-radius: 16px; + font-size: 14px; + outline: none; + background: rgba(248, 250, 252, 0.92); + color: var(--color-text-primary); + margin-bottom: 14px; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; +} + +.login-auth-input:focus { + border-color: color-mix(in srgb, var(--color-primary) 56%, transparent); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary) 12%, transparent); + background: var(--color-bg-card); +} + +.login-auth-submit { + width: 100%; + padding: 13px 0; + font-size: 14px; + font-weight: 700; + border-radius: 16px; + box-shadow: 0 16px 24px rgba(99, 102, 241, 0.2); +} + +.login-auth-note { + margin-top: 16px; + font-size: 12px; + line-height: 1.7; + color: var(--color-text-muted); +} + +.login-auth-footer { + margin-top: 18px; + padding-top: 14px; + border-top: 1px solid rgba(226, 232, 240, 0.86); + font-size: 12px; + color: var(--color-text-muted); +} + +[data-theme="dark"] .login-surface, +[data-theme="dark"] .brand-mark-frame, +[data-theme="dark"] .brand-mark-canvas, +[data-theme="dark"] .login-auth-stage, +[data-theme="dark"] .login-auth-panel { + border-color: color-mix(in srgb, var(--color-border) 86%, transparent); +} + +[data-theme="dark"] .login-shell, +[data-theme="dark"] .login-surface, +[data-theme="dark"] .brand-mark-frame, +[data-theme="dark"] .brand-mark-canvas, +[data-theme="dark"] .login-auth-stage, +[data-theme="dark"] .login-auth-panel { + background: color-mix(in srgb, var(--color-bg-card) 92%, #0f172a); +} + +[data-theme="dark"] .login-brand-kicker, +[data-theme="dark"] .login-compat-line, +[data-theme="dark"] .login-capability-index, +[data-theme="dark"] .login-icon-link, +[data-theme="dark"] .login-auth-note, +[data-theme="dark"] .login-auth-footer { + color: #cbd5e1; +} + +[data-theme="dark"] .login-capability-title { + color: #f8fafc; +} + +[data-theme="dark"] .login-auth-footer { + border-top-color: rgba(51, 65, 85, 0.9); +} + +@media (max-width: 1080px) { + .login-surface { + grid-template-columns: 1fr; + } + + .login-brand-panel { + min-height: auto; + } +} + +@media (max-width: 900px) { + .login-shell { + padding: 18px; + } + + .login-brand-panel { + min-height: auto; + padding: 36px 32px; + } + + .login-auth-stage { + padding: 20px; + } + + .login-auth-panel { + padding: 34px 28px 26px; + } + + .login-capability-row { + grid-template-columns: 32px minmax(0, 1fr); + gap: 12px; + } +} + +@media (max-width: 640px) { + .login-shell { + padding: 12px; + } + + .login-surface { + padding: 12px; + border-radius: 24px; + } + + .login-brand-panel { + padding: 28px 20px; + border-radius: 24px; + } + + .login-auth-stage { + padding: 14px; + border-radius: 24px; + } + + .login-auth-panel { + padding: 28px 22px 22px; + border-radius: 22px; + } + + .brand-mark-frame-hero { + width: 96px; + height: 96px; + border-radius: 28px; + } + + .login-brand-copy { + font-size: 15px; + } + + .login-brand-footer { + margin-top: 0; + } +} + /* ===== Empty State ===== */ .empty-state { text-align: center;