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

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

parent b4412353
<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">
<!-- Left: Mobile Menu Toggle + Page Title -->
<div class="flex items-center gap-4">
......@@ -335,4 +335,13 @@ onBeforeUnmount(() => {
opacity: 0;
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>
<template>
<div class="min-h-screen bg-gray-50 dark:bg-dark-950">
<div class="app-root min-h-screen">
<!-- 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 -->
<AppSidebar />
......@@ -50,3 +50,25 @@ onMounted(() => {
defineExpose({ replayTour })
</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 @@
<div v-if="!sidebarCollapsed" class="sidebar-section-title">
{{ t('nav.myAccount') }}
</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
v-for="item in personalNavItems"
......@@ -105,7 +105,7 @@
</nav>
<!-- 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 -->
<button
@click="toggleTheme"
......
<template>
<div class="auth-root relative flex min-h-screen items-center justify-center overflow-hidden p-4">
<!-- Background -->
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="auth-orb auth-orb-tr"></div>
<div class="auth-orb auth-orb-bl"></div>
<div class="auth-grid"></div>
<div class="auth-scan"></div>
</div>
<div class="auth-split min-h-screen">
<!-- Content -->
<div class="relative z-10 w-full max-w-md">
<!-- ══════════════════════════════════════════
LEFT PANEL — AI Visual (hidden on mobile)
══════════════════════════════════════════ -->
<div class="auth-left hidden lg:flex">
<!-- Brand -->
<div class="mb-8 text-center">
<template v-if="settingsLoaded">
<!-- Logo -->
<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" />
<!-- Background layers -->
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="left-aurora left-aurora-1"></div>
<div class="left-aurora left-aurora-2"></div>
</div>
<div class="relative z-10 flex h-full flex-col px-12 py-10">
<!-- Logo + Brand -->
<div class="flex items-center gap-3">
<template v-if="settingsLoaded">
<div class="left-logo-wrap">
<img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
</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>
<!-- Orbiting model chips -->
<div class="orbit-chip chip-0">
<span class="chip-dot" style="background: #f97316"></span>
<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>
<!-- Connecting lines (SVG) -->
<svg class="orbit-lines" viewBox="0 0 340 340" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 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>
</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>
<!-- Site name -->
<h1 class="auth-site-name mb-2 text-3xl font-black tracking-tight">
{{ siteName }}
</h1>
<!-- Copyright -->
<div class="pt-6 font-mono text-[10px] text-slate-700">
&copy; {{ currentYear }} {{ siteName }}
</div>
</div>
</div>
<!-- Subtitle -->
<p class="auth-subtitle font-mono text-xs uppercase tracking-widest">
// {{ siteSubtitle }}
</p>
<!-- ══════════════════════════════════════════
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>
</div>
<!-- Card -->
<div class="auth-card rounded-2xl p-8">
<!-- Form card -->
<div class="auth-form-card">
<slot />
</div>
<!-- Footer links -->
<div class="mt-6 text-center text-sm">
<div class="mt-5 text-center text-sm">
<slot name="footer" />
</div>
<!-- Copyright -->
<div class="mt-8 text-center font-mono text-xs text-slate-600">
<!-- Copyright (mobile only) -->
<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.
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed, defineComponent, h, onMounted } from 'vue'
import { useAppStore } from '@/stores'
import { sanitizeUrl } from '@/utils/url'
......@@ -61,117 +157,295 @@ const siteName = computed(() => appStore.siteName || 'TrafficAPI')
const siteLogo = computed(() => sanitizeUrl(appStore.siteLogo || '', { allowRelative: true, allowDataUrl: true }))
const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'Intelligent AI Traffic Routing & Management')
const settingsLoaded = computed(() => appStore.publicSettingsLoaded)
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(() => {
appStore.fetchPublicSettings()
})
</script>
<style scoped>
/* ── Root ────────────────────────────────── */
.auth-root {
background: #182a40;
color: white;
/* ══════════════════════════════════════════════
SPLIT LAYOUT
══════════════════════════════════════════════ */
.auth-split {
display: flex;
min-height: 100vh;
}
/* ── LEFT PANEL ────────────────────────────── */
.auth-left {
position: relative;
width: 55%;
max-width: 600px;
flex-shrink: 0;
background: #060c1a;
overflow: hidden;
flex-direction: column;
}
/* ── Background layers ───────────────────── */
.auth-orb {
/* Aurora blobs on left panel */
.left-aurora {
position: absolute;
border-radius: 50%;
filter: blur(90px);
filter: blur(100px);
pointer-events: none;
}
.auth-orb-tr {
top: -10%;
right: -10%;
width: 420px;
height: 420px;
background: radial-gradient(ellipse, rgba(20,184,166,0.18) 0%, transparent 70%);
.left-aurora-1 {
top: -15%;
right: -15%;
width: 450px;
height: 450px;
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%;
left: -10%;
width: 380px;
height: 380px;
background: radial-gradient(ellipse, rgba(6,182,212,0.13) 0%, transparent 70%);
width: 350px;
height: 350px;
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); }
}
.auth-grid {
.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;
}
/* ── ORBITAL VISUALIZATION ──────────────────── */
.orbital-viz {
position: relative;
width: 340px;
height: 340px;
}
/* Hub */
.hub-outer {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(20,184,166,0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(20,184,166,0.07) 1px, transparent 1px);
background-size: 48px 48px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
}
.auth-scan {
.hub-ring {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(20,184,166,0.45) 30%, rgba(20,184,166,0.7) 50%, rgba(20,184,166,0.45) 70%, transparent 100%);
animation: auth-scan 12s linear infinite;
box-shadow: 0 0 8px rgba(20,184,166,0.35);
}
@keyframes auth-scan {
0% { top: 0%; opacity: 0; }
4% { opacity: 1; }
96% { opacity: 0.4; }
100% { top: 100%; opacity: 0; }
}
/* ── Logo ────────────────────────────────── */
.auth-logo-wrap {
box-shadow:
0 0 0 1px rgba(20,184,166,0.3),
0 0 20px rgba(20,184,166,0.18),
0 8px 24px rgba(0,0,0,0.4);
}
/* ── Brand text ──────────────────────────── */
.auth-site-name {
background: linear-gradient(135deg, #ffffff 0%, #7dd3c8 45%, #14b8a6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.auth-subtitle {
color: rgba(20,184,166,0.5);
}
/* ── Card ────────────────────────────────── */
.auth-card {
background: rgba(26,46,72,0.72);
border: 1px solid rgba(255,255,255,0.09);
backdrop-filter: blur(18px);
box-shadow:
0 0 0 1px rgba(20,184,166,0.08),
0 24px 60px rgba(0,0,0,0.45),
0 0 50px rgba(20,184,166,0.05);
}
/* ── Slot overrides: make child inputs/links readable ── */
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;
}
@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;
display: flex;
align-items: center;
gap: 6px;
padding: 5px 11px 5px 8px;
border-radius: 20px;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(15,22,40,0.85);
backdrop-filter: blur(8px);
font-size: 11px;
font-weight: 500;
color: #cbd5e1;
white-space: nowrap;
animation: chip-float 4s ease-in-out infinite;
}
.chip-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
/* Position each chip around the orbital */
.chip-0 { top: 6%; left: 62%; animation-delay: 0.0s; }
.chip-1 { top: 44%; left: 80%; animation-delay: 0.6s; }
.chip-2 { top: 78%; left: 52%; animation-delay: 1.2s; }
.chip-3 { top: 72%; left: 6%; animation-delay: 1.8s; }
.chip-4 { top: 12%; left: 2%; animation-delay: 2.4s; }
@keyframes chip-float {
0%,100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
/* SVG connecting lines */
.orbit-lines {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
/* Feature rows */
.left-feature-row {
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) {
color: #374151;
}
:global(.dark) :deep(.input-label) {
color: #94a3b8;
}
:deep(.input) {
background: #f9fafb;
border-color: #e5e7eb;
color: #0f172a;
}
:global(.dark) :deep(.input) {
background: rgba(15,28,48,0.7);
border-color: rgba(255,255,255,0.1);
color: white;
}
:deep(.input:focus) {
border-color: rgba(20,184,166,0.5);
box-shadow: 0 0 0 3px rgba(20,184,166,0.12);
border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
}
:deep(.input::placeholder) {
color: #475569;
color: #9ca3af;
}
:deep(h2) {
color: white !important;
:global(.dark) :deep(.input::placeholder) {
color: #475569;
}
:deep(.text-gray-500),
:deep(.dark\:text-dark-400) {
color: #6b7280 !important;
}
:global(.dark) :deep(.text-gray-500),
:global(.dark) :deep(.dark\:text-dark-400) {
color: #64748b !important;
}
</style>
......@@ -40,6 +40,13 @@ export default {
}
}
},
// Pipeline visualization labels
pipeline: {
gateway: 'Gateway',
balancer: 'Balancer',
aiPool: 'AI Pool',
response: 'Response',
},
// Solutions section
solutions: {
title: 'TrafficAPI Solves This',
......
......@@ -40,6 +40,13 @@ export default {
}
}
},
// 管道可视化标签
pipeline: {
gateway: '网关',
balancer: '均衡器',
aiPool: 'AI 池',
response: '响应',
},
// 解决方案区块
solutions: {
title: 'TrafficAPI 为你解决',
......
......@@ -96,16 +96,26 @@
@apply inline-flex items-center justify-center gap-2;
@apply rounded-xl px-4 py-2.5 text-sm font-medium;
@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 active:scale-[0.98];
}
.btn-primary {
@apply bg-gradient-to-r from-primary-500 to-primary-600;
@apply text-white shadow-md shadow-primary-500/25;
@apply hover:from-primary-600 hover:to-primary-700 hover:shadow-lg hover:shadow-primary-500/30;
@apply dark:shadow-primary-500/20;
background: linear-gradient(135deg, #7c3aed, #8b5cf6);
color: white;
box-shadow: 0 4px 14px rgba(139, 92, 246, 0.35);
}
.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 {
......@@ -165,7 +175,7 @@
@apply text-gray-900 dark:text-gray-100;
@apply placeholder:text-gray-400 dark:placeholder:text-dark-400;
@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;
}
......@@ -522,11 +532,12 @@
@apply border-l-primary-500;
}
/* ============ 侧边栏 ============ */
/* ============ 侧边栏 — 恒暗风格 ============ */
.sidebar {
@apply fixed inset-y-0 left-0 z-40;
@apply w-64 bg-white dark:bg-dark-900;
@apply border-r border-gray-200 dark:border-dark-800;
@apply w-64;
background: #0c1220;
border-right: 1px solid rgba(139, 92, 246, 0.08);
@apply flex flex-col;
@apply transition-transform duration-300;
}
......@@ -534,7 +545,7 @@
.sidebar-header {
@apply h-16 px-6;
@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 {
......@@ -544,16 +555,26 @@
.sidebar-link {
@apply flex items-center gap-3 rounded-xl px-3 py-2.5;
@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 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 {
@apply bg-primary-50 dark:bg-primary-900/20;
@apply text-primary-600 dark:text-primary-400;
@apply hover:bg-primary-100 dark:hover:bg-primary-900/30;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.18), rgba(20, 184, 166, 0.12));
border-left: 2px solid #14b8a6;
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 {
......@@ -563,7 +584,7 @@
.sidebar-section-title {
@apply mb-2 px-3;
@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 @@
<div v-else v-html="homeContent"></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">
<!-- ══════════════════════════════════════════
BACKGROUND
AURORA BACKGROUND
══════════════════════════════════════════ -->
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<!-- Glow orbs -->
<div class="orb orb-tr"></div>
<div class="orb orb-bl"></div>
<div class="orb orb-center"></div>
<!-- Grid -->
<div class="grid-overlay"></div>
<!-- Scan line -->
<div class="scan-line"></div>
<div class="aurora aurora-violet"></div>
<div class="aurora aurora-teal"></div>
<div class="aurora aurora-indigo"></div>
<!-- Subtle noise texture overlay -->
<div class="noise-overlay"></div>
</div>
<!-- ══════════════════════════════════════════
HEADER
══════════════════════════════════════════ -->
<header class="home-header relative z-20 px-6 py-4">
<nav class="mx-auto flex max-w-6xl items-center justify-between">
<header class="home-header relative z-20">
<nav class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
<!-- Logo + Brand -->
<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" />
</div>
<span class="hidden font-mono text-sm font-semibold uppercase tracking-widest text-white/70 sm:block">
{{ siteName }}
</span>
<span class="hidden font-semibold text-white/80 sm:block">{{ siteName }}</span>
</div>
<!-- Actions -->
<div class="flex items-center gap-2">
<LocaleSwitcher />
<a
v-if="docUrl"
:href="docUrl"
target="_blank"
rel="noopener noreferrer"
class="nav-btn"
:title="t('home.viewDocs')"
class="nav-pill"
>
<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>
</a>
<button @click="toggleTheme" class="nav-btn icon-only" :title="isDark ? t('home.switchToLight') : t('home.switchToDark')">
<button @click="toggleTheme" class="nav-icon-btn">
<Icon v-if="isDark" name="sun" size="sm" />
<Icon v-else name="moon" size="sm" />
</button>
<router-link
v-if="isAuthenticated"
:to="dashboardPath"
class="cta-nav-btn"
>
<span class="user-dot">{{ userInitial }}</span>
<router-link v-if="isAuthenticated" :to="dashboardPath" class="cta-header-btn">
<span class="avatar-dot">{{ userInitial }}</span>
{{ 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 v-else to="/login" class="cta-nav-btn">
<router-link v-else to="/login" class="cta-header-btn">
{{ t('home.login') }}
</router-link>
</div>
......@@ -85,184 +69,252 @@
<!-- ══════════════════════════════════════════
MAIN
══════════════════════════════════════════ -->
<main class="relative z-10 flex-1 px-6">
<!-- ── HERO ────────────────────────────── -->
<section class="mx-auto max-w-6xl py-20 lg:py-28">
<div class="flex flex-col items-center gap-14 lg:flex-row lg:gap-20">
<!-- Text side -->
<div class="flex-1 text-center lg:text-left">
<!-- 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">
<span class="h-1.5 w-1.5 animate-pulse rounded-full bg-teal-400 shadow-[0_0_6px_#14b8a6]"></span>
AI API GATEWAY · ENTERPRISE GRADE
</div>
<main class="relative z-10 flex-1">
<!-- Headline -->
<h1 class="mb-5 font-black leading-[1.08] tracking-tight">
<span class="gradient-text block whitespace-nowrap text-[clamp(1.6rem,4.5vw,3.75rem)]">{{ t('home.heroSubtitle') }}</span>
</h1>
<!-- ── HERO (Centered) ─────────────────── -->
<section class="mx-auto max-w-5xl px-6 pb-8 pt-20 text-center lg:pt-28">
<!-- Mono accent line -->
<p class="mb-5 font-mono text-[11px] uppercase tracking-[0.2em] text-teal-600/60">
// {{ siteName }} · intelligent traffic layer
</p>
<!-- Badge -->
<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-violet-400 shadow-[0_0_6px_#a78bfa]"></span>
<span class="font-mono text-[11px] font-medium uppercase tracking-widest text-violet-300">
AI API Gateway · Next Generation
</span>
</div>
<p class="mb-10 max-w-lg text-base leading-relaxed text-slate-400 lg:text-lg">
{{ t('home.heroDescription') }}
</p>
<!-- Headline -->
<h1 class="hero-headline mb-6">
{{ t('home.heroSubtitle') }}
</h1>
<!-- CTA buttons -->
<div class="flex flex-wrap items-center justify-center gap-4 lg:justify-start">
<router-link
: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"
>
{{ isAuthenticated ? t('home.goToDashboard') : t('home.getStarted') }}
<Icon name="arrowRight" size="sm" :stroke-width="2.5" />
</router-link>
<a
v-if="docUrl"
:href="docUrl"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 rounded-lg border border-white/10 px-7 py-3 text-sm font-medium text-slate-300 transition-all hover:border-teal-500/30 hover:text-teal-300"
>
<Icon name="book" size="sm" />
{{ t('home.viewDocs') }}
</a>
</div>
<!-- Subline -->
<p class="mx-auto mb-10 max-w-2xl text-lg leading-relaxed text-slate-400">
{{ t('home.heroDescription') }}
</p>
<!-- Stats -->
<div class="mt-12 flex flex-wrap items-center justify-center gap-8 lg:justify-start">
<div v-for="s in stats" :key="s.label" class="stat-item">
<span class="stat-value">{{ s.value }}</span>
<span class="stat-label">{{ s.label }}</span>
</div>
</div>
</div>
<!-- CTA -->
<div class="mb-16 flex flex-wrap items-center justify-center gap-4">
<router-link
:to="isAuthenticated ? dashboardPath : '/login'"
class="cta-primary-btn"
>
{{ isAuthenticated ? t('home.goToDashboard') : t('home.getStarted') }}
<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>
<a
v-if="docUrl"
:href="docUrl"
target="_blank"
rel="noopener noreferrer"
class="cta-ghost-btn"
>
<Icon name="book" size="sm" />
{{ t('home.viewDocs') }}
</a>
</div>
<!-- Terminal side -->
<div class="flex flex-1 justify-center lg:justify-end">
<div class="terminal-wrap">
<div class="terminal-glow"></div>
<div class="terminal-window">
<!-- Header bar -->
<div class="terminal-header">
<div class="terminal-dots">
<span class="dot-red"></span>
<span class="dot-yellow"></span>
<span class="dot-green"></span>
</div>
<span class="terminal-title">trafficapi · gateway</span>
<span class="terminal-live">
<span class="h-1.5 w-1.5 animate-pulse rounded-full bg-teal-400 inline-block shadow-[0_0_5px_#14b8a6]"></span>
LIVE
</span>
<!-- ── Pipeline Visualization ─────────── -->
<div class="pipeline-wrapper">
<div class="pipeline-glow"></div>
<div class="pipeline-card">
<div class="pipeline-inner">
<!-- 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>
<span class="pipe-label">Client</span>
</div>
<!-- Code body -->
<div class="terminal-body">
<div class="t-line line-1">
<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 class="t-line line-2">
<span class="t-comment">&nbsp;&nbsp;⠿ routing to pool · 3 accounts available</span>
</div>
<div class="t-line line-3">
<span class="t-ok">✓ 200</span>
<span class="t-ms">38ms</span>
<span class="t-dim">· account[1] selected</span>
</div>
<div class="pipe-connector">
<div class="pipe-line"></div>
<div class="pipe-dot pipe-dot-1"></div>
</div>
<div class="t-sep sep-1"></div>
<!-- Step: Gateway -->
<div class="pipe-node">
<div class="pipe-icon pipe-icon-gateway">
<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="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" />
</svg>
</div>
<span class="pipe-label">{{ t('home.pipeline.gateway') }}</span>
</div>
<div class="t-line line-4">
<span class="t-prompt"></span>
<span class="t-method">POST</span>
<span class="t-path">/v1/chat/completions</span>
<span class="t-dim ml-auto">gpt-4o</span>
</div>
<div class="t-line line-5">
<span class="t-comment">&nbsp;&nbsp;⠿ load balancing · 2 upstream accounts</span>
</div>
<div class="t-line line-6">
<span class="t-ok">✓ 200</span>
<span class="t-ms">57ms</span>
<span class="t-dim">· account[3] selected</span>
</div>
<div class="pipe-connector">
<div class="pipe-line"></div>
<div class="pipe-dot pipe-dot-2"></div>
</div>
<div class="t-sep sep-2"></div>
<!-- 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>
<span class="pipe-label">{{ t('home.pipeline.balancer') }}</span>
</div>
<div class="t-line line-7">
<span class="t-stat">📊 analytics</span>
<span class="t-dim">&nbsp;req: 1.4k/min · p99: 68ms · err: 0%</span>
</div>
<div class="t-line line-8">
<span class="t-prompt">_</span>
<span class="t-cursor"></span>
</div>
<div class="pipe-connector">
<div class="pipe-line"></div>
<div class="pipe-dot pipe-dot-3"></div>
</div>
<!-- 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>
<span class="pipe-label">{{ t('home.pipeline.aiPool') }}</span>
</div>
<div class="pipe-connector">
<div class="pipe-line"></div>
<div class="pipe-dot pipe-dot-4"></div>
</div>
<!-- Step: Response -->
<div class="pipe-node">
<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>
<span class="pipe-label">{{ t('home.pipeline.response') }}</span>
</div>
</div>
</div>
</div>
</section>
<!-- ── TAGS BAR ─────────────────────────── -->
<section class="mx-auto max-w-6xl pb-14">
<div class="flex flex-wrap items-center justify-center gap-3">
<div v-for="tag in featureTags" :key="tag.text" class="tag-pill">
<span class="tag-pip"></span>
{{ tag.text }}
<!-- 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>
</section>
<!-- ── FEATURES ─────────────────────────── -->
<section class="mx-auto max-w-6xl pb-16">
<div class="mb-10 text-center">
<h2 class="mb-2 text-2xl font-bold text-white md:text-3xl">
<span class="gradient-text">{{ t('home.solutions.title') }}</span>
<!-- ── BENTO FEATURES ─────────────────── -->
<section class="mx-auto max-w-7xl px-6 pb-20">
<div class="mb-12 text-center">
<h2 class="mb-3 text-3xl font-bold text-white">
<span class="bento-title-gradient">{{ t('home.solutions.title') }}</span>
</h2>
<p class="font-mono text-xs uppercase tracking-widest text-slate-600">
// {{ t('home.solutions.subtitle') }}
<p class="text-sm font-mono uppercase tracking-widest text-slate-600">
{{ t('home.solutions.subtitle') }}
</p>
</div>
<div class="grid gap-4 md:grid-cols-3">
<div
v-for="feat in features"
:key="feat.key"
class="feat-card group"
>
<div class="feat-accent" :style="{ background: feat.color }"></div>
<div class="feat-corner-tr"></div>
<div class="feat-corner-bl"></div>
<div
class="feat-icon mb-5"
:style="{ background: feat.iconBg, boxShadow: `0 4px 20px ${feat.glow}` }"
>
<component :is="feat.iconComponent" />
<div class="bento-grid">
<!-- LARGE CARD: Unified Gateway -->
<div class="bento-card bento-large group">
<div class="bento-accent-bar" style="background: linear-gradient(180deg, #3b82f6, #6366f1)"></div>
<div class="flex h-full flex-col">
<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)">
<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>
<!-- 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>
<h3 class="mb-2 font-semibold text-white">{{ t(`home.features.${feat.key}`) }}</h3>
<p class="text-sm leading-relaxed text-slate-500 transition-colors group-hover:text-slate-400">
{{ t(`home.features.${feat.key}Desc`) }}
<!-- REGULAR CARD: Load Balancing -->
<div class="bento-card bento-regular group">
<div class="bento-accent-bar" style="background: linear-gradient(180deg, #14b8a6, #06b6d4)"></div>
<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)">
<component :is="IconRoute" />
</div>
<h3 class="mb-2 text-lg font-semibold text-white">{{ t(`home.features.multiAccount`) }}</h3>
<p class="text-sm leading-relaxed text-slate-500 group-hover:text-slate-400">
{{ 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>
<!-- STATS CARD -->
<div class="bento-card bento-stats">
<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>
<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>
<!-- WIDE CARD: Analytics -->
<div class="bento-card bento-wide group">
<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>
<!-- 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>
</section>
<!-- ── PROVIDERS ────────────────────────── -->
<section class="mx-auto max-w-6xl pb-20">
<!-- ── PROVIDERS MARQUEE ──────────────── -->
<section class="mx-auto max-w-7xl px-6 pb-20">
<div class="mb-8 text-center">
<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">
......@@ -270,24 +322,24 @@
</p>
</div>
<div class="flex flex-wrap items-center justify-center gap-3">
<div
v-for="p in providerList"
:key="p.name"
class="provider-card"
:class="{ 'provider-active': p.live, 'provider-inactive': !p.live }"
>
<span
class="provider-status-dot"
:class="p.live ? 'dot-live' : 'dot-offline'"
></span>
<div class="provider-icon-badge" :style="{ background: p.gradient }">
<span class="text-[11px] font-bold text-white">{{ p.letter }}</span>
<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
v-for="p in [...providerList, ...providerList]"
:key="p.name + Math.random()"
class="provider-chip"
:class="p.live ? 'provider-chip-live' : 'provider-chip-dim'"
>
<div class="provider-badge" :style="{ background: p.gradient }">
<span class="text-[10px] font-bold text-white">{{ p.letter }}</span>
</div>
<span class="text-sm font-medium">{{ p.name }}</span>
<span v-if="p.live" class="provider-live-dot"></span>
</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>
</section>
......@@ -297,29 +349,16 @@
<!-- ══════════════════════════════════════════
FOOTER
══════════════════════════════════════════ -->
<footer class="home-footer relative z-10 px-6 py-6">
<div class="mx-auto flex max-w-6xl flex-col items-center justify-between gap-3 text-center sm:flex-row sm:text-left">
<footer class="home-footer relative z-10">
<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">
&copy; {{ currentYear }} {{ siteName }} · {{ t('home.footer.allRightsReserved') }}
</p>
<div class="flex items-center gap-5">
<a
v-if="docUrl"
:href="docUrl"
target="_blank"
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>
<a v-if="docUrl" :href="docUrl" target="_blank" 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>
</footer>
......@@ -338,7 +377,6 @@ const { t } = useI18n()
const authStore = useAuthStore()
const appStore = useAppStore()
// Site settings
const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'TrafficAPI')
const siteLogo = computed(() => appStore.cachedPublicSettings?.site_logo || appStore.siteLogo || '')
const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '')
......@@ -349,13 +387,8 @@ const isHomeContentUrl = computed(() => {
return content.startsWith('http://') || content.startsWith('https://')
})
// Theme
const isDark = ref(document.documentElement.classList.contains('dark'))
// GitHub
const githubUrl = 'https://github.com/Wei-Shaw/sub2api'
// Auth
const isAuthenticated = computed(() => authStore.isAuthenticated)
const isAdmin = computed(() => authStore.isAdmin)
const dashboardPath = computed(() => isAdmin.value ? '/admin/dashboard' : '/dashboard')
......@@ -364,24 +397,35 @@ const userInitial = computed(() => {
if (!user?.email) return ''
return user.email.charAt(0).toUpperCase()
})
const currentYear = computed(() => new Date().getFullYear())
// Stats
const stats = [
{ value: '3+', label: 'AI Providers' },
{ value: '99.9%', label: 'SLA Uptime' },
{ 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(() => [
{ text: t('home.tags.subscriptionToApi') },
{ text: t('home.tags.stickySession') },
{ text: t('home.tags.realtimeBilling') },
])
// SVG icon components (inline)
// Inline SVG icon components
const IconServer = defineComponent({
render: () => h('svg', { class: 'h-5 w-5 text-white', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [
h('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', d: 'M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z' })
......@@ -393,46 +437,19 @@ const IconRoute = 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' })
])
})
// Features config — all icons use inline components, no string name needed
const features = [
{
key: 'unifiedGateway',
iconComponent: IconServer,
iconBg: 'linear-gradient(135deg, #3b82f6, #1d4ed8)',
glow: 'rgba(59,130,246,0.3)',
color: '#3b82f6',
},
{
key: 'multiAccount',
iconComponent: IconRoute,
iconBg: 'linear-gradient(135deg, #14b8a6, #0d9488)',
glow: 'rgba(20,184,166,0.3)',
color: '#14b8a6',
},
{
key: 'balanceQuota',
iconComponent: IconChart,
iconBg: 'linear-gradient(135deg, #a855f7, #7c3aed)',
glow: 'rgba(168,85,247,0.3)',
color: '#a855f7',
},
]
// Providers
const providerList = computed(() => [
{ name: t('home.providers.claude'), letter: 'C', gradient: 'linear-gradient(135deg,#f97316,#ea580c)', live: true },
{ name: 'GPT', letter: 'G', gradient: 'linear-gradient(135deg,#22c55e,#16a34a)', live: true },
{ name: t('home.providers.gemini'), letter: 'G', gradient: 'linear-gradient(135deg,#3b82f6,#2563eb)', live: true },
{ name: t('home.providers.antigravity'), letter: 'A', gradient: 'linear-gradient(135deg,#f43f5e,#db2777)', live: true },
{ name: t('home.providers.more'), letter: '+', gradient: 'linear-gradient(135deg,#475569,#334155)', live: false },
{ name: t('home.providers.claude'), letter: 'C', gradient: 'linear-gradient(135deg,#f97316,#ea580c)', live: true },
{ name: 'GPT', letter: 'G', gradient: 'linear-gradient(135deg,#22c55e,#16a34a)', live: true },
{ name: t('home.providers.gemini'), letter: 'G', gradient: 'linear-gradient(135deg,#3b82f6,#2563eb)', live: true },
{ name: t('home.providers.antigravity'), letter: 'A', gradient: 'linear-gradient(135deg,#f43f5e,#db2777)', live: true },
{ name: t('home.providers.more'), letter: '+', gradient: 'linear-gradient(135deg,#475569,#334155)', live: false },
])
// Theme toggle
function toggleTheme() {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
......@@ -458,120 +475,130 @@ onMounted(() => {
<style scoped>
/* ══════════════════════════════════════════════
ROOT — always dark for this landing page
ROOT
══════════════════════════════════════════════ */
.home-root {
background: #182a40;
background: #080e1a;
color: white;
}
/* ══════════════════════════════════════════════
BACKGROUND LAYERS
AURORA BACKGROUND
══════════════════════════════════════════════ */
.orb {
.aurora {
position: absolute;
border-radius: 50%;
filter: blur(100px);
filter: blur(120px);
pointer-events: none;
}
.orb-tr {
top: -15%;
right: -15%;
.aurora-violet {
top: -20%;
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;
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 {
bottom: -15%;
left: -15%;
.aurora-indigo {
top: 40%;
left: 25%;
width: 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 {
top: 30%;
left: 40%;
width: 400px;
height: 400px;
background: radial-gradient(ellipse, rgba(20,184,166,0.08) 0%, transparent 70%);
@keyframes aurora-drift-1 {
from { transform: translate(0, 0) scale(1); }
to { transform: translate(-40px, 30px) scale(1.08); }
}
.grid-overlay {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(20,184,166,0.09) 1px, transparent 1px),
linear-gradient(90deg, rgba(20,184,166,0.09) 1px, transparent 1px);
background-size: 48px 48px;
@keyframes aurora-drift-2 {
from { transform: translate(0, 0) scale(1); }
to { transform: translate(30px, -40px) scale(1.05); }
}
.scan-line {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(20,184,166,0.5) 30%, rgba(20,184,166,0.8) 50%, rgba(20,184,166,0.5) 70%, transparent 100%);
animation: scan 10s linear infinite;
box-shadow: 0 0 8px rgba(20,184,166,0.4);
@keyframes aurora-drift-3 {
from { transform: translate(0, 0) scale(1); }
to { transform: translate(-20px, -30px) scale(0.95); }
}
@keyframes scan {
0% { top: 0%; opacity: 0; }
3% { opacity: 1; }
97% { opacity: 0.4; }
100% { top: 100%; opacity: 0; }
.noise-overlay {
position: absolute;
inset: 0;
opacity: 0.025;
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
══════════════════════════════════════════════ */
.home-header {
border-bottom: 1px solid rgba(20,184,166,0.12);
backdrop-filter: blur(12px);
background: rgba(24,42,64,0.8);
}
.logo-ring {
box-shadow: 0 0 0 1px rgba(20,184,166,0.25), 0 0 16px rgba(20,184,166,0.15);
border-bottom: 1px solid rgba(255,255,255,0.05);
backdrop-filter: blur(16px);
background: rgba(8,14,26,0.7);
}
.logo-badge {
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;
align-items: center;
padding: 6px 10px;
padding: 5px 10px;
border-radius: 6px;
font-size: 12px;
color: #94a3b8;
transition: all 0.2s;
}
.nav-btn:hover {
background: rgba(20,184,166,0.08);
color: #5eead4;
}
.nav-btn.icon-only {
padding: 6px 8px;
.nav-pill:hover { background: rgba(255,255,255,0.06); color: #e2e8f0; }
.nav-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
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;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 7px;
border: 1px solid rgba(20,184,166,0.3);
background: rgba(20,184,166,0.08);
border-radius: 8px;
border: 1px solid rgba(139,92,246,0.3);
background: rgba(139,92,246,0.1);
font-size: 12px;
font-weight: 500;
color: #5eead4;
color: #c4b5fd;
transition: all 0.2s;
}
.cta-nav-btn:hover {
border-color: rgba(20,184,166,0.6);
background: rgba(20,184,166,0.15);
box-shadow: 0 0 14px rgba(20,184,166,0.2);
.cta-header-btn:hover {
border-color: rgba(139,92,246,0.6);
background: rgba(139,92,246,0.18);
box-shadow: 0 0 16px rgba(139,92,246,0.2);
}
.user-dot {
.avatar-dot {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
width: 16px; height: 16px;
border-radius: 50%;
background: linear-gradient(135deg, #14b8a6, #0d9488);
background: linear-gradient(135deg, #8b5cf6, #6366f1);
font-size: 9px;
font-weight: 700;
color: white;
......@@ -580,374 +607,387 @@ onMounted(() => {
/* ══════════════════════════════════════════════
HERO
══════════════════════════════════════════════ */
.gradient-text {
background: linear-gradient(135deg, #ffffff 0%, #7dd3c8 40%, #14b8a6 100%);
.hero-headline {
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-text-fill-color: transparent;
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%;
animation: gradient-shift 4s ease infinite;
box-shadow: 0 0 0 1px rgba(20,184,166,0.4), 0 4px 20px rgba(20,184,166,0.25);
animation: btn-gradient 4s ease infinite;
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;
}
.cta-primary:hover {
box-shadow: 0 0 0 1px rgba(20,184,166,0.7), 0 6px 28px rgba(20,184,166,0.4);
.cta-primary-btn:hover {
box-shadow: 0 0 0 1px rgba(139,92,246,0.7), 0 8px 32px rgba(139,92,246,0.45);
transform: translateY(-2px);
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
@keyframes btn-gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
100% { background-position: 0% 50%; }
}
/* Stats */
.stat-item {
display: flex;
flex-direction: column;
gap: 2px;
position: relative;
}
.stat-item + .stat-item::before {
content: '';
position: absolute;
left: -16px;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 28px;
background: rgba(20,184,166,0.2);
}
.stat-value {
font-size: 1.6rem;
font-weight: 900;
font-family: ui-monospace, monospace;
color: white;
line-height: 1;
letter-spacing: -0.02em;
.cta-ghost-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
color: #94a3b8;
border: 1px solid rgba(255,255,255,0.08);
transition: all 0.2s;
}
.stat-label {
font-size: 10px;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.1em;
.cta-ghost-btn:hover {
border-color: rgba(139,92,246,0.3);
color: #c4b5fd;
}
/* ══════════════════════════════════════════════
TERMINAL
PIPELINE VISUALIZATION
══════════════════════════════════════════════ */
.terminal-wrap {
.pipeline-wrapper {
position: relative;
display: inline-block;
width: 100%;
}
.terminal-glow {
.pipeline-glow {
position: absolute;
inset: -30px;
background: radial-gradient(ellipse at center, rgba(20,184,166,0.1) 0%, transparent 65%);
inset: -20px;
background: radial-gradient(ellipse at center, rgba(99,102,241,0.08) 0%, transparent 60%);
pointer-events: none;
z-index: 0;
}
.terminal-window {
.pipeline-card {
position: relative;
z-index: 1;
width: 460px;
max-width: 100%;
background: linear-gradient(160deg, #1a2e48 0%, #162540 100%);
border-radius: 13px;
border: 1px solid rgba(20,184,166,0.18);
box-shadow:
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);
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.07);
background: rgba(15,20,40,0.7);
backdrop-filter: blur(20px);
padding: 28px 24px 20px;
box-shadow: 0 0 0 1px rgba(99,102,241,0.06), 0 24px 60px rgba(0,0,0,0.6);
overflow: hidden;
transform: perspective(900px) rotateX(1.5deg) rotateY(-2.5deg);
transition: transform 0.4s ease, box-shadow 0.4s ease;
}
.terminal-window:hover {
transform: perspective(900px) rotateX(0deg) rotateY(0deg) translateY(-8px);
box-shadow:
0 0 0 1px rgba(20,184,166,0.28),
0 36px 80px rgba(0,0,0,0.75),
0 0 70px rgba(20,184,166,0.12);
.pipeline-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(139,92,246,0.4), rgba(20,184,166,0.4), transparent);
}
.terminal-header {
.pipeline-inner {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 16px;
background: rgba(22,37,60,0.9);
border-bottom: 1px solid rgba(20,184,166,0.12);
justify-content: center;
flex-wrap: nowrap;
gap: 0;
overflow-x: auto;
padding-bottom: 4px;
}
.terminal-dots {
.pipe-node {
display: flex;
gap: 6px;
}
.terminal-dots span {
width: 11px;
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;
flex-direction: column;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.terminal-live {
.pipe-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
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-family: ui-monospace, monospace;
color: #14b8a6;
letter-spacing: 0.06em;
color: #475569;
white-space: nowrap;
}
.terminal-body {
padding: 18px 22px 22px;
font-family: ui-monospace, 'Fira Code', monospace;
font-size: 12.5px;
line-height: 1.85;
.pipe-connector {
position: relative;
width: 60px;
height: 2px;
flex-shrink: 0;
margin-bottom: 20px;
}
/* Terminal lines */
.t-line {
.pipe-line {
position: absolute;
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;
align-items: center;
gap: 7px;
opacity: 0;
animation: t-appear 0.35s ease forwards;
justify-content: center;
gap: 32px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgba(255,255,255,0.05);
}
.t-sep {
height: 1px;
background: rgba(20,184,166,0.07);
margin: 5px 0;
opacity: 0;
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;
.pipe-metric-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.t-ms { color: #fcd34d; font-size: 11px; }
.t-dim { color: #1e3a4a; font-size: 11px; }
.t-stat { color: #a78bfa; }
/* Cursor */
.t-cursor {
display: inline-block;
width: 7px;
height: 13px;
background: #14b8a6;
box-shadow: 0 0 8px rgba(20,184,166,0.7);
animation: blink 1.1s step-end infinite;
border-radius: 1px;
.pipe-metric-val {
font-family: ui-monospace, monospace;
font-size: 13px;
font-weight: 700;
color: #5eead4;
}
@keyframes blink {
0%,44% { opacity: 1; }
50%,94% { opacity: 0; }
100% { opacity: 1; }
.pipe-metric-label {
font-family: ui-monospace, monospace;
font-size: 10px;
color: #334155;
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* ══════════════════════════════════════════════
FEATURE TAGS
BENTO FEATURES
══════════════════════════════════════════════ */
.tag-pill {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid rgba(20,184,166,0.14);
background: rgba(20,184,166,0.04);
border-radius: 9999px;
padding: 7px 18px;
font-size: 12px;
font-weight: 500;
color: #64748b;
transition: all 0.2s;
cursor: default;
}
.tag-pill:hover {
border-color: rgba(20,184,166,0.3);
background: rgba(20,184,166,0.08);
color: #94a3b8;
.bento-title-gradient {
background: linear-gradient(135deg, #ffffff, #c4b5fd 50%, #5eead4);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.tag-pip {
width: 5px;
height: 5px;
border-radius: 50%;
background: #14b8a6;
box-shadow: 0 0 7px rgba(20,184,166,0.9);
flex-shrink: 0;
.bento-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: auto;
gap: 16px;
}
/* ══════════════════════════════════════════════
FEATURE CARDS
══════════════════════════════════════════════ */
.feat-card {
.bento-card {
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;
background: rgba(26,46,72,0.65);
border: 1px solid rgba(255,255,255,0.09);
border-radius: 13px;
backdrop-filter: blur(14px);
overflow: hidden;
transition: all 0.3s ease;
transition: all 0.35s ease;
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
}
.feat-card:hover {
border-color: rgba(255,255,255,0.15);
background: rgba(26,46,72,0.88);
.bento-card:hover {
border-color: rgba(255,255,255,0.12);
background: rgba(15,22,45,0.88);
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;
left: 0;
top: 0;
left: 0; top: 0;
width: 3px;
height: 100%;
opacity: 0.55;
transition: opacity 0.3s, box-shadow 0.3s;
}
.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;
opacity: 0.6;
border-radius: 0 0 0 16px;
transition: opacity 0.3s;
}
.feat-corner-tr {
top: 10px;
right: 10px;
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 {
.bento-card:hover .bento-accent-bar { opacity: 1; }
.bento-icon-wrap {
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s, box-shadow 0.3s;
}
.feat-card:hover .feat-icon {
transform: scale(1.08);
/* 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;
align-items: center;
gap: 10px;
padding: 10px 16px;
padding: 10px 18px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(26,46,72,0.55);
border: 1px solid rgba(255,255,255,0.08);
background: rgba(15,22,45,0.6);
backdrop-filter: blur(8px);
white-space: nowrap;
transition: all 0.25s;
}
.provider-active:hover {
border-color: rgba(20,184,166,0.22);
background: rgba(20,184,166,0.05);
box-shadow: 0 4px 20px rgba(0,0,0,0.35);
}
.provider-inactive {
opacity: 0.38;
}
.provider-status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
.provider-chip-live {
color: #cbd5e1;
}
.dot-live {
background: #34d399;
box-shadow: 0 0 7px rgba(52,211,153,0.8);
.provider-chip-dim {
opacity: 0.35;
color: #475569;
}
.dot-offline {
background: #334155;
.provider-chip-live:hover {
border-color: rgba(139,92,246,0.2);
background: rgba(139,92,246,0.06);
}
.provider-icon-badge {
width: 28px;
height: 28px;
.provider-badge {
width: 26px;
height: 26px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.provider-tag {
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 9999px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.tag-live {
background: rgba(20,184,166,0.1);
color: #2dd4bf;
border: 1px solid rgba(20,184,166,0.2);
}
.tag-soon {
background: rgba(71,85,105,0.2);
color: #475569;
border: 1px solid rgba(71,85,105,0.25);
.provider-live-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: #34d399;
box-shadow: 0 0 6px rgba(52,211,153,0.8);
flex-shrink: 0;
}
/* ══════════════════════════════════════════════
FOOTER
══════════════════════════════════════════════ */
.home-footer {
border-top: 1px solid rgba(20,184,166,0.1);
background: rgba(24,42,64,0.8);
border-top: 1px solid rgba(255,255,255,0.05);
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>
......@@ -80,7 +80,7 @@
<router-link
v-if="passwordResetEnabled && !backendModeEnabled"
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') }}
</router-link>
......
......@@ -67,8 +67,8 @@ export default {
boxShadow: {
glass: '0 8px 32px rgba(0, 0, 0, 0.08)',
'glass-sm': '0 4px 16px rgba(0, 0, 0, 0.06)',
glow: '0 0 20px rgba(20, 184, 166, 0.25)',
'glow-lg': '0 0 40px rgba(20, 184, 166, 0.35)',
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(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-hover': '0 10px 40px rgba(0, 0, 0, 0.08)',
'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