mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-05 07:52:13 +08:00
Squash of 124 commits from the legacy develop branch (develop-0.1.75) onto a clean v0.1.75 upstream base, to simplify future upstream merges. Key changes included: - Refactor scope-level rate limiting to model-level rate limiting - Antigravity gateway service improvements (smart retry, error policy) - Digest session store (flat cache replacing Trie-based store) - Client disconnect detection during streaming - Gemini messages compatibility service enhancements - Scheduler shuffle for thundering herd prevention - Session hash generation improvements - Frontend customizations (WeChat service, HomeView, etc.) - Ops monitoring scope cleanup
766 lines
32 KiB
Vue
766 lines
32 KiB
Vue
<template>
|
|
<!-- Custom Home Content: Full Page Mode -->
|
|
<div v-if="homeContent" class="min-h-screen">
|
|
<!-- iframe mode -->
|
|
<iframe
|
|
v-if="isHomeContentUrl"
|
|
:src="homeContent.trim()"
|
|
class="h-screen w-full border-0"
|
|
allowfullscreen
|
|
></iframe>
|
|
<!-- HTML mode - SECURITY: homeContent is admin-only setting, XSS risk is acceptable -->
|
|
<div v-else v-html="homeContent"></div>
|
|
</div>
|
|
|
|
<!-- Default Home Page -->
|
|
<div
|
|
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 Decorations -->
|
|
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
|
<div
|
|
class="absolute -right-40 -top-40 h-96 w-96 rounded-full bg-primary-400/20 blur-3xl"
|
|
></div>
|
|
<div
|
|
class="absolute -bottom-40 -left-40 h-96 w-96 rounded-full bg-primary-500/15 blur-3xl"
|
|
></div>
|
|
<div
|
|
class="absolute left-1/3 top-1/4 h-72 w-72 rounded-full bg-primary-300/10 blur-3xl"
|
|
></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>
|
|
|
|
<!-- Header -->
|
|
<header class="relative z-20 px-6 py-4">
|
|
<nav class="mx-auto flex max-w-6xl items-center justify-between">
|
|
<!-- Logo -->
|
|
<div class="flex items-center">
|
|
<div class="h-10 w-10 overflow-hidden rounded-xl shadow-md">
|
|
<img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nav Actions -->
|
|
<div class="flex items-center gap-3">
|
|
<!-- Language Switcher -->
|
|
<LocaleSwitcher />
|
|
|
|
<!-- Doc Link -->
|
|
<a
|
|
v-if="docUrl"
|
|
:href="docUrl"
|
|
target="_blank"
|
|
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"
|
|
:title="t('home.viewDocs')"
|
|
>
|
|
<Icon name="book" size="md" />
|
|
</a>
|
|
|
|
<!-- Theme Toggle -->
|
|
<button
|
|
@click="toggleTheme"
|
|
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>
|
|
|
|
<!-- Login / Dashboard Button -->
|
|
<router-link
|
|
v-if="isAuthenticated"
|
|
: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"
|
|
>
|
|
<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"
|
|
>
|
|
{{ userInitial }}
|
|
</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>
|
|
</router-link>
|
|
<router-link
|
|
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') }}
|
|
</router-link>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main class="relative z-10 flex-1 px-6 py-16">
|
|
<div class="mx-auto max-w-6xl">
|
|
<!-- Hero Section - Left/Right Layout -->
|
|
<div class="mb-12 flex flex-col items-center justify-between gap-12 lg:flex-row lg:gap-16">
|
|
<!-- Left: Text Content -->
|
|
<div class="flex-1 text-center lg:text-left">
|
|
<h1
|
|
class="mb-4 text-4xl font-bold text-gray-900 dark:text-white md:text-5xl lg:text-6xl"
|
|
>
|
|
{{ siteName }}
|
|
</h1>
|
|
<p class="mb-3 text-xl font-semibold text-primary-600 dark:text-primary-400 md:text-2xl">
|
|
{{ t('home.heroSubtitle') }}
|
|
</p>
|
|
<p class="mb-8 text-base text-gray-600 dark:text-dark-300 md:text-lg">
|
|
{{ t('home.heroDescription') }}
|
|
</p>
|
|
|
|
<!-- CTA Button -->
|
|
<div>
|
|
<router-link
|
|
:to="isAuthenticated ? dashboardPath : '/login'"
|
|
class="btn btn-primary px-8 py-3 text-base shadow-lg shadow-primary-500/30"
|
|
>
|
|
{{ isAuthenticated ? t('home.goToDashboard') : t('home.getStarted') }}
|
|
<Icon name="arrowRight" size="md" class="ml-2" :stroke-width="2" />
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: Terminal Animation -->
|
|
<div class="flex flex-1 justify-center lg:justify-end">
|
|
<div class="terminal-container">
|
|
<div class="terminal-window">
|
|
<!-- Window header -->
|
|
<div class="terminal-header">
|
|
<div class="terminal-buttons">
|
|
<span class="btn-close"></span>
|
|
<span class="btn-minimize"></span>
|
|
<span class="btn-maximize"></span>
|
|
</div>
|
|
<span class="terminal-title">terminal</span>
|
|
</div>
|
|
<!-- Terminal content -->
|
|
<div class="terminal-body">
|
|
<div class="code-line line-1">
|
|
<span class="code-prompt">$</span>
|
|
<span class="code-cmd">curl</span>
|
|
<span class="code-flag">-X POST</span>
|
|
<span class="code-url">/v1/messages</span>
|
|
</div>
|
|
<div class="code-line line-2">
|
|
<span class="code-comment"># Routing to upstream...</span>
|
|
</div>
|
|
<div class="code-line line-3">
|
|
<span class="code-success">200 OK</span>
|
|
<span class="code-response">{ "content": "Hello!" }</span>
|
|
</div>
|
|
<div class="code-line line-4">
|
|
<span class="code-prompt">$</span>
|
|
<span class="cursor"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Feature Tags - Centered -->
|
|
<div class="mb-16 flex flex-wrap items-center justify-center gap-4 md:gap-6">
|
|
<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="swap" size="sm" class="text-primary-500" />
|
|
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{
|
|
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>
|
|
|
|
<!-- Pain Points Section -->
|
|
<div class="mb-16">
|
|
<h2 class="mb-8 text-center text-2xl font-bold text-gray-900 dark:text-white md:text-3xl">
|
|
{{ t('home.painPoints.title') }}
|
|
</h2>
|
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<!-- Pain Point 1: Expensive -->
|
|
<div class="rounded-xl border border-red-200/50 bg-red-50/50 p-5 dark:border-red-900/30 dark:bg-red-950/20">
|
|
<div class="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-red-100 dark:bg-red-900/30">
|
|
<svg class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="mb-1.5 font-semibold text-gray-900 dark:text-white">{{ t('home.painPoints.items.expensive.title') }}</h3>
|
|
<p class="text-sm text-gray-600 dark:text-dark-400">{{ t('home.painPoints.items.expensive.desc') }}</p>
|
|
</div>
|
|
<!-- Pain Point 2: Complex -->
|
|
<div class="rounded-xl border border-orange-200/50 bg-orange-50/50 p-5 dark:border-orange-900/30 dark:bg-orange-950/20">
|
|
<div class="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/30">
|
|
<svg class="h-5 w-5 text-orange-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="mb-1.5 font-semibold text-gray-900 dark:text-white">{{ t('home.painPoints.items.complex.title') }}</h3>
|
|
<p class="text-sm text-gray-600 dark:text-dark-400">{{ t('home.painPoints.items.complex.desc') }}</p>
|
|
</div>
|
|
<!-- Pain Point 3: Unstable -->
|
|
<div class="rounded-xl border border-yellow-200/50 bg-yellow-50/50 p-5 dark:border-yellow-900/30 dark:bg-yellow-950/20">
|
|
<div class="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100 dark:bg-yellow-900/30">
|
|
<svg class="h-5 w-5 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="mb-1.5 font-semibold text-gray-900 dark:text-white">{{ t('home.painPoints.items.unstable.title') }}</h3>
|
|
<p class="text-sm text-gray-600 dark:text-dark-400">{{ t('home.painPoints.items.unstable.desc') }}</p>
|
|
</div>
|
|
<!-- Pain Point 4: No Control -->
|
|
<div class="rounded-xl border border-gray-200/50 bg-gray-50/50 p-5 dark:border-dark-700/50 dark:bg-dark-800/50">
|
|
<div class="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 dark:bg-dark-700">
|
|
<svg class="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="mb-1.5 font-semibold text-gray-900 dark:text-white">{{ t('home.painPoints.items.noControl.title') }}</h3>
|
|
<p class="text-sm text-gray-600 dark:text-dark-400">{{ t('home.painPoints.items.noControl.desc') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Solutions Section Title -->
|
|
<div class="mb-8 text-center">
|
|
<h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white md:text-3xl">
|
|
{{ t('home.solutions.title') }}
|
|
</h2>
|
|
<p class="text-gray-600 dark:text-dark-400">{{ t('home.solutions.subtitle') }}</p>
|
|
</div>
|
|
|
|
<!-- Features Grid -->
|
|
<div class="mb-12 grid gap-6 md:grid-cols-3">
|
|
<!-- Feature 1: Unified Gateway -->
|
|
<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
|
|
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"
|
|
>
|
|
<Icon name="server" size="lg" class="text-white" />
|
|
</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="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
|
|
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"
|
|
>
|
|
<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
|
|
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"
|
|
>
|
|
<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="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>
|
|
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
|
|
{{ t('home.features.balanceQuota') }}
|
|
</h3>
|
|
<p class="text-sm leading-relaxed text-gray-600 dark:text-dark-400">
|
|
{{ t('home.features.balanceQuotaDesc') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Supported Providers -->
|
|
<div class="mb-8 text-center">
|
|
<h2 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">
|
|
{{ t('home.providers.title') }}
|
|
</h2>
|
|
<p class="text-sm text-gray-600 dark:text-dark-400">
|
|
{{ t('home.providers.description') }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mb-16 flex flex-wrap items-center justify-center gap-4">
|
|
<!-- 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
|
|
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-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
|
|
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>
|
|
<!-- More - Coming Soon -->
|
|
<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>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">{{ t('home.providers.more') }}</span>
|
|
<span
|
|
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"
|
|
>{{ t('home.providers.soon') }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comparison Table -->
|
|
<div class="mb-16">
|
|
<h2 class="mb-8 text-center text-2xl font-bold text-gray-900 dark:text-white md:text-3xl">
|
|
{{ t('home.comparison.title') }}
|
|
</h2>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full rounded-xl border border-gray-200/50 bg-white/60 backdrop-blur-sm dark:border-dark-700/50 dark:bg-dark-800/60">
|
|
<thead>
|
|
<tr class="border-b border-gray-200/50 dark:border-dark-700/50">
|
|
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900 dark:text-white">{{ t('home.comparison.headers.feature') }}</th>
|
|
<th class="px-6 py-4 text-center text-sm font-semibold text-gray-500 dark:text-dark-400">{{ t('home.comparison.headers.official') }}</th>
|
|
<th class="px-6 py-4 text-center text-sm font-semibold text-primary-600 dark:text-primary-400">{{ t('home.comparison.headers.us') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200/50 dark:divide-dark-700/50">
|
|
<tr>
|
|
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.pricing.feature') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.pricing.official') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.pricing.us') }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.models.feature') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.models.official') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.models.us') }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.management.feature') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.management.official') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.management.us') }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.stability.feature') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.stability.official') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.stability.us') }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.control.feature') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.control.official') }}</td>
|
|
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.control.us') }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CTA Section -->
|
|
<div class="mb-8 rounded-2xl bg-gradient-to-r from-primary-500 to-primary-600 p-8 text-center shadow-xl shadow-primary-500/20 md:p-12">
|
|
<h2 class="mb-3 text-2xl font-bold text-white md:text-3xl">
|
|
{{ t('home.cta.title') }}
|
|
</h2>
|
|
<p class="mb-6 text-primary-100">
|
|
{{ t('home.cta.description') }}
|
|
</p>
|
|
<router-link
|
|
v-if="!isAuthenticated"
|
|
to="/register"
|
|
class="inline-flex items-center gap-2 rounded-full bg-white px-8 py-3 font-semibold text-primary-600 shadow-lg transition-all hover:bg-gray-50 hover:shadow-xl"
|
|
>
|
|
{{ t('home.cta.button') }}
|
|
<Icon name="arrowRight" size="md" :stroke-width="2" />
|
|
</router-link>
|
|
<router-link
|
|
v-else
|
|
:to="dashboardPath"
|
|
class="inline-flex items-center gap-2 rounded-full bg-white px-8 py-3 font-semibold text-primary-600 shadow-lg transition-all hover:bg-gray-50 hover:shadow-xl"
|
|
>
|
|
{{ t('home.goToDashboard') }}
|
|
<Icon name="arrowRight" size="md" :stroke-width="2" />
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="relative z-10 border-t border-gray-200/50 px-6 py-8 dark:border-dark-800/50">
|
|
<div
|
|
class="mx-auto flex max-w-6xl flex-col items-center justify-center gap-4 text-center sm:flex-row sm:text-left"
|
|
>
|
|
<p class="text-sm text-gray-500 dark:text-dark-400">
|
|
© {{ currentYear }} {{ siteName }}. {{ t('home.footer.allRightsReserved') }}
|
|
</p>
|
|
<a
|
|
v-if="docUrl"
|
|
:href="docUrl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-white"
|
|
>
|
|
{{ t('home.docs') }}
|
|
</a>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- 微信客服悬浮按钮 -->
|
|
<WechatServiceButton />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useAuthStore, useAppStore } from '@/stores'
|
|
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
|
|
import Icon from '@/components/icons/Icon.vue'
|
|
import WechatServiceButton from '@/components/common/WechatServiceButton.vue'
|
|
|
|
const { t } = useI18n()
|
|
|
|
const authStore = useAuthStore()
|
|
const appStore = useAppStore()
|
|
|
|
// Site settings - directly from appStore (already initialized from injected config)
|
|
const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'Sub2API')
|
|
const siteLogo = computed(() => appStore.cachedPublicSettings?.site_logo || appStore.siteLogo || '')
|
|
const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '')
|
|
const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '')
|
|
|
|
// Check if homeContent is a URL (for iframe display)
|
|
const isHomeContentUrl = computed(() => {
|
|
const content = homeContent.value.trim()
|
|
return content.startsWith('http://') || content.startsWith('https://')
|
|
})
|
|
|
|
// Theme
|
|
const isDark = ref(document.documentElement.classList.contains('dark'))
|
|
|
|
// Auth state
|
|
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
const isAdmin = computed(() => authStore.isAdmin)
|
|
const dashboardPath = computed(() => isAdmin.value ? '/admin/dashboard' : '/dashboard')
|
|
const userInitial = computed(() => {
|
|
const user = authStore.user
|
|
if (!user || !user.email) return ''
|
|
return user.email.charAt(0).toUpperCase()
|
|
})
|
|
|
|
// Current year for footer
|
|
const currentYear = computed(() => new Date().getFullYear())
|
|
|
|
// Toggle theme
|
|
function toggleTheme() {
|
|
isDark.value = !isDark.value
|
|
document.documentElement.classList.toggle('dark', isDark.value)
|
|
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
|
}
|
|
|
|
// Initialize theme
|
|
function initTheme() {
|
|
const savedTheme = localStorage.getItem('theme')
|
|
if (
|
|
savedTheme === 'dark' ||
|
|
(!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
) {
|
|
isDark.value = true
|
|
document.documentElement.classList.add('dark')
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
initTheme()
|
|
|
|
// Check auth state
|
|
authStore.checkAuth()
|
|
|
|
// Ensure public settings are loaded (will use cache if already loaded from injected config)
|
|
if (!appStore.publicSettingsLoaded) {
|
|
appStore.fetchPublicSettings()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Terminal Container */
|
|
.terminal-container {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Terminal Window */
|
|
.terminal-window {
|
|
width: 420px;
|
|
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%);
|
|
border-radius: 14px;
|
|
box-shadow:
|
|
0 25px 50px -12px rgba(0, 0, 0, 0.4),
|
|
0 0 0 1px rgba(255, 255, 255, 0.1),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
overflow: hidden;
|
|
transform: perspective(1000px) rotateX(2deg) rotateY(-2deg);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.terminal-window:hover {
|
|
transform: perspective(1000px) rotateX(0deg) rotateY(0deg) translateY(-4px);
|
|
}
|
|
|
|
/* Terminal Header */
|
|
.terminal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
background: rgba(30, 41, 59, 0.8);
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.terminal-buttons {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.terminal-buttons span {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.btn-close {
|
|
background: #ef4444;
|
|
}
|
|
.btn-minimize {
|
|
background: #eab308;
|
|
}
|
|
.btn-maximize {
|
|
background: #22c55e;
|
|
}
|
|
|
|
.terminal-title {
|
|
flex: 1;
|
|
text-align: center;
|
|
font-size: 12px;
|
|
font-family: ui-monospace, monospace;
|
|
color: #64748b;
|
|
margin-right: 52px;
|
|
}
|
|
|
|
/* Terminal Body */
|
|
.terminal-body {
|
|
padding: 20px 24px;
|
|
font-family: ui-monospace, 'Fira Code', monospace;
|
|
font-size: 14px;
|
|
line-height: 2;
|
|
}
|
|
|
|
.code-line {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
opacity: 0;
|
|
animation: line-appear 0.5s ease forwards;
|
|
}
|
|
|
|
.line-1 {
|
|
animation-delay: 0.3s;
|
|
}
|
|
.line-2 {
|
|
animation-delay: 1s;
|
|
}
|
|
.line-3 {
|
|
animation-delay: 1.8s;
|
|
}
|
|
.line-4 {
|
|
animation-delay: 2.5s;
|
|
}
|
|
|
|
@keyframes line-appear {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(5px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.code-prompt {
|
|
color: #22c55e;
|
|
font-weight: bold;
|
|
}
|
|
.code-cmd {
|
|
color: #38bdf8;
|
|
}
|
|
.code-flag {
|
|
color: #a78bfa;
|
|
}
|
|
.code-url {
|
|
color: #14b8a6;
|
|
}
|
|
.code-comment {
|
|
color: #64748b;
|
|
font-style: italic;
|
|
}
|
|
.code-success {
|
|
color: #22c55e;
|
|
background: rgba(34, 197, 94, 0.15);
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-weight: 600;
|
|
}
|
|
.code-response {
|
|
color: #fbbf24;
|
|
}
|
|
|
|
/* Blinking Cursor */
|
|
.cursor {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 16px;
|
|
background: #22c55e;
|
|
animation: blink 1s step-end infinite;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%,
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
51%,
|
|
100% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* Dark mode adjustments */
|
|
:deep(.dark) .terminal-window {
|
|
box-shadow:
|
|
0 25px 50px -12px rgba(0, 0, 0, 0.6),
|
|
0 0 0 1px rgba(20, 184, 166, 0.2),
|
|
0 0 40px rgba(20, 184, 166, 0.1),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
}
|
|
</style>
|