mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-09 03:02:26 +08:00
Initial commit
This commit is contained in:
167
frontend/src/views/public/CliSection.vue
Normal file
167
frontend/src/views/public/CliSection.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<section
|
||||
ref="sectionRef"
|
||||
class="min-h-screen snap-start flex items-center px-16 lg:px-20 py-20"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto grid md:grid-cols-2 gap-12 items-center">
|
||||
<!-- Content column -->
|
||||
<div :class="contentOrder">
|
||||
<!-- Badge -->
|
||||
<div
|
||||
class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-medium mb-4 transition-all duration-500"
|
||||
:class="badgeClass"
|
||||
:style="badgeStyle"
|
||||
>
|
||||
<component :is="badgeIcon" class="h-3 w-3" />
|
||||
{{ badgeText }}
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h2
|
||||
class="text-4xl md:text-5xl font-bold text-[#191919] dark:text-white mb-6 transition-all duration-700"
|
||||
:style="titleStyle"
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
|
||||
<!-- Description -->
|
||||
<p
|
||||
class="text-lg text-[#666663] dark:text-gray-300 mb-4 transition-all duration-700"
|
||||
:style="descStyle"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<!-- Install command -->
|
||||
<div
|
||||
class="mb-4 transition-all duration-700 relative z-10"
|
||||
:style="cardStyleFn(0)"
|
||||
>
|
||||
<div :class="[panelClasses.commandPanel, 'flex flex-wrap items-center gap-3 px-4 py-3']">
|
||||
<PlatformSelect
|
||||
:model-value="platformValue"
|
||||
@update:model-value="$emit('update:platformValue', $event)"
|
||||
:options="platformOptions"
|
||||
class="shrink-0"
|
||||
/>
|
||||
<div class="flex-1 min-w-[180px]">
|
||||
<CodeHighlight :code="installCommand" language="bash" dense />
|
||||
</div>
|
||||
<button
|
||||
@click="$emit('copy', installCommand)"
|
||||
:class="panelClasses.iconButtonSmall"
|
||||
title="复制配置"
|
||||
>
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Config files -->
|
||||
<div
|
||||
v-for="(config, idx) in configFiles"
|
||||
:key="config.path"
|
||||
class="transition-all duration-700"
|
||||
:class="idx < configFiles.length - 1 ? 'mb-3' : ''"
|
||||
:style="cardStyleFn(idx + 1)"
|
||||
>
|
||||
<div :class="[panelClasses.configPanel, 'overflow-hidden']">
|
||||
<div :class="panelClasses.panelHeader">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-[#666663] dark:text-muted-foreground">
|
||||
{{ config.path }}
|
||||
</span>
|
||||
<button
|
||||
@click="$emit('copy', config.content)"
|
||||
:class="panelClasses.iconButtonSmall"
|
||||
title="复制配置"
|
||||
>
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="panelClasses.codeBody">
|
||||
<div class="config-code-wrapper">
|
||||
<CodeHighlight :code="config.content" :language="config.language" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logo placeholder column -->
|
||||
<div :class="logoOrder" class="flex items-center justify-center h-full min-h-[300px] relative">
|
||||
<slot name="logo" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, type CSSProperties, type Component } from 'vue'
|
||||
import { Copy } from 'lucide-vue-next'
|
||||
import PlatformSelect from '@/components/PlatformSelect.vue'
|
||||
import CodeHighlight from '@/components/CodeHighlight.vue'
|
||||
import { panelClasses, type PlatformOption } from './home-config'
|
||||
|
||||
// Expose section element for parent scroll tracking
|
||||
const sectionRef = ref<HTMLElement | null>(null)
|
||||
defineExpose({ sectionEl: sectionRef })
|
||||
|
||||
interface ConfigFile {
|
||||
path: string
|
||||
content: string
|
||||
language: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
description: string
|
||||
badgeIcon: Component
|
||||
badgeText: string
|
||||
badgeClass: string
|
||||
platformValue: string
|
||||
platformOptions: PlatformOption[]
|
||||
installCommand: string
|
||||
configFiles: ConfigFile[]
|
||||
// Style props
|
||||
badgeStyle: CSSProperties
|
||||
titleStyle: CSSProperties
|
||||
descStyle: CSSProperties
|
||||
cardStyleFn: (cardIndex: number) => CSSProperties
|
||||
// Layout: 'left' means content on left, 'right' means content on right
|
||||
contentPosition?: 'left' | 'right'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
contentPosition: 'left'
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
copy: [text: string]
|
||||
'update:platformValue': [value: string]
|
||||
}>()
|
||||
|
||||
const contentOrder = computed(() =>
|
||||
props.contentPosition === 'right' ? 'md:order-2' : ''
|
||||
)
|
||||
|
||||
const logoOrder = computed(() =>
|
||||
props.contentPosition === 'right' ? 'md:order-1' : ''
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-code-wrapper :deep(.code-highlight pre) {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
background-color: transparent !important;
|
||||
padding: 1rem 1.2rem !important;
|
||||
}
|
||||
|
||||
/* Header separator line */
|
||||
.panel-header {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
</style>
|
||||
752
frontend/src/views/public/Home.vue
Normal file
752
frontend/src/views/public/Home.vue
Normal file
@@ -0,0 +1,752 @@
|
||||
<template>
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="relative h-screen overflow-y-auto snap-y snap-mandatory scroll-smooth literary-grid literary-paper"
|
||||
>
|
||||
<!-- Fixed scroll indicator -->
|
||||
<nav class="scroll-indicator">
|
||||
<button
|
||||
v-for="(section, index) in sections"
|
||||
:key="index"
|
||||
@click="scrollToSection(index)"
|
||||
class="scroll-indicator-btn group"
|
||||
>
|
||||
<span class="scroll-indicator-label">{{ section.name }}</span>
|
||||
<div
|
||||
class="scroll-indicator-dot"
|
||||
:class="{ active: currentSection === index }"
|
||||
/>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="fixed top-0 left-0 right-0 z-50 border-b border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/90 dark:bg-[#191714]/95 backdrop-blur-xl transition-all">
|
||||
<div class="mx-auto max-w-7xl px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo & Brand -->
|
||||
<div
|
||||
class="flex items-center gap-3 group/logo cursor-pointer"
|
||||
@click="scrollToSection(0)"
|
||||
>
|
||||
<HeaderLogo size="h-9 w-9" className="text-[#191919] dark:text-white" />
|
||||
<div class="flex flex-col justify-center">
|
||||
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">Aether</h1>
|
||||
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">API Gateway</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Navigation -->
|
||||
<nav class="hidden md:flex items-center gap-2">
|
||||
<button
|
||||
v-for="(section, index) in sections"
|
||||
:key="index"
|
||||
@click="scrollToSection(index)"
|
||||
class="group relative px-3 py-2 text-sm font-medium transition"
|
||||
:class="currentSection === index
|
||||
? 'text-[#cc785c] dark:text-[#d4a27f]'
|
||||
: 'text-[#666663] dark:text-muted-foreground hover:text-[#191919] dark:hover:text-white'"
|
||||
>
|
||||
{{ section.name }}
|
||||
<div
|
||||
class="absolute bottom-0 left-0 right-0 h-0.5 rounded-full transition-all duration-300"
|
||||
:class="currentSection === index ? 'bg-[#cc785c] dark:bg-[#d4a27f] scale-x-100' : 'bg-transparent scale-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Right Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="toggleDarkMode"
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
||||
:title="themeMode === 'system' ? '跟随系统' : themeMode === 'dark' ? '深色模式' : '浅色模式'"
|
||||
>
|
||||
<SunMoon v-if="themeMode === 'system'" class="h-4 w-4" />
|
||||
<Sun v-else-if="themeMode === 'light'" class="h-4 w-4" />
|
||||
<Moon v-else class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<RouterLink
|
||||
v-if="authStore.isAuthenticated"
|
||||
:to="dashboardPath"
|
||||
class="rounded-xl bg-[#191919] dark:bg-[#cc785c] px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-[#262625] dark:hover:bg-[#b86d52]"
|
||||
>
|
||||
控制台
|
||||
</RouterLink>
|
||||
<button
|
||||
v-else
|
||||
@click="showLoginDialog = true"
|
||||
class="rounded-xl bg-[#cc785c] px-4 py-2 text-sm font-medium text-white shadow-lg shadow-[#cc785c]/30 transition hover:bg-[#d4a27f]"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="relative z-10">
|
||||
<!-- Fixed Logo Container -->
|
||||
<div class="fixed inset-0 z-20 pointer-events-none flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
class="transform-gpu logo-container"
|
||||
:class="[currentSection === SECTIONS.HOME ? 'home-section' : '', `logo-transition-${scrollDirection}`]"
|
||||
:style="fixedLogoStyle"
|
||||
>
|
||||
<Transition :name="logoTransitionName">
|
||||
<AetherLineByLineLogo
|
||||
v-if="currentSection === SECTIONS.HOME"
|
||||
ref="aetherLogoRef"
|
||||
key="aether-logo"
|
||||
:size="400"
|
||||
:line-delay="50"
|
||||
:stroke-duration="1200"
|
||||
:fill-duration="1500"
|
||||
:auto-start="false"
|
||||
:loop="true"
|
||||
:loop-pause="800"
|
||||
:stroke-width="3.5"
|
||||
:cycle-colors="true"
|
||||
:is-dark="isDark"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:key="`ripple-wrapper-${currentLogoType}`"
|
||||
:class="{ 'heartbeat-wrapper': currentSection === SECTIONS.GEMINI && geminiFillComplete }"
|
||||
>
|
||||
<RippleLogo
|
||||
ref="rippleLogoRef"
|
||||
:type="currentLogoType"
|
||||
:size="320"
|
||||
:use-adaptive="false"
|
||||
:disable-ripple="currentSection === SECTIONS.GEMINI || currentSection === SECTIONS.FEATURES"
|
||||
:anim-delay="logoTransitionDelay"
|
||||
:static="currentSection === SECTIONS.FEATURES"
|
||||
:class="[currentLogoClass, 'logo-active']"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 0: Introduction -->
|
||||
<section ref="section0" class="min-h-screen snap-start flex items-center justify-center px-16 lg:px-20 py-20">
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<div class="h-80 w-full mb-16" />
|
||||
<h1
|
||||
class="mb-6 text-5xl md:text-7xl font-bold text-[#191919] dark:text-white leading-tight transition-all duration-700"
|
||||
:style="getTitleStyle(SECTIONS.HOME)"
|
||||
>
|
||||
欢迎使用 <span class="text-primary">Aether</span>
|
||||
</h1>
|
||||
<p
|
||||
class="mb-8 text-xl text-[#666663] dark:text-gray-300 max-w-2xl mx-auto transition-all duration-700"
|
||||
:style="getDescStyle(SECTIONS.HOME)"
|
||||
>
|
||||
AI 开发工具统一接入平台<br />
|
||||
整合 Claude Code、Codex CLI、Gemini CLI 等多个 AI 编程助手
|
||||
</p>
|
||||
<button
|
||||
@click="scrollToSection(SECTIONS.CLAUDE)"
|
||||
class="mt-16 transition-all duration-700 cursor-pointer hover:scale-110"
|
||||
:style="getScrollIndicatorStyle(SECTIONS.HOME)"
|
||||
>
|
||||
<ChevronDown class="h-8 w-8 mx-auto text-[#91918d] dark:text-muted-foreground/80 animate-bounce" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 1: Claude Code -->
|
||||
<CliSection
|
||||
ref="section1"
|
||||
title="Claude Code"
|
||||
description="直接在您的终端中释放Claude的原始力量。瞬间搜索百万行代码库。将数小时的流程转化为单一命令。您的工具。您的流程。您的代码库,以思维速度进化。"
|
||||
:badge-icon="Code2"
|
||||
badge-text="IDE 集成"
|
||||
badge-class="bg-[#cc785c]/10 dark:bg-amber-900/30 border border-[#cc785c]/20 dark:border-amber-800 text-[#cc785c] dark:text-amber-400"
|
||||
v-model:platform-value="claudePlatform"
|
||||
:platform-options="platformPresets.claude.options"
|
||||
:install-command="claudeInstallCommand"
|
||||
:config-files="[{ path: '~/.claude/settings.json', content: claudeConfig, language: 'json' }]"
|
||||
:badge-style="getBadgeStyle(SECTIONS.CLAUDE)"
|
||||
:title-style="getTitleStyle(SECTIONS.CLAUDE)"
|
||||
:desc-style="getDescStyle(SECTIONS.CLAUDE)"
|
||||
:card-style-fn="(idx) => getCardStyle(SECTIONS.CLAUDE, idx)"
|
||||
content-position="right"
|
||||
@copy="copyToClipboard"
|
||||
/>
|
||||
|
||||
<!-- Section 2: Codex CLI -->
|
||||
<CliSection
|
||||
ref="section2"
|
||||
title="Codex CLI"
|
||||
description="Codex CLI 是一款可在本地终端运行的编程助手工具,它能够读取、修改并执行用户指定目录中的代码。"
|
||||
:badge-icon="Terminal"
|
||||
badge-text="命令行工具"
|
||||
badge-class="bg-[#cc785c]/10 dark:bg-emerald-900/30 border border-[#cc785c]/20 dark:border-emerald-800 text-[#cc785c] dark:text-emerald-400"
|
||||
v-model:platform-value="codexPlatform"
|
||||
:platform-options="platformPresets.codex.options"
|
||||
:install-command="codexInstallCommand"
|
||||
:config-files="[
|
||||
{ path: '~/.codex/config.toml', content: codexConfig, language: 'toml' },
|
||||
{ path: '~/.codex/auth.json', content: codexAuthConfig, language: 'json' }
|
||||
]"
|
||||
:badge-style="getBadgeStyle(SECTIONS.CODEX)"
|
||||
:title-style="getTitleStyle(SECTIONS.CODEX)"
|
||||
:desc-style="getDescStyle(SECTIONS.CODEX)"
|
||||
:card-style-fn="(idx) => getCardStyle(SECTIONS.CODEX, idx)"
|
||||
content-position="left"
|
||||
@copy="copyToClipboard"
|
||||
/>
|
||||
|
||||
<!-- Section 3: Gemini CLI -->
|
||||
<CliSection
|
||||
ref="section3"
|
||||
title="Gemini CLI"
|
||||
description="Gemini CLI 是一款开源人工智能代理,可将 Gemini 的强大功能直接带入你的终端。它提供了对 Gemini 的轻量级访问,为你提供了从提示符到我们模型的最直接路径。"
|
||||
:badge-icon="Sparkles"
|
||||
badge-text="多模态 AI"
|
||||
badge-class="bg-[#cc785c]/10 dark:bg-primary/20 border border-[#cc785c]/20 dark:border-primary/30 text-[#cc785c] dark:text-primary"
|
||||
v-model:platform-value="geminiPlatform"
|
||||
:platform-options="platformPresets.gemini.options"
|
||||
:install-command="geminiInstallCommand"
|
||||
:config-files="[
|
||||
{ path: '~/.gemini/.env', content: geminiEnvConfig, language: 'dotenv' },
|
||||
{ path: '~/.gemini/settings.json', content: geminiSettingsConfig, language: 'json' }
|
||||
]"
|
||||
:badge-style="getBadgeStyle(SECTIONS.GEMINI)"
|
||||
:title-style="getTitleStyle(SECTIONS.GEMINI)"
|
||||
:desc-style="getDescStyle(SECTIONS.GEMINI)"
|
||||
:card-style-fn="(idx) => getCardStyle(SECTIONS.GEMINI, idx)"
|
||||
content-position="right"
|
||||
@copy="copyToClipboard"
|
||||
>
|
||||
<template #logo>
|
||||
<GeminiStarCluster :is-visible="currentSection === SECTIONS.GEMINI && sectionVisibility[SECTIONS.GEMINI] > 0.05" />
|
||||
</template>
|
||||
</CliSection>
|
||||
|
||||
<!-- Section 4: Features -->
|
||||
<section ref="section4" class="min-h-screen snap-start flex items-center justify-center px-16 lg:px-20 py-20 relative overflow-hidden">
|
||||
<div class="max-w-4xl mx-auto text-center relative z-10">
|
||||
<div
|
||||
class="inline-flex items-center gap-2 rounded-full bg-[#cc785c]/10 dark:bg-purple-500/20 border border-[#cc785c]/20 dark:border-purple-500/40 px-4 py-2 text-sm font-medium text-[#cc785c] dark:text-purple-300 mb-6 backdrop-blur-sm transition-all duration-500"
|
||||
:style="getBadgeStyle(SECTIONS.FEATURES)"
|
||||
>
|
||||
<Sparkles class="h-4 w-4" />
|
||||
项目进度
|
||||
</div>
|
||||
|
||||
<h2
|
||||
class="text-4xl md:text-5xl font-bold text-[#191919] dark:text-white mb-6 transition-all duration-700"
|
||||
:style="getTitleStyle(SECTIONS.FEATURES)"
|
||||
>
|
||||
功能开发进度
|
||||
</h2>
|
||||
|
||||
<p
|
||||
class="text-lg text-[#666663] dark:text-gray-300 mb-12 max-w-2xl mx-auto transition-all duration-700"
|
||||
:style="getDescStyle(SECTIONS.FEATURES)"
|
||||
>
|
||||
核心 API 代理功能已完成,正在载入更多功能
|
||||
</p>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="(feature, idx) in featureCards"
|
||||
:key="idx"
|
||||
class="bg-white/70 dark:bg-[#262624]/80 backdrop-blur-sm rounded-2xl p-6 border border-[#e5e4df] dark:border-[rgba(227,224,211,0.16)] hover:border-[#cc785c]/30 dark:hover:border-[#d4a27f]/40 transition-all duration-700"
|
||||
:style="getFeatureCardStyle(SECTIONS.FEATURES, idx)"
|
||||
>
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl mb-4 mx-auto"
|
||||
:class="feature.status === 'completed'
|
||||
? 'bg-emerald-500/10 dark:bg-emerald-500/15'
|
||||
: 'bg-[#cc785c]/10 dark:bg-[#cc785c]/15'"
|
||||
>
|
||||
<component
|
||||
:is="feature.icon"
|
||||
class="h-6 w-6"
|
||||
:class="feature.status === 'completed'
|
||||
? 'text-emerald-500 dark:text-emerald-400'
|
||||
: 'text-[#cc785c] dark:text-[#d4a27f] animate-spin'"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-[#191919] dark:text-white mb-2">{{ feature.title }}</h3>
|
||||
<p class="text-sm text-[#666663] dark:text-[#c9c3b4]">{{ feature.desc }}</p>
|
||||
<div
|
||||
class="mt-3 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium"
|
||||
:class="feature.status === 'completed'
|
||||
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-amber-500/10 text-amber-600 dark:text-amber-400'"
|
||||
>
|
||||
{{ feature.status === 'completed' ? '已完成' : '进行中' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 transition-all duration-700" :style="getButtonsStyle(SECTIONS.FEATURES)">
|
||||
<RouterLink
|
||||
v-if="authStore.isAuthenticated"
|
||||
:to="dashboardPath"
|
||||
class="inline-flex items-center gap-2 rounded-xl bg-primary hover:bg-primary/90 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-primary/30 transition hover:shadow-primary/50 hover:scale-105"
|
||||
>
|
||||
<Rocket class="h-5 w-5" />
|
||||
立即开始使用
|
||||
</RouterLink>
|
||||
<button
|
||||
v-else
|
||||
@click="showLoginDialog = true"
|
||||
class="inline-flex items-center gap-2 rounded-xl bg-primary hover:bg-primary/90 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-primary/30 transition hover:shadow-primary/50 hover:scale-105"
|
||||
>
|
||||
<Rocket class="h-5 w-5" />
|
||||
立即开始使用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="relative z-10 border-t border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/90 dark:bg-[#191714]/95 backdrop-blur-md py-8">
|
||||
<div class="mx-auto max-w-7xl px-6">
|
||||
<div class="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<p class="text-sm text-[#91918d] dark:text-muted-foreground">© 2025 Aether. 团队内部使用</p>
|
||||
<div class="flex items-center gap-6 text-sm text-[#91918d] dark:text-muted-foreground">
|
||||
<a href="#" class="transition hover:text-[#191919] dark:hover:text-white">使用条款</a>
|
||||
<a href="#" class="transition hover:text-[#191919] dark:hover:text-white">隐私政策</a>
|
||||
<a href="#" class="transition hover:text-[#191919] dark:hover:text-white">技术支持</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<LoginDialog v-model="showLoginDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import {
|
||||
ChevronDown,
|
||||
Code2,
|
||||
Moon,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
Sun,
|
||||
SunMoon,
|
||||
Terminal
|
||||
} from 'lucide-vue-next'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useDarkMode } from '@/composables/useDarkMode'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import LoginDialog from '@/features/auth/components/LoginDialog.vue'
|
||||
import RippleLogo from '@/components/RippleLogo.vue'
|
||||
import HeaderLogo from '@/components/HeaderLogo.vue'
|
||||
import AetherLineByLineLogo from '@/components/AetherLineByLineLogo.vue'
|
||||
import GeminiStarCluster from '@/components/GeminiStarCluster.vue'
|
||||
import CliSection from './CliSection.vue'
|
||||
import {
|
||||
SECTIONS,
|
||||
sections,
|
||||
featureCards,
|
||||
useCliConfigs,
|
||||
platformPresets,
|
||||
getInstallCommand,
|
||||
getLogoType,
|
||||
getLogoClass
|
||||
} from './home-config'
|
||||
import {
|
||||
useSectionAnimations,
|
||||
useLogoPosition,
|
||||
useLogoTransition
|
||||
} from './useSectionAnimations'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const { isDark, themeMode, toggleDarkMode } = useDarkMode()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
|
||||
const dashboardPath = computed(() =>
|
||||
authStore.user?.role === 'admin' ? '/admin/dashboard' : '/dashboard'
|
||||
)
|
||||
const baseUrl = computed(() => window.location.origin)
|
||||
|
||||
// Scroll state
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
const currentSection = ref(0)
|
||||
const previousSection = ref(0)
|
||||
const scrollDirection = ref<'up' | 'down'>('down')
|
||||
const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
|
||||
const sectionVisibility = ref<number[]>([0, 0, 0, 0, 0])
|
||||
let lastScrollY = 0
|
||||
|
||||
// Section refs - section0 and section4 are direct HTML elements, section1-3 are CliSection components
|
||||
const section0 = ref<HTMLElement | null>(null)
|
||||
const section1 = ref<InstanceType<typeof CliSection> | null>(null)
|
||||
const section2 = ref<InstanceType<typeof CliSection> | null>(null)
|
||||
const section3 = ref<InstanceType<typeof CliSection> | null>(null)
|
||||
const section4 = ref<HTMLElement | null>(null)
|
||||
|
||||
// Helper to get DOM element from ref (handles both direct elements and component instances)
|
||||
const getSectionElement = (index: number): HTMLElement | null => {
|
||||
switch (index) {
|
||||
case 0: return section0.value
|
||||
case 1: return (section1.value?.sectionEl as HTMLElement | null | undefined) ?? null
|
||||
case 2: return (section2.value?.sectionEl as HTMLElement | null | undefined) ?? null
|
||||
case 3: return (section3.value?.sectionEl as HTMLElement | null | undefined) ?? null
|
||||
case 4: return section4.value
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
// Logo refs
|
||||
const aetherLogoRef = ref<InstanceType<typeof AetherLineByLineLogo> | null>(null)
|
||||
const rippleLogoRef = ref<InstanceType<typeof RippleLogo> | null>(null)
|
||||
const hasLogoAnimationStarted = ref(false)
|
||||
const geminiFillComplete = ref(false)
|
||||
|
||||
// Animation composables
|
||||
const {
|
||||
getBadgeStyle,
|
||||
getTitleStyle,
|
||||
getDescStyle,
|
||||
getButtonsStyle,
|
||||
getScrollIndicatorStyle,
|
||||
getCardStyle,
|
||||
getFeatureCardStyle
|
||||
} = useSectionAnimations(sectionVisibility)
|
||||
|
||||
const { fixedLogoStyle } = useLogoPosition(currentSection, windowWidth)
|
||||
const { logoTransitionName } = useLogoTransition(currentSection, previousSection)
|
||||
|
||||
// Logo computed
|
||||
const currentLogoType = computed(() => getLogoType(currentSection.value))
|
||||
const currentLogoClass = computed(() => getLogoClass(currentSection.value))
|
||||
const logoTransitionDelay = computed(() => {
|
||||
if (currentSection.value === SECTIONS.FEATURES) return 0
|
||||
if (previousSection.value === SECTIONS.FEATURES) return 200
|
||||
return 500
|
||||
})
|
||||
|
||||
// Platform states
|
||||
const claudePlatform = ref(platformPresets.claude.defaultValue)
|
||||
const codexPlatform = ref(platformPresets.codex.defaultValue)
|
||||
const geminiPlatform = ref(platformPresets.gemini.defaultValue)
|
||||
|
||||
// Install commands
|
||||
const claudeInstallCommand = computed(() => getInstallCommand('claude', claudePlatform.value))
|
||||
const codexInstallCommand = computed(() => getInstallCommand('codex', codexPlatform.value))
|
||||
const geminiInstallCommand = computed(() => getInstallCommand('gemini', geminiPlatform.value))
|
||||
|
||||
// CLI configs
|
||||
const { claudeConfig, codexConfig, codexAuthConfig, geminiEnvConfig, geminiSettingsConfig } =
|
||||
useCliConfigs(baseUrl)
|
||||
|
||||
// Dialog state
|
||||
const showLoginDialog = ref(false)
|
||||
|
||||
// Scroll handling
|
||||
let scrollEndTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const calculateVisibility = (element: HTMLElement | null): number => {
|
||||
if (!element) return 0
|
||||
const rect = element.getBoundingClientRect()
|
||||
const containerHeight = window.innerHeight
|
||||
if (rect.bottom < 0 || rect.top > containerHeight) return 0
|
||||
const elementCenter = rect.top + rect.height / 2
|
||||
const viewportCenter = containerHeight / 2
|
||||
const distanceFromCenter = Math.abs(elementCenter - viewportCenter)
|
||||
const maxDistance = containerHeight / 2 + rect.height / 2
|
||||
return Math.max(0, 1 - distanceFromCenter / maxDistance)
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!scrollContainer.value) return
|
||||
|
||||
const containerHeight = window.innerHeight
|
||||
const newScrollY = scrollContainer.value.scrollTop
|
||||
|
||||
// Track scroll direction
|
||||
scrollDirection.value = newScrollY > lastScrollY ? 'down' : 'up'
|
||||
lastScrollY = newScrollY
|
||||
|
||||
// Update visibility
|
||||
for (let i = 0; i < 5; i++) {
|
||||
sectionVisibility.value[i] = calculateVisibility(getSectionElement(i))
|
||||
}
|
||||
|
||||
// Update current section
|
||||
const scrollMiddle = newScrollY + containerHeight / 2
|
||||
for (let i = 4; i >= 0; i--) {
|
||||
const section = getSectionElement(i)
|
||||
if (section && section.offsetTop <= scrollMiddle) {
|
||||
if (currentSection.value !== i) {
|
||||
previousSection.value = currentSection.value
|
||||
currentSection.value = i
|
||||
hasLogoAnimationStarted.value = false
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Detect snap complete
|
||||
if (scrollEndTimer) clearTimeout(scrollEndTimer)
|
||||
scrollEndTimer = setTimeout(() => {
|
||||
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
|
||||
hasLogoAnimationStarted.value = true
|
||||
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
|
||||
}
|
||||
}, 150)
|
||||
}
|
||||
|
||||
const scrollToSection = (index: number) => {
|
||||
const target = getSectionElement(index)
|
||||
if (target) target.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
// Watch Gemini fill complete
|
||||
watch(
|
||||
() => rippleLogoRef.value?.fillComplete,
|
||||
(val) => {
|
||||
if (currentSection.value === SECTIONS.GEMINI && val) geminiFillComplete.value = true
|
||||
}
|
||||
)
|
||||
|
||||
watch(currentSection, (_, old) => {
|
||||
if (old === SECTIONS.GEMINI) geminiFillComplete.value = false
|
||||
})
|
||||
|
||||
const handleResize = () => {
|
||||
windowWidth.value = window.innerWidth
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scrollContainer.value?.addEventListener('scroll', handleScroll, { passive: true })
|
||||
window.addEventListener('resize', handleResize, { passive: true })
|
||||
handleScroll()
|
||||
|
||||
// Initial animation
|
||||
setTimeout(() => {
|
||||
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
|
||||
hasLogoAnimationStarted.value = true
|
||||
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
|
||||
}
|
||||
}, 300)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
scrollContainer.value?.removeEventListener('scroll', handleScroll)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (scrollEndTimer) clearTimeout(scrollEndTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Typography */
|
||||
h1, h2, h3 {
|
||||
font-family: var(--serif);
|
||||
letter-spacing: -0.02em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: var(--serif);
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
button, nav, a, .inline-flex {
|
||||
font-family: var(--sans-serif);
|
||||
}
|
||||
|
||||
/* Panel styles */
|
||||
.command-panel-surface {
|
||||
border-color: var(--color-border);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.dark .command-panel-surface {
|
||||
background: rgba(38, 38, 36, 0.3);
|
||||
}
|
||||
|
||||
/* Performance */
|
||||
h1, h2, p {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* Scroll indicator */
|
||||
.scroll-indicator {
|
||||
position: fixed;
|
||||
right: 2rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.scroll-indicator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-indicator-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.scroll-indicator-label {
|
||||
position: absolute;
|
||||
right: 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: #666663;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
white-space: nowrap;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dark .scroll-indicator-label {
|
||||
color: #a0a0a0;
|
||||
background: rgba(25, 23, 20, 0.9);
|
||||
}
|
||||
|
||||
.scroll-indicator-btn:hover .scroll-indicator-label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scroll-indicator-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #d4d4d4;
|
||||
background: transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .scroll-indicator-dot {
|
||||
border-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.scroll-indicator-dot.active {
|
||||
background: #cc785c;
|
||||
border-color: #cc785c;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
/* Logo transitions */
|
||||
.logo-scale-enter-active {
|
||||
transition: opacity 0.5s ease-out, transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.logo-scale-leave-active {
|
||||
transition: opacity 0.3s ease-in, transform 0.3s ease-in;
|
||||
}
|
||||
|
||||
.logo-scale-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.6) rotate(-8deg);
|
||||
}
|
||||
|
||||
.logo-scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(1.2) rotate(8deg);
|
||||
}
|
||||
|
||||
.logo-slide-left-enter-active,
|
||||
.logo-slide-right-enter-active {
|
||||
transition: opacity 0.4s ease-out, transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.logo-slide-left-leave-active,
|
||||
.logo-slide-right-leave-active {
|
||||
transition: opacity 0.25s ease-in, transform 0.3s ease-in;
|
||||
}
|
||||
|
||||
.logo-slide-left-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(60px) scale(0.9);
|
||||
}
|
||||
|
||||
.logo-slide-left-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-60px) scale(0.9);
|
||||
}
|
||||
|
||||
.logo-slide-right-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-60px) scale(0.9);
|
||||
}
|
||||
|
||||
.logo-slide-right-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(60px) scale(0.9);
|
||||
}
|
||||
|
||||
/* Logo container */
|
||||
.logo-container {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-container.home-section {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.logo-container > * {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.logo-container {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
}
|
||||
.logo-container.home-section {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heartbeat animation */
|
||||
.heartbeat-wrapper {
|
||||
animation: heartbeat 1.5s ease-in-out infinite;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@keyframes heartbeat {
|
||||
0%, 70%, 100% { transform: scale(1); }
|
||||
14% { transform: scale(1.06); }
|
||||
28% { transform: scale(1); }
|
||||
42% { transform: scale(1.1); }
|
||||
}
|
||||
</style>
|
||||
253
frontend/src/views/public/LogoColorDemo.vue
Normal file
253
frontend/src/views/public/LogoColorDemo.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-[#fafaf7] dark:bg-[#191714] p-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl font-bold text-center mb-2 text-[#191919] dark:text-white">Logo 颜色方案对比</h1>
|
||||
<p class="text-center text-[#666663] dark:text-gray-400 mb-8">点击任意方案可以放大预览</p>
|
||||
|
||||
<!-- Color schemes grid -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="(scheme, index) in colorSchemes"
|
||||
:key="index"
|
||||
class="relative bg-white dark:bg-[#262624] rounded-2xl p-6 border border-[#e5e4df] dark:border-[rgba(227,224,211,0.16)] cursor-pointer transition-all hover:shadow-lg hover:scale-[1.02]"
|
||||
:class="{ 'ring-2 ring-primary': selectedScheme === index }"
|
||||
@click="selectScheme(index)"
|
||||
>
|
||||
<!-- Scheme name badge -->
|
||||
<div class="absolute top-3 left-3 px-2 py-1 rounded-full text-xs font-medium"
|
||||
:style="{ backgroundColor: scheme.primary + '20', color: scheme.primary }">
|
||||
{{ scheme.name }}
|
||||
</div>
|
||||
|
||||
<!-- Logo preview -->
|
||||
<div class="flex items-center justify-center h-48 mb-4">
|
||||
<svg
|
||||
viewBox="0 0 800 800"
|
||||
class="w-40 h-40"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient :id="`gradient-${index}`" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" :stop-color="scheme.primary" />
|
||||
<stop offset="50%" :stop-color="scheme.secondary" />
|
||||
<stop offset="100%" :stop-color="scheme.primary" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Fill -->
|
||||
<path
|
||||
:d="fullPath"
|
||||
:fill="`url(#gradient-${index})`"
|
||||
fill-rule="evenodd"
|
||||
opacity="0.7"
|
||||
/>
|
||||
|
||||
<!-- Lines -->
|
||||
<path
|
||||
v-for="(path, pathIndex) in linePaths"
|
||||
:key="pathIndex"
|
||||
:d="path"
|
||||
fill="none"
|
||||
:stroke="scheme.primary"
|
||||
stroke-width="3.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Color swatches -->
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full border-2 border-white shadow"
|
||||
:style="{ backgroundColor: scheme.primary }"
|
||||
></div>
|
||||
<span class="text-xs text-[#666663] dark:text-gray-400 mt-1">{{ scheme.primary }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full border-2 border-white shadow"
|
||||
:style="{ backgroundColor: scheme.secondary }"
|
||||
></div>
|
||||
<span class="text-xs text-[#666663] dark:text-gray-400 mt-1">{{ scheme.secondary }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-center text-sm text-[#666663] dark:text-gray-400 mt-3">{{ scheme.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Large preview modal -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showPreview"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
@click="showPreview = false"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-[#262624] rounded-3xl p-8 max-w-lg w-full mx-4 shadow-2xl"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-[#191919] dark:text-white">
|
||||
{{ colorSchemes[selectedScheme].name }}
|
||||
</h2>
|
||||
<button
|
||||
@click="showPreview = false"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Animated logo -->
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<AetherLineByLineLogo
|
||||
:key="selectedScheme"
|
||||
:size="300"
|
||||
:line-delay="120"
|
||||
:stroke-duration="2000"
|
||||
:color-duration="1200"
|
||||
:auto-start="true"
|
||||
:loop="true"
|
||||
:loop-pause="300"
|
||||
:stroke-width="3.5"
|
||||
:outline-color="colorSchemes[selectedScheme].primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Color info -->
|
||||
<div class="flex items-center justify-center gap-6 mt-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-6 h-6 rounded-full border-2 border-white shadow"
|
||||
:style="{ backgroundColor: colorSchemes[selectedScheme].primary }"
|
||||
></div>
|
||||
<span class="text-sm font-mono text-[#666663] dark:text-gray-400">
|
||||
{{ colorSchemes[selectedScheme].primary }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-6 h-6 rounded-full border-2 border-white shadow"
|
||||
:style="{ backgroundColor: colorSchemes[selectedScheme].secondary }"
|
||||
></div>
|
||||
<span class="text-sm font-mono text-[#666663] dark:text-gray-400">
|
||||
{{ colorSchemes[selectedScheme].secondary }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apply button -->
|
||||
<div class="mt-6 text-center">
|
||||
<button
|
||||
@click="applyScheme"
|
||||
class="px-6 py-2 bg-primary text-white rounded-xl font-medium hover:bg-primary/90 transition"
|
||||
>
|
||||
应用此方案
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Back button -->
|
||||
<div class="mt-8 text-center">
|
||||
<RouterLink
|
||||
to="/"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 text-[#666663] dark:text-gray-400 hover:text-[#191919] dark:hover:text-white transition"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
返回首页
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import AetherLineByLineLogo from '@/components/AetherLineByLineLogo.vue'
|
||||
import { AETHER_LINE_PATHS, AETHER_FULL_PATH } from '@/constants/logoPaths'
|
||||
|
||||
const linePaths = AETHER_LINE_PATHS
|
||||
const fullPath = AETHER_FULL_PATH
|
||||
|
||||
const colorSchemes = [
|
||||
{
|
||||
name: '当前配色 - 暖橙',
|
||||
primary: '#cc785c',
|
||||
secondary: '#e8a882',
|
||||
description: '温暖的赤陶色,亲和力强'
|
||||
},
|
||||
{
|
||||
name: '深金色调',
|
||||
primary: '#b08d57',
|
||||
secondary: '#d4b896',
|
||||
description: '古铜金色,更加沉稳大气'
|
||||
},
|
||||
{
|
||||
name: '玫瑰红调',
|
||||
primary: '#a4636c',
|
||||
secondary: '#d4a5aa',
|
||||
description: '优雅的玫瑰红,细腻温婉'
|
||||
},
|
||||
{
|
||||
name: '青铜绿调',
|
||||
primary: '#7a8c70',
|
||||
secondary: '#a8b8a0',
|
||||
description: '自然青铜色,清新科技感'
|
||||
},
|
||||
{
|
||||
name: '深紫藤调',
|
||||
primary: '#7d6b8a',
|
||||
secondary: '#b5a8c2',
|
||||
description: '紫藤色调,神秘优雅'
|
||||
},
|
||||
{
|
||||
name: '黑金商务',
|
||||
primary: '#3d3833',
|
||||
secondary: '#8b7355',
|
||||
description: '炭黑配金色,高端商务风'
|
||||
},
|
||||
{
|
||||
name: '海蓝科技',
|
||||
primary: '#4a7c8c',
|
||||
secondary: '#8ab8c8',
|
||||
description: '深海蓝色,科技专业感'
|
||||
},
|
||||
{
|
||||
name: '焦糖棕调',
|
||||
primary: '#8b6b4a',
|
||||
secondary: '#c4a882',
|
||||
description: '焦糖棕色,复古温暖'
|
||||
},
|
||||
{
|
||||
name: '石墨灰调',
|
||||
primary: '#5a5a5a',
|
||||
secondary: '#9a9a9a',
|
||||
description: '中性石墨色,简约现代'
|
||||
}
|
||||
]
|
||||
|
||||
const selectedScheme = ref(0)
|
||||
const showPreview = ref(false)
|
||||
|
||||
const selectScheme = (index: number) => {
|
||||
selectedScheme.value = index
|
||||
showPreview.value = true
|
||||
}
|
||||
|
||||
const applyScheme = () => {
|
||||
const scheme = colorSchemes[selectedScheme.value]
|
||||
alert(`应用方案: ${scheme.name}\n\n请将以下颜色值更新到代码中:\n主色: ${scheme.primary}\n过渡色: ${scheme.secondary}\n\n需要修改的文件:\n1. AetherLineByLineLogo.vue - gradient 颜色\n2. Home.vue - outlineColor 属性`)
|
||||
showPreview.value = false
|
||||
}
|
||||
</script>
|
||||
171
frontend/src/views/public/home-config.ts
Normal file
171
frontend/src/views/public/home-config.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { computed, type Ref } from 'vue'
|
||||
import { Apple, Box, Check, Loader2, Monitor, Terminal } from 'lucide-vue-next'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
// Section index constants
|
||||
export const SECTIONS = {
|
||||
HOME: 0,
|
||||
CLAUDE: 1,
|
||||
CODEX: 2,
|
||||
GEMINI: 3,
|
||||
FEATURES: 4
|
||||
} as const
|
||||
|
||||
export type SectionIndex = (typeof SECTIONS)[keyof typeof SECTIONS]
|
||||
|
||||
// Section navigation configuration
|
||||
export const sections = [
|
||||
{ name: '首页' },
|
||||
{ name: 'Claude' },
|
||||
{ name: 'OpenAI' },
|
||||
{ name: 'Gemini' },
|
||||
{ name: '更多' }
|
||||
] as const
|
||||
|
||||
// Feature cards data
|
||||
export const featureCards = [
|
||||
{
|
||||
icon: Check,
|
||||
title: 'Claude / OpenAI / Gemini',
|
||||
desc: '已完整接入三大主流 AI 编程助手的标准 API',
|
||||
status: 'completed' as const
|
||||
},
|
||||
{
|
||||
icon: Check,
|
||||
title: '灵活的 API 扩展',
|
||||
desc: '插件化架构,可快速接入其他 LLM Provider',
|
||||
status: 'completed' as const
|
||||
},
|
||||
{
|
||||
icon: Loader2,
|
||||
title: '项目追踪',
|
||||
desc: '开发日志、代码 Review、文档生成等功能即将到来',
|
||||
status: 'in-progress' as const
|
||||
}
|
||||
]
|
||||
|
||||
// CLI configuration generators
|
||||
export function useCliConfigs(baseUrl: Ref<string>) {
|
||||
const claudeConfig = computed(() => `{
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "your-api-key",
|
||||
"ANTHROPIC_BASE_URL": "${baseUrl.value}"
|
||||
}
|
||||
}`)
|
||||
|
||||
const codexConfig = computed(() => `model_provider = "aether"
|
||||
model = "latest-model-name"
|
||||
model_reasoning_effort = "high"
|
||||
network_access = "enabled"
|
||||
disable_response_storage = true
|
||||
|
||||
[model_providers.aether]
|
||||
name = "aether"
|
||||
base_url = "${baseUrl.value}/v1"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = true`)
|
||||
|
||||
const codexAuthConfig = computed(() => `{
|
||||
"OPENAI_API_KEY": "your-api-key"
|
||||
}`)
|
||||
|
||||
const geminiEnvConfig = computed(() => `GOOGLE_GEMINI_BASE_URL=${baseUrl.value}
|
||||
GEMINI_API_KEY=your-api-key
|
||||
GEMINI_MODEL=latest-model-name`)
|
||||
|
||||
const geminiSettingsConfig = computed(() => `{
|
||||
"ide": {
|
||||
"enabled": true
|
||||
},
|
||||
"security": {
|
||||
"auth": {
|
||||
"selectedType": "gemini-api-key"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
return {
|
||||
claudeConfig,
|
||||
codexConfig,
|
||||
codexAuthConfig,
|
||||
geminiEnvConfig,
|
||||
geminiSettingsConfig
|
||||
}
|
||||
}
|
||||
|
||||
// CSS class constants
|
||||
export const panelClasses = {
|
||||
commandPanel: 'rounded-xl border command-panel-surface',
|
||||
configPanel: 'rounded-xl border config-panel',
|
||||
panelHeader: 'px-4 py-2 panel-header',
|
||||
codeBody: 'code-panel-body',
|
||||
iconButtonSmall: [
|
||||
'flex items-center justify-center rounded-lg border h-7 w-7',
|
||||
'border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)]',
|
||||
'bg-transparent',
|
||||
'text-[#666663] dark:text-[#f1ead8]',
|
||||
'transition hover:bg-[#f0f0eb] dark:hover:bg-[#3a3731]'
|
||||
].join(' ')
|
||||
} as const
|
||||
|
||||
// Platform option type
|
||||
export interface PlatformOption {
|
||||
value: string
|
||||
label: string
|
||||
hint: string
|
||||
icon: Component
|
||||
command: string
|
||||
}
|
||||
|
||||
// Platform presets configuration
|
||||
export const platformPresets = {
|
||||
claude: {
|
||||
options: [
|
||||
{ value: 'mac', label: 'Mac / Linux', hint: 'Terminal', icon: Terminal, command: 'curl -fsSL https://claude.ai/install.sh | bash' },
|
||||
{ value: 'windows', label: 'Windows', hint: 'PowerShell', icon: Monitor, command: 'irm https://claude.ai/install.ps1 | iex' },
|
||||
{ value: 'nodejs', label: 'Node.js', hint: 'npm', icon: Box, command: 'npm install -g @anthropic-ai/claude-code' },
|
||||
{ value: 'homebrew', label: 'Mac', hint: 'Homebrew', icon: Apple, command: 'brew install --cask claude-code' }
|
||||
] as PlatformOption[],
|
||||
defaultValue: 'mac'
|
||||
},
|
||||
codex: {
|
||||
options: [
|
||||
{ value: 'nodejs', label: 'Node.js', hint: 'npm', icon: Box, command: 'npm install -g @openai/codex' },
|
||||
{ value: 'homebrew', label: 'Mac', hint: 'Homebrew', icon: Apple, command: 'brew install --cask codex' }
|
||||
] as PlatformOption[],
|
||||
defaultValue: 'nodejs'
|
||||
},
|
||||
gemini: {
|
||||
options: [
|
||||
{ value: 'nodejs', label: 'Node.js', hint: 'npm', icon: Box, command: 'npm install -g @google/gemini-cli' },
|
||||
{ value: 'homebrew', label: 'Mac', hint: 'Homebrew', icon: Apple, command: 'brew install gemini-cli' }
|
||||
] as PlatformOption[],
|
||||
defaultValue: 'nodejs'
|
||||
}
|
||||
} as const
|
||||
|
||||
// Helper to get command by platform value
|
||||
export function getInstallCommand(preset: keyof typeof platformPresets, value: string): string {
|
||||
const config = platformPresets[preset]
|
||||
return config.options.find((opt) => opt.value === value)?.command ?? ''
|
||||
}
|
||||
|
||||
// Logo type mapping
|
||||
export function getLogoType(section: number): 'claude' | 'openai' | 'gemini' | 'aether' {
|
||||
switch (section) {
|
||||
case SECTIONS.CLAUDE: return 'claude'
|
||||
case SECTIONS.CODEX: return 'openai'
|
||||
case SECTIONS.GEMINI: return 'gemini'
|
||||
default: return 'aether'
|
||||
}
|
||||
}
|
||||
|
||||
// Logo color class mapping
|
||||
export function getLogoClass(section: number): string {
|
||||
switch (section) {
|
||||
case SECTIONS.CLAUDE: return 'text-[#D97757]'
|
||||
case SECTIONS.CODEX: return 'text-[#191919] dark:text-white'
|
||||
case SECTIONS.GEMINI: return '' // Gemini uses gradient
|
||||
default: return 'text-[#191919] dark:text-white'
|
||||
}
|
||||
}
|
||||
199
frontend/src/views/public/useSectionAnimations.ts
Normal file
199
frontend/src/views/public/useSectionAnimations.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { computed, type Ref, type CSSProperties } from 'vue'
|
||||
import { SECTIONS } from './home-config'
|
||||
|
||||
// Animation configuration constants
|
||||
const ANIMATION_CONFIG = {
|
||||
delays: {
|
||||
badge: 0,
|
||||
title: 0.1,
|
||||
desc: 0.2,
|
||||
buttons: 0.3,
|
||||
scrollIndicator: 0.4,
|
||||
cardBase: 0.25,
|
||||
cardIncrement: 0.1,
|
||||
featureCardBase: 0.2,
|
||||
featureCardIncrement: 0.15
|
||||
},
|
||||
translateY: {
|
||||
home: 30,
|
||||
cli: 10,
|
||||
badge: 8,
|
||||
featureCard: 30
|
||||
},
|
||||
translateX: {
|
||||
badge: 24,
|
||||
title: 32,
|
||||
desc: 28,
|
||||
buttons: 24,
|
||||
card: 20
|
||||
}
|
||||
} as const
|
||||
|
||||
// Get horizontal direction based on section layout
|
||||
// Claude(1) and Gemini(3) content on right, slide from right
|
||||
// Codex(2) content on left, slides from left
|
||||
function getDirectionMultiplier(index: number): number {
|
||||
if (index === SECTIONS.CLAUDE || index === SECTIONS.GEMINI) return 1
|
||||
if (index === SECTIONS.CODEX) return -1
|
||||
return 0
|
||||
}
|
||||
|
||||
function getHorizontalOffset(index: number, distance: number, progress: number): number {
|
||||
const direction = getDirectionMultiplier(index)
|
||||
if (direction === 0) return 0
|
||||
return (1 - progress) * distance * direction
|
||||
}
|
||||
|
||||
export function useSectionAnimations(sectionVisibility: Ref<number[]>) {
|
||||
const { delays, translateY, translateX } = ANIMATION_CONFIG
|
||||
|
||||
// Style generators for different elements
|
||||
const getBadgeStyle = (index: number): CSSProperties => {
|
||||
const visibility = sectionVisibility.value[index]
|
||||
const opacity = Math.min(1, visibility * 3)
|
||||
const progress = Math.min(1, visibility * 2)
|
||||
const direction = getDirectionMultiplier(index)
|
||||
const offsetX = getHorizontalOffset(index, translateX.badge, progress)
|
||||
const offsetY = direction === 0 ? (1 - progress) * translateY.badge : 0
|
||||
return {
|
||||
opacity,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`
|
||||
}
|
||||
}
|
||||
|
||||
const getTitleStyle = (index: number): CSSProperties => {
|
||||
const visibility = sectionVisibility.value[index]
|
||||
const adjustedVisibility = Math.max(0, visibility - delays.title) / (1 - delays.title)
|
||||
const progress = Math.min(1, adjustedVisibility * 2)
|
||||
const yBase = getDirectionMultiplier(index) === 0 ? translateY.home : translateY.cli
|
||||
const offsetY = (1 - progress) * yBase
|
||||
const offsetX = getHorizontalOffset(index, translateX.title, progress)
|
||||
return {
|
||||
opacity: progress,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`
|
||||
}
|
||||
}
|
||||
|
||||
const getDescStyle = (index: number): CSSProperties => {
|
||||
const visibility = sectionVisibility.value[index]
|
||||
const adjustedVisibility = Math.max(0, visibility - delays.desc) / (1 - delays.desc)
|
||||
const progress = Math.min(1, adjustedVisibility * 2)
|
||||
const yBase = getDirectionMultiplier(index) === 0 ? translateY.home : translateY.badge
|
||||
const offsetY = (1 - progress) * yBase
|
||||
const offsetX = getHorizontalOffset(index, translateX.desc, progress)
|
||||
return {
|
||||
opacity: progress,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`
|
||||
}
|
||||
}
|
||||
|
||||
const getButtonsStyle = (index: number): CSSProperties => {
|
||||
const visibility = sectionVisibility.value[index]
|
||||
const adjustedVisibility = Math.max(0, visibility - delays.buttons) / (1 - delays.buttons)
|
||||
const progress = Math.min(1, adjustedVisibility * 2)
|
||||
const yBase = getDirectionMultiplier(index) === 0 ? 20 : translateY.badge
|
||||
const offsetY = (1 - progress) * yBase
|
||||
const offsetX = getHorizontalOffset(index, translateX.buttons, progress)
|
||||
return {
|
||||
opacity: progress,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`
|
||||
}
|
||||
}
|
||||
|
||||
const getScrollIndicatorStyle = (index: number): CSSProperties => {
|
||||
const visibility = sectionVisibility.value[index]
|
||||
const adjustedVisibility = Math.max(0, visibility - delays.scrollIndicator) / (1 - delays.scrollIndicator)
|
||||
const opacity = Math.min(1, adjustedVisibility * 2)
|
||||
return { opacity }
|
||||
}
|
||||
|
||||
const getCardStyle = (sectionIndex: number, cardIndex: number): CSSProperties => {
|
||||
const visibility = sectionVisibility.value[sectionIndex]
|
||||
const totalDelay = delays.cardBase + cardIndex * delays.cardIncrement
|
||||
const adjustedVisibility = Math.max(0, visibility - totalDelay) / (1 - totalDelay)
|
||||
const progress = Math.min(1, adjustedVisibility * 2)
|
||||
const yBase = getDirectionMultiplier(sectionIndex) === 0 ? 20 : translateY.cli
|
||||
const offsetY = (1 - progress) * yBase
|
||||
const offsetX = getHorizontalOffset(sectionIndex, translateX.card, progress)
|
||||
return {
|
||||
opacity: progress,
|
||||
transform: `translate(${offsetX}px, ${offsetY}px)`
|
||||
}
|
||||
}
|
||||
|
||||
const getFeatureCardStyle = (sectionIndex: number, cardIndex: number): CSSProperties => {
|
||||
const visibility = sectionVisibility.value[sectionIndex]
|
||||
const totalDelay = delays.featureCardBase + cardIndex * delays.featureCardIncrement
|
||||
const adjustedVisibility = Math.max(0, visibility - totalDelay) / (1 - totalDelay)
|
||||
const opacity = Math.min(1, adjustedVisibility * 2)
|
||||
const offsetY = (1 - Math.min(1, adjustedVisibility * 2)) * translateY.featureCard
|
||||
const scale = 0.9 + Math.min(1, adjustedVisibility * 2) * 0.1
|
||||
return {
|
||||
opacity,
|
||||
transform: `translateY(${offsetY}px) scale(${scale})`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getBadgeStyle,
|
||||
getTitleStyle,
|
||||
getDescStyle,
|
||||
getButtonsStyle,
|
||||
getScrollIndicatorStyle,
|
||||
getCardStyle,
|
||||
getFeatureCardStyle
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed logo position style based on current section
|
||||
export function useLogoPosition(
|
||||
currentSection: Ref<number>,
|
||||
windowWidth: Ref<number>
|
||||
) {
|
||||
const fixedLogoStyle = computed(() => {
|
||||
const section = currentSection.value
|
||||
const isDesktop = windowWidth.value >= 768
|
||||
let transform = ''
|
||||
let opacity = 1
|
||||
|
||||
if (section === SECTIONS.HOME) {
|
||||
transform = 'scale(1.1) translateY(-18vh)'
|
||||
opacity = 0.25
|
||||
} else if (section === SECTIONS.CLAUDE) {
|
||||
transform = isDesktop ? 'translateX(-25vw) scale(1)' : 'translateY(-20vh) scale(0.8)'
|
||||
} else if (section === SECTIONS.CODEX) {
|
||||
transform = isDesktop ? 'translateX(25vw) scale(1)' : 'translateY(-20vh) scale(0.8)'
|
||||
} else if (section === SECTIONS.GEMINI) {
|
||||
transform = isDesktop ? 'translateX(-25vw) scale(1)' : 'translateY(-20vh) scale(0.8)'
|
||||
} else {
|
||||
transform = isDesktop ? 'translateX(0) scale(1)' : 'translateY(-20vh) scale(0.8)'
|
||||
opacity = 0.15
|
||||
}
|
||||
|
||||
return {
|
||||
transform,
|
||||
opacity,
|
||||
transition: 'transform 0.8s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.6s ease-out'
|
||||
}
|
||||
})
|
||||
|
||||
return { fixedLogoStyle }
|
||||
}
|
||||
|
||||
// Logo transition name based on scroll direction
|
||||
export function useLogoTransition(
|
||||
currentSection: Ref<number>,
|
||||
previousSection: Ref<number>
|
||||
) {
|
||||
const logoTransitionName = computed(() => {
|
||||
if (currentSection.value === SECTIONS.HOME || previousSection.value === SECTIONS.HOME) {
|
||||
return 'logo-scale'
|
||||
}
|
||||
if (currentSection.value > previousSection.value) {
|
||||
return 'logo-slide-left'
|
||||
}
|
||||
return 'logo-slide-right'
|
||||
})
|
||||
|
||||
return { logoTransitionName }
|
||||
}
|
||||
Reference in New Issue
Block a user