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 || ''
......
<template> <template>
<!-- Custom Home Content: Full Page Mode --> <!-- Custom Home Content: Full Page Mode -->
<div v-if="homeContent" class="min-h-screen"> <div v-if="homeContent" class="min-h-screen">
<!-- iframe mode -->
<iframe <iframe
v-if="isHomeContentUrl" v-if="isHomeContentUrl"
:src="homeContent.trim()" :src="homeContent.trim()"
...@@ -12,418 +11,339 @@ ...@@ -12,418 +11,339 @@
<div v-else v-html="homeContent"></div> <div v-else v-html="homeContent"></div>
</div> </div>
<!-- Default Home Page --> <!-- Default Home Page — Tech Redesign -->
<div <div v-else class="home-root relative flex min-h-screen flex-col overflow-hidden">
v-else
class="relative flex min-h-screen flex-col overflow-hidden 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" <!-- ══════════════════════════════════════════
> BACKGROUND
<!-- Background Decorations --> ══════════════════════════════════════════ -->
<div class="pointer-events-none absolute inset-0 overflow-hidden"> <div class="pointer-events-none absolute inset-0 overflow-hidden">
<div <!-- Glow orbs -->
class="absolute -right-40 -top-40 h-96 w-96 rounded-full bg-primary-400/20 blur-3xl" <div class="orb orb-tr"></div>
></div> <div class="orb orb-bl"></div>
<div <div class="orb orb-center"></div>
class="absolute -bottom-40 -left-40 h-96 w-96 rounded-full bg-primary-500/15 blur-3xl" <!-- Grid -->
></div> <div class="grid-overlay"></div>
<div <!-- Scan line -->
class="absolute left-1/3 top-1/4 h-72 w-72 rounded-full bg-primary-300/10 blur-3xl" <div class="scan-line"></div>
></div>
<div
class="absolute bottom-1/4 right-1/4 h-64 w-64 rounded-full bg-primary-400/10 blur-3xl"
></div>
<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>
<!-- Header --> <!-- ══════════════════════════════════════════
<header class="relative z-20 px-6 py-4"> HEADER
══════════════════════════════════════════ -->
<header class="home-header relative z-20 px-6 py-4">
<nav class="mx-auto flex max-w-6xl items-center justify-between"> <nav class="mx-auto flex max-w-6xl items-center justify-between">
<!-- Logo --> <!-- Logo + Brand -->
<div class="flex items-center"> <div class="flex items-center gap-3">
<div class="h-10 w-10 overflow-hidden rounded-xl shadow-md"> <div class="logo-ring h-9 w-9 overflow-hidden rounded-lg">
<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>
<span class="hidden font-mono text-sm font-semibold uppercase tracking-widest text-white/70 sm:block">
{{ siteName }}
</span>
</div> </div>
<!-- Nav Actions --> <!-- Actions -->
<div class="flex items-center gap-3"> <div class="flex items-center gap-2">
<!-- Language Switcher -->
<LocaleSwitcher /> <LocaleSwitcher />
<!-- Doc Link -->
<a <a
v-if="docUrl" v-if="docUrl"
:href="docUrl" :href="docUrl"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-dark-400 dark:hover:bg-dark-800 dark:hover:text-white" class="nav-btn"
:title="t('home.viewDocs')" :title="t('home.viewDocs')"
> >
<Icon name="book" size="md" /> <Icon name="book" size="sm" class="mr-1" />
<span class="hidden sm:inline">{{ t('home.docs') }}</span>
</a> </a>
<!-- Theme Toggle --> <button @click="toggleTheme" class="nav-btn icon-only" :title="isDark ? t('home.switchToLight') : t('home.switchToDark')">
<button <Icon v-if="isDark" name="sun" size="sm" />
@click="toggleTheme" <Icon v-else name="moon" size="sm" />
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-dark-400 dark:hover:bg-dark-800 dark:hover:text-white"
:title="isDark ? t('home.switchToLight') : t('home.switchToDark')"
>
<Icon v-if="isDark" name="sun" size="md" />
<Icon v-else name="moon" size="md" />
</button> </button>
<!-- Login / Dashboard Button -->
<router-link <router-link
v-if="isAuthenticated" v-if="isAuthenticated"
:to="dashboardPath" :to="dashboardPath"
class="inline-flex items-center gap-1.5 rounded-full bg-gray-900 py-1 pl-1 pr-2.5 transition-colors hover:bg-gray-800 dark:bg-gray-800 dark:hover:bg-gray-700" class="cta-nav-btn"
> >
<span <span class="user-dot">{{ userInitial }}</span>
class="flex h-5 w-5 items-center justify-center rounded-full bg-gradient-to-br from-primary-400 to-primary-600 text-[10px] font-semibold text-white" {{ t('home.dashboard') }}
> <svg class="h-3 w-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
{{ userInitial }} <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
</span>
<span class="text-xs font-medium text-white">{{ t('home.dashboard') }}</span>
<svg
class="h-3 w-3 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25"
/>
</svg> </svg>
</router-link> </router-link>
<router-link <router-link v-else to="/login" class="cta-nav-btn">
v-else
to="/login"
class="inline-flex items-center rounded-full bg-gray-900 px-3 py-1 text-xs font-medium text-white transition-colors hover:bg-gray-800 dark:bg-gray-800 dark:hover:bg-gray-700"
>
{{ t('home.login') }} {{ t('home.login') }}
</router-link> </router-link>
</div> </div>
</nav> </nav>
</header> </header>
<!-- Main Content --> <!-- ══════════════════════════════════════════
<main class="relative z-10 flex-1 px-6 py-16"> MAIN
<div class="mx-auto max-w-6xl"> ══════════════════════════════════════════ -->
<!-- Hero Section - Left/Right Layout --> <main class="relative z-10 flex-1 px-6">
<div class="mb-12 flex flex-col items-center justify-between gap-12 lg:flex-row lg:gap-16">
<!-- Left: Text Content --> <!-- ── HERO ────────────────────────────── -->
<section class="mx-auto max-w-6xl py-20 lg:py-28">
<div class="flex flex-col items-center gap-14 lg:flex-row lg:gap-20">
<!-- Text side -->
<div class="flex-1 text-center lg:text-left"> <div class="flex-1 text-center lg:text-left">
<h1 <!-- Badge -->
class="mb-4 text-4xl font-bold text-gray-900 dark:text-white md:text-5xl lg:text-6xl" <div class="mb-7 inline-flex items-center gap-2.5 rounded-full border border-teal-500/25 bg-teal-500/8 px-4 py-1.5 font-mono text-xs font-medium text-teal-400">
> <span class="h-1.5 w-1.5 animate-pulse rounded-full bg-teal-400 shadow-[0_0_6px_#14b8a6]"></span>
{{ siteName }} AI API GATEWAY · ENTERPRISE GRADE
</div>
<!-- Headline -->
<h1 class="mb-5 font-black leading-[1.08] tracking-tight">
<span class="gradient-text block whitespace-nowrap text-[clamp(1.6rem,4.5vw,3.75rem)]">{{ t('home.heroSubtitle') }}</span>
</h1> </h1>
<p class="mb-8 text-lg text-gray-600 dark:text-dark-300 md:text-xl">
{{ siteSubtitle }} <!-- Mono accent line -->
<p class="mb-5 font-mono text-[11px] uppercase tracking-[0.2em] text-teal-600/60">
// {{ siteName }} · intelligent traffic layer
</p>
<p class="mb-10 max-w-lg text-base leading-relaxed text-slate-400 lg:text-lg">
{{ t('home.heroDescription') }}
</p> </p>
<!-- CTA Button --> <!-- CTA buttons -->
<div> <div class="flex flex-wrap items-center justify-center gap-4 lg:justify-start">
<router-link <router-link
:to="isAuthenticated ? dashboardPath : '/login'" :to="isAuthenticated ? dashboardPath : '/login'"
class="btn btn-primary px-8 py-3 text-base shadow-lg shadow-primary-500/30" class="cta-primary inline-flex items-center gap-2 rounded-lg px-7 py-3 text-sm font-semibold text-white"
> >
{{ isAuthenticated ? t('home.goToDashboard') : t('home.getStarted') }} {{ isAuthenticated ? t('home.goToDashboard') : t('home.getStarted') }}
<Icon name="arrowRight" size="md" class="ml-2" :stroke-width="2" /> <Icon name="arrowRight" size="sm" :stroke-width="2.5" />
</router-link> </router-link>
<a
v-if="docUrl"
:href="docUrl"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 rounded-lg border border-white/10 px-7 py-3 text-sm font-medium text-slate-300 transition-all hover:border-teal-500/30 hover:text-teal-300"
>
<Icon name="book" size="sm" />
{{ t('home.viewDocs') }}
</a>
</div>
<!-- Stats -->
<div class="mt-12 flex flex-wrap items-center justify-center gap-8 lg:justify-start">
<div v-for="s in stats" :key="s.label" class="stat-item">
<span class="stat-value">{{ s.value }}</span>
<span class="stat-label">{{ s.label }}</span>
</div>
</div> </div>
</div> </div>
<!-- Right: Terminal Animation --> <!-- Terminal side -->
<div class="flex flex-1 justify-center lg:justify-end"> <div class="flex flex-1 justify-center lg:justify-end">
<div class="terminal-container"> <div class="terminal-wrap">
<div class="terminal-glow"></div>
<div class="terminal-window"> <div class="terminal-window">
<!-- Window header -->
<!-- Header bar -->
<div class="terminal-header"> <div class="terminal-header">
<div class="terminal-buttons"> <div class="terminal-dots">
<span class="btn-close"></span> <span class="dot-red"></span>
<span class="btn-minimize"></span> <span class="dot-yellow"></span>
<span class="btn-maximize"></span> <span class="dot-green"></span>
</div> </div>
<span class="terminal-title">terminal</span> <span class="terminal-title">trafficapi · gateway</span>
<span class="terminal-live">
<span class="h-1.5 w-1.5 animate-pulse rounded-full bg-teal-400 inline-block shadow-[0_0_5px_#14b8a6]"></span>
LIVE
</span>
</div> </div>
<!-- Terminal content -->
<!-- Code body -->
<div class="terminal-body"> <div class="terminal-body">
<div class="code-line line-1"> <div class="t-line line-1">
<span class="code-prompt">$</span> <span class="t-prompt"></span>
<span class="code-cmd">curl</span> <span class="t-method">POST</span>
<span class="code-flag">-X POST</span> <span class="t-path">/v1/messages</span>
<span class="code-url">/v1/messages</span> <span class="t-dim ml-auto">claude-3-5-sonnet</span>
</div>
<div class="t-line line-2">
<span class="t-comment">&nbsp;&nbsp;⠿ routing to pool · 3 accounts available</span>
</div>
<div class="t-line line-3">
<span class="t-ok">✓ 200</span>
<span class="t-ms">38ms</span>
<span class="t-dim">· account[1] selected</span>
</div>
<div class="t-sep sep-1"></div>
<div class="t-line line-4">
<span class="t-prompt"></span>
<span class="t-method">POST</span>
<span class="t-path">/v1/chat/completions</span>
<span class="t-dim ml-auto">gpt-4o</span>
</div>
<div class="t-line line-5">
<span class="t-comment">&nbsp;&nbsp;⠿ load balancing · 2 upstream accounts</span>
</div> </div>
<div class="code-line line-2"> <div class="t-line line-6">
<span class="code-comment"># Routing to upstream...</span> <span class="t-ok">✓ 200</span>
<span class="t-ms">57ms</span>
<span class="t-dim">· account[3] selected</span>
</div> </div>
<div class="code-line line-3">
<span class="code-success">200 OK</span> <div class="t-sep sep-2"></div>
<span class="code-response">{ "content": "Hello!" }</span>
<div class="t-line line-7">
<span class="t-stat">📊 analytics</span>
<span class="t-dim">&nbsp;req: 1.4k/min · p99: 68ms · err: 0%</span>
</div> </div>
<div class="code-line line-4"> <div class="t-line line-8">
<span class="code-prompt">$</span> <span class="t-prompt">_</span>
<span class="cursor"></span> <span class="t-cursor"></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section>
<!-- Feature Tags - Centered -->
<div class="mb-12 flex flex-wrap items-center justify-center gap-4 md:gap-6"> <!-- ── TAGS BAR ─────────────────────────── -->
<div <section class="mx-auto max-w-6xl pb-14">
class="inline-flex items-center gap-2.5 rounded-full border border-gray-200/50 bg-white/80 px-5 py-2.5 shadow-sm backdrop-blur-sm dark:border-dark-700/50 dark:bg-dark-800/80" <div class="flex flex-wrap items-center justify-center gap-3">
> <div v-for="tag in featureTags" :key="tag.text" class="tag-pill">
<Icon name="swap" size="sm" class="text-primary-500" /> <span class="tag-pip"></span>
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{ {{ tag.text }}
t('home.tags.subscriptionToApi')
}}</span>
</div>
<div
class="inline-flex items-center gap-2.5 rounded-full border border-gray-200/50 bg-white/80 px-5 py-2.5 shadow-sm backdrop-blur-sm dark:border-dark-700/50 dark:bg-dark-800/80"
>
<Icon name="shield" size="sm" class="text-primary-500" />
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{
t('home.tags.stickySession')
}}</span>
</div>
<div
class="inline-flex items-center gap-2.5 rounded-full border border-gray-200/50 bg-white/80 px-5 py-2.5 shadow-sm backdrop-blur-sm dark:border-dark-700/50 dark:bg-dark-800/80"
>
<Icon name="chart" size="sm" class="text-primary-500" />
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{
t('home.tags.realtimeBilling')
}}</span>
</div> </div>
</div> </div>
</section>
<!-- Features Grid --> <!-- ── FEATURES ─────────────────────────── -->
<div class="mb-12 grid gap-6 md:grid-cols-3"> <section class="mx-auto max-w-6xl pb-16">
<!-- Feature 1: Unified Gateway --> <div class="mb-10 text-center">
<div <h2 class="mb-2 text-2xl font-bold text-white md:text-3xl">
class="group rounded-2xl border border-gray-200/50 bg-white/60 p-6 backdrop-blur-sm transition-all duration-300 hover:shadow-xl hover:shadow-primary-500/10 dark:border-dark-700/50 dark:bg-dark-800/60" <span class="gradient-text">{{ t('home.solutions.title') }}</span>
> </h2>
<div <p class="font-mono text-xs uppercase tracking-widest text-slate-600">
class="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg shadow-blue-500/30 transition-transform group-hover:scale-110" // {{ t('home.solutions.subtitle') }}
> </p>
<Icon name="server" size="lg" class="text-white" /> </div>
</div>
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
{{ t('home.features.unifiedGateway') }}
</h3>
<p class="text-sm leading-relaxed text-gray-600 dark:text-dark-400">
{{ t('home.features.unifiedGatewayDesc') }}
</p>
</div>
<!-- Feature 2: Account Pool --> <div class="grid gap-4 md:grid-cols-3">
<div <div
class="group rounded-2xl border border-gray-200/50 bg-white/60 p-6 backdrop-blur-sm transition-all duration-300 hover:shadow-xl hover:shadow-primary-500/10 dark:border-dark-700/50 dark:bg-dark-800/60" v-for="feat in features"
:key="feat.key"
class="feat-card group"
> >
<div <div class="feat-accent" :style="{ background: feat.color }"></div>
class="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary-500 to-primary-600 shadow-lg shadow-primary-500/30 transition-transform group-hover:scale-110" <div class="feat-corner-tr"></div>
> <div class="feat-corner-bl"></div>
<svg
class="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z"
/>
</svg>
</div>
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
{{ t('home.features.multiAccount') }}
</h3>
<p class="text-sm leading-relaxed text-gray-600 dark:text-dark-400">
{{ t('home.features.multiAccountDesc') }}
</p>
</div>
<!-- Feature 3: Billing & Quota -->
<div
class="group rounded-2xl border border-gray-200/50 bg-white/60 p-6 backdrop-blur-sm transition-all duration-300 hover:shadow-xl hover:shadow-primary-500/10 dark:border-dark-700/50 dark:bg-dark-800/60"
>
<div <div
class="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 shadow-lg shadow-purple-500/30 transition-transform group-hover:scale-110" class="feat-icon mb-5"
:style="{ background: feat.iconBg, boxShadow: `0 4px 20px ${feat.glow}` }"
> >
<svg <component :is="feat.iconComponent" />
class="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z"
/>
</svg>
</div> </div>
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
{{ t('home.features.balanceQuota') }} <h3 class="mb-2 font-semibold text-white">{{ t(`home.features.${feat.key}`) }}</h3>
</h3> <p class="text-sm leading-relaxed text-slate-500 transition-colors group-hover:text-slate-400">
<p class="text-sm leading-relaxed text-gray-600 dark:text-dark-400"> {{ t(`home.features.${feat.key}Desc`) }}
{{ t('home.features.balanceQuotaDesc') }}
</p> </p>
</div> </div>
</div> </div>
</section>
<!-- Supported Providers --> <!-- ── PROVIDERS ────────────────────────── -->
<section class="mx-auto max-w-6xl pb-20">
<div class="mb-8 text-center"> <div class="mb-8 text-center">
<h2 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white"> <h2 class="mb-2 text-xl font-bold text-white">{{ t('home.providers.title') }}</h2>
{{ t('home.providers.title') }} <p class="font-mono text-xs uppercase tracking-[0.2em] text-slate-600">
</h2> // {{ t('home.providers.description') }}
<p class="text-sm text-gray-600 dark:text-dark-400">
{{ t('home.providers.description') }}
</p> </p>
</div> </div>
<div class="mb-16 flex flex-wrap items-center justify-center gap-4"> <div class="flex flex-wrap items-center justify-center gap-3">
<!-- Claude - Supported -->
<div
class="flex items-center gap-2 rounded-xl border border-primary-200 bg-white/60 px-5 py-3 ring-1 ring-primary-500/20 backdrop-blur-sm dark:border-primary-800 dark:bg-dark-800/60"
>
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-orange-400 to-orange-500"
>
<span class="text-xs font-bold text-white">C</span>
</div>
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{ t('home.providers.claude') }}</span>
<span
class="rounded bg-primary-100 px-1.5 py-0.5 text-[10px] font-medium text-primary-600 dark:bg-primary-900/30 dark:text-primary-400"
>{{ t('home.providers.supported') }}</span
>
</div>
<!-- GPT - Supported -->
<div
class="flex items-center gap-2 rounded-xl border border-primary-200 bg-white/60 px-5 py-3 ring-1 ring-primary-500/20 backdrop-blur-sm dark:border-primary-800 dark:bg-dark-800/60"
>
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-green-500 to-green-600"
>
<span class="text-xs font-bold text-white">G</span>
</div>
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">GPT</span>
<span
class="rounded bg-primary-100 px-1.5 py-0.5 text-[10px] font-medium text-primary-600 dark:bg-primary-900/30 dark:text-primary-400"
>{{ t('home.providers.supported') }}</span
>
</div>
<!-- Gemini - Supported -->
<div
class="flex items-center gap-2 rounded-xl border border-primary-200 bg-white/60 px-5 py-3 ring-1 ring-primary-500/20 backdrop-blur-sm dark:border-primary-800 dark:bg-dark-800/60"
>
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"
>
<span class="text-xs font-bold text-white">G</span>
</div>
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{ t('home.providers.gemini') }}</span>
<span
class="rounded bg-primary-100 px-1.5 py-0.5 text-[10px] font-medium text-primary-600 dark:bg-primary-900/30 dark:text-primary-400"
>{{ t('home.providers.supported') }}</span
>
</div>
<!-- Antigravity - Supported -->
<div <div
class="flex items-center gap-2 rounded-xl border border-primary-200 bg-white/60 px-5 py-3 ring-1 ring-primary-500/20 backdrop-blur-sm dark:border-primary-800 dark:bg-dark-800/60" v-for="p in providerList"
:key="p.name"
class="provider-card"
:class="{ 'provider-active': p.live, 'provider-inactive': !p.live }"
> >
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-rose-500 to-pink-600"
>
<span class="text-xs font-bold text-white">A</span>
</div>
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{ t('home.providers.antigravity') }}</span>
<span <span
class="rounded bg-primary-100 px-1.5 py-0.5 text-[10px] font-medium text-primary-600 dark:bg-primary-900/30 dark:text-primary-400" class="provider-status-dot"
>{{ t('home.providers.supported') }}</span :class="p.live ? 'dot-live' : 'dot-offline'"
> ></span>
</div> <div class="provider-icon-badge" :style="{ background: p.gradient }">
<!-- More - Coming Soon --> <span class="text-[11px] font-bold text-white">{{ p.letter }}</span>
<div
class="flex items-center gap-2 rounded-xl border border-gray-200/50 bg-white/40 px-5 py-3 opacity-60 backdrop-blur-sm dark:border-dark-700/50 dark:bg-dark-800/40"
>
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-gray-500 to-gray-600"
>
<span class="text-xs font-bold text-white">+</span>
</div> </div>
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{ t('home.providers.more') }}</span> <span class="text-sm font-medium text-slate-300">{{ p.name }}</span>
<span <span class="provider-tag" :class="p.live ? 'tag-live' : 'tag-soon'">
class="rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500 dark:bg-dark-700 dark:text-dark-400" {{ p.live ? t('home.providers.supported') : t('home.providers.soon') }}
>{{ t('home.providers.soon') }}</span </span>
>
</div> </div>
</div> </div>
</div> </section>
</main> </main>
<!-- Footer --> <!-- ══════════════════════════════════════════
<footer class="relative z-10 border-t border-gray-200/50 px-6 py-8 dark:border-dark-800/50"> FOOTER
<div ══════════════════════════════════════════ -->
class="mx-auto flex max-w-6xl flex-col items-center justify-center gap-4 text-center sm:flex-row sm:text-left" <footer class="home-footer relative z-10 px-6 py-6">
> <div class="mx-auto flex max-w-6xl flex-col items-center justify-between gap-3 text-center sm:flex-row sm:text-left">
<p class="text-sm text-gray-500 dark:text-dark-400"> <p class="font-mono text-xs text-slate-700">
&copy; {{ currentYear }} {{ siteName }}. {{ t('home.footer.allRightsReserved') }} &copy; {{ currentYear }} {{ siteName }} · {{ t('home.footer.allRightsReserved') }}
</p> </p>
<div class="flex items-center gap-4"> <div class="flex items-center gap-5">
<a <a
v-if="docUrl" v-if="docUrl"
:href="docUrl" :href="docUrl"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-white" class="font-mono text-xs text-slate-700 transition-colors hover:text-teal-400"
> >
{{ t('home.docs') }} /docs
</a> </a>
<a <a
:href="githubUrl" :href="githubUrl"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-white" class="font-mono text-xs text-slate-700 transition-colors hover:text-teal-400"
> >
GitHub /github
</a> </a>
</div> </div>
</div> </div>
</footer> </footer>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, defineComponent, h } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAuthStore, useAppStore } from '@/stores' import { useAuthStore, useAppStore } from '@/stores'
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue' import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
import Icon from '@/components/icons/Icon.vue' import Icon from '@/components/icons/Icon.vue'
const { t } = useI18n() const { t } = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
const appStore = useAppStore() const appStore = useAppStore()
// Site settings - directly from appStore (already initialized from injected config) // Site settings
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 siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'AI API Gateway Platform')
const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '') const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '')
const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '') const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '')
// Check if homeContent is a URL (for iframe display)
const isHomeContentUrl = computed(() => { const isHomeContentUrl = computed(() => {
const content = homeContent.value.trim() const content = homeContent.value.trim()
return content.startsWith('http://') || content.startsWith('https://') return content.startsWith('http://') || content.startsWith('https://')
...@@ -432,36 +352,96 @@ const isHomeContentUrl = computed(() => { ...@@ -432,36 +352,96 @@ const isHomeContentUrl = computed(() => {
// Theme // Theme
const isDark = ref(document.documentElement.classList.contains('dark')) const isDark = ref(document.documentElement.classList.contains('dark'))
// GitHub URL // GitHub
const githubUrl = 'https://github.com/Wei-Shaw/sub2api' const githubUrl = 'https://github.com/Wei-Shaw/sub2api'
// Auth state // Auth
const isAuthenticated = computed(() => authStore.isAuthenticated) const isAuthenticated = computed(() => authStore.isAuthenticated)
const isAdmin = computed(() => authStore.isAdmin) const isAdmin = computed(() => authStore.isAdmin)
const dashboardPath = computed(() => isAdmin.value ? '/admin/dashboard' : '/dashboard') const dashboardPath = computed(() => isAdmin.value ? '/admin/dashboard' : '/dashboard')
const userInitial = computed(() => { const userInitial = computed(() => {
const user = authStore.user const user = authStore.user
if (!user || !user.email) return '' if (!user?.email) return ''
return user.email.charAt(0).toUpperCase() return user.email.charAt(0).toUpperCase()
}) })
// Current year for footer
const currentYear = computed(() => new Date().getFullYear()) const currentYear = computed(() => new Date().getFullYear())
// Toggle theme // Stats
const stats = [
{ value: '3+', label: 'AI Providers' },
{ value: '99.9%', label: 'SLA Uptime' },
{ value: '1', label: 'API Key' },
]
// Feature tags
const featureTags = computed(() => [
{ text: t('home.tags.subscriptionToApi') },
{ text: t('home.tags.stickySession') },
{ text: t('home.tags.realtimeBilling') },
])
// SVG icon components (inline)
const IconServer = defineComponent({
render: () => h('svg', { class: 'h-5 w-5 text-white', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [
h('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', d: 'M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z' })
])
})
const IconRoute = defineComponent({
render: () => h('svg', { class: 'h-5 w-5 text-white', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [
h('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', d: 'M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5' })
])
})
const IconChart = defineComponent({
render: () => h('svg', { class: 'h-5 w-5 text-white', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [
h('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', d: 'M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z' })
])
})
// Features config — all icons use inline components, no string name needed
const features = [
{
key: 'unifiedGateway',
iconComponent: IconServer,
iconBg: 'linear-gradient(135deg, #3b82f6, #1d4ed8)',
glow: 'rgba(59,130,246,0.3)',
color: '#3b82f6',
},
{
key: 'multiAccount',
iconComponent: IconRoute,
iconBg: 'linear-gradient(135deg, #14b8a6, #0d9488)',
glow: 'rgba(20,184,166,0.3)',
color: '#14b8a6',
},
{
key: 'balanceQuota',
iconComponent: IconChart,
iconBg: 'linear-gradient(135deg, #a855f7, #7c3aed)',
glow: 'rgba(168,85,247,0.3)',
color: '#a855f7',
},
]
// Providers
const providerList = computed(() => [
{ name: t('home.providers.claude'), letter: 'C', gradient: 'linear-gradient(135deg,#f97316,#ea580c)', live: true },
{ name: 'GPT', letter: 'G', gradient: 'linear-gradient(135deg,#22c55e,#16a34a)', live: true },
{ name: t('home.providers.gemini'), letter: 'G', gradient: 'linear-gradient(135deg,#3b82f6,#2563eb)', live: true },
{ name: t('home.providers.antigravity'), letter: 'A', gradient: 'linear-gradient(135deg,#f43f5e,#db2777)', live: true },
{ name: t('home.providers.more'), letter: '+', gradient: 'linear-gradient(135deg,#475569,#334155)', live: false },
])
// Theme toggle
function toggleTheme() { function toggleTheme() {
isDark.value = !isDark.value isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value) document.documentElement.classList.toggle('dark', isDark.value)
localStorage.setItem('theme', isDark.value ? 'dark' : 'light') localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
} }
// Initialize theme
function initTheme() { function initTheme() {
const savedTheme = localStorage.getItem('theme') const savedTheme = localStorage.getItem('theme')
if ( if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
savedTheme === 'dark' ||
(!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
isDark.value = true isDark.value = true
document.documentElement.classList.add('dark') document.documentElement.classList.add('dark')
} }
...@@ -469,11 +449,7 @@ function initTheme() { ...@@ -469,11 +449,7 @@ function initTheme() {
onMounted(() => { onMounted(() => {
initTheme() initTheme()
// Check auth state
authStore.checkAuth() authStore.checkAuth()
// Ensure public settings are loaded (will use cache if already loaded from injected config)
if (!appStore.publicSettingsLoaded) { if (!appStore.publicSettingsLoaded) {
appStore.fetchPublicSettings() appStore.fetchPublicSettings()
} }
...@@ -481,164 +457,497 @@ onMounted(() => { ...@@ -481,164 +457,497 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
/* Terminal Container */ /* ══════════════════════════════════════════════
.terminal-container { ROOT — always dark for this landing page
══════════════════════════════════════════════ */
.home-root {
background: #182a40;
color: white;
}
/* ══════════════════════════════════════════════
BACKGROUND LAYERS
══════════════════════════════════════════════ */
.orb {
position: absolute;
border-radius: 50%;
filter: blur(100px);
pointer-events: none;
}
.orb-tr {
top: -15%;
right: -15%;
width: 600px;
height: 600px;
background: radial-gradient(ellipse, rgba(20,184,166,0.18) 0%, transparent 70%);
}
.orb-bl {
bottom: -15%;
left: -15%;
width: 500px;
height: 500px;
background: radial-gradient(ellipse, rgba(6,182,212,0.13) 0%, transparent 70%);
}
.orb-center {
top: 30%;
left: 40%;
width: 400px;
height: 400px;
background: radial-gradient(ellipse, rgba(20,184,166,0.08) 0%, transparent 70%);
}
.grid-overlay {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(20,184,166,0.09) 1px, transparent 1px),
linear-gradient(90deg, rgba(20,184,166,0.09) 1px, transparent 1px);
background-size: 48px 48px;
}
.scan-line {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(20,184,166,0.5) 30%, rgba(20,184,166,0.8) 50%, rgba(20,184,166,0.5) 70%, transparent 100%);
animation: scan 10s linear infinite;
box-shadow: 0 0 8px rgba(20,184,166,0.4);
}
@keyframes scan {
0% { top: 0%; opacity: 0; }
3% { opacity: 1; }
97% { opacity: 0.4; }
100% { top: 100%; opacity: 0; }
}
/* ══════════════════════════════════════════════
HEADER
══════════════════════════════════════════════ */
.home-header {
border-bottom: 1px solid rgba(20,184,166,0.12);
backdrop-filter: blur(12px);
background: rgba(24,42,64,0.8);
}
.logo-ring {
box-shadow: 0 0 0 1px rgba(20,184,166,0.25), 0 0 16px rgba(20,184,166,0.15);
}
.nav-btn {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
color: #94a3b8;
transition: all 0.2s;
}
.nav-btn:hover {
background: rgba(20,184,166,0.08);
color: #5eead4;
}
.nav-btn.icon-only {
padding: 6px 8px;
}
.cta-nav-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 7px;
border: 1px solid rgba(20,184,166,0.3);
background: rgba(20,184,166,0.08);
font-size: 12px;
font-weight: 500;
color: #5eead4;
transition: all 0.2s;
}
.cta-nav-btn:hover {
border-color: rgba(20,184,166,0.6);
background: rgba(20,184,166,0.15);
box-shadow: 0 0 14px rgba(20,184,166,0.2);
}
.user-dot {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(135deg, #14b8a6, #0d9488);
font-size: 9px;
font-weight: 700;
color: white;
}
/* ══════════════════════════════════════════════
HERO
══════════════════════════════════════════════ */
.gradient-text {
background: linear-gradient(135deg, #ffffff 0%, #7dd3c8 40%, #14b8a6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.cta-primary {
background: linear-gradient(135deg, #0f766e, #14b8a6, #0d9488);
background-size: 200% 200%;
animation: gradient-shift 4s ease infinite;
box-shadow: 0 0 0 1px rgba(20,184,166,0.4), 0 4px 20px rgba(20,184,166,0.25);
transition: box-shadow 0.25s, transform 0.25s;
}
.cta-primary:hover {
box-shadow: 0 0 0 1px rgba(20,184,166,0.7), 0 6px 28px rgba(20,184,166,0.4);
transform: translateY(-2px);
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Stats */
.stat-item {
display: flex;
flex-direction: column;
gap: 2px;
position: relative; position: relative;
display: inline-block; }
.stat-item + .stat-item::before {
content: '';
position: absolute;
left: -16px;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 28px;
background: rgba(20,184,166,0.2);
}
.stat-value {
font-size: 1.6rem;
font-weight: 900;
font-family: ui-monospace, monospace;
color: white;
line-height: 1;
letter-spacing: -0.02em;
}
.stat-label {
font-size: 10px;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.1em;
} }
/* Terminal Window */ /* ══════════════════════════════════════════════
TERMINAL
══════════════════════════════════════════════ */
.terminal-wrap {
position: relative;
display: inline-block;
}
.terminal-glow {
position: absolute;
inset: -30px;
background: radial-gradient(ellipse at center, rgba(20,184,166,0.1) 0%, transparent 65%);
pointer-events: none;
z-index: 0;
}
.terminal-window { .terminal-window {
width: 420px; position: relative;
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%); z-index: 1;
border-radius: 14px; width: 460px;
max-width: 100%;
background: linear-gradient(160deg, #1a2e48 0%, #162540 100%);
border-radius: 13px;
border: 1px solid rgba(20,184,166,0.18);
box-shadow: box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255,255,255,0.025),
0 0 0 1px rgba(255, 255, 255, 0.1), 0 28px 64px rgba(0,0,0,0.7),
inset 0 1px 0 rgba(255, 255, 255, 0.1); 0 0 50px rgba(20,184,166,0.07);
overflow: hidden; overflow: hidden;
transform: perspective(1000px) rotateX(2deg) rotateY(-2deg); transform: perspective(900px) rotateX(1.5deg) rotateY(-2.5deg);
transition: transform 0.3s ease; transition: transform 0.4s ease, box-shadow 0.4s ease;
} }
.terminal-window:hover { .terminal-window:hover {
transform: perspective(1000px) rotateX(0deg) rotateY(0deg) translateY(-4px); transform: perspective(900px) rotateX(0deg) rotateY(0deg) translateY(-8px);
box-shadow:
0 0 0 1px rgba(20,184,166,0.28),
0 36px 80px rgba(0,0,0,0.75),
0 0 70px rgba(20,184,166,0.12);
} }
/* Terminal Header */
.terminal-header { .terminal-header {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px 16px; gap: 10px;
background: rgba(30, 41, 59, 0.8); padding: 11px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05); background: rgba(22,37,60,0.9);
border-bottom: 1px solid rgba(20,184,166,0.12);
} }
.terminal-dots {
.terminal-buttons {
display: flex; display: flex;
gap: 8px; gap: 6px;
} }
.terminal-dots span {
.terminal-buttons span { width: 11px;
width: 12px; height: 11px;
height: 12px;
border-radius: 50%; border-radius: 50%;
} }
.dot-red { background: #ef4444; }
.btn-close { .dot-yellow { background: #eab308; }
background: #ef4444; .dot-green { background: #22c55e; }
}
.btn-minimize {
background: #eab308;
}
.btn-maximize {
background: #22c55e;
}
.terminal-title { .terminal-title {
flex: 1; flex: 1;
text-align: center; text-align: center;
font-size: 12px; font-size: 11px;
font-family: ui-monospace, monospace; font-family: ui-monospace, monospace;
color: #64748b; color: #334155;
margin-right: 52px; }
.terminal-live {
display: flex;
align-items: center;
gap: 5px;
font-size: 10px;
font-family: ui-monospace, monospace;
color: #14b8a6;
letter-spacing: 0.06em;
} }
/* Terminal Body */
.terminal-body { .terminal-body {
padding: 20px 24px; padding: 18px 22px 22px;
font-family: ui-monospace, 'Fira Code', monospace; font-family: ui-monospace, 'Fira Code', monospace;
font-size: 14px; font-size: 12.5px;
line-height: 2; line-height: 1.85;
} }
.code-line { /* Terminal lines */
.t-line {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 7px;
flex-wrap: wrap;
opacity: 0; opacity: 0;
animation: line-appear 0.5s ease forwards; animation: t-appear 0.35s ease forwards;
} }
.t-sep {
.line-1 { height: 1px;
animation-delay: 0.3s; background: rgba(20,184,166,0.07);
margin: 5px 0;
opacity: 0;
animation: t-appear 0.2s ease forwards;
} }
.line-2 { .line-1 { animation-delay: 0.2s; }
animation-delay: 1s; .line-2 { animation-delay: 0.7s; }
.line-3 { animation-delay: 1.2s; }
.sep-1 { animation-delay: 1.75s; }
.line-4 { animation-delay: 2.0s; }
.line-5 { animation-delay: 2.5s; }
.line-6 { animation-delay: 3.0s; }
.sep-2 { animation-delay: 3.5s; }
.line-7 { animation-delay: 3.8s; }
.line-8 { animation-delay: 4.4s; }
@keyframes t-appear {
from { opacity: 0; transform: translateX(-5px); }
to { opacity: 1; transform: translateX(0); }
} }
.line-3 {
animation-delay: 1.8s; .t-prompt { color: #14b8a6; font-weight: 700; }
.t-method { color: #38bdf8; font-weight: 600; }
.t-path { color: #bfdbfe; }
.t-comment{ color: #1e3a4a; font-style: italic; }
.t-ok {
color: #4ade80;
background: rgba(74,222,128,0.07);
border: 1px solid rgba(74,222,128,0.15);
padding: 1px 6px;
border-radius: 3px;
font-weight: 700;
font-size: 11px;
} }
.line-4 { .t-ms { color: #fcd34d; font-size: 11px; }
animation-delay: 2.5s; .t-dim { color: #1e3a4a; font-size: 11px; }
.t-stat { color: #a78bfa; }
/* Cursor */
.t-cursor {
display: inline-block;
width: 7px;
height: 13px;
background: #14b8a6;
box-shadow: 0 0 8px rgba(20,184,166,0.7);
animation: blink 1.1s step-end infinite;
border-radius: 1px;
}
@keyframes blink {
0%,44% { opacity: 1; }
50%,94% { opacity: 0; }
100% { opacity: 1; }
} }
@keyframes line-appear { /* ══════════════════════════════════════════════
from { FEATURE TAGS
opacity: 0; ══════════════════════════════════════════════ */
transform: translateY(5px); .tag-pill {
} display: inline-flex;
to { align-items: center;
opacity: 1; gap: 8px;
transform: translateY(0); border: 1px solid rgba(20,184,166,0.14);
} background: rgba(20,184,166,0.04);
border-radius: 9999px;
padding: 7px 18px;
font-size: 12px;
font-weight: 500;
color: #64748b;
transition: all 0.2s;
cursor: default;
}
.tag-pill:hover {
border-color: rgba(20,184,166,0.3);
background: rgba(20,184,166,0.08);
color: #94a3b8;
}
.tag-pip {
width: 5px;
height: 5px;
border-radius: 50%;
background: #14b8a6;
box-shadow: 0 0 7px rgba(20,184,166,0.9);
flex-shrink: 0;
} }
.code-prompt { /* ══════════════════════════════════════════════
color: #22c55e; FEATURE CARDS
font-weight: bold; ══════════════════════════════════════════════ */
.feat-card {
position: relative;
padding: 26px;
background: rgba(26,46,72,0.65);
border: 1px solid rgba(255,255,255,0.09);
border-radius: 13px;
backdrop-filter: blur(14px);
overflow: hidden;
transition: all 0.3s ease;
} }
.code-cmd { .feat-card:hover {
color: #38bdf8; border-color: rgba(255,255,255,0.15);
background: rgba(26,46,72,0.88);
transform: translateY(-3px);
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
} }
.code-flag { /* left accent bar */
color: #a78bfa; .feat-accent {
position: absolute;
left: 0;
top: 0;
width: 3px;
height: 100%;
opacity: 0.55;
transition: opacity 0.3s, box-shadow 0.3s;
} }
.code-url { .feat-card:hover .feat-accent {
color: #14b8a6; opacity: 1;
box-shadow: 0 0 12px currentColor;
} }
.code-comment { /* corner brackets */
color: #64748b; .feat-corner-tr,
font-style: italic; .feat-corner-bl {
position: absolute;
width: 10px;
height: 10px;
opacity: 0;
transition: opacity 0.3s;
} }
.code-success { .feat-corner-tr {
color: #22c55e; top: 10px;
background: rgba(34, 197, 94, 0.15); right: 10px;
padding: 2px 8px; border-top: 1px solid rgba(20,184,166,0.45);
border-radius: 4px; border-right: 1px solid rgba(20,184,166,0.45);
font-weight: 600;
} }
.code-response { .feat-corner-bl {
color: #fbbf24; bottom: 10px;
right: 10px;
border-bottom: 1px solid rgba(20,184,166,0.45);
border-right: 1px solid rgba(20,184,166,0.45);
} }
.feat-card:hover .feat-corner-tr,
/* Blinking Cursor */ .feat-card:hover .feat-corner-bl {
.cursor { opacity: 1;
display: inline-block; }
width: 8px; .feat-icon {
height: 16px; width: 44px;
background: #22c55e; height: 44px;
animation: blink 1s step-end infinite; border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s, box-shadow 0.3s;
}
.feat-card:hover .feat-icon {
transform: scale(1.08);
} }
@keyframes blink { /* ══════════════════════════════════════════════
0%, PROVIDERS
50% { ══════════════════════════════════════════════ */
opacity: 1; .provider-card {
} display: flex;
51%, align-items: center;
100% { gap: 10px;
opacity: 0; padding: 10px 16px;
} border-radius: 10px;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(26,46,72,0.55);
backdrop-filter: blur(8px);
transition: all 0.25s;
}
.provider-active:hover {
border-color: rgba(20,184,166,0.22);
background: rgba(20,184,166,0.05);
box-shadow: 0 4px 20px rgba(0,0,0,0.35);
}
.provider-inactive {
opacity: 0.38;
}
.provider-status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.dot-live {
background: #34d399;
box-shadow: 0 0 7px rgba(52,211,153,0.8);
}
.dot-offline {
background: #334155;
}
.provider-icon-badge {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.provider-tag {
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 9999px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.tag-live {
background: rgba(20,184,166,0.1);
color: #2dd4bf;
border: 1px solid rgba(20,184,166,0.2);
}
.tag-soon {
background: rgba(71,85,105,0.2);
color: #475569;
border: 1px solid rgba(71,85,105,0.25);
} }
/* Dark mode adjustments */ /* ══════════════════════════════════════════════
:deep(.dark) .terminal-window { FOOTER
box-shadow: ══════════════════════════════════════════════ */
0 25px 50px -12px rgba(0, 0, 0, 0.6), .home-footer {
0 0 0 1px rgba(20, 184, 166, 0.2), border-top: 1px solid rgba(20,184,166,0.1);
0 0 40px rgba(20, 184, 166, 0.1), background: rgba(24,42,64,0.8);
inset 0 1px 0 rgba(255, 255, 255, 0.1);
} }
</style> </style>
...@@ -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