Commit 8d3f79cf authored by 陈曦's avatar 陈曦
Browse files

继续修改首页、登录页、后台的所有设计风格

parent b4412353
<template> <template>
<header class="glass sticky top-0 z-30 border-b border-gray-200/50 dark:border-dark-700/50"> <header class="app-header sticky top-0 z-30 border-b border-gray-200/40 dark:border-white/[0.05]">
<div class="flex h-16 items-center justify-between px-4 md:px-6"> <div class="flex h-16 items-center justify-between px-4 md:px-6">
<!-- Left: Mobile Menu Toggle + Page Title --> <!-- Left: Mobile Menu Toggle + Page Title -->
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
...@@ -335,4 +335,13 @@ onBeforeUnmount(() => { ...@@ -335,4 +335,13 @@ onBeforeUnmount(() => {
opacity: 0; opacity: 0;
transform: scale(0.95) translateY(-4px); transform: scale(0.95) translateY(-4px);
} }
.app-header {
background: rgba(241, 245, 251, 0.85);
backdrop-filter: blur(20px);
}
:global(.dark) .app-header {
background: rgba(8, 14, 28, 0.85);
backdrop-filter: blur(20px);
}
</style> </style>
<template> <template>
<div class="min-h-screen bg-gray-50 dark:bg-dark-950"> <div class="app-root min-h-screen">
<!-- Background Decoration --> <!-- Background Decoration -->
<div class="pointer-events-none fixed inset-0 bg-mesh-gradient"></div> <div class="pointer-events-none fixed inset-0 app-bg-layer"></div>
<!-- Sidebar --> <!-- Sidebar -->
<AppSidebar /> <AppSidebar />
...@@ -50,3 +50,25 @@ onMounted(() => { ...@@ -50,3 +50,25 @@ onMounted(() => {
defineExpose({ replayTour }) defineExpose({ replayTour })
</script> </script>
<style scoped>
.app-root {
background: #f1f5fb;
}
:global(.dark) .app-root {
background: #080e1c;
}
.app-bg-layer {
background:
radial-gradient(at 15% 30%, rgba(99, 102, 241, 0.06) 0px, transparent 50%),
radial-gradient(at 85% 10%, rgba(20, 184, 166, 0.05) 0px, transparent 50%),
radial-gradient(at 60% 80%, rgba(139, 92, 246, 0.04) 0px, transparent 50%);
}
:global(.dark) .app-bg-layer {
background:
radial-gradient(at 15% 30%, rgba(99, 102, 241, 0.08) 0px, transparent 50%),
radial-gradient(at 85% 10%, rgba(20, 184, 166, 0.06) 0px, transparent 50%),
radial-gradient(at 60% 80%, rgba(139, 92, 246, 0.05) 0px, transparent 50%);
}
</style>
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
<div v-if="!sidebarCollapsed" class="sidebar-section-title"> <div v-if="!sidebarCollapsed" class="sidebar-section-title">
{{ t('nav.myAccount') }} {{ t('nav.myAccount') }}
</div> </div>
<div v-else class="mx-3 my-3 h-px bg-gray-200 dark:bg-dark-700"></div> <div v-else class="mx-3 my-3 h-px" style="background: rgba(255,255,255,0.06)"></div>
<router-link <router-link
v-for="item in personalNavItems" v-for="item in personalNavItems"
...@@ -105,7 +105,7 @@ ...@@ -105,7 +105,7 @@
</nav> </nav>
<!-- Bottom Section --> <!-- Bottom Section -->
<div class="mt-auto border-t border-gray-100 p-3 dark:border-dark-800"> <div class="mt-auto p-3" style="border-top: 1px solid rgba(255,255,255,0.05)">
<!-- Theme Toggle --> <!-- Theme Toggle -->
<button <button
@click="toggleTheme" @click="toggleTheme"
......
<template> <template>
<div class="auth-root relative flex min-h-screen items-center justify-center overflow-hidden p-4"> <div class="auth-split min-h-screen">
<!-- Background --> <!-- ══════════════════════════════════════════
LEFT PANEL — AI Visual (hidden on mobile)
══════════════════════════════════════════ -->
<div class="auth-left hidden lg:flex">
<!-- Background layers -->
<div class="pointer-events-none absolute inset-0 overflow-hidden"> <div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="auth-orb auth-orb-tr"></div> <div class="left-aurora left-aurora-1"></div>
<div class="auth-orb auth-orb-bl"></div> <div class="left-aurora left-aurora-2"></div>
<div class="auth-grid"></div>
<div class="auth-scan"></div>
</div> </div>
<!-- Content --> <div class="relative z-10 flex h-full flex-col px-12 py-10">
<div class="relative z-10 w-full max-w-md">
<!-- Brand --> <!-- Logo + Brand -->
<div class="mb-8 text-center"> <div class="flex items-center gap-3">
<template v-if="settingsLoaded"> <template v-if="settingsLoaded">
<!-- Logo --> <div class="left-logo-wrap">
<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>
<div>
<div class="text-base font-bold text-white">{{ siteName }}</div>
<div class="font-mono text-[10px] uppercase tracking-widest text-violet-400/60">// AI Gateway</div>
</div>
</template>
</div>
<!-- Center: Orbital AI Visualization -->
<div class="flex flex-1 flex-col items-center justify-center">
<div class="orbital-viz">
<!-- Hub -->
<div class="hub-outer">
<div class="hub-ring hub-ring-1"></div>
<div class="hub-ring hub-ring-2"></div>
<div class="hub-core">
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
</svg>
</div>
</div>
<!-- Site name --> <!-- Orbiting model chips -->
<h1 class="auth-site-name mb-2 text-3xl font-black tracking-tight"> <div class="orbit-chip chip-0">
{{ siteName }} <span class="chip-dot" style="background: #f97316"></span>
</h1> <span>Claude</span>
</div>
<div class="orbit-chip chip-1">
<span class="chip-dot" style="background: #22c55e"></span>
<span>GPT-4o</span>
</div>
<div class="orbit-chip chip-2">
<span class="chip-dot" style="background: #3b82f6"></span>
<span>Gemini</span>
</div>
<div class="orbit-chip chip-3">
<span class="chip-dot" style="background: #f43f5e"></span>
<span>DeepSeek</span>
</div>
<div class="orbit-chip chip-4">
<span class="chip-dot" style="background: #a855f7"></span>
<span>Llama</span>
</div>
<!-- Subtitle --> <!-- Connecting lines (SVG) -->
<p class="auth-subtitle font-mono text-xs uppercase tracking-widest"> <svg class="orbit-lines" viewBox="0 0 340 340" fill="none" xmlns="http://www.w3.org/2000/svg">
// {{ siteSubtitle }} <!-- Lines from center (170,170) to chip positions -->
<line x1="170" y1="170" x2="275" y2="90" stroke="rgba(139,92,246,0.18)" stroke-width="1" stroke-dasharray="4 4">
<animate attributeName="stroke-dashoffset" values="0;-16" dur="1.2s" repeatCount="indefinite"/>
</line>
<line x1="170" y1="170" x2="300" y2="200" stroke="rgba(20,184,166,0.18)" stroke-width="1" stroke-dasharray="4 4">
<animate attributeName="stroke-dashoffset" values="0;-16" dur="1.6s" repeatCount="indefinite"/>
</line>
<line x1="170" y1="170" x2="200" y2="295" stroke="rgba(59,130,246,0.18)" stroke-width="1" stroke-dasharray="4 4">
<animate attributeName="stroke-dashoffset" values="0;-16" dur="2.0s" repeatCount="indefinite"/>
</line>
<line x1="170" y1="170" x2="65" y2="270" stroke="rgba(244,63,94,0.18)" stroke-width="1" stroke-dasharray="4 4">
<animate attributeName="stroke-dashoffset" values="0;-16" dur="1.4s" repeatCount="indefinite"/>
</line>
<line x1="170" y1="170" x2="55" y2="130" stroke="rgba(168,85,247,0.18)" stroke-width="1" stroke-dasharray="4 4">
<animate attributeName="stroke-dashoffset" values="0;-16" dur="1.8s" repeatCount="indefinite"/>
</line>
</svg>
</div>
<!-- Tagline -->
<div class="mt-10 text-center">
<h2 class="mb-3 text-2xl font-bold text-white">One Key.<br>Every Model.</h2>
<p class="max-w-xs text-sm leading-relaxed text-slate-400">
{{ siteSubtitle }}
</p> </p>
</div>
</div>
<!-- Feature bullets -->
<div class="space-y-4 pb-4">
<div v-for="f in leftFeatures" :key="f.text" class="left-feature-row">
<div class="left-feature-icon" :style="{ background: f.iconBg }">
<component :is="f.icon" />
</div>
<div>
<div class="text-sm font-medium text-slate-200">{{ f.text }}</div>
<div class="text-xs text-slate-500">{{ f.desc }}</div>
</div>
</div>
</div>
<!-- Copyright -->
<div class="pt-6 font-mono text-[10px] text-slate-700">
&copy; {{ currentYear }} {{ siteName }}
</div>
</div>
</div>
<!-- ══════════════════════════════════════════
RIGHT PANEL — Form
══════════════════════════════════════════ -->
<div class="auth-right">
<!-- Mobile: tiny logo strip -->
<div class="mb-8 flex items-center gap-2.5 lg:hidden">
<template v-if="settingsLoaded">
<div class="h-8 w-8 overflow-hidden rounded-lg shadow-glow">
<img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
</div>
<span class="text-base font-bold text-gray-900 dark:text-white">{{ siteName }}</span>
</template> </template>
</div> </div>
<!-- Card --> <!-- Form card -->
<div class="auth-card rounded-2xl p-8"> <div class="auth-form-card">
<slot /> <slot />
</div> </div>
<!-- Footer links --> <!-- Footer links -->
<div class="mt-6 text-center text-sm"> <div class="mt-5 text-center text-sm">
<slot name="footer" /> <slot name="footer" />
</div> </div>
<!-- Copyright --> <!-- Copyright (mobile only) -->
<div class="mt-8 text-center font-mono text-xs text-slate-600"> <div class="mt-8 text-center font-mono text-xs text-gray-400 dark:text-slate-700 lg:hidden">
&copy; {{ currentYear }} {{ siteName }}. All rights reserved. &copy; {{ currentYear }} {{ siteName }}. All rights reserved.
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from 'vue' import { computed, defineComponent, h, onMounted } from 'vue'
import { useAppStore } from '@/stores' import { useAppStore } from '@/stores'
import { sanitizeUrl } from '@/utils/url' import { sanitizeUrl } from '@/utils/url'
...@@ -61,117 +157,295 @@ const siteName = computed(() => appStore.siteName || 'TrafficAPI') ...@@ -61,117 +157,295 @@ 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 || 'Intelligent AI Traffic Routing & Management') 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())
// Inline icon components for feature bullets
const ShieldIcon = defineComponent({
render: () => h('svg', { class: 'h-4 w-4 text-white', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [
h('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', d: 'M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z' })
])
})
const BoltIcon = defineComponent({
render: () => h('svg', { class: 'h-4 w-4 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.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z' })
])
})
const ChartIcon = defineComponent({
render: () => h('svg', { class: 'h-4 w-4 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' })
])
})
const leftFeatures = [
{
text: '统一 API 接入点',
desc: '一个 Key 打通所有 AI 服务商,无需多套凭证',
icon: ShieldIcon,
iconBg: 'linear-gradient(135deg,#6366f1,#8b5cf6)',
},
{
text: '智能负载均衡',
desc: '自动分发流量,账户异常时即时切换,零中断',
icon: BoltIcon,
iconBg: 'linear-gradient(135deg,#14b8a6,#06b6d4)',
},
{
text: '实时流量分析',
desc: '请求量、延迟、费用全透明,按用户精细管控',
icon: ChartIcon,
iconBg: 'linear-gradient(135deg,#a855f7,#ec4899)',
},
]
onMounted(() => { onMounted(() => {
appStore.fetchPublicSettings() appStore.fetchPublicSettings()
}) })
</script> </script>
<style scoped> <style scoped>
/* ── Root ────────────────────────────────── */ /* ══════════════════════════════════════════════
.auth-root { SPLIT LAYOUT
background: #182a40; ══════════════════════════════════════════════ */
color: white; .auth-split {
display: flex;
min-height: 100vh;
} }
/* ── Background layers ───────────────────── */ /* ── LEFT PANEL ────────────────────────────── */
.auth-orb { .auth-left {
position: relative;
width: 55%;
max-width: 600px;
flex-shrink: 0;
background: #060c1a;
overflow: hidden;
flex-direction: column;
}
/* Aurora blobs on left panel */
.left-aurora {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
filter: blur(90px); filter: blur(100px);
pointer-events: none; pointer-events: none;
} }
.auth-orb-tr { .left-aurora-1 {
top: -10%; top: -15%;
right: -10%; right: -15%;
width: 420px; width: 450px;
height: 420px; height: 450px;
background: radial-gradient(ellipse, rgba(20,184,166,0.18) 0%, transparent 70%); background: radial-gradient(ellipse, rgba(139,92,246,0.22) 0%, transparent 65%);
animation: la-drift-1 20s ease-in-out infinite alternate;
} }
.auth-orb-bl { .left-aurora-2 {
bottom: -10%; bottom: -10%;
left: -10%; left: -10%;
width: 380px; width: 350px;
height: 380px; height: 350px;
background: radial-gradient(ellipse, rgba(6,182,212,0.13) 0%, transparent 70%); background: radial-gradient(ellipse, rgba(20,184,166,0.18) 0%, transparent 65%);
animation: la-drift-2 24s ease-in-out infinite alternate;
}
@keyframes la-drift-1 {
from { transform: translate(0,0) scale(1); }
to { transform: translate(-20px,20px) scale(1.06); }
}
@keyframes la-drift-2 {
from { transform: translate(0,0) scale(1); }
to { transform: translate(20px,-20px) scale(1.04); }
}
.left-logo-wrap {
width: 36px;
height: 36px;
border-radius: 9px;
overflow: hidden;
box-shadow: 0 0 0 1px rgba(139,92,246,0.3), 0 0 16px rgba(139,92,246,0.15);
flex-shrink: 0;
} }
.auth-grid {
/* ── ORBITAL VISUALIZATION ──────────────────── */
.orbital-viz {
position: relative;
width: 340px;
height: 340px;
}
/* Hub */
.hub-outer {
position: absolute; position: absolute;
inset: 0; top: 50%;
background-image: left: 50%;
linear-gradient(rgba(20,184,166,0.07) 1px, transparent 1px), transform: translate(-50%, -50%);
linear-gradient(90deg, rgba(20,184,166,0.07) 1px, transparent 1px); display: flex;
background-size: 48px 48px; align-items: center;
justify-content: center;
}
.hub-ring {
position: absolute;
border-radius: 50%;
border: 1px solid rgba(139,92,246,0.15);
animation: hub-pulse 3s ease-in-out infinite;
}
.hub-ring-1 {
width: 80px;
height: 80px;
animation-delay: 0s;
}
.hub-ring-2 {
width: 110px;
height: 110px;
border-color: rgba(20,184,166,0.1);
animation-delay: 0.8s;
} }
.auth-scan { @keyframes hub-pulse {
0%,100% { transform: scale(1); opacity: 0.6; }
50% { transform: scale(1.06); opacity: 1; }
}
.hub-core {
width: 64px;
height: 64px;
border-radius: 16px;
background: linear-gradient(135deg, #3730a3, #4f46e5, #7c3aed);
box-shadow: 0 0 32px rgba(99,102,241,0.5), 0 0 64px rgba(99,102,241,0.2);
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 2;
}
/* Orbit chips — positioned around center */
.orbit-chip {
position: absolute; position: absolute;
left: 0; display: flex;
right: 0; align-items: center;
height: 1px; gap: 6px;
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%); padding: 5px 11px 5px 8px;
animation: auth-scan 12s linear infinite; border-radius: 20px;
box-shadow: 0 0 8px rgba(20,184,166,0.35); border: 1px solid rgba(255,255,255,0.08);
} background: rgba(15,22,40,0.85);
@keyframes auth-scan { backdrop-filter: blur(8px);
0% { top: 0%; opacity: 0; } font-size: 11px;
4% { opacity: 1; } font-weight: 500;
96% { opacity: 0.4; } color: #cbd5e1;
100% { top: 100%; opacity: 0; } white-space: nowrap;
} animation: chip-float 4s ease-in-out infinite;
}
/* ── Logo ────────────────────────────────── */ .chip-dot {
.auth-logo-wrap { width: 7px;
box-shadow: height: 7px;
0 0 0 1px rgba(20,184,166,0.3), border-radius: 50%;
0 0 20px rgba(20,184,166,0.18), flex-shrink: 0;
0 8px 24px rgba(0,0,0,0.4); }
}
/* Position each chip around the orbital */
/* ── Brand text ──────────────────────────── */ .chip-0 { top: 6%; left: 62%; animation-delay: 0.0s; }
.auth-site-name { .chip-1 { top: 44%; left: 80%; animation-delay: 0.6s; }
background: linear-gradient(135deg, #ffffff 0%, #7dd3c8 45%, #14b8a6 100%); .chip-2 { top: 78%; left: 52%; animation-delay: 1.2s; }
-webkit-background-clip: text; .chip-3 { top: 72%; left: 6%; animation-delay: 1.8s; }
-webkit-text-fill-color: transparent; .chip-4 { top: 12%; left: 2%; animation-delay: 2.4s; }
background-clip: text;
} @keyframes chip-float {
.auth-subtitle { 0%,100% { transform: translateY(0); }
color: rgba(20,184,166,0.5); 50% { transform: translateY(-6px); }
} }
/* ── Card ────────────────────────────────── */ /* SVG connecting lines */
.auth-card { .orbit-lines {
background: rgba(26,46,72,0.72); position: absolute;
border: 1px solid rgba(255,255,255,0.09); inset: 0;
backdrop-filter: blur(18px); width: 100%;
box-shadow: height: 100%;
0 0 0 1px rgba(20,184,166,0.08), pointer-events: none;
0 24px 60px rgba(0,0,0,0.45), }
0 0 50px rgba(20,184,166,0.05);
} /* Feature rows */
.left-feature-row {
/* ── Slot overrides: make child inputs/links readable ── */ display: flex;
align-items: flex-start;
gap: 12px;
}
.left-feature-icon {
width: 34px;
height: 34px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* ── RIGHT PANEL ─────────────────────────── */
.auth-right {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 32px;
background: #f8fafc;
min-height: 100vh;
}
/* Dark mode right panel */
:global(.dark) .auth-right {
background: #0f172a;
}
.auth-form-card {
width: 100%;
max-width: 400px;
background: white;
border-radius: 20px;
border: 1px solid rgba(0,0,0,0.06);
box-shadow: 0 4px 32px rgba(0,0,0,0.08), 0 1px 4px rgba(0,0,0,0.04);
padding: 36px 32px;
}
:global(.dark) .auth-form-card {
background: #1e293b;
border-color: rgba(255,255,255,0.08);
box-shadow: 0 4px 32px rgba(0,0,0,0.4);
}
/* Override slot content for right panel (light mode) */
:deep(h2) {
color: #0f172a !important;
}
:global(.dark) :deep(h2) {
color: white !important;
}
:deep(.input-label) { :deep(.input-label) {
color: #374151;
}
:global(.dark) :deep(.input-label) {
color: #94a3b8; color: #94a3b8;
} }
:deep(.input) { :deep(.input) {
background: #f9fafb;
border-color: #e5e7eb;
color: #0f172a;
}
:global(.dark) :deep(.input) {
background: rgba(15,28,48,0.7); background: rgba(15,28,48,0.7);
border-color: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.1);
color: white; color: white;
} }
:deep(.input:focus) { :deep(.input:focus) {
border-color: rgba(20,184,166,0.5); border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(20,184,166,0.12); box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
} }
:deep(.input::placeholder) { :deep(.input::placeholder) {
color: #475569; color: #9ca3af;
} }
:deep(h2) { :global(.dark) :deep(.input::placeholder) {
color: white !important; color: #475569;
} }
:deep(.text-gray-500), :deep(.text-gray-500),
:deep(.dark\:text-dark-400) { :deep(.dark\:text-dark-400) {
color: #6b7280 !important;
}
:global(.dark) :deep(.text-gray-500),
:global(.dark) :deep(.dark\:text-dark-400) {
color: #64748b !important; color: #64748b !important;
} }
</style> </style>
...@@ -40,6 +40,13 @@ export default { ...@@ -40,6 +40,13 @@ export default {
} }
} }
}, },
// Pipeline visualization labels
pipeline: {
gateway: 'Gateway',
balancer: 'Balancer',
aiPool: 'AI Pool',
response: 'Response',
},
// Solutions section // Solutions section
solutions: { solutions: {
title: 'TrafficAPI Solves This', title: 'TrafficAPI Solves This',
......
...@@ -40,6 +40,13 @@ export default { ...@@ -40,6 +40,13 @@ export default {
} }
} }
}, },
// 管道可视化标签
pipeline: {
gateway: '网关',
balancer: '均衡器',
aiPool: 'AI 池',
response: '响应',
},
// 解决方案区块 // 解决方案区块
solutions: { solutions: {
title: 'TrafficAPI 为你解决', title: 'TrafficAPI 为你解决',
......
...@@ -96,16 +96,26 @@ ...@@ -96,16 +96,26 @@
@apply inline-flex items-center justify-center gap-2; @apply inline-flex items-center justify-center gap-2;
@apply rounded-xl px-4 py-2.5 text-sm font-medium; @apply rounded-xl px-4 py-2.5 text-sm font-medium;
@apply transition-all duration-200 ease-out; @apply transition-all duration-200 ease-out;
@apply focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2; @apply focus:outline-none;
@apply disabled:transform-none disabled:cursor-not-allowed disabled:opacity-50; @apply disabled:transform-none disabled:cursor-not-allowed disabled:opacity-50;
@apply active:scale-[0.98]; @apply active:scale-[0.98];
} }
.btn-primary { .btn-primary {
@apply bg-gradient-to-r from-primary-500 to-primary-600; background: linear-gradient(135deg, #7c3aed, #8b5cf6);
@apply text-white shadow-md shadow-primary-500/25; color: white;
@apply hover:from-primary-600 hover:to-primary-700 hover:shadow-lg hover:shadow-primary-500/30; box-shadow: 0 4px 14px rgba(139, 92, 246, 0.35);
@apply dark:shadow-primary-500/20; }
.btn-primary:hover {
background: linear-gradient(135deg, #6d28d9, #7c3aed);
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.45);
transform: translateY(-1px);
}
.btn-primary:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.35), 0 4px 14px rgba(139, 92, 246, 0.3);
} }
.btn-secondary { .btn-secondary {
...@@ -165,7 +175,7 @@ ...@@ -165,7 +175,7 @@
@apply text-gray-900 dark:text-gray-100; @apply text-gray-900 dark:text-gray-100;
@apply placeholder:text-gray-400 dark:placeholder:text-dark-400; @apply placeholder:text-gray-400 dark:placeholder:text-dark-400;
@apply transition-all duration-200; @apply transition-all duration-200;
@apply focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/30; @apply focus:outline-none focus:border-[#8b5cf6] focus:ring-2 focus:ring-[#8b5cf6]/20;
@apply disabled:cursor-not-allowed disabled:bg-gray-100 dark:disabled:bg-dark-900; @apply disabled:cursor-not-allowed disabled:bg-gray-100 dark:disabled:bg-dark-900;
} }
...@@ -522,11 +532,12 @@ ...@@ -522,11 +532,12 @@
@apply border-l-primary-500; @apply border-l-primary-500;
} }
/* ============ 侧边栏 ============ */ /* ============ 侧边栏 — 恒暗风格 ============ */
.sidebar { .sidebar {
@apply fixed inset-y-0 left-0 z-40; @apply fixed inset-y-0 left-0 z-40;
@apply w-64 bg-white dark:bg-dark-900; @apply w-64;
@apply border-r border-gray-200 dark:border-dark-800; background: #0c1220;
border-right: 1px solid rgba(139, 92, 246, 0.08);
@apply flex flex-col; @apply flex flex-col;
@apply transition-transform duration-300; @apply transition-transform duration-300;
} }
...@@ -534,7 +545,7 @@ ...@@ -534,7 +545,7 @@
.sidebar-header { .sidebar-header {
@apply h-16 px-6; @apply h-16 px-6;
@apply flex items-center gap-3; @apply flex items-center gap-3;
@apply border-b border-gray-100 dark:border-dark-800; border-bottom: 1px solid rgba(255, 255, 255, 0.05);
} }
.sidebar-nav { .sidebar-nav {
...@@ -544,16 +555,26 @@ ...@@ -544,16 +555,26 @@
.sidebar-link { .sidebar-link {
@apply flex items-center gap-3 rounded-xl px-3 py-2.5; @apply flex items-center gap-3 rounded-xl px-3 py-2.5;
@apply text-sm font-medium; @apply text-sm font-medium;
@apply text-gray-600 dark:text-dark-300; color: rgba(148, 163, 184, 0.85);
@apply transition-all duration-200; @apply transition-all duration-200;
@apply hover:bg-gray-100 dark:hover:bg-dark-800; }
@apply hover:text-gray-900 dark:hover:text-white;
.sidebar-link:hover {
background: rgba(255, 255, 255, 0.055);
color: rgba(226, 232, 240, 1);
} }
.sidebar-link-active { .sidebar-link-active {
@apply bg-primary-50 dark:bg-primary-900/20; background: linear-gradient(135deg, rgba(99, 102, 241, 0.18), rgba(20, 184, 166, 0.12));
@apply text-primary-600 dark:text-primary-400; border-left: 2px solid #14b8a6;
@apply hover:bg-primary-100 dark:hover:bg-primary-900/30; padding-left: calc(0.75rem - 2px);
color: #5eead4 !important;
box-shadow: inset 0 0 16px rgba(20, 184, 166, 0.04);
}
.sidebar-link-active:hover {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.24), rgba(20, 184, 166, 0.18));
color: #5eead4 !important;
} }
.sidebar-section { .sidebar-section {
...@@ -563,7 +584,7 @@ ...@@ -563,7 +584,7 @@
.sidebar-section-title { .sidebar-section-title {
@apply mb-2 px-3; @apply mb-2 px-3;
@apply text-xs font-semibold uppercase tracking-wider; @apply text-xs font-semibold uppercase tracking-wider;
@apply text-gray-400 dark:text-dark-500; color: rgba(100, 116, 139, 0.7);
} }
/* ============ 页面头部 ============ */ /* ============ 页面头部 ============ */
......
...@@ -11,71 +11,55 @@ ...@@ -11,71 +11,55 @@
<div v-else v-html="homeContent"></div> <div v-else v-html="homeContent"></div>
</div> </div>
<!-- Default Home Page — Tech Redesign --> <!-- Default Home Page — Aurora Bento Redesign -->
<div v-else class="home-root relative flex min-h-screen flex-col overflow-hidden"> <div v-else class="home-root relative flex min-h-screen flex-col overflow-hidden">
<!-- ══════════════════════════════════════════ <!-- ══════════════════════════════════════════
BACKGROUND AURORA BACKGROUND
══════════════════════════════════════════ --> ══════════════════════════════════════════ -->
<div class="pointer-events-none absolute inset-0 overflow-hidden"> <div class="pointer-events-none absolute inset-0 overflow-hidden">
<!-- Glow orbs --> <div class="aurora aurora-violet"></div>
<div class="orb orb-tr"></div> <div class="aurora aurora-teal"></div>
<div class="orb orb-bl"></div> <div class="aurora aurora-indigo"></div>
<div class="orb orb-center"></div> <!-- Subtle noise texture overlay -->
<!-- Grid --> <div class="noise-overlay"></div>
<div class="grid-overlay"></div>
<!-- Scan line -->
<div class="scan-line"></div>
</div> </div>
<!-- ══════════════════════════════════════════ <!-- ══════════════════════════════════════════
HEADER HEADER
══════════════════════════════════════════ --> ══════════════════════════════════════════ -->
<header class="home-header relative z-20 px-6 py-4"> <header class="home-header relative z-20">
<nav class="mx-auto flex max-w-6xl items-center justify-between"> <nav class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
<!-- Logo + Brand --> <!-- Logo + Brand -->
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="logo-ring h-9 w-9 overflow-hidden rounded-lg"> <div class="logo-badge">
<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"> <span class="hidden font-semibold text-white/80 sm:block">{{ siteName }}</span>
{{ siteName }}
</span>
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<LocaleSwitcher /> <LocaleSwitcher />
<a <a
v-if="docUrl" v-if="docUrl"
:href="docUrl" :href="docUrl"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="nav-btn" class="nav-pill"
:title="t('home.viewDocs')"
> >
<Icon name="book" size="sm" class="mr-1" /> <Icon name="book" size="sm" class="mr-1.5" />
<span class="hidden sm:inline">{{ t('home.docs') }}</span> <span class="hidden sm:inline">{{ t('home.docs') }}</span>
</a> </a>
<button @click="toggleTheme" class="nav-icon-btn">
<button @click="toggleTheme" class="nav-btn icon-only" :title="isDark ? t('home.switchToLight') : t('home.switchToDark')">
<Icon v-if="isDark" name="sun" size="sm" /> <Icon v-if="isDark" name="sun" size="sm" />
<Icon v-else name="moon" size="sm" /> <Icon v-else name="moon" size="sm" />
</button> </button>
<router-link v-if="isAuthenticated" :to="dashboardPath" class="cta-header-btn">
<router-link <span class="avatar-dot">{{ userInitial }}</span>
v-if="isAuthenticated"
:to="dashboardPath"
class="cta-nav-btn"
>
<span class="user-dot">{{ userInitial }}</span>
{{ t('home.dashboard') }} {{ t('home.dashboard') }}
<svg class="h-3 w-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
</svg>
</router-link> </router-link>
<router-link v-else to="/login" class="cta-nav-btn"> <router-link v-else to="/login" class="cta-header-btn">
{{ t('home.login') }} {{ t('home.login') }}
</router-link> </router-link>
</div> </div>
...@@ -85,184 +69,252 @@ ...@@ -85,184 +69,252 @@
<!-- ══════════════════════════════════════════ <!-- ══════════════════════════════════════════
MAIN MAIN
══════════════════════════════════════════ --> ══════════════════════════════════════════ -->
<main class="relative z-10 flex-1 px-6"> <main class="relative z-10 flex-1">
<!-- ── HERO ────────────────────────────── --> <!-- ── HERO (Centered) ─────────────────── -->
<section class="mx-auto max-w-6xl py-20 lg:py-28"> <section class="mx-auto max-w-5xl px-6 pb-8 pt-20 text-center lg:pt-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">
<!-- Badge --> <!-- Badge -->
<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"> <div class="mb-8 inline-flex items-center gap-2 rounded-full border border-violet-500/20 bg-violet-500/8 px-4 py-1.5">
<span class="h-1.5 w-1.5 animate-pulse rounded-full bg-teal-400 shadow-[0_0_6px_#14b8a6]"></span> <span class="h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400 shadow-[0_0_6px_#a78bfa]"></span>
AI API GATEWAY · ENTERPRISE GRADE <span class="font-mono text-[11px] font-medium uppercase tracking-widest text-violet-300">
AI API Gateway · Next Generation
</span>
</div> </div>
<!-- Headline --> <!-- Headline -->
<h1 class="mb-5 font-black leading-[1.08] tracking-tight"> <h1 class="hero-headline mb-6">
<span class="gradient-text block whitespace-nowrap text-[clamp(1.6rem,4.5vw,3.75rem)]">{{ t('home.heroSubtitle') }}</span> {{ t('home.heroSubtitle') }}
</h1> </h1>
<!-- Mono accent line --> <!-- Subline -->
<p class="mb-5 font-mono text-[11px] uppercase tracking-[0.2em] text-teal-600/60"> <p class="mx-auto mb-10 max-w-2xl text-lg leading-relaxed text-slate-400">
// {{ 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') }} {{ t('home.heroDescription') }}
</p> </p>
<!-- CTA buttons --> <!-- CTA -->
<div class="flex flex-wrap items-center justify-center gap-4 lg:justify-start"> <div class="mb-16 flex flex-wrap items-center justify-center gap-4">
<router-link <router-link
:to="isAuthenticated ? dashboardPath : '/login'" :to="isAuthenticated ? dashboardPath : '/login'"
class="cta-primary inline-flex items-center gap-2 rounded-lg px-7 py-3 text-sm font-semibold text-white" class="cta-primary-btn"
> >
{{ isAuthenticated ? t('home.goToDashboard') : t('home.getStarted') }} {{ isAuthenticated ? t('home.goToDashboard') : t('home.getStarted') }}
<Icon name="arrowRight" size="sm" :stroke-width="2.5" /> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
</svg>
</router-link> </router-link>
<a <a
v-if="docUrl" v-if="docUrl"
:href="docUrl" :href="docUrl"
target="_blank" target="_blank"
rel="noopener noreferrer" 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" class="cta-ghost-btn"
> >
<Icon name="book" size="sm" /> <Icon name="book" size="sm" />
{{ t('home.viewDocs') }} {{ t('home.viewDocs') }}
</a> </a>
</div> </div>
<!-- Stats --> <!-- ── Pipeline Visualization ─────────── -->
<div class="mt-12 flex flex-wrap items-center justify-center gap-8 lg:justify-start"> <div class="pipeline-wrapper">
<div v-for="s in stats" :key="s.label" class="stat-item"> <div class="pipeline-glow"></div>
<span class="stat-value">{{ s.value }}</span> <div class="pipeline-card">
<span class="stat-label">{{ s.label }}</span> <div class="pipeline-inner">
</div>
<!-- Step: Client -->
<div class="pipe-node">
<div class="pipe-icon pipe-icon-client">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0H3" />
</svg>
</div> </div>
<span class="pipe-label">Client</span>
</div> </div>
<!-- Terminal side --> <div class="pipe-connector">
<div class="flex flex-1 justify-center lg:justify-end"> <div class="pipe-line"></div>
<div class="terminal-wrap"> <div class="pipe-dot pipe-dot-1"></div>
<div class="terminal-glow"></div> </div>
<div class="terminal-window">
<!-- Header bar --> <!-- Step: Gateway -->
<div class="terminal-header"> <div class="pipe-node">
<div class="terminal-dots"> <div class="pipe-icon pipe-icon-gateway">
<span class="dot-red"></span> <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<span class="dot-yellow"></span> <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" />
<span class="dot-green"></span> </svg>
</div> </div>
<span class="terminal-title">trafficapi · gateway</span> <span class="pipe-label">{{ t('home.pipeline.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>
<!-- Code body --> <div class="pipe-connector">
<div class="terminal-body"> <div class="pipe-line"></div>
<div class="t-line line-1"> <div class="pipe-dot pipe-dot-2"></div>
<span class="t-prompt"></span>
<span class="t-method">POST</span>
<span class="t-path">/v1/messages</span>
<span class="t-dim ml-auto">claude-3-5-sonnet</span>
</div> </div>
<div class="t-line line-2">
<span class="t-comment">&nbsp;&nbsp;⠿ routing to pool · 3 accounts available</span> <!-- Step: Balancer -->
<div class="pipe-node">
<div class="pipe-icon pipe-icon-balancer">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<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" />
</svg>
</div> </div>
<div class="t-line line-3"> <span class="pipe-label">{{ t('home.pipeline.balancer') }}</span>
<span class="t-ok">✓ 200</span>
<span class="t-ms">38ms</span>
<span class="t-dim">· account[1] selected</span>
</div> </div>
<div class="t-sep sep-1"></div> <div class="pipe-connector">
<div class="pipe-line"></div>
<div class="t-line line-4"> <div class="pipe-dot pipe-dot-3"></div>
<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>
<div class="t-line line-5">
<span class="t-comment">&nbsp;&nbsp;⠿ load balancing · 2 upstream accounts</span> <!-- Step: AI Pool -->
<div class="pipe-node">
<div class="pipe-icon pipe-icon-ai">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
</svg>
</div> </div>
<div class="t-line line-6"> <span class="pipe-label">{{ t('home.pipeline.aiPool') }}</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="t-sep sep-2"></div> <div class="pipe-connector">
<div class="pipe-line"></div>
<div class="pipe-dot pipe-dot-4"></div>
</div>
<div class="t-line line-7"> <!-- Step: Response -->
<span class="t-stat">📊 analytics</span> <div class="pipe-node">
<span class="t-dim">&nbsp;req: 1.4k/min · p99: 68ms · err: 0%</span> <div class="pipe-icon pipe-icon-response">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div> </div>
<div class="t-line line-8"> <span class="pipe-label">{{ t('home.pipeline.response') }}</span>
<span class="t-prompt">_</span>
<span class="t-cursor"></span>
</div> </div>
</div> </div>
<!-- Pipeline metrics bar -->
<div class="pipe-metrics">
<div v-for="m in pipeMetrics" :key="m.label" class="pipe-metric-item">
<span class="pipe-metric-val">{{ m.value }}</span>
<span class="pipe-metric-label">{{ m.label }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- ── TAGS BAR ─────────────────────────── --> <!-- ── BENTO FEATURES ─────────────────── -->
<section class="mx-auto max-w-6xl pb-14"> <section class="mx-auto max-w-7xl px-6 pb-20">
<div class="flex flex-wrap items-center justify-center gap-3"> <div class="mb-12 text-center">
<div v-for="tag in featureTags" :key="tag.text" class="tag-pill"> <h2 class="mb-3 text-3xl font-bold text-white">
<span class="tag-pip"></span> <span class="bento-title-gradient">{{ t('home.solutions.title') }}</span>
{{ tag.text }} </h2>
</div> <p class="text-sm font-mono uppercase tracking-widest text-slate-600">
{{ t('home.solutions.subtitle') }}
</p>
</div> </div>
</section>
<!-- ── FEATURES ─────────────────────────── --> <div class="bento-grid">
<section class="mx-auto max-w-6xl pb-16">
<div class="mb-10 text-center"> <!-- LARGE CARD: Unified Gateway -->
<h2 class="mb-2 text-2xl font-bold text-white md:text-3xl"> <div class="bento-card bento-large group">
<span class="gradient-text">{{ t('home.solutions.title') }}</span> <div class="bento-accent-bar" style="background: linear-gradient(180deg, #3b82f6, #6366f1)"></div>
</h2> <div class="flex h-full flex-col">
<p class="font-mono text-xs uppercase tracking-widest text-slate-600"> <div class="bento-icon-wrap mb-5" style="background: linear-gradient(135deg, #3b82f6, #6366f1); box-shadow: 0 8px 24px rgba(99,102,241,0.3)">
// {{ t('home.solutions.subtitle') }} <component :is="IconServer" />
</div>
<h3 class="mb-3 text-xl font-semibold text-white">{{ t(`home.features.unifiedGateway`) }}</h3>
<p class="mb-6 text-sm leading-relaxed text-slate-500 group-hover:text-slate-400">
{{ t(`home.features.unifiedGatewayDesc`) }}
</p> </p>
<!-- Code snippet decoration -->
<div class="mt-auto rounded-lg border border-white/5 bg-black/30 p-3 font-mono text-xs">
<span class="text-slate-600">POST </span>
<span class="text-blue-400">/v1/chat/completions</span>
<br>
<span class="text-slate-600">Authorization: </span>
<span class="text-emerald-400">Bearer sk-****</span>
<br>
<span class="text-violet-400"></span>
<span class="text-teal-400">auto-routed to best provider</span>
</div>
</div>
</div> </div>
<div class="grid gap-4 md:grid-cols-3"> <!-- REGULAR CARD: Load Balancing -->
<div <div class="bento-card bento-regular group">
v-for="feat in features" <div class="bento-accent-bar" style="background: linear-gradient(180deg, #14b8a6, #06b6d4)"></div>
:key="feat.key" <div class="bento-icon-wrap mb-4" style="background: linear-gradient(135deg, #14b8a6, #0d9488); box-shadow: 0 8px 24px rgba(20,184,166,0.3)">
class="feat-card group" <component :is="IconRoute" />
> </div>
<div class="feat-accent" :style="{ background: feat.color }"></div> <h3 class="mb-2 text-lg font-semibold text-white">{{ t(`home.features.multiAccount`) }}</h3>
<div class="feat-corner-tr"></div> <p class="text-sm leading-relaxed text-slate-500 group-hover:text-slate-400">
<div class="feat-corner-bl"></div> {{ t(`home.features.multiAccountDesc`) }}
</p>
<!-- Mini balance visualization -->
<div class="mt-4 space-y-2">
<div v-for="(bar, i) in loadBars" :key="i" class="flex items-center gap-2">
<span class="w-14 font-mono text-[10px] text-slate-600">{{ bar.label }}</span>
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-white/5">
<div class="h-full rounded-full transition-all" :style="{ width: bar.width, background: bar.color }"></div>
</div>
<span class="w-8 text-right font-mono text-[10px] text-teal-500">{{ bar.pct }}</span>
</div>
</div>
</div>
<div <!-- STATS CARD -->
class="feat-icon mb-5" <div class="bento-card bento-stats">
:style="{ background: feat.iconBg, boxShadow: `0 4px 20px ${feat.glow}` }" <div class="flex h-full flex-col justify-between">
> <div class="mb-2 font-mono text-[10px] uppercase tracking-wider text-slate-600">// Live Stats</div>
<component :is="feat.iconComponent" /> <div class="space-y-4">
<div v-for="s in stats" :key="s.label" class="stat-row">
<span class="stat-big-value">{{ s.value }}</span>
<span class="stat-big-label">{{ s.label }}</span>
</div>
</div>
<div class="mt-4 flex items-center gap-1.5">
<span class="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-400 shadow-[0_0_6px_#34d399]"></span>
<span class="font-mono text-[10px] text-emerald-500">99.9% SLA Uptime</span>
</div>
</div>
</div> </div>
<h3 class="mb-2 font-semibold text-white">{{ t(`home.features.${feat.key}`) }}</h3> <!-- WIDE CARD: Analytics -->
<p class="text-sm leading-relaxed text-slate-500 transition-colors group-hover:text-slate-400"> <div class="bento-card bento-wide group">
{{ t(`home.features.${feat.key}Desc`) }} <div class="bento-accent-bar" style="background: linear-gradient(180deg, #a855f7, #7c3aed)"></div>
<div class="flex h-full flex-col">
<div class="mb-4 flex items-start justify-between">
<div>
<div class="bento-icon-wrap mb-3" style="background: linear-gradient(135deg, #a855f7, #7c3aed); box-shadow: 0 8px 24px rgba(168,85,247,0.3); width: 40px; height: 40px">
<component :is="IconChart" />
</div>
<h3 class="text-lg font-semibold text-white">{{ t(`home.features.balanceQuota`) }}</h3>
</div>
<div class="flex gap-1.5">
<div v-for="tag in featureTags" :key="tag.text" class="mini-tag">{{ tag.text }}</div>
</div>
</div>
<p class="text-sm leading-relaxed text-slate-500 group-hover:text-slate-400">
{{ t(`home.features.balanceQuotaDesc`) }}
</p> </p>
<!-- Sparkline decoration -->
<div class="mt-4 flex items-end gap-1">
<div
v-for="(h, i) in sparkline"
:key="i"
class="sparkbar"
:style="{ height: h + 'px', animationDelay: (i * 0.08) + 's' }"
></div>
</div>
</div> </div>
</div> </div>
</div>
</section> </section>
<!-- ── PROVIDERS ────────────────────────── --> <!-- ── PROVIDERS MARQUEE ──────────────── -->
<section class="mx-auto max-w-6xl pb-20"> <section class="mx-auto max-w-7xl px-6 pb-20">
<div class="mb-8 text-center"> <div class="mb-8 text-center">
<h2 class="mb-2 text-xl font-bold text-white">{{ t('home.providers.title') }}</h2> <h2 class="mb-2 text-xl font-bold text-white">{{ t('home.providers.title') }}</h2>
<p class="font-mono text-xs uppercase tracking-[0.2em] text-slate-600"> <p class="font-mono text-xs uppercase tracking-[0.2em] text-slate-600">
...@@ -270,24 +322,24 @@ ...@@ -270,24 +322,24 @@
</p> </p>
</div> </div>
<div class="flex flex-wrap items-center justify-center gap-3"> <div class="marquee-container">
<div class="marquee-fade-left"></div>
<div class="marquee-fade-right"></div>
<div class="marquee-track">
<div class="marquee-inner">
<div <div
v-for="p in providerList" v-for="p in [...providerList, ...providerList]"
:key="p.name" :key="p.name + Math.random()"
class="provider-card" class="provider-chip"
:class="{ 'provider-active': p.live, 'provider-inactive': !p.live }" :class="p.live ? 'provider-chip-live' : 'provider-chip-dim'"
> >
<span <div class="provider-badge" :style="{ background: p.gradient }">
class="provider-status-dot" <span class="text-[10px] font-bold text-white">{{ p.letter }}</span>
:class="p.live ? 'dot-live' : 'dot-offline'" </div>
></span> <span class="text-sm font-medium">{{ p.name }}</span>
<div class="provider-icon-badge" :style="{ background: p.gradient }"> <span v-if="p.live" class="provider-live-dot"></span>
<span class="text-[11px] font-bold text-white">{{ p.letter }}</span> </div>
</div> </div>
<span class="text-sm font-medium text-slate-300">{{ p.name }}</span>
<span class="provider-tag" :class="p.live ? 'tag-live' : 'tag-soon'">
{{ p.live ? t('home.providers.supported') : t('home.providers.soon') }}
</span>
</div> </div>
</div> </div>
</section> </section>
...@@ -297,29 +349,16 @@ ...@@ -297,29 +349,16 @@
<!-- ══════════════════════════════════════════ <!-- ══════════════════════════════════════════
FOOTER FOOTER
══════════════════════════════════════════ --> ══════════════════════════════════════════ -->
<footer class="home-footer relative z-10 px-6 py-6"> <footer class="home-footer relative z-10">
<div class="mx-auto flex max-w-6xl flex-col items-center justify-between gap-3 text-center sm:flex-row sm:text-left"> <div class="mx-auto flex max-w-7xl flex-col items-center justify-between gap-3 px-6 py-6 text-center sm:flex-row sm:text-left">
<p class="font-mono text-xs text-slate-700"> <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-5"> <div class="flex items-center gap-5">
<a <a v-if="docUrl" :href="docUrl" target="_blank" rel="noopener noreferrer"
v-if="docUrl" class="font-mono text-xs text-slate-700 transition-colors hover:text-teal-400">/docs</a>
:href="docUrl" <a :href="githubUrl" target="_blank" rel="noopener noreferrer"
target="_blank" class="font-mono text-xs text-slate-700 transition-colors hover:text-teal-400">/github</a>
rel="noopener noreferrer"
class="font-mono text-xs text-slate-700 transition-colors hover:text-teal-400"
>
/docs
</a>
<a
:href="githubUrl"
target="_blank"
rel="noopener noreferrer"
class="font-mono text-xs text-slate-700 transition-colors hover:text-teal-400"
>
/github
</a>
</div> </div>
</div> </div>
</footer> </footer>
...@@ -338,7 +377,6 @@ const { t } = useI18n() ...@@ -338,7 +377,6 @@ const { t } = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
const appStore = useAppStore() const appStore = useAppStore()
// Site settings
const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'TrafficAPI') 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 || '')
...@@ -349,13 +387,8 @@ const isHomeContentUrl = computed(() => { ...@@ -349,13 +387,8 @@ const isHomeContentUrl = computed(() => {
return content.startsWith('http://') || content.startsWith('https://') return content.startsWith('http://') || content.startsWith('https://')
}) })
// Theme
const isDark = ref(document.documentElement.classList.contains('dark')) const isDark = ref(document.documentElement.classList.contains('dark'))
// GitHub
const githubUrl = 'https://github.com/Wei-Shaw/sub2api' const githubUrl = 'https://github.com/Wei-Shaw/sub2api'
// 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')
...@@ -364,24 +397,35 @@ const userInitial = computed(() => { ...@@ -364,24 +397,35 @@ const userInitial = computed(() => {
if (!user?.email) return '' if (!user?.email) return ''
return user.email.charAt(0).toUpperCase() return user.email.charAt(0).toUpperCase()
}) })
const currentYear = computed(() => new Date().getFullYear()) const currentYear = computed(() => new Date().getFullYear())
// Stats
const stats = [ const stats = [
{ value: '3+', label: 'AI Providers' }, { value: '3+', label: 'AI Providers' },
{ value: '99.9%', label: 'SLA Uptime' }, { value: '99.9%', label: 'SLA Uptime' },
{ value: '1', label: 'API Key' }, { value: '1', label: 'API Key' },
] ]
// Feature tags const pipeMetrics = [
{ value: '< 50ms', label: 'p99 Latency' },
{ value: '0%', label: 'Error Rate' },
{ value: '1.4k/m', label: 'Req/min' },
]
const loadBars = [
{ label: 'acct[0]', width: '72%', pct: '72%', color: '#14b8a6' },
{ label: 'acct[1]', width: '45%', pct: '45%', color: '#06b6d4' },
{ label: 'acct[2]', width: '61%', pct: '61%', color: '#0ea5e9' },
]
const sparkline = [12, 20, 16, 28, 22, 35, 30, 42, 38, 50, 45, 55, 48, 60, 55, 65, 58, 70, 62, 75]
const featureTags = computed(() => [ const featureTags = computed(() => [
{ text: t('home.tags.subscriptionToApi') }, { text: t('home.tags.subscriptionToApi') },
{ text: t('home.tags.stickySession') }, { text: t('home.tags.stickySession') },
{ text: t('home.tags.realtimeBilling') }, { text: t('home.tags.realtimeBilling') },
]) ])
// SVG icon components (inline) // Inline SVG icon components
const IconServer = defineComponent({ 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' }, [ 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' }) 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' })
...@@ -393,37 +437,11 @@ const IconRoute = defineComponent({ ...@@ -393,37 +437,11 @@ const IconRoute = defineComponent({
]) ])
}) })
const IconChart = defineComponent({ 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' }, [ render: () => h('svg', { class: 'h-4 w-4 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' }) 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(() => [ const providerList = computed(() => [
{ name: t('home.providers.claude'), letter: 'C', gradient: 'linear-gradient(135deg,#f97316,#ea580c)', live: true }, { 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: 'GPT', letter: 'G', gradient: 'linear-gradient(135deg,#22c55e,#16a34a)', live: true },
...@@ -432,7 +450,6 @@ const providerList = computed(() => [ ...@@ -432,7 +450,6 @@ const providerList = computed(() => [
{ name: t('home.providers.more'), letter: '+', gradient: 'linear-gradient(135deg,#475569,#334155)', live: false }, { 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)
...@@ -458,120 +475,130 @@ onMounted(() => { ...@@ -458,120 +475,130 @@ onMounted(() => {
<style scoped> <style scoped>
/* ══════════════════════════════════════════════ /* ══════════════════════════════════════════════
ROOT — always dark for this landing page ROOT
══════════════════════════════════════════════ */ ══════════════════════════════════════════════ */
.home-root { .home-root {
background: #182a40; background: #080e1a;
color: white; color: white;
} }
/* ══════════════════════════════════════════════ /* ══════════════════════════════════════════════
BACKGROUND LAYERS AURORA BACKGROUND
══════════════════════════════════════════════ */ ══════════════════════════════════════════════ */
.orb { .aurora {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
filter: blur(100px); filter: blur(120px);
pointer-events: none; pointer-events: none;
} }
.orb-tr { .aurora-violet {
top: -15%; top: -20%;
right: -15%; right: -10%;
width: 700px;
height: 700px;
background: radial-gradient(ellipse, rgba(139,92,246,0.18) 0%, transparent 65%);
animation: aurora-drift-1 18s ease-in-out infinite alternate;
}
.aurora-teal {
bottom: -15%;
left: -10%;
width: 600px; width: 600px;
height: 600px; height: 600px;
background: radial-gradient(ellipse, rgba(20,184,166,0.18) 0%, transparent 70%); background: radial-gradient(ellipse, rgba(20,184,166,0.15) 0%, transparent 65%);
animation: aurora-drift-2 22s ease-in-out infinite alternate;
} }
.orb-bl { .aurora-indigo {
bottom: -15%; top: 40%;
left: -15%; left: 25%;
width: 500px; width: 500px;
height: 500px; height: 500px;
background: radial-gradient(ellipse, rgba(6,182,212,0.13) 0%, transparent 70%); background: radial-gradient(ellipse, rgba(99,102,241,0.10) 0%, transparent 65%);
animation: aurora-drift-3 26s ease-in-out infinite alternate;
} }
.orb-center { @keyframes aurora-drift-1 {
top: 30%; from { transform: translate(0, 0) scale(1); }
left: 40%; to { transform: translate(-40px, 30px) scale(1.08); }
width: 400px;
height: 400px;
background: radial-gradient(ellipse, rgba(20,184,166,0.08) 0%, transparent 70%);
} }
.grid-overlay { @keyframes aurora-drift-2 {
position: absolute; from { transform: translate(0, 0) scale(1); }
inset: 0; to { transform: translate(30px, -40px) scale(1.05); }
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 { @keyframes aurora-drift-3 {
position: absolute; from { transform: translate(0, 0) scale(1); }
left: 0; to { transform: translate(-20px, -30px) scale(0.95); }
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 { .noise-overlay {
0% { top: 0%; opacity: 0; } position: absolute;
3% { opacity: 1; } inset: 0;
97% { opacity: 0.4; } opacity: 0.025;
100% { top: 100%; opacity: 0; } background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 128px 128px;
} }
/* ══════════════════════════════════════════════ /* ══════════════════════════════════════════════
HEADER HEADER
══════════════════════════════════════════════ */ ══════════════════════════════════════════════ */
.home-header { .home-header {
border-bottom: 1px solid rgba(20,184,166,0.12); border-bottom: 1px solid rgba(255,255,255,0.05);
backdrop-filter: blur(12px); backdrop-filter: blur(16px);
background: rgba(24,42,64,0.8); background: rgba(8,14,26,0.7);
} }
.logo-ring { .logo-badge {
box-shadow: 0 0 0 1px rgba(20,184,166,0.25), 0 0 16px rgba(20,184,166,0.15); width: 34px;
height: 34px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 0 1px rgba(139,92,246,0.25), 0 0 16px rgba(139,92,246,0.12);
} }
.nav-btn { .nav-pill {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 6px 10px; padding: 5px 10px;
border-radius: 6px; border-radius: 6px;
font-size: 12px; font-size: 12px;
color: #94a3b8; color: #94a3b8;
transition: all 0.2s; transition: all 0.2s;
} }
.nav-btn:hover { .nav-pill:hover { background: rgba(255,255,255,0.06); color: #e2e8f0; }
background: rgba(20,184,166,0.08); .nav-icon-btn {
color: #5eead4; display: inline-flex;
} align-items: center;
.nav-btn.icon-only { justify-content: center;
padding: 6px 8px; padding: 7px;
border-radius: 6px;
color: #94a3b8;
transition: all 0.2s;
background: transparent;
border: none;
cursor: pointer;
} }
.cta-nav-btn { .nav-icon-btn:hover { background: rgba(255,255,255,0.06); color: #e2e8f0; }
.cta-header-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 6px 14px; padding: 6px 14px;
border-radius: 7px; border-radius: 8px;
border: 1px solid rgba(20,184,166,0.3); border: 1px solid rgba(139,92,246,0.3);
background: rgba(20,184,166,0.08); background: rgba(139,92,246,0.1);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: #5eead4; color: #c4b5fd;
transition: all 0.2s; transition: all 0.2s;
} }
.cta-nav-btn:hover { .cta-header-btn:hover {
border-color: rgba(20,184,166,0.6); border-color: rgba(139,92,246,0.6);
background: rgba(20,184,166,0.15); background: rgba(139,92,246,0.18);
box-shadow: 0 0 14px rgba(20,184,166,0.2); box-shadow: 0 0 16px rgba(139,92,246,0.2);
} }
.user-dot { .avatar-dot {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 16px; width: 16px; height: 16px;
height: 16px;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, #14b8a6, #0d9488); background: linear-gradient(135deg, #8b5cf6, #6366f1);
font-size: 9px; font-size: 9px;
font-weight: 700; font-weight: 700;
color: white; color: white;
...@@ -580,374 +607,387 @@ onMounted(() => { ...@@ -580,374 +607,387 @@ onMounted(() => {
/* ══════════════════════════════════════════════ /* ══════════════════════════════════════════════
HERO HERO
══════════════════════════════════════════════ */ ══════════════════════════════════════════════ */
.gradient-text { .hero-headline {
background: linear-gradient(135deg, #ffffff 0%, #7dd3c8 40%, #14b8a6 100%); font-size: clamp(2rem, 5vw, 4rem);
font-weight: 900;
line-height: 1.08;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #ffffff 0%, #c4b5fd 35%, #5eead4 70%, #14b8a6 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.cta-primary {
background: linear-gradient(135deg, #0f766e, #14b8a6, #0d9488); /* CTA Buttons */
.cta-primary-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #7c3aed, #8b5cf6, #6366f1);
background-size: 200% 200%; background-size: 200% 200%;
animation: gradient-shift 4s ease infinite; animation: btn-gradient 4s ease infinite;
box-shadow: 0 0 0 1px rgba(20,184,166,0.4), 0 4px 20px rgba(20,184,166,0.25); box-shadow: 0 0 0 1px rgba(139,92,246,0.4), 0 4px 24px rgba(139,92,246,0.3);
transition: box-shadow 0.25s, transform 0.25s; transition: box-shadow 0.25s, transform 0.25s;
} }
.cta-primary:hover { .cta-primary-btn:hover {
box-shadow: 0 0 0 1px rgba(20,184,166,0.7), 0 6px 28px rgba(20,184,166,0.4); box-shadow: 0 0 0 1px rgba(139,92,246,0.7), 0 8px 32px rgba(139,92,246,0.45);
transform: translateY(-2px); transform: translateY(-2px);
} }
@keyframes gradient-shift { @keyframes btn-gradient {
0% { background-position: 0% 50%; } 0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; } 50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; } 100% { background-position: 0% 50%; }
} }
.cta-ghost-btn {
/* Stats */ display: inline-flex;
.stat-item { align-items: center;
display: flex; gap: 8px;
flex-direction: column; padding: 12px 24px;
gap: 2px; border-radius: 10px;
position: relative; font-size: 14px;
} font-weight: 500;
.stat-item + .stat-item::before { color: #94a3b8;
content: ''; border: 1px solid rgba(255,255,255,0.08);
position: absolute; transition: all 0.2s;
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 { .cta-ghost-btn:hover {
font-size: 10px; border-color: rgba(139,92,246,0.3);
color: #475569; color: #c4b5fd;
text-transform: uppercase;
letter-spacing: 0.1em;
} }
/* ══════════════════════════════════════════════ /* ══════════════════════════════════════════════
TERMINAL PIPELINE VISUALIZATION
══════════════════════════════════════════════ */ ══════════════════════════════════════════════ */
.terminal-wrap { .pipeline-wrapper {
position: relative; position: relative;
display: inline-block; display: inline-block;
width: 100%;
} }
.terminal-glow { .pipeline-glow {
position: absolute; position: absolute;
inset: -30px; inset: -20px;
background: radial-gradient(ellipse at center, rgba(20,184,166,0.1) 0%, transparent 65%); background: radial-gradient(ellipse at center, rgba(99,102,241,0.08) 0%, transparent 60%);
pointer-events: none; pointer-events: none;
z-index: 0;
} }
.terminal-window { .pipeline-card {
position: relative; position: relative;
z-index: 1; border-radius: 16px;
width: 460px; border: 1px solid rgba(255,255,255,0.07);
max-width: 100%; background: rgba(15,20,40,0.7);
background: linear-gradient(160deg, #1a2e48 0%, #162540 100%); backdrop-filter: blur(20px);
border-radius: 13px; padding: 28px 24px 20px;
border: 1px solid rgba(20,184,166,0.18); box-shadow: 0 0 0 1px rgba(99,102,241,0.06), 0 24px 60px rgba(0,0,0,0.6);
box-shadow:
0 0 0 1px rgba(255,255,255,0.025),
0 28px 64px rgba(0,0,0,0.7),
0 0 50px rgba(20,184,166,0.07);
overflow: hidden; overflow: hidden;
transform: perspective(900px) rotateX(1.5deg) rotateY(-2.5deg);
transition: transform 0.4s ease, box-shadow 0.4s ease;
} }
.terminal-window:hover { .pipeline-card::before {
transform: perspective(900px) rotateX(0deg) rotateY(0deg) translateY(-8px); content: '';
box-shadow: position: absolute;
0 0 0 1px rgba(20,184,166,0.28), top: 0; left: 0; right: 0;
0 36px 80px rgba(0,0,0,0.75), height: 1px;
0 0 70px rgba(20,184,166,0.12); background: linear-gradient(90deg, transparent, rgba(139,92,246,0.4), rgba(20,184,166,0.4), transparent);
} }
.terminal-header { .pipeline-inner {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; justify-content: center;
padding: 11px 16px; flex-wrap: nowrap;
background: rgba(22,37,60,0.9); gap: 0;
border-bottom: 1px solid rgba(20,184,166,0.12); overflow-x: auto;
padding-bottom: 4px;
} }
.terminal-dots { .pipe-node {
display: flex; display: flex;
gap: 6px; flex-direction: column;
} align-items: center;
.terminal-dots span { gap: 8px;
width: 11px; flex-shrink: 0;
height: 11px;
border-radius: 50%;
}
.dot-red { background: #ef4444; }
.dot-yellow { background: #eab308; }
.dot-green { background: #22c55e; }
.terminal-title {
flex: 1;
text-align: center;
font-size: 11px;
font-family: ui-monospace, monospace;
color: #334155;
} }
.terminal-live { .pipe-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; justify-content: center;
border: 1px solid rgba(255,255,255,0.08);
}
.pipe-icon-client { background: rgba(15,23,42,0.8); color: #94a3b8; }
.pipe-icon-gateway { background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(99,102,241,0.2)); color: #93c5fd; border-color: rgba(99,102,241,0.2); }
.pipe-icon-balancer{ background: linear-gradient(135deg, rgba(20,184,166,0.2), rgba(6,182,212,0.2)); color: #5eead4; border-color: rgba(20,184,166,0.2); }
.pipe-icon-ai { background: linear-gradient(135deg, rgba(168,85,247,0.2), rgba(139,92,246,0.2)); color: #d8b4fe; border-color: rgba(168,85,247,0.2); }
.pipe-icon-response{ background: linear-gradient(135deg, rgba(52,211,153,0.2), rgba(16,185,129,0.2)); color: #6ee7b7; border-color: rgba(52,211,153,0.2); }
.pipe-label {
font-size: 10px; font-size: 10px;
font-family: ui-monospace, monospace; font-family: ui-monospace, monospace;
color: #14b8a6; color: #475569;
letter-spacing: 0.06em; white-space: nowrap;
} }
.terminal-body { .pipe-connector {
padding: 18px 22px 22px; position: relative;
font-family: ui-monospace, 'Fira Code', monospace; width: 60px;
font-size: 12.5px; height: 2px;
line-height: 1.85; flex-shrink: 0;
margin-bottom: 20px;
} }
.pipe-line {
/* Terminal lines */ position: absolute;
.t-line { top: 0; left: 0; right: 0;
height: 100%;
background: linear-gradient(90deg, rgba(99,102,241,0.15), rgba(20,184,166,0.3), rgba(99,102,241,0.15));
}
.pipe-dot {
position: absolute;
top: -3px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #14b8a6;
box-shadow: 0 0 8px rgba(20,184,166,0.8);
}
.pipe-dot-1 { animation: pipe-flow 2.0s linear infinite; }
.pipe-dot-2 { animation: pipe-flow 2.0s linear 0.4s infinite; }
.pipe-dot-3 { animation: pipe-flow 2.0s linear 0.8s infinite; }
.pipe-dot-4 { animation: pipe-flow 2.0s linear 1.2s infinite; }
@keyframes pipe-flow {
from { left: -5%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
to { left: 105%; opacity: 0; }
}
.pipe-metrics {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 7px; justify-content: center;
opacity: 0; gap: 32px;
animation: t-appear 0.35s ease forwards; margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgba(255,255,255,0.05);
} }
.t-sep { .pipe-metric-item {
height: 1px; display: flex;
background: rgba(20,184,166,0.07); flex-direction: column;
margin: 5px 0; align-items: center;
opacity: 0; gap: 2px;
animation: t-appear 0.2s ease forwards;
}
.line-1 { animation-delay: 0.2s; }
.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); }
}
.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;
} }
.t-ms { color: #fcd34d; font-size: 11px; } .pipe-metric-val {
.t-dim { color: #1e3a4a; font-size: 11px; } font-family: ui-monospace, monospace;
.t-stat { color: #a78bfa; } font-size: 13px;
font-weight: 700;
/* Cursor */ color: #5eead4;
.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 { .pipe-metric-label {
0%,44% { opacity: 1; } font-family: ui-monospace, monospace;
50%,94% { opacity: 0; } font-size: 10px;
100% { opacity: 1; } color: #334155;
text-transform: uppercase;
letter-spacing: 0.08em;
} }
/* ══════════════════════════════════════════════ /* ══════════════════════════════════════════════
FEATURE TAGS BENTO FEATURES
══════════════════════════════════════════════ */ ══════════════════════════════════════════════ */
.tag-pill { .bento-title-gradient {
display: inline-flex; background: linear-gradient(135deg, #ffffff, #c4b5fd 50%, #5eead4);
align-items: center; -webkit-background-clip: text;
gap: 8px; -webkit-text-fill-color: transparent;
border: 1px solid rgba(20,184,166,0.14); background-clip: text;
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; .bento-grid {
height: 5px; display: grid;
border-radius: 50%; grid-template-columns: repeat(3, 1fr);
background: #14b8a6; grid-auto-rows: auto;
box-shadow: 0 0 7px rgba(20,184,166,0.9); gap: 16px;
flex-shrink: 0;
} }
/* ══════════════════════════════════════════════ .bento-card {
FEATURE CARDS
══════════════════════════════════════════════ */
.feat-card {
position: relative; position: relative;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.07);
background: rgba(12,18,36,0.7);
backdrop-filter: blur(16px);
padding: 26px; 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; overflow: hidden;
transition: all 0.3s ease; transition: all 0.35s ease;
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
} }
.feat-card:hover { .bento-card:hover {
border-color: rgba(255,255,255,0.15); border-color: rgba(255,255,255,0.12);
background: rgba(26,46,72,0.88); background: rgba(15,22,45,0.88);
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 20px 50px rgba(0,0,0,0.5); box-shadow: 0 16px 48px rgba(0,0,0,0.5);
} }
/* left accent bar */
.feat-accent { /* Bento layout sizes */
.bento-large { grid-column: span 2; min-height: 260px; }
.bento-regular{ grid-column: span 1; }
.bento-stats { grid-column: span 1; }
.bento-wide { grid-column: span 2; }
/* Left accent bar */
.bento-accent-bar {
position: absolute; position: absolute;
left: 0; left: 0; top: 0;
top: 0;
width: 3px; width: 3px;
height: 100%; height: 100%;
opacity: 0.55; opacity: 0.6;
transition: opacity 0.3s, box-shadow 0.3s; border-radius: 0 0 0 16px;
}
.feat-card:hover .feat-accent {
opacity: 1;
box-shadow: 0 0 12px currentColor;
}
/* corner brackets */
.feat-corner-tr,
.feat-corner-bl {
position: absolute;
width: 10px;
height: 10px;
opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
} }
.feat-corner-tr { .bento-card:hover .bento-accent-bar { opacity: 1; }
top: 10px;
right: 10px; .bento-icon-wrap {
border-top: 1px solid rgba(20,184,166,0.45);
border-right: 1px solid rgba(20,184,166,0.45);
}
.feat-corner-bl {
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,
.feat-card:hover .feat-corner-bl {
opacity: 1;
}
.feat-icon {
width: 44px; width: 44px;
height: 44px; height: 44px;
border-radius: 10px; border-radius: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: transform 0.3s, box-shadow 0.3s;
} }
.feat-card:hover .feat-icon {
transform: scale(1.08); /* Stats card specific */
.stat-row { display: flex; flex-direction: column; gap: 2px; }
.stat-big-value {
font-size: 1.8rem;
font-weight: 900;
font-family: ui-monospace, monospace;
color: white;
line-height: 1;
letter-spacing: -0.02em;
}
.stat-big-label {
font-size: 10px;
color: #334155;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* Mini tag */
.mini-tag {
font-size: 10px;
padding: 2px 8px;
border-radius: 9999px;
border: 1px solid rgba(139,92,246,0.2);
background: rgba(139,92,246,0.08);
color: #c4b5fd;
white-space: nowrap;
}
/* Sparkline bars */
.sparkbar {
width: 6px;
border-radius: 2px;
background: linear-gradient(180deg, #a855f7, #7c3aed);
opacity: 0.7;
animation: spark-grow 0.4s ease forwards;
}
@keyframes spark-grow {
from { transform: scaleY(0); transform-origin: bottom; opacity: 0; }
to { transform: scaleY(1); transform-origin: bottom; opacity: 0.7; }
} }
/* ══════════════════════════════════════════════ /* ══════════════════════════════════════════════
PROVIDERS PROVIDERS MARQUEE
══════════════════════════════════════════════ */ ══════════════════════════════════════════════ */
.provider-card { .marquee-container {
position: relative;
overflow: hidden;
}
.marquee-fade-left,
.marquee-fade-right {
position: absolute;
top: 0;
bottom: 0;
width: 80px;
z-index: 2;
pointer-events: none;
}
.marquee-fade-left {
left: 0;
background: linear-gradient(90deg, #080e1a, transparent);
}
.marquee-fade-right {
right: 0;
background: linear-gradient(-90deg, #080e1a, transparent);
}
.marquee-track {
overflow: hidden;
}
.marquee-inner {
display: flex;
gap: 16px;
width: max-content;
animation: marquee-scroll 24s linear infinite;
}
@keyframes marquee-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.provider-chip {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 10px 16px; padding: 10px 18px;
border-radius: 10px; border-radius: 10px;
border: 1px solid rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.08);
background: rgba(26,46,72,0.55); background: rgba(15,22,45,0.6);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
white-space: nowrap;
transition: all 0.25s; transition: all 0.25s;
} }
.provider-active:hover { .provider-chip-live {
border-color: rgba(20,184,166,0.22); color: #cbd5e1;
background: rgba(20,184,166,0.05);
box-shadow: 0 4px 20px rgba(0,0,0,0.35);
} }
.provider-inactive { .provider-chip-dim {
opacity: 0.38; opacity: 0.35;
} color: #475569;
.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 { .provider-chip-live:hover {
background: #334155; border-color: rgba(139,92,246,0.2);
background: rgba(139,92,246,0.06);
} }
.provider-icon-badge { .provider-badge {
width: 28px; width: 26px;
height: 28px; height: 26px;
border-radius: 6px; border-radius: 6px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
} }
.provider-tag { .provider-live-dot {
font-size: 10px; width: 5px;
font-weight: 600; height: 5px;
padding: 2px 8px; border-radius: 50%;
border-radius: 9999px; background: #34d399;
letter-spacing: 0.05em; box-shadow: 0 0 6px rgba(52,211,153,0.8);
text-transform: uppercase; flex-shrink: 0;
}
.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);
} }
/* ══════════════════════════════════════════════ /* ══════════════════════════════════════════════
FOOTER FOOTER
══════════════════════════════════════════════ */ ══════════════════════════════════════════════ */
.home-footer { .home-footer {
border-top: 1px solid rgba(20,184,166,0.1); border-top: 1px solid rgba(255,255,255,0.05);
background: rgba(24,42,64,0.8); background: rgba(8,14,26,0.8);
}
/* Responsive */
@media (max-width: 768px) {
.bento-large { grid-column: span 3; }
.bento-regular { grid-column: span 3; }
.bento-stats { grid-column: span 3; }
.bento-wide { grid-column: span 3; }
.pipeline-inner { justify-content: flex-start; }
.pipe-connector { width: 32px; }
}
@media (min-width: 769px) and (max-width: 1024px) {
.bento-large { grid-column: span 2; }
.bento-wide { grid-column: span 3; }
} }
</style> </style>
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
<router-link <router-link
v-if="passwordResetEnabled && !backendModeEnabled" v-if="passwordResetEnabled && !backendModeEnabled"
to="/forgot-password" to="/forgot-password"
class="text-sm font-medium text-primary-600 transition-colors hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300" class="text-sm font-medium transition-colors" style="color: #8b5cf6;"
> >
{{ t('auth.forgotPassword') }} {{ t('auth.forgotPassword') }}
</router-link> </router-link>
......
...@@ -67,8 +67,8 @@ export default { ...@@ -67,8 +67,8 @@ export default {
boxShadow: { boxShadow: {
glass: '0 8px 32px rgba(0, 0, 0, 0.08)', glass: '0 8px 32px rgba(0, 0, 0, 0.08)',
'glass-sm': '0 4px 16px rgba(0, 0, 0, 0.06)', 'glass-sm': '0 4px 16px rgba(0, 0, 0, 0.06)',
glow: '0 0 20px rgba(20, 184, 166, 0.25)', glow: '0 0 0 1px rgba(99, 102, 241, 0.25), 0 0 20px rgba(99, 102, 241, 0.15)',
'glow-lg': '0 0 40px rgba(20, 184, 166, 0.35)', 'glow-lg': '0 0 40px rgba(99, 102, 241, 0.35)',
card: '0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06)', card: '0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06)',
'card-hover': '0 10px 40px rgba(0, 0, 0, 0.08)', 'card-hover': '0 10px 40px rgba(0, 0, 0, 0.08)',
'inner-glow': 'inset 0 1px 0 rgba(255, 255, 255, 0.1)' 'inner-glow': 'inset 0 1px 0 rgba(255, 255, 255, 0.1)'
......
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