Commit 3909a33b authored by 陈曦's avatar 陈曦
Browse files

1.去除自动migration 2.前端home、login改变界面风格 3.调试pay

parent ef5c8e68
...@@ -11,8 +11,6 @@ import ( ...@@ -11,8 +11,6 @@ import (
"github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone" "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
"github.com/Wei-Shaw/sub2api/migrations"
"entgo.io/ent/dialect" "entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql" entsql "entgo.io/ent/dialect/sql"
_ "github.com/lib/pq" // PostgreSQL 驱动,通过副作用导入注册驱动 _ "github.com/lib/pq" // PostgreSQL 驱动,通过副作用导入注册驱动
...@@ -57,17 +55,19 @@ func InitEnt(cfg *config.Config) (*ent.Client, *sql.DB, error) { ...@@ -57,17 +55,19 @@ func InitEnt(cfg *config.Config) (*ent.Client, *sql.DB, error) {
// 确保数据库 schema 已准备就绪。 // 确保数据库 schema 已准备就绪。
// SQL 迁移文件是 schema 的权威来源(source of truth)。 // SQL 迁移文件是 schema 的权威来源(source of truth)。
// 这种方式比 Ent 的自动迁移更可控,支持复杂的迁移场景。 // 这种方式比 Ent 的自动迁移更可控,支持复杂的迁移场景。
migrationCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) // migrationCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel() // defer cancel()
if err := applyMigrationsFS(migrationCtx, drv.DB(), migrations.FS); err != nil { // if err := applyMigrationsFS(migrationCtx, drv.DB(), migrations.FS); err != nil {
_ = drv.Close() // 迁移失败时关闭驱动,避免资源泄露 // _ = drv.Close() // 迁移失败时关闭驱动,避免资源泄露
return nil, nil, err // return nil, nil, err
} // }
// 创建 Ent 客户端,绑定到已配置的数据库驱动。 // 创建 Ent 客户端,绑定到已配置的数据库驱动。
client := ent.NewClient(ent.Driver(drv)) client := ent.NewClient(ent.Driver(drv))
// 启动阶段:从配置或数据库中确保系统密钥可用。 // 启动阶段:从配置或数据库中确保系统密钥可用。
migrationCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
if err := ensureBootstrapSecrets(migrationCtx, client, cfg); err != nil { if err := ensureBootstrapSecrets(migrationCtx, client, cfg); err != nil {
_ = client.Close() _ = client.Close()
return nil, nil, err return nil, nil, err
......
...@@ -14,7 +14,6 @@ import ( ...@@ -14,7 +14,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger" "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/repository"
"github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/service"
_ "github.com/lib/pq" _ "github.com/lib/pq"
...@@ -345,9 +344,10 @@ func initializeDatabase(cfg *SetupConfig) error { ...@@ -345,9 +344,10 @@ func initializeDatabase(cfg *SetupConfig) error {
} }
}() }()
migrationCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // migrationCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel() // defer cancel()
return repository.ApplyMigrations(migrationCtx, db) // return repository.ApplyMigrations(migrationCtx, db)
return nil
} }
func createAdminUser(cfg *SetupConfig) (bool, string, error) { func createAdminUser(cfg *SetupConfig) (bool, string, error) {
......
<template> <template>
<div class="relative flex min-h-screen items-center justify-center overflow-hidden p-4"> <div class="auth-root relative flex min-h-screen items-center justify-center overflow-hidden p-4">
<!-- Background -->
<div
class="absolute inset-0 bg-gradient-to-br from-gray-50 via-primary-50/30 to-gray-100 dark:from-dark-950 dark:via-dark-900 dark:to-dark-950"
></div>
<!-- Decorative Elements --> <!-- Background -->
<div class="pointer-events-none absolute inset-0 overflow-hidden"> <div class="pointer-events-none absolute inset-0 overflow-hidden">
<!-- Gradient Orbs --> <div class="auth-orb auth-orb-tr"></div>
<div <div class="auth-orb auth-orb-bl"></div>
class="absolute -right-40 -top-40 h-80 w-80 rounded-full bg-primary-400/20 blur-3xl" <div class="auth-grid"></div>
></div> <div class="auth-scan"></div>
<div
class="absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-primary-500/15 blur-3xl"
></div>
<div
class="absolute left-1/2 top-1/2 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-300/10 blur-3xl"
></div>
<!-- Grid Pattern -->
<div
class="absolute inset-0 bg-[linear-gradient(rgba(20,184,166,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(20,184,166,0.03)_1px,transparent_1px)] bg-[size:64px_64px]"
></div>
</div> </div>
<!-- Content Container --> <!-- Content -->
<div class="relative z-10 w-full max-w-md"> <div class="relative z-10 w-full max-w-md">
<!-- Logo/Brand -->
<!-- Brand -->
<div class="mb-8 text-center"> <div class="mb-8 text-center">
<!-- Custom Logo or Default Logo -->
<template v-if="settingsLoaded"> <template v-if="settingsLoaded">
<div <!-- Logo -->
class="mb-4 inline-flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl shadow-lg shadow-primary-500/30" <div class="auth-logo-wrap mb-5 inline-flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl">
>
<img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" /> <img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
</div> </div>
<h1 class="text-gradient mb-2 text-3xl font-bold">
<!-- Site name -->
<h1 class="auth-site-name mb-2 text-3xl font-black tracking-tight">
{{ siteName }} {{ siteName }}
</h1> </h1>
<p class="text-sm text-gray-500 dark:text-dark-400">
{{ siteSubtitle }} <!-- Subtitle -->
<p class="auth-subtitle font-mono text-xs uppercase tracking-widest">
// {{ siteSubtitle }}
</p> </p>
</template> </template>
</div> </div>
<!-- Card Container --> <!-- Card -->
<div class="card-glass rounded-2xl p-8 shadow-glass"> <div class="auth-card rounded-2xl p-8">
<slot /> <slot />
</div> </div>
<!-- Footer Links --> <!-- Footer links -->
<div class="mt-6 text-center text-sm"> <div class="mt-6 text-center text-sm">
<slot name="footer" /> <slot name="footer" />
</div> </div>
<!-- Copyright --> <!-- Copyright -->
<div class="mt-8 text-center text-xs text-gray-400 dark:text-dark-500"> <div class="mt-8 text-center font-mono text-xs text-slate-600">
&copy; {{ currentYear }} {{ siteName }}. All rights reserved. &copy; {{ currentYear }} {{ siteName }}. All rights reserved.
</div> </div>
</div> </div>
...@@ -69,9 +57,9 @@ import { sanitizeUrl } from '@/utils/url' ...@@ -69,9 +57,9 @@ import { sanitizeUrl } from '@/utils/url'
const appStore = useAppStore() const appStore = useAppStore()
const siteName = computed(() => appStore.siteName || 'Sub2API') const siteName = computed(() => appStore.siteName || 'TrafficAPI')
const siteLogo = computed(() => sanitizeUrl(appStore.siteLogo || '', { allowRelative: true, allowDataUrl: true })) const siteLogo = computed(() => sanitizeUrl(appStore.siteLogo || '', { allowRelative: true, allowDataUrl: true }))
const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'Subscription to API Conversion Platform') const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'Intelligent AI Traffic Routing & Management')
const settingsLoaded = computed(() => appStore.publicSettingsLoaded) const settingsLoaded = computed(() => appStore.publicSettingsLoaded)
const currentYear = computed(() => new Date().getFullYear()) const currentYear = computed(() => new Date().getFullYear())
...@@ -82,7 +70,108 @@ onMounted(() => { ...@@ -82,7 +70,108 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.text-gradient { /* ── Root ────────────────────────────────── */
@apply bg-gradient-to-r from-primary-600 to-primary-500 bg-clip-text text-transparent; .auth-root {
background: #182a40;
color: white;
}
/* ── Background layers ───────────────────── */
.auth-orb {
position: absolute;
border-radius: 50%;
filter: blur(90px);
pointer-events: none;
}
.auth-orb-tr {
top: -10%;
right: -10%;
width: 420px;
height: 420px;
background: radial-gradient(ellipse, rgba(20,184,166,0.18) 0%, transparent 70%);
}
.auth-orb-bl {
bottom: -10%;
left: -10%;
width: 380px;
height: 380px;
background: radial-gradient(ellipse, rgba(6,182,212,0.13) 0%, transparent 70%);
}
.auth-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(20,184,166,0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(20,184,166,0.07) 1px, transparent 1px);
background-size: 48px 48px;
}
.auth-scan {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(20,184,166,0.45) 30%, rgba(20,184,166,0.7) 50%, rgba(20,184,166,0.45) 70%, transparent 100%);
animation: auth-scan 12s linear infinite;
box-shadow: 0 0 8px rgba(20,184,166,0.35);
}
@keyframes auth-scan {
0% { top: 0%; opacity: 0; }
4% { opacity: 1; }
96% { opacity: 0.4; }
100% { top: 100%; opacity: 0; }
}
/* ── Logo ────────────────────────────────── */
.auth-logo-wrap {
box-shadow:
0 0 0 1px rgba(20,184,166,0.3),
0 0 20px rgba(20,184,166,0.18),
0 8px 24px rgba(0,0,0,0.4);
}
/* ── Brand text ──────────────────────────── */
.auth-site-name {
background: linear-gradient(135deg, #ffffff 0%, #7dd3c8 45%, #14b8a6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.auth-subtitle {
color: rgba(20,184,166,0.5);
}
/* ── Card ────────────────────────────────── */
.auth-card {
background: rgba(26,46,72,0.72);
border: 1px solid rgba(255,255,255,0.09);
backdrop-filter: blur(18px);
box-shadow:
0 0 0 1px rgba(20,184,166,0.08),
0 24px 60px rgba(0,0,0,0.45),
0 0 50px rgba(20,184,166,0.05);
}
/* ── Slot overrides: make child inputs/links readable ── */
:deep(.input-label) {
color: #94a3b8;
}
:deep(.input) {
background: rgba(15,28,48,0.7);
border-color: rgba(255,255,255,0.1);
color: white;
}
:deep(.input:focus) {
border-color: rgba(20,184,166,0.5);
box-shadow: 0 0 0 3px rgba(20,184,166,0.12);
}
:deep(.input::placeholder) {
color: #475569;
}
:deep(h2) {
color: white !important;
}
:deep(.text-gray-500),
:deep(.dark\:text-dark-400) {
color: #64748b !important;
} }
</style> </style>
...@@ -10,89 +10,89 @@ export default { ...@@ -10,89 +10,89 @@ export default {
login: 'Login', login: 'Login',
getStarted: 'Get Started', getStarted: 'Get Started',
goToDashboard: 'Go to Dashboard', goToDashboard: 'Go to Dashboard',
// User-focused value proposition // TrafficAPI value proposition
heroSubtitle: 'One Key, All AI Models', heroSubtitle: 'Route Smarter. Scale Faster.',
heroDescription: 'No need to manage multiple subscriptions. Access Claude, GPT, Gemini and more with a single API key', heroDescription: 'TrafficAPI is the intelligent AI API gateway that routes your requests across Claude, GPT, Gemini and more with automatic load balancing, zero downtime, and real-time traffic analytics.',
tags: { tags: {
subscriptionToApi: 'Subscription to API', subscriptionToApi: 'Intelligent Routing',
stickySession: 'Session Persistence', stickySession: 'Auto Load Balancing',
realtimeBilling: 'Pay As You Go' realtimeBilling: 'Real-time Analytics'
}, },
// Pain points section // Pain points section
painPoints: { painPoints: {
title: 'Sound Familiar?', title: 'Sound Familiar?',
items: { items: {
expensive: { expensive: {
title: 'High Subscription Costs', title: 'Fragmented AI Costs',
desc: 'Paying for multiple AI subscriptions that add up every month' desc: 'Paying for multiple AI subscriptions that add up every month with no unified control'
}, },
complex: { complex: {
title: 'Account Chaos', title: 'Multi-Provider Chaos',
desc: 'Managing scattered accounts and API keys across different platforms' desc: 'Juggling different SDKs, credentials, and endpoints across Claude, GPT, Gemini and more'
}, },
unstable: { unstable: {
title: 'Service Interruptions', title: 'Rate Limit Downtime',
desc: 'Single accounts hitting rate limits and disrupting your workflow' desc: 'A single upstream account hitting limits can bring your entire application offline'
}, },
noControl: { noControl: {
title: 'No Usage Control', title: 'Zero Traffic Visibility',
desc: "Can't track where your money goes or limit team member usage" desc: "No insight into request volume, latency, or spend across providers and team members"
} }
} }
}, },
// Solutions section // Solutions section
solutions: { solutions: {
title: 'We Solve These Problems', title: 'TrafficAPI Solves This',
subtitle: 'Three simple steps to stress-free AI access' subtitle: 'One gateway to route, balance, and monitor all your AI traffic'
}, },
features: { features: {
unifiedGateway: 'One-Click Access', unifiedGateway: 'Unified API Endpoint',
unifiedGatewayDesc: 'Get a single API key to call all connected AI models. No separate applications needed.', unifiedGatewayDesc: 'One endpoint, one key — access every connected AI provider. No SDK changes, no credential juggling, zero migration friction.',
multiAccount: 'Always Reliable', multiAccount: 'Smart Load Balancing',
multiAccountDesc: 'Smart routing across multiple upstream accounts with automatic failover. Say goodbye to errors.', multiAccountDesc: 'Traffic is distributed intelligently across multiple upstream accounts. Automatic failover ensures zero downtime even when upstream limits are hit.',
balanceQuota: 'Pay What You Use', balanceQuota: 'Traffic Analytics',
balanceQuotaDesc: 'Usage-based billing with quota limits. Full visibility into team consumption.' balanceQuotaDesc: 'Monitor request volume, response latency, and per-model costs in real time. Set spending quotas per user or team with granular control.'
}, },
// Comparison section // Comparison section
comparison: { comparison: {
title: 'Why Choose Us?', title: 'Why TrafficAPI?',
headers: { headers: {
feature: 'Comparison', feature: 'Feature',
official: 'Official Subscriptions', official: 'Direct Provider',
us: 'Our Platform' us: 'TrafficAPI'
}, },
items: { items: {
pricing: { pricing: {
feature: 'Pricing', feature: 'Pricing',
official: 'Fixed monthly fee, pay even if unused', official: 'Fixed monthly fee per provider',
us: 'Pay only for what you use' us: 'Pay per request, unified billing'
}, },
models: { models: {
feature: 'Model Selection', feature: 'Model Access',
official: 'Single provider only', official: 'Single provider only',
us: 'Switch between models freely' us: 'Any model, one endpoint'
}, },
management: { management: {
feature: 'Account Management', feature: 'Account Management',
official: 'Manage each service separately', official: 'Each service managed separately',
us: 'Unified key, one dashboard' us: 'Unified dashboard, one API key'
}, },
stability: { stability: {
feature: 'Stability', feature: 'Reliability',
official: 'Single account rate limits', official: 'Single account, no failover',
us: 'Multi-account pool, auto-failover' us: 'Multi-account pool, auto-failover'
}, },
control: { control: {
feature: 'Usage Control', feature: 'Traffic Control',
official: 'Not available', official: 'Not available',
us: 'Quotas & detailed analytics' us: 'Quotas, rate limits & analytics'
} }
} }
}, },
providers: { providers: {
title: 'Supported AI Models', title: 'Supported AI Providers',
description: 'One API, Multiple Choices', description: 'One Gateway, Every Model',
supported: 'Supported', supported: 'Live',
soon: 'Soon', soon: 'Soon',
claude: 'Claude', claude: 'Claude',
gemini: 'Gemini', gemini: 'Gemini',
...@@ -101,9 +101,9 @@ export default { ...@@ -101,9 +101,9 @@ export default {
}, },
// CTA section // CTA section
cta: { cta: {
title: 'Ready to Get Started?', title: 'Start Routing Your AI Traffic',
description: 'Sign up now and get free trial credits to experience seamless AI access', description: 'Join teams who use TrafficAPI to unify their AI access, eliminate downtime, and gain full visibility into usage.',
button: 'Sign Up Free' button: 'Get Started Free'
}, },
footer: { footer: {
allRightsReserved: 'All rights reserved.' allRightsReserved: 'All rights reserved.'
...@@ -182,8 +182,8 @@ export default { ...@@ -182,8 +182,8 @@ export default {
// Setup Wizard // Setup Wizard
setup: { setup: {
title: 'Sub2API Setup', title: 'TrafficAPI Setup',
description: 'Configure your Sub2API instance', description: 'Configure your TrafficAPI instance',
database: { database: {
title: 'Database Configuration', title: 'Database Configuration',
description: 'Connect to your PostgreSQL database', description: 'Connect to your PostgreSQL database',
...@@ -357,8 +357,8 @@ export default { ...@@ -357,8 +357,8 @@ export default {
// Auth // Auth
auth: { auth: {
welcomeBack: 'Welcome Back', welcomeBack: 'Welcome Back to TrafficAPI',
signInToAccount: 'Sign in to your account to continue', signInToAccount: 'Sign in to manage your AI traffic, keys, and analytics',
signIn: 'Sign In', signIn: 'Sign In',
signingIn: 'Signing in...', signingIn: 'Signing in...',
createAccount: 'Create Account', createAccount: 'Create Account',
...@@ -1085,7 +1085,7 @@ export default { ...@@ -1085,7 +1085,7 @@ export default {
step1: { step1: {
title: 'Create an R2 Bucket', title: 'Create an R2 Bucket',
line1: 'Log in to the Cloudflare Dashboard (dash.cloudflare.com), select "R2 Object Storage" from the sidebar', line1: 'Log in to the Cloudflare Dashboard (dash.cloudflare.com), select "R2 Object Storage" from the sidebar',
line2: 'Click "Create bucket", enter a name (e.g. sub2api-backups), choose a region', line2: 'Click "Create bucket", enter a name (e.g. trafficapi-backups), choose a region',
line3: 'Click create to finish' line3: 'Click create to finish'
}, },
step2: { step2: {
...@@ -1934,7 +1934,7 @@ export default { ...@@ -1934,7 +1934,7 @@ export default {
antigravityOauth: 'Antigravity OAuth', antigravityOauth: 'Antigravity OAuth',
antigravityApikey: 'Connect via Base URL + API Key', antigravityApikey: 'Connect via Base URL + API Key',
soraApiKey: 'API Key / Upstream', soraApiKey: 'API Key / Upstream',
soraApiKeyHint: 'Connect to another Sub2API or compatible API', soraApiKeyHint: 'Connect to another TrafficAPI or compatible API',
soraBaseUrlRequired: 'Sora API Key account requires a Base URL', soraBaseUrlRequired: 'Sora API Key account requires a Base URL',
soraBaseUrlInvalidScheme: 'Base URL must start with http:// or https://', soraBaseUrlInvalidScheme: 'Base URL must start with http:// or https://',
upstream: 'Upstream', upstream: 'Upstream',
...@@ -2235,7 +2235,7 @@ export default { ...@@ -2235,7 +2235,7 @@ export default {
poolMode: 'Pool Mode', poolMode: 'Pool Mode',
poolModeHint: 'Enable when upstream is an account pool; errors won\'t mark local account status', poolModeHint: 'Enable when upstream is an account pool; errors won\'t mark local account status',
poolModeInfo: poolModeInfo:
'When enabled, upstream 429/403/401 errors will auto-retry without marking the account as rate-limited or errored. Suitable for upstream pointing to another sub2api instance.', 'When enabled, upstream 429/403/401 errors will auto-retry without marking the account as rate-limited or errored. Suitable for upstream pointing to another trafficapi instance.',
poolModeRetryCount: 'Same-Account Retries', poolModeRetryCount: 'Same-Account Retries',
poolModeRetryCountHint: poolModeRetryCountHint:
'Only applies in pool mode. Use 0 to disable in-place retry. Default {default}, maximum {max}.', 'Only applies in pool mode. Use 0 to disable in-place retry. Default {default}, maximum {max}.',
...@@ -2743,7 +2743,7 @@ export default { ...@@ -2743,7 +2743,7 @@ export default {
geminiImageTestMode: 'Mode: Gemini image generation test', geminiImageTestMode: 'Mode: Gemini image generation test',
geminiImagePreview: 'Generated images:', geminiImagePreview: 'Generated images:',
geminiImageReceived: 'Received test image #{count}', geminiImageReceived: 'Received test image #{count}',
soraUpstreamBaseUrlHint: 'Upstream Sora service URL (another Sub2API instance or compatible API)', soraUpstreamBaseUrlHint: 'Upstream Sora service URL (another TrafficAPI instance or compatible API)',
soraTestHint: 'Sora test runs connectivity and capability checks (/backend/me, subscription, Sora2 invite and remaining quota).', soraTestHint: 'Sora test runs connectivity and capability checks (/backend/me, subscription, Sora2 invite and remaining quota).',
soraTestTarget: 'Target: Sora account capability', soraTestTarget: 'Target: Sora account capability',
soraTestMode: 'Mode: Connectivity + Capability checks', soraTestMode: 'Mode: Connectivity + Capability checks',
...@@ -4123,7 +4123,7 @@ export default { ...@@ -4123,7 +4123,7 @@ export default {
secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.' }, secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.' },
linuxdo: { linuxdo: {
title: 'LinuxDo Connect Login', title: 'LinuxDo Connect Login',
description: 'Configure LinuxDo Connect OAuth for Sub2API end-user login', description: 'Configure LinuxDo Connect OAuth for TrafficAPI end-user login',
enable: 'Enable LinuxDo Login', enable: 'Enable LinuxDo Login',
enableHint: 'Show LinuxDo login on the login/register pages', enableHint: 'Show LinuxDo login on the login/register pages',
clientId: 'Client ID', clientId: 'Client ID',
...@@ -4190,7 +4190,7 @@ export default { ...@@ -4190,7 +4190,7 @@ export default {
backendModeDescription: backendModeDescription:
'Disables user registration, public site, and self-service features. Only admin can log in and manage the platform.', 'Disables user registration, public site, and self-service features. Only admin can log in and manage the platform.',
siteName: 'Site Name', siteName: 'Site Name',
siteNamePlaceholder: 'Sub2API', siteNamePlaceholder: 'TrafficAPI',
siteNameHint: 'Displayed in emails and page titles', siteNameHint: 'Displayed in emails and page titles',
siteSubtitle: 'Site Subtitle', siteSubtitle: 'Site Subtitle',
siteSubtitlePlaceholder: 'Subscription to API Conversion Platform', siteSubtitlePlaceholder: 'Subscription to API Conversion Platform',
...@@ -4290,7 +4290,7 @@ export default { ...@@ -4290,7 +4290,7 @@ export default {
fromEmail: 'From Email', fromEmail: 'From Email',
fromEmailPlaceholder: "noreply{'@'}example.com", fromEmailPlaceholder: "noreply{'@'}example.com",
fromName: 'From Name', fromName: 'From Name',
fromNamePlaceholder: 'Sub2API', fromNamePlaceholder: 'TrafficAPI',
useTls: 'Use TLS', useTls: 'Use TLS',
useTlsHint: 'Enable TLS encryption for SMTP connection' useTlsHint: 'Enable TLS encryption for SMTP connection'
}, },
...@@ -4721,14 +4721,14 @@ export default { ...@@ -4721,14 +4721,14 @@ export default {
// Admin tour steps // Admin tour steps
admin: { admin: {
welcome: { welcome: {
title: '👋 Welcome to Sub2API', title: '👋 Welcome to TrafficAPI',
description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Sub2API is a powerful AI service gateway platform that helps you easily manage and distribute AI services.</p><p style="margin-bottom: 12px;"><b>🎯 Core Features:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>Group Management</b> - Create service tiers (VIP, Free Trial, etc.)</li><li>🔗 <b>Account Pool</b> - Connect multiple upstream AI service accounts</li><li>🔑 <b>Key Distribution</b> - Generate independent API Keys for users</li><li>💰 <b>Billing Control</b> - Flexible rate and quota management</li></ul><p style="color: #10b981; font-weight: 600;">Let\'s complete the initial setup in 3 minutes →</p></div>', description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">TrafficAPI is your intelligent AI traffic gateway — route, balance, and monitor requests across every major AI provider from a single control plane.</p><p style="margin-bottom: 12px;"><b>🎯 Core Capabilities:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>Traffic Groups</b> - Define service tiers with routing policies (VIP, Free Trial, etc.)</li><li>🔗 <b>Account Pool</b> - Connect multiple upstream accounts for load balancing & failover</li><li>🔑 <b>API Key Distribution</b> - Issue independent keys per user with quota controls</li><li>📊 <b>Traffic Analytics</b> - Real-time visibility into requests, latency, and costs</li></ul><p style="color: #10b981; font-weight: 600;">Let\'s complete the initial setup in 3 minutes →</p></div>',
nextBtn: 'Start Setup 🚀', nextBtn: 'Start Setup 🚀',
prevBtn: 'Skip' prevBtn: 'Skip'
}, },
groupManage: { groupManage: {
title: '📦 Step 1: Group Management', title: '📦 Step 1: Group Management',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>What is a Group?</b></p><p style="margin-bottom: 12px;">Groups are the core concept of Sub2API, like a "service package":</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 Each group can contain multiple upstream accounts</li><li>💰 Each group has independent billing multiplier</li><li>👥 Can be set as public or exclusive</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Example:</b> You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "Group Management" on the left sidebar</p></div>' description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>What is a Group?</b></p><p style="margin-bottom: 12px;">Groups are the core concept of TrafficAPI, like a "service package":</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 Each group can contain multiple upstream accounts</li><li>💰 Each group has independent billing multiplier</li><li>👥 Can be set as public or exclusive</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Example:</b> You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "Group Management" on the left sidebar</p></div>'
}, },
createGroup: { createGroup: {
title: '➕ Create New Group', title: '➕ Create New Group',
...@@ -4821,8 +4821,8 @@ export default { ...@@ -4821,8 +4821,8 @@ export default {
// User tour steps // User tour steps
user: { user: {
welcome: { welcome: {
title: '👋 Welcome to Sub2API', title: '👋 Welcome to TrafficAPI',
description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Hello! Welcome to the Sub2API AI service platform.</p><p style="margin-bottom: 12px;"><b>🎯 Quick Start:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 Create API Key</li><li>📋 Copy key to your application</li><li>🚀 Start using AI services</li></ul><p style="color: #10b981; font-weight: 600;">Just 1 minute, let\'s get started →</p></div>', description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Hello! Welcome to TrafficAPI — the intelligent gateway for routing your AI API traffic across Claude, GPT, Gemini and more.</p><p style="margin-bottom: 12px;"><b>🎯 Quick Start:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 Create your API Key</li><li>📋 Use it with any OpenAI-compatible client</li><li>📊 Monitor traffic & usage in real time</li></ul><p style="color: #10b981; font-weight: 600;">Just 1 minute, let\'s get started →</p></div>',
nextBtn: 'Start 🚀', nextBtn: 'Start 🚀',
prevBtn: 'Skip' prevBtn: 'Skip'
}, },
......
...@@ -10,90 +10,90 @@ export default { ...@@ -10,90 +10,90 @@ export default {
login: '登录', login: '登录',
getStarted: '立即开始', getStarted: '立即开始',
goToDashboard: '进入控制台', goToDashboard: '进入控制台',
// 新增:面向用户的价值主张 // TrafficAPI 价值主张
heroSubtitle: '一个密钥,畅用多个 AI 模型', heroSubtitle: '智能路由,极速扩展。',
heroDescription: '无需管理多个订阅账号,一站式接入 Claude、GPT、Gemini 等主流 AI 服务', heroDescription: 'TrafficAPI 是面向企业的智能 AI API 网关,自动将请求路由至 Claude、GPT、Gemini 等多个服务商,具备负载均衡、零停机故障转移与实时流量分析能力。',
tags: { tags: {
subscriptionToApi: '订阅转 API', subscriptionToApi: '智能请求路由',
stickySession: '会话保持', stickySession: '自动负载均衡',
realtimeBilling: '按量计费' realtimeBilling: '实时流量分析'
}, },
// 用户痛点区块 // 用户痛点区块
painPoints: { painPoints: {
title: '你是否也遇到这些问题?', title: '你是否也遇到这些问题?',
items: { items: {
expensive: { expensive: {
title: '订阅费用高', title: 'AI 成本分散难控',
desc: '个 AI 服务都要单独订阅,每月支出越来越多' desc: '个 AI 服务各自订阅、各自付费,没有统一管控,月度支出节节攀升'
}, },
complex: { complex: {
title: '账号难管理', title: '服务商集成混乱',
desc: '不同平台的账号、密钥分散各处,管理起来很麻烦' desc: '不同平台的 SDK、凭证和接口地址各不相同,集成和维护成本极高'
}, },
unstable: { unstable: {
title: '服务不稳定', title: '限流导致服务中断',
desc: '单一账号容易触发限制,影响正常使用' desc: '单一上游账号触发限流,便可导致整个应用离线,严重影响业务可用性'
}, },
noControl: { noControl: {
title: '用量无法控制', title: '流量完全不透明',
desc: '不知道钱花在哪了,也无法限制团队成员的使用' desc: '无从了解各服务商的请求量、延迟分布与实际费用,团队用量更是无法管控'
} }
} }
}, },
// 解决方案区块 // 解决方案区块
solutions: { solutions: {
title: '我们帮你解决', title: 'TrafficAPI 为你解决',
subtitle: '简单三步,开始省心使用 AI' subtitle: '统一网关,路由、均衡、监控 AI 流量全掌握'
}, },
features: { features: {
unifiedGateway: '一键接入', unifiedGateway: '统一 API 接入',
unifiedGatewayDesc: '获取一个 API 密钥,即可调用所有已接入的 AI 模型,无需分别申请', unifiedGatewayDesc: '一个接入地址,一个 API Key,即可调用所有已接入的 AI 服务商。无需更改 SDK,无需管理多套凭证,迁移零成本',
multiAccount: '稳定可靠', multiAccount: '智能负载均衡',
multiAccountDesc: '智能调度多个上游账号,自动切换和负载均衡,告别频繁报错', multiAccountDesc: '流量自动分发至多个上游账号,智能调度与自动故障转移确保服务 7×24 小时在线,彻底告别限流宕机',
balanceQuota: '用多少付多少', balanceQuota: '流量分析与管控',
balanceQuotaDesc: '按实际使用量计费,支持设置配额上限,团队用量一目了然' balanceQuotaDesc: '实时监控请求量、响应延迟与各模型费用,按用户或团队设置消费配额,AI 流量全链路可视化、可管控'
}, },
// 优势对比 // 优势对比
comparison: { comparison: {
title: '为什么选择我们', title: '为什么选择 TrafficAPI',
headers: { headers: {
feature: '对比', feature: '对比维度',
official: '官方订阅', official: '直连服务商',
us: '本平台' us: 'TrafficAPI'
}, },
items: { items: {
pricing: { pricing: {
feature: '费方式', feature: '费方式',
official: '固定月费,用不完也付', official: '各服务商固定月费',
us: '量付费,用多少付多少' us: '请求计费,统一结算'
}, },
models: { models: {
feature: '模型选择', feature: '模型覆盖',
official: '单一服务商', official: '单一服务商',
us: '多模型随意切换' us: '任意模型,统一入口'
}, },
management: { management: {
feature: '账号管理', feature: '账号管理',
official: '每个服务单独管理', official: '各服务分散管理',
us: '统一密钥,一站管理' us: '统一控制台,一个 Key'
}, },
stability: { stability: {
feature: '服务稳定', feature: '服务可靠',
official: '单账号易触发限制', official: '单账号,无故障转移',
us: '多账号池,自动切换' us: '多账号池,自动故障转移'
}, },
control: { control: {
feature: '用量控制', feature: '流量管控',
official: '无法限制', official: '不支持',
us: '可设配额、查明细' us: '配额限制 + 详细分析'
} }
} }
}, },
providers: { providers: {
title: '已支持的 AI 模型', title: '已支持的 AI 服务商',
description: '一个 API,多种选择', description: '一个网关,接入所有模型',
supported: '支持', supported: '上线',
soon: '即将推出', soon: '即将支持',
claude: 'Claude', claude: 'Claude',
gemini: 'Gemini', gemini: 'Gemini',
antigravity: 'Antigravity', antigravity: 'Antigravity',
...@@ -101,9 +101,9 @@ export default { ...@@ -101,9 +101,9 @@ export default {
}, },
// CTA 区块 // CTA 区块
cta: { cta: {
title: '准备好开始了吗?', title: '开始路由你的 AI 流量',
description: '注册即可获得免费试用额度,体验一站式 AI 服务', description: '加入正在使用 TrafficAPI 的团队,统一 AI 接入、消除停机风险、获得完整的流量可见性。',
button: '免费注册' button: '免费开始使用'
}, },
footer: { footer: {
allRightsReserved: '保留所有权利。' allRightsReserved: '保留所有权利。'
...@@ -182,8 +182,8 @@ export default { ...@@ -182,8 +182,8 @@ export default {
// Setup Wizard // Setup Wizard
setup: { setup: {
title: 'Sub2API 安装向导', title: 'TrafficAPI 安装向导',
description: '配置您的 Sub2API 实例', description: '配置您的 TrafficAPI 实例',
database: { database: {
title: '数据库配置', title: '数据库配置',
description: '连接到您的 PostgreSQL 数据库', description: '连接到您的 PostgreSQL 数据库',
...@@ -357,8 +357,8 @@ export default { ...@@ -357,8 +357,8 @@ export default {
// Auth // Auth
auth: { auth: {
welcomeBack: '欢迎回', welcomeBack: '欢迎回到 TrafficAPI',
signInToAccount: '登录您的账户以继续', signInToAccount: '登录以管理您的 AI 流量、密钥与数据分析',
signIn: '登录', signIn: '登录',
signingIn: '登录中...', signingIn: '登录中...',
createAccount: '创建账户', createAccount: '创建账户',
...@@ -1107,7 +1107,7 @@ export default { ...@@ -1107,7 +1107,7 @@ export default {
step1: { step1: {
title: '创建 R2 存储桶', title: '创建 R2 存储桶',
line1: '登录 Cloudflare Dashboard (dash.cloudflare.com),左侧菜单选择「R2 对象存储」', line1: '登录 Cloudflare Dashboard (dash.cloudflare.com),左侧菜单选择「R2 对象存储」',
line2: '点击「创建存储桶」,输入名称(如 sub2api-backups),选择区域', line2: '点击「创建存储桶」,输入名称(如 trafficapi-backups),选择区域',
line3: '点击创建完成' line3: '点击创建完成'
}, },
step2: { step2: {
...@@ -2115,7 +2115,7 @@ export default { ...@@ -2115,7 +2115,7 @@ export default {
antigravityOauth: 'Antigravity OAuth', antigravityOauth: 'Antigravity OAuth',
antigravityApikey: '通过 Base URL + API Key 连接', antigravityApikey: '通过 Base URL + API Key 连接',
soraApiKey: 'API Key / 上游透传', soraApiKey: 'API Key / 上游透传',
soraApiKeyHint: '连接另一个 Sub2API 或兼容 API', soraApiKeyHint: '连接另一个 TrafficAPI 或兼容 API',
soraBaseUrlRequired: 'Sora apikey 账号必须设置上游地址(Base URL)', soraBaseUrlRequired: 'Sora apikey 账号必须设置上游地址(Base URL)',
soraBaseUrlInvalidScheme: 'Base URL 必须以 http:// 或 https:// 开头', soraBaseUrlInvalidScheme: 'Base URL 必须以 http:// 或 https:// 开头',
upstream: '对接上游', upstream: '对接上游',
...@@ -2382,7 +2382,7 @@ export default { ...@@ -2382,7 +2382,7 @@ export default {
poolMode: '池模式', poolMode: '池模式',
poolModeHint: '上游为账号池时启用,错误不标记本地账号状态', poolModeHint: '上游为账号池时启用,错误不标记本地账号状态',
poolModeInfo: poolModeInfo:
'启用后,上游 429/403/401 错误将自动重试而不标记账号限流或错误,适用于上游指向另一个 sub2api 实例的场景。', '启用后,上游 429/403/401 错误将自动重试而不标记账号限流或错误,适用于上游指向另一个 trafficapi 实例的场景。',
poolModeRetryCount: '同账号重试次数', poolModeRetryCount: '同账号重试次数',
poolModeRetryCountHint: '仅在池模式下生效。0 表示不原地重试;默认 {default},最大 {max}。', poolModeRetryCountHint: '仅在池模式下生效。0 表示不原地重试;默认 {default},最大 {max}。',
customErrorCodes: '自定义错误码', customErrorCodes: '自定义错误码',
...@@ -2874,7 +2874,7 @@ export default { ...@@ -2874,7 +2874,7 @@ export default {
geminiImageTestMode: '模式:Gemini 生图测试', geminiImageTestMode: '模式:Gemini 生图测试',
geminiImagePreview: '生成结果:', geminiImagePreview: '生成结果:',
geminiImageReceived: '已收到第 {count} 张测试图片', geminiImageReceived: '已收到第 {count} 张测试图片',
soraUpstreamBaseUrlHint: '上游 Sora 服务地址(另一个 Sub2API 实例或兼容 API)', soraUpstreamBaseUrlHint: '上游 Sora 服务地址(另一个 TrafficAPI 实例或兼容 API)',
soraTestHint: 'Sora 测试将执行连通性与能力检测(/backend/me、订阅信息、Sora2 邀请码与剩余额度)。', soraTestHint: 'Sora 测试将执行连通性与能力检测(/backend/me、订阅信息、Sora2 邀请码与剩余额度)。',
soraTestTarget: '检测目标:Sora 账号能力', soraTestTarget: '检测目标:Sora 账号能力',
soraTestMode: '模式:连通性 + 能力探测', soraTestMode: '模式:连通性 + 能力探测',
...@@ -4290,7 +4290,7 @@ export default { ...@@ -4290,7 +4290,7 @@ export default {
}, },
linuxdo: { linuxdo: {
title: 'LinuxDo Connect 登录', title: 'LinuxDo Connect 登录',
description: '配置 LinuxDo Connect OAuth,用于 Sub2API 用户登录', description: '配置 LinuxDo Connect OAuth,用于 TrafficAPI 用户登录',
enable: '启用 LinuxDo 登录', enable: '启用 LinuxDo 登录',
enableHint: '在登录/注册页面显示 LinuxDo 登录入口', enableHint: '在登录/注册页面显示 LinuxDo 登录入口',
clientId: 'Client ID', clientId: 'Client ID',
...@@ -4354,7 +4354,7 @@ export default { ...@@ -4354,7 +4354,7 @@ export default {
'禁用用户注册、公开页面和自助服务功能。仅管理员可以登录和管理平台。', '禁用用户注册、公开页面和自助服务功能。仅管理员可以登录和管理平台。',
siteName: '站点名称', siteName: '站点名称',
siteNameHint: '显示在邮件和页面标题中', siteNameHint: '显示在邮件和页面标题中',
siteNamePlaceholder: 'Sub2API', siteNamePlaceholder: 'TrafficAPI',
siteSubtitle: '站点副标题', siteSubtitle: '站点副标题',
siteSubtitleHint: '显示在登录和注册页面', siteSubtitleHint: '显示在登录和注册页面',
siteSubtitlePlaceholder: '订阅转 API 转换平台', siteSubtitlePlaceholder: '订阅转 API 转换平台',
...@@ -4455,7 +4455,7 @@ export default { ...@@ -4455,7 +4455,7 @@ export default {
fromEmail: '发件人邮箱', fromEmail: '发件人邮箱',
fromEmailPlaceholder: "noreply{'@'}example.com", fromEmailPlaceholder: "noreply{'@'}example.com",
fromName: '发件人名称', fromName: '发件人名称',
fromNamePlaceholder: 'Sub2API', fromNamePlaceholder: 'TrafficAPI',
useTls: '使用 TLS', useTls: '使用 TLS',
useTlsHint: '为 SMTP 连接启用 TLS 加密' useTlsHint: '为 SMTP 连接启用 TLS 加密'
}, },
...@@ -4883,16 +4883,16 @@ export default { ...@@ -4883,16 +4883,16 @@ export default {
// Admin tour steps // Admin tour steps
admin: { admin: {
welcome: { welcome: {
title: '👋 欢迎使用 Sub2API', title: '👋 欢迎使用 TrafficAPI',
description: description:
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Sub2API 是一个强大的 AI 服务中转平台,让您轻松管理和分发 AI 服务。</p><p style="margin-bottom: 12px;"><b>🎯 核心能:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>分组管理</b> - 创建不同的服务套餐(VIP、免费试用等)</li><li>🔗 <b>账号池</b> - 连接多个上游 AI 服务商账号</li><li>🔑 <b>密钥分发</b> - 为用户生成独立 API Key</li><li>💰 <b>计费管理</b> - 灵活的费率和配额控制</li></ul><p style="color: #10b981; font-weight: 600;">接下来,我们将用 3 分钟带您完成首次配置 →</p></div>', '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">TrafficAPI 是智能 AI 流量网关,帮助您统一路由、负载均衡并实时监控多个 AI 服务商的请求流量。</p><p style="margin-bottom: 12px;"><b>🎯 核心能:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>流量分组</b> - 按路由策略创建服务套餐(VIP、免费试用等)</li><li>🔗 <b>账号池</b> - 连接多个上游账号,实现负载均衡与故障转移</li><li>🔑 <b>密钥分发</b> - 为用户生成独立 API Key,支持配额管控</li><li>📊 <b>流量分析</b> - 实时查看请求量、响应延迟与费用</li></ul><p style="color: #10b981; font-weight: 600;">接下来,我们将用 3 分钟带您完成首次配置 →</p></div>',
nextBtn: '开始配置 🚀', nextBtn: '开始配置 🚀',
prevBtn: '跳过' prevBtn: '跳过'
}, },
groupManage: { groupManage: {
title: '📦 第一步:分组管理', title: '📦 第一步:分组管理',
description: description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>什么是分组?</b></p><p style="margin-bottom: 12px;">分组是 Sub2API 的核心概念,它就像一个"服务套餐":</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 每个分组可以包含多个上游账号</li><li>💰 每个分组有独立的计费倍率</li><li>👥 可以设置为公开或专属分组</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 示例:</b>您可以创建"VIP专线"(高倍率)和"免费试用"(低倍率)两个分组</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"分组管理"开始</p></div>' '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>什么是分组?</b></p><p style="margin-bottom: 12px;">分组是 TrafficAPI 的核心流量管理单元,类似一个"服务套餐":</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 每个分组可以包含多个上游账号</li><li>💰 每个分组有独立的计费倍率</li><li>👥 可以设置为公开或专属分组</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 示例:</b>您可以创建"VIP专线"(高倍率)和"免费试用"(低倍率)两个分组</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"分组管理"开始</p></div>'
}, },
createGroup: { createGroup: {
title: '➕ 创建新分组', title: '➕ 创建新分组',
...@@ -5004,9 +5004,9 @@ export default { ...@@ -5004,9 +5004,9 @@ export default {
// User tour steps // User tour steps
user: { user: {
welcome: { welcome: {
title: '👋 欢迎使用 Sub2API', title: '👋 欢迎使用 TrafficAPI',
description: description:
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">您好!欢迎来到 Sub2API AI 服务平台。</p><p style="margin-bottom: 12px;"><b>🎯 快速开始:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 创建 API 密钥</li><li>📋 复制密钥到您的应用</li><li>🚀 开始使用 AI 服务</li></ul><p style="color: #10b981; font-weight: 600;">只需 1 分钟,让我们开始吧 →</p></div>', '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">您好!欢迎来到 TrafficAPI —— 智能路由 Claude、GPT、Gemini 等多服务商 AI 请求的统一网关平台。</p><p style="margin-bottom: 12px;"><b>🎯 快速开始:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 创建您的 API 密钥</li><li>📋 在任意兼容 OpenAI 的客户端中使用</li><li>📊 实时查看流量与使用情况</li></ul><p style="color: #10b981; font-weight: 600;">只需 1 分钟,让我们开始吧 →</p></div>',
nextBtn: '开始 🚀', nextBtn: '开始 🚀',
prevBtn: '跳过' prevBtn: '跳过'
}, },
......
...@@ -28,7 +28,7 @@ async function bootstrap() { ...@@ -28,7 +28,7 @@ async function bootstrap() {
appStore.initFromInjectedConfig() appStore.initFromInjectedConfig()
// Set document title immediately after config is loaded // Set document title immediately after config is loaded
if (appStore.siteName && appStore.siteName !== 'Sub2API') { if (appStore.siteName && appStore.siteName !== 'TrafficAPI') {
document.title = `${appStore.siteName} - AI API Gateway` document.title = `${appStore.siteName} - AI API Gateway`
} }
......
...@@ -11,8 +11,8 @@ describe('resolveDocumentTitle', () => { ...@@ -11,8 +11,8 @@ describe('resolveDocumentTitle', () => {
}) })
it('站点名为空时,回退默认站点名', () => { it('站点名为空时,回退默认站点名', () => {
expect(resolveDocumentTitle('Dashboard', '')).toBe('Dashboard - Sub2API') expect(resolveDocumentTitle('Dashboard', '')).toBe('Dashboard - TrafficAPI')
expect(resolveDocumentTitle(undefined, ' ')).toBe('Sub2API') expect(resolveDocumentTitle(undefined, ' ')).toBe('TrafficAPI')
}) })
it('站点名变更时仅影响后续路由标题计算', () => { it('站点名变更时仅影响后续路由标题计算', () => {
......
...@@ -435,7 +435,7 @@ router.beforeEach((to, _from, next) => { ...@@ -435,7 +435,7 @@ router.beforeEach((to, _from, next) => {
const menuItem = publicItems.find((item) => item.id === id) const menuItem = publicItems.find((item) => item.id === id)
?? (authStore.isAdmin ? adminSettingsStore.customMenuItems.find((item) => item.id === id) : undefined) ?? (authStore.isAdmin ? adminSettingsStore.customMenuItems.find((item) => item.id === id) : undefined)
if (menuItem?.label) { if (menuItem?.label) {
const siteName = appStore.siteName || 'Sub2API' const siteName = appStore.siteName || 'TrafficAPI'
document.title = `${menuItem.label} - ${siteName}` document.title = `${menuItem.label} - ${siteName}`
} else { } else {
document.title = resolveDocumentTitle(to.meta.title, appStore.siteName, to.meta.titleKey as string) document.title = resolveDocumentTitle(to.meta.title, appStore.siteName, to.meta.titleKey as string)
......
...@@ -5,7 +5,7 @@ import { i18n } from '@/i18n' ...@@ -5,7 +5,7 @@ import { i18n } from '@/i18n'
* 优先使用 titleKey 通过 i18n 翻译,fallback 到静态 routeTitle。 * 优先使用 titleKey 通过 i18n 翻译,fallback 到静态 routeTitle。
*/ */
export function resolveDocumentTitle(routeTitle: unknown, siteName?: string, titleKey?: string): string { export function resolveDocumentTitle(routeTitle: unknown, siteName?: string, titleKey?: string): string {
const normalizedSiteName = typeof siteName === 'string' && siteName.trim() ? siteName.trim() : 'Sub2API' const normalizedSiteName = typeof siteName === 'string' && siteName.trim() ? siteName.trim() : 'TrafficAPI'
if (typeof titleKey === 'string' && titleKey.trim()) { if (typeof titleKey === 'string' && titleKey.trim()) {
const translated = i18n.global.t(titleKey) const translated = i18n.global.t(titleKey)
......
...@@ -24,7 +24,7 @@ export const useAppStore = defineStore('app', () => { ...@@ -24,7 +24,7 @@ export const useAppStore = defineStore('app', () => {
// Public settings cache state // Public settings cache state
const publicSettingsLoaded = ref<boolean>(false) const publicSettingsLoaded = ref<boolean>(false)
const publicSettingsLoading = ref<boolean>(false) const publicSettingsLoading = ref<boolean>(false)
const siteName = ref<string>('Sub2API') const siteName = ref<string>('TrafficAPI')
const siteLogo = ref<string>('') const siteLogo = ref<string>('')
const siteVersion = ref<string>('') const siteVersion = ref<string>('')
const contactInfo = ref<string>('') const contactInfo = ref<string>('')
...@@ -285,7 +285,7 @@ export const useAppStore = defineStore('app', () => { ...@@ -285,7 +285,7 @@ export const useAppStore = defineStore('app', () => {
*/ */
function applySettings(config: PublicSettings): void { function applySettings(config: PublicSettings): void {
cachedPublicSettings.value = config cachedPublicSettings.value = config
siteName.value = config.site_name || 'Sub2API' siteName.value = config.site_name || 'TrafficAPI'
siteLogo.value = config.site_logo || '' siteLogo.value = config.site_logo || ''
siteVersion.value = config.version || '' siteVersion.value = config.version || ''
contactInfo.value = config.contact_info || '' contactInfo.value = config.contact_info || ''
......
This diff is collapsed.
...@@ -372,7 +372,7 @@ const appStore = useAppStore() ...@@ -372,7 +372,7 @@ const appStore = useAppStore()
// ==================== Site Settings (same as HomeView) ==================== // ==================== Site Settings (same as HomeView) ====================
const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'Sub2API') const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'TrafficAPI')
const siteLogo = computed(() => appStore.cachedPublicSettings?.site_logo || appStore.siteLogo || '') const siteLogo = computed(() => appStore.cachedPublicSettings?.site_logo || appStore.siteLogo || '')
const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '') const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '')
const githubUrl = 'https://github.com/Wei-Shaw/sub2api' const githubUrl = 'https://github.com/Wei-Shaw/sub2api'
......
...@@ -2121,9 +2121,9 @@ const form = reactive<SettingsForm>({ ...@@ -2121,9 +2121,9 @@ const form = reactive<SettingsForm>({
default_balance: 0, default_balance: 0,
default_concurrency: 1, default_concurrency: 1,
default_subscriptions: [], default_subscriptions: [],
site_name: 'Sub2API', site_name: 'TrafficAPI',
site_logo: '', site_logo: '',
site_subtitle: 'Subscription to API Conversion Platform', site_subtitle: 'Intelligent AI Traffic Routing & Management',
api_base_url: '', api_base_url: '',
contact_info: '', contact_info: '',
doc_url: '', doc_url: '',
......
...@@ -212,7 +212,7 @@ const hasRegisterData = ref<boolean>(false) ...@@ -212,7 +212,7 @@ const hasRegisterData = ref<boolean>(false)
// Public settings // Public settings
const turnstileEnabled = ref<boolean>(false) const turnstileEnabled = ref<boolean>(false)
const turnstileSiteKey = ref<string>('') const turnstileSiteKey = ref<string>('')
const siteName = ref<string>('Sub2API') const siteName = ref<string>('TrafficAPI')
const registrationEmailSuffixWhitelist = ref<string[]>([]) const registrationEmailSuffixWhitelist = ref<string[]>([])
// Turnstile for resend // Turnstile for resend
...@@ -249,7 +249,7 @@ onMounted(async () => { ...@@ -249,7 +249,7 @@ onMounted(async () => {
const settings = await getPublicSettings() const settings = await getPublicSettings()
turnstileEnabled.value = settings.turnstile_enabled turnstileEnabled.value = settings.turnstile_enabled
turnstileSiteKey.value = settings.turnstile_site_key || '' turnstileSiteKey.value = settings.turnstile_site_key || ''
siteName.value = settings.site_name || 'Sub2API' siteName.value = settings.site_name || 'TrafficAPI'
registrationEmailSuffixWhitelist.value = normalizeRegistrationEmailSuffixWhitelist( registrationEmailSuffixWhitelist.value = normalizeRegistrationEmailSuffixWhitelist(
settings.registration_email_suffix_whitelist || [] settings.registration_email_suffix_whitelist || []
) )
......
...@@ -322,7 +322,7 @@ const promoCodeEnabled = ref<boolean>(true) ...@@ -322,7 +322,7 @@ const promoCodeEnabled = ref<boolean>(true)
const invitationCodeEnabled = ref<boolean>(false) const invitationCodeEnabled = ref<boolean>(false)
const turnstileEnabled = ref<boolean>(false) const turnstileEnabled = ref<boolean>(false)
const turnstileSiteKey = ref<string>('') const turnstileSiteKey = ref<string>('')
const siteName = ref<string>('Sub2API') const siteName = ref<string>('TrafficAPI')
const linuxdoOAuthEnabled = ref<boolean>(false) const linuxdoOAuthEnabled = ref<boolean>(false)
const registrationEmailSuffixWhitelist = ref<string[]>([]) const registrationEmailSuffixWhitelist = ref<string[]>([])
...@@ -374,7 +374,7 @@ onMounted(async () => { ...@@ -374,7 +374,7 @@ onMounted(async () => {
invitationCodeEnabled.value = settings.invitation_code_enabled invitationCodeEnabled.value = settings.invitation_code_enabled
turnstileEnabled.value = settings.turnstile_enabled turnstileEnabled.value = settings.turnstile_enabled
turnstileSiteKey.value = settings.turnstile_site_key || '' turnstileSiteKey.value = settings.turnstile_site_key || ''
siteName.value = settings.site_name || 'Sub2API' siteName.value = settings.site_name || 'TrafficAPI'
linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled
registrationEmailSuffixWhitelist.value = normalizeRegistrationEmailSuffixWhitelist( registrationEmailSuffixWhitelist.value = normalizeRegistrationEmailSuffixWhitelist(
settings.registration_email_suffix_whitelist || [] settings.registration_email_suffix_whitelist || []
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment