mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor(frontend): 代码格式化与移动端响应式优化
- 修正 Vue 组件 defineProps/defineEmits 声明顺序 - 优化首页移动端布局,调整 Logo 位置与透明度 - 为 Aether 标题添加打字机动画效果 - 修复 HTML 属性中引号的实体转义
This commit is contained in:
@@ -17,6 +17,11 @@ import {
|
|||||||
type ChartOptions
|
type ChartOptions
|
||||||
} from 'chart.js'
|
} from 'chart.js'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
height: 300,
|
||||||
|
options: undefined
|
||||||
|
})
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
ArcElement,
|
ArcElement,
|
||||||
DoughnutController,
|
DoughnutController,
|
||||||
@@ -31,11 +36,6 @@ interface Props {
|
|||||||
height?: number
|
height?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
height: 300,
|
|
||||||
options: undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const chartRef = ref<HTMLCanvasElement>()
|
const chartRef = ref<HTMLCanvasElement>()
|
||||||
let chart: ChartJS<'doughnut'> | null = null
|
let chart: ChartJS<'doughnut'> | null = null
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="masked" class="group relative">
|
<div
|
||||||
|
v-if="masked"
|
||||||
|
class="group relative"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
:class="inputClass"
|
:class="inputClass"
|
||||||
@@ -25,8 +28,14 @@
|
|||||||
:aria-label="isVisible ? '隐藏内容' : '显示内容'"
|
:aria-label="isVisible ? '隐藏内容' : '显示内容'"
|
||||||
@click="toggleVisibility"
|
@click="toggleVisibility"
|
||||||
>
|
>
|
||||||
<EyeOff v-if="isVisible" class="h-4 w-4" />
|
<EyeOff
|
||||||
<Eye v-else class="h-4 w-4" />
|
v-if="isVisible"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Eye
|
||||||
|
v-else
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -54,6 +63,12 @@ import { computed, useAttrs, ref } from 'vue'
|
|||||||
import { Eye, EyeOff } from 'lucide-vue-next'
|
import { Eye, EyeOff } from 'lucide-vue-next'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
// 开发环境警告:type="password" 已被弃用
|
// 开发环境警告:type="password" 已被弃用
|
||||||
const warnPasswordType = import.meta.env.DEV
|
const warnPasswordType = import.meta.env.DEV
|
||||||
? (() => {
|
? (() => {
|
||||||
@@ -96,12 +111,7 @@ interface Props {
|
|||||||
disableAutofill?: boolean
|
disableAutofill?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: string]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const inputRef = ref<HTMLInputElement | null>(null)
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
const isVisible = ref(false)
|
const isVisible = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,19 @@
|
|||||||
size="md"
|
size="md"
|
||||||
@update:open="$emit('update:open', $event)"
|
@update:open="$emit('update:open', $event)"
|
||||||
>
|
>
|
||||||
<form :name="`provider-auth-${Date.now()}`" autocomplete="off" @submit.prevent>
|
<form
|
||||||
|
:name="`provider-auth-${Date.now()}`"
|
||||||
|
autocomplete="off"
|
||||||
|
@submit.prevent
|
||||||
|
>
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div
|
<div
|
||||||
v-if="isLoadingConfig"
|
v-if="isLoadingConfig"
|
||||||
class="flex items-center justify-center py-8"
|
class="flex items-center justify-center py-8"
|
||||||
>
|
>
|
||||||
<div class="text-sm text-muted-foreground">加载配置中...</div>
|
<div class="text-sm text-muted-foreground">
|
||||||
|
加载配置中...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -283,9 +289,6 @@ import {
|
|||||||
type AuthTemplateFieldGroup,
|
type AuthTemplateFieldGroup,
|
||||||
} from '../auth-templates'
|
} from '../auth-templates'
|
||||||
|
|
||||||
// 敏感字段列表(用于验证和加载配置时的特殊处理)
|
|
||||||
const SENSITIVE_FIELDS = ['api_key', 'password', 'session_token', 'session_cookie', 'token_cookie', 'auth_cookie', 'cookie_string', 'cookie', 'proxy_password'] as const
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
providerId: string
|
providerId: string
|
||||||
@@ -298,6 +301,9 @@ const emit = defineEmits<{
|
|||||||
(e: 'saved'): void
|
(e: 'saved'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// 敏感字段列表(用于验证和加载配置时的特殊处理)
|
||||||
|
const SENSITIVE_FIELDS = ['api_key', 'password', 'session_token', 'session_cookie', 'token_cookie', 'auth_cookie', 'cookie_string', 'cookie', 'proxy_password'] as const
|
||||||
|
|
||||||
const { success: showSuccess, error: showError } = useToast()
|
const { success: showSuccess, error: showError } = useToast()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|||||||
@@ -245,7 +245,10 @@
|
|||||||
<header class="hidden lg:flex h-16 px-8 items-center justify-between shrink-0 border-b border-[#3d3929]/5 dark:border-white/5 sticky top-0 z-40 backdrop-blur-md bg-[#faf9f5]/90 dark:bg-[#191714]/90">
|
<header class="hidden lg:flex h-16 px-8 items-center justify-between shrink-0 border-b border-[#3d3929]/5 dark:border-white/5 sticky top-0 z-40 backdrop-blur-md bg-[#faf9f5]/90 dark:bg-[#191714]/90">
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<template v-for="(crumb, index) in breadcrumbs" :key="index">
|
<template
|
||||||
|
v-for="(crumb, index) in breadcrumbs"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
<template v-if="index > 0">
|
<template v-if="index > 0">
|
||||||
<ChevronRight class="w-3 h-3 opacity-50" />
|
<ChevronRight class="w-3 h-3 opacity-50" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="fetchModules"
|
@click="fetchModules"
|
||||||
>
|
>
|
||||||
<RefreshCw class="w-4 h-4 mr-2" :class="{ 'animate-spin': loading }" />
|
<RefreshCw
|
||||||
|
class="w-4 h-4 mr-2"
|
||||||
|
:class="{ 'animate-spin': loading }"
|
||||||
|
/>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
@@ -62,10 +65,15 @@
|
|||||||
? 'bg-primary/15 text-primary'
|
? 'bg-primary/15 text-primary'
|
||||||
: 'bg-muted text-muted-foreground group-hover:bg-muted/80'"
|
: 'bg-muted text-muted-foreground group-hover:bg-muted/80'"
|
||||||
>
|
>
|
||||||
<component :is="getCategoryIcon(module.category)" class="w-5 h-5" />
|
<component
|
||||||
|
:is="getCategoryIcon(module.category)"
|
||||||
|
class="w-5 h-5"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0 pt-1">
|
<div class="flex-1 min-w-0 pt-1">
|
||||||
<h4 class="font-semibold text-base truncate">{{ module.display_name }}</h4>
|
<h4 class="font-semibold text-base truncate">
|
||||||
|
{{ module.display_name }}
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,7 +99,10 @@
|
|||||||
@update:model-value="(val: boolean) => toggleModule(module.name, val)"
|
@update:model-value="(val: boolean) => toggleModule(module.name, val)"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-sm" :class="module.enabled ? 'text-foreground' : 'text-muted-foreground'">
|
<span
|
||||||
|
class="text-sm"
|
||||||
|
:class="module.enabled ? 'text-foreground' : 'text-muted-foreground'"
|
||||||
|
>
|
||||||
{{ module.enabled ? '已启用' : '已禁用' }}
|
{{ module.enabled ? '已启用' : '已禁用' }}
|
||||||
</span>
|
</span>
|
||||||
<!-- 配置未验证提示(小字) -->
|
<!-- 配置未验证提示(小字) -->
|
||||||
@@ -123,7 +134,9 @@
|
|||||||
class="text-center py-16"
|
class="text-center py-16"
|
||||||
>
|
>
|
||||||
<Search class="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
<Search class="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||||
<p class="text-muted-foreground">没有找到匹配的模块</p>
|
<p class="text-muted-foreground">
|
||||||
|
没有找到匹配的模块
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
@@ -132,7 +145,9 @@
|
|||||||
class="text-center py-16"
|
class="text-center py-16"
|
||||||
>
|
>
|
||||||
<Puzzle class="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
<Puzzle class="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||||
<p class="text-muted-foreground">暂无可管理的模块</p>
|
<p class="text-muted-foreground">
|
||||||
|
暂无可管理的模块
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
@@ -177,7 +177,7 @@
|
|||||||
v-model="form.attribute_mapping_json"
|
v-model="form.attribute_mapping_json"
|
||||||
class="mt-1 font-mono text-xs"
|
class="mt-1 font-mono text-xs"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder='{"id": "user_id", "username": "login"}'
|
placeholder="{"id": "user_id", "username": "login"}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
v-model="form.extra_config_json"
|
v-model="form.extra_config_json"
|
||||||
class="mt-1 font-mono text-xs"
|
class="mt-1 font-mono text-xs"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder='{"min_trust_level": 1}'
|
placeholder="{"min_trust_level": 1}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -187,7 +187,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</CardSection>
|
</CardSection>
|
||||||
|
|
||||||
@@ -538,7 +537,9 @@
|
|||||||
<li>
|
<li>
|
||||||
API Keys: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.api_keys?.length || 0), 0) }} 个
|
API Keys: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.api_keys?.length || 0), 0) }} 个
|
||||||
</li>
|
</li>
|
||||||
<li v-if="importPreview.ldap_config">LDAP 配置: 1 个</li>
|
<li v-if="importPreview.ldap_config">
|
||||||
|
LDAP 配置: 1 个
|
||||||
|
</li>
|
||||||
<li v-if="importPreview.oauth_providers?.length">
|
<li v-if="importPreview.oauth_providers?.length">
|
||||||
OAuth Providers: {{ importPreview.oauth_providers.length }} 个
|
OAuth Providers: {{ importPreview.oauth_providers.length }} 个
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
ref="sectionRef"
|
ref="sectionRef"
|
||||||
class="min-h-screen snap-start flex items-center px-4 sm:px-8 md:px-16 lg:px-20 py-20"
|
class="min-h-screen snap-start flex items-center justify-center px-4 sm:px-8 md:px-16 lg:px-20 py-20"
|
||||||
>
|
>
|
||||||
<div class="max-w-7xl mx-auto grid md:grid-cols-2 gap-12 items-center">
|
<div class="max-w-7xl mx-auto grid md:grid-cols-2 gap-12 items-center w-full">
|
||||||
<!-- Content column -->
|
<!-- Content column -->
|
||||||
<div :class="contentOrder">
|
<div :class="contentOrder">
|
||||||
<!-- Badge -->
|
<!-- Badge -->
|
||||||
@@ -105,10 +105,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Logo placeholder column -->
|
<!-- Logo placeholder column - hidden on mobile since logo is fixed positioned -->
|
||||||
<div
|
<div
|
||||||
:class="logoOrder"
|
:class="logoOrder"
|
||||||
class="flex items-center justify-center h-full min-h-[300px] relative"
|
class="hidden md:flex items-center justify-center h-full min-h-[300px] relative"
|
||||||
>
|
>
|
||||||
<slot name="logo" />
|
<slot name="logo" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -116,6 +116,17 @@
|
|||||||
<main class="relative z-10">
|
<main class="relative z-10">
|
||||||
<!-- Fixed Logo Container -->
|
<!-- Fixed Logo Container -->
|
||||||
<div class="fixed top-0 left-0 right-0 bottom-0 z-20 pointer-events-none flex items-center justify-center overflow-hidden">
|
<div class="fixed top-0 left-0 right-0 bottom-0 z-20 pointer-events-none flex items-center justify-center overflow-hidden">
|
||||||
|
<!-- Gemini Star Cluster - positioned behind logo -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<GeminiStarCluster
|
||||||
|
v-if="currentSection === SECTIONS.GEMINI"
|
||||||
|
:is-visible="sectionVisibility[SECTIONS.GEMINI] > 0.05"
|
||||||
|
class="absolute gemini-stars"
|
||||||
|
:class="windowWidth < 768 ? 'scale-75 opacity-60' : ''"
|
||||||
|
:style="fixedLogoStyle"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="transform-gpu logo-container"
|
class="transform-gpu logo-container"
|
||||||
:class="[currentSection === SECTIONS.HOME ? 'home-section' : '', `logo-transition-${scrollDirection}`]"
|
:class="[currentSection === SECTIONS.HOME ? 'home-section' : '', `logo-transition-${scrollDirection}`]"
|
||||||
@@ -126,14 +137,14 @@
|
|||||||
v-if="currentSection === SECTIONS.HOME"
|
v-if="currentSection === SECTIONS.HOME"
|
||||||
ref="aetherLogoRef"
|
ref="aetherLogoRef"
|
||||||
key="aether-logo"
|
key="aether-logo"
|
||||||
:size="400"
|
:size="homeLogoSize"
|
||||||
:line-delay="50"
|
:line-delay="50"
|
||||||
:stroke-duration="1200"
|
:stroke-duration="1200"
|
||||||
:fill-duration="1500"
|
:fill-duration="1500"
|
||||||
:auto-start="false"
|
:auto-start="false"
|
||||||
:loop="true"
|
:loop="true"
|
||||||
:loop-pause="800"
|
:loop-pause="800"
|
||||||
:stroke-width="3.5"
|
:stroke-width="windowWidth < 768 ? 2.5 : 3.5"
|
||||||
:cycle-colors="true"
|
:cycle-colors="true"
|
||||||
:is-dark="isDark"
|
:is-dark="isDark"
|
||||||
/>
|
/>
|
||||||
@@ -145,7 +156,7 @@
|
|||||||
<RippleLogo
|
<RippleLogo
|
||||||
ref="rippleLogoRef"
|
ref="rippleLogoRef"
|
||||||
:type="currentLogoType"
|
:type="currentLogoType"
|
||||||
:size="320"
|
:size="windowWidth < 768 ? 200 : 320"
|
||||||
:use-adaptive="false"
|
:use-adaptive="false"
|
||||||
:disable-ripple="currentSection === SECTIONS.GEMINI || currentSection === SECTIONS.FEATURES"
|
:disable-ripple="currentSection === SECTIONS.GEMINI || currentSection === SECTIONS.FEATURES"
|
||||||
:anim-delay="logoTransitionDelay"
|
:anim-delay="logoTransitionDelay"
|
||||||
@@ -164,12 +175,12 @@
|
|||||||
class="min-h-screen snap-start flex items-center justify-center px-4 sm:px-8 md:px-16 lg:px-20 py-20"
|
class="min-h-screen snap-start flex items-center justify-center px-4 sm:px-8 md:px-16 lg:px-20 py-20"
|
||||||
>
|
>
|
||||||
<div class="max-w-4xl mx-auto text-center">
|
<div class="max-w-4xl mx-auto text-center">
|
||||||
<div class="h-48 sm:h-64 md:h-80 w-full mb-8 sm:mb-12 md:mb-16 mt-4 sm:mt-8" />
|
<div class="h-64 sm:h-80 md:h-[26rem] w-full mb-12 sm:mb-8 md:mb-10 mt-8 sm:mt-12" />
|
||||||
<h1
|
<h1
|
||||||
class="mb-6 text-3xl sm:text-5xl md:text-7xl font-bold text-[#191919] dark:text-white leading-tight transition-all duration-700"
|
class="mb-6 text-3xl sm:text-5xl md:text-7xl font-bold text-[#191919] dark:text-white leading-tight transition-all duration-700"
|
||||||
:style="getTitleStyle(SECTIONS.HOME)"
|
:style="getTitleStyle(SECTIONS.HOME)"
|
||||||
>
|
>
|
||||||
欢迎使用 <span class="text-primary">Aether</span>
|
欢迎使用 <span class="text-primary typewriter">{{ aetherText }}<span class="cursor" :class="{ 'cursor-hidden': !showCursor }">_</span></span>
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<p
|
||||||
class="mb-8 text-base sm:text-lg md:text-xl text-[#666663] dark:text-gray-300 max-w-2xl mx-auto transition-all duration-700"
|
class="mb-8 text-base sm:text-lg md:text-xl text-[#666663] dark:text-gray-300 max-w-2xl mx-auto transition-all duration-700"
|
||||||
@@ -252,69 +263,65 @@
|
|||||||
:card-style-fn="(idx) => getCardStyle(SECTIONS.GEMINI, idx)"
|
:card-style-fn="(idx) => getCardStyle(SECTIONS.GEMINI, idx)"
|
||||||
content-position="right"
|
content-position="right"
|
||||||
@copy="copyToClipboard"
|
@copy="copyToClipboard"
|
||||||
>
|
/>
|
||||||
<template #logo>
|
|
||||||
<GeminiStarCluster :is-visible="currentSection === SECTIONS.GEMINI && sectionVisibility[SECTIONS.GEMINI] > 0.05" />
|
|
||||||
</template>
|
|
||||||
</CliSection>
|
|
||||||
|
|
||||||
<!-- Section 4: Features -->
|
<!-- Section 4: Features -->
|
||||||
<section
|
<section
|
||||||
ref="section4"
|
ref="section4"
|
||||||
class="min-h-screen snap-start flex items-center justify-center px-4 sm:px-8 md:px-16 lg:px-20 py-20 relative overflow-hidden"
|
class="min-h-screen snap-start flex items-center justify-center px-4 sm:px-8 md:px-16 lg:px-20 py-12 md:py-20 relative overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="max-w-4xl mx-auto text-center relative z-10">
|
<div class="max-w-4xl mx-auto text-center relative z-10">
|
||||||
<div
|
<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"
|
class="inline-flex items-center gap-1.5 md:gap-2 rounded-full bg-[#cc785c]/10 dark:bg-purple-500/20 border border-[#cc785c]/20 dark:border-purple-500/40 px-3 md:px-4 py-1.5 md:py-2 text-xs md:text-sm font-medium text-[#cc785c] dark:text-purple-300 mb-4 md:mb-6 backdrop-blur-sm transition-all duration-500"
|
||||||
:style="getBadgeStyle(SECTIONS.FEATURES)"
|
:style="getBadgeStyle(SECTIONS.FEATURES)"
|
||||||
>
|
>
|
||||||
<Sparkles class="h-4 w-4" />
|
<Sparkles class="h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||||
项目进度
|
项目进度
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2
|
<h2
|
||||||
class="text-4xl md:text-5xl font-bold text-[#191919] dark:text-white mb-6 transition-all duration-700"
|
class="text-2xl md:text-5xl font-bold text-[#191919] dark:text-white mb-3 md:mb-6 transition-all duration-700"
|
||||||
:style="getTitleStyle(SECTIONS.FEATURES)"
|
:style="getTitleStyle(SECTIONS.FEATURES)"
|
||||||
>
|
>
|
||||||
功能开发进度
|
功能开发进度
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="text-lg text-[#666663] dark:text-gray-300 mb-12 max-w-2xl mx-auto transition-all duration-700"
|
class="text-base md:text-lg text-[#666663] dark:text-gray-300 mb-6 md:mb-12 max-w-2xl mx-auto transition-all duration-700"
|
||||||
:style="getDescStyle(SECTIONS.FEATURES)"
|
:style="getDescStyle(SECTIONS.FEATURES)"
|
||||||
>
|
>
|
||||||
核心 API 代理功能已完成,正在载入更多功能
|
核心 API 代理功能已完成,正在载入更多功能
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="grid md:grid-cols-3 gap-6">
|
<div class="grid md:grid-cols-3 gap-3 md:gap-6">
|
||||||
<div
|
<div
|
||||||
v-for="(feature, idx) in featureCards"
|
v-for="(feature, idx) in featureCards"
|
||||||
:key="idx"
|
: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"
|
class="bg-white/70 dark:bg-[#262624]/80 backdrop-blur-sm rounded-xl md:rounded-2xl p-4 md: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)"
|
:style="getFeatureCardStyle(SECTIONS.FEATURES, idx)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex h-12 w-12 items-center justify-center rounded-xl mb-4 mx-auto"
|
class="flex h-10 w-10 md:h-12 md:w-12 items-center justify-center rounded-lg md:rounded-xl mb-2 md:mb-4 mx-auto"
|
||||||
:class="feature.status === 'completed'
|
:class="feature.status === 'completed'
|
||||||
? 'bg-emerald-500/10 dark:bg-emerald-500/15'
|
? 'bg-emerald-500/10 dark:bg-emerald-500/15'
|
||||||
: 'bg-[#cc785c]/10 dark:bg-[#cc785c]/15'"
|
: 'bg-[#cc785c]/10 dark:bg-[#cc785c]/15'"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="feature.icon"
|
:is="feature.icon"
|
||||||
class="h-6 w-6"
|
class="h-5 w-5 md:h-6 md:w-6"
|
||||||
:class="feature.status === 'completed'
|
:class="feature.status === 'completed'
|
||||||
? 'text-emerald-500 dark:text-emerald-400'
|
? 'text-emerald-500 dark:text-emerald-400'
|
||||||
: 'text-[#cc785c] dark:text-[#d4a27f] animate-spin'"
|
: 'text-[#cc785c] dark:text-[#d4a27f] animate-spin'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-bold text-[#191919] dark:text-white mb-2">
|
<h3 class="text-base md:text-lg font-bold text-[#191919] dark:text-white mb-1 md:mb-2">
|
||||||
{{ feature.title }}
|
{{ feature.title }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-[#666663] dark:text-[#c9c3b4]">
|
<p class="text-xs md:text-sm text-[#666663] dark:text-[#c9c3b4]">
|
||||||
{{ feature.desc }}
|
{{ feature.desc }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
class="mt-3 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium"
|
class="mt-2 md:mt-3 inline-flex items-center gap-1.5 px-2 md:px-2.5 py-0.5 md:py-1 rounded-full text-xs font-medium"
|
||||||
:class="feature.status === 'completed'
|
:class="feature.status === 'completed'
|
||||||
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||||
: 'bg-amber-500/10 text-amber-600 dark:text-amber-400'"
|
: 'bg-amber-500/10 text-amber-600 dark:text-amber-400'"
|
||||||
@@ -325,7 +332,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="mt-12 transition-all duration-700"
|
class="mt-6 md:mt-12 transition-all duration-700"
|
||||||
:style="getButtonsStyle(SECTIONS.FEATURES)"
|
:style="getButtonsStyle(SECTIONS.FEATURES)"
|
||||||
>
|
>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
@@ -452,6 +459,9 @@ const { logoTransitionName } = useLogoTransition(currentSection, previousSection
|
|||||||
// Logo computed
|
// Logo computed
|
||||||
const currentLogoType = computed(() => getLogoType(currentSection.value))
|
const currentLogoType = computed(() => getLogoType(currentSection.value))
|
||||||
const currentLogoClass = computed(() => getLogoClass(currentSection.value))
|
const currentLogoClass = computed(() => getLogoClass(currentSection.value))
|
||||||
|
|
||||||
|
// Responsive logo size - matches .logo-container.home-section CSS
|
||||||
|
const homeLogoSize = computed(() => windowWidth.value < 768 ? 280 : 400)
|
||||||
const logoTransitionDelay = computed(() => {
|
const logoTransitionDelay = computed(() => {
|
||||||
if (currentSection.value === SECTIONS.FEATURES) return 0
|
if (currentSection.value === SECTIONS.FEATURES) return 0
|
||||||
if (previousSection.value === SECTIONS.FEATURES) return 200
|
if (previousSection.value === SECTIONS.FEATURES) return 200
|
||||||
@@ -475,6 +485,57 @@ const { claudeConfig, codexConfig, codexAuthConfig, geminiEnvConfig, geminiSetti
|
|||||||
// Dialog state
|
// Dialog state
|
||||||
const showLoginDialog = ref(false)
|
const showLoginDialog = ref(false)
|
||||||
|
|
||||||
|
// Typewriter effect for "Aether"
|
||||||
|
const aetherText = ref('')
|
||||||
|
const showCursor = ref(true)
|
||||||
|
const typewriterFullText = 'Aether'
|
||||||
|
let typewriterTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let hasTypewriterStarted = ref(false)
|
||||||
|
|
||||||
|
const startTypewriter = () => {
|
||||||
|
if (hasTypewriterStarted.value) return
|
||||||
|
hasTypewriterStarted.value = true
|
||||||
|
aetherText.value = ''
|
||||||
|
showCursor.value = true
|
||||||
|
|
||||||
|
const typeSpeed = 200
|
||||||
|
const deleteSpeed = 120
|
||||||
|
const pauseAfterType = 3500
|
||||||
|
const pauseAfterDelete = 1000
|
||||||
|
|
||||||
|
const typeLoop = () => {
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
// Type phase
|
||||||
|
const typeNextChar = () => {
|
||||||
|
if (index < typewriterFullText.length) {
|
||||||
|
aetherText.value = typewriterFullText.slice(0, index + 1)
|
||||||
|
index++
|
||||||
|
typewriterTimer = setTimeout(typeNextChar, typeSpeed)
|
||||||
|
} else {
|
||||||
|
// Pause then start deleting
|
||||||
|
typewriterTimer = setTimeout(deleteChars, pauseAfterType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete phase
|
||||||
|
const deleteChars = () => {
|
||||||
|
if (aetherText.value.length > 0) {
|
||||||
|
aetherText.value = aetherText.value.slice(0, -1)
|
||||||
|
typewriterTimer = setTimeout(deleteChars, deleteSpeed)
|
||||||
|
} else {
|
||||||
|
// Pause then restart typing
|
||||||
|
typewriterTimer = setTimeout(typeLoop, pauseAfterDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeNextChar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start typing after a short delay
|
||||||
|
typewriterTimer = setTimeout(typeLoop, 400)
|
||||||
|
}
|
||||||
|
|
||||||
// Scroll handling
|
// Scroll handling
|
||||||
let scrollEndTimer: ReturnType<typeof setTimeout> | null = null
|
let scrollEndTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
@@ -525,6 +586,7 @@ const handleScroll = () => {
|
|||||||
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
|
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
|
||||||
hasLogoAnimationStarted.value = true
|
hasLogoAnimationStarted.value = true
|
||||||
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
|
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
|
||||||
|
startTypewriter()
|
||||||
}
|
}
|
||||||
}, 150)
|
}, 150)
|
||||||
}
|
}
|
||||||
@@ -560,6 +622,7 @@ onMounted(() => {
|
|||||||
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
|
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
|
||||||
hasLogoAnimationStarted.value = true
|
hasLogoAnimationStarted.value = true
|
||||||
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
|
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
|
||||||
|
startTypewriter()
|
||||||
}
|
}
|
||||||
}, 300)
|
}, 300)
|
||||||
})
|
})
|
||||||
@@ -568,6 +631,7 @@ onUnmounted(() => {
|
|||||||
scrollContainer.value?.removeEventListener('scroll', handleScroll)
|
scrollContainer.value?.removeEventListener('scroll', handleScroll)
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
if (scrollEndTimer) clearTimeout(scrollEndTimer)
|
if (scrollEndTimer) clearTimeout(scrollEndTimer)
|
||||||
|
if (typewriterTimer) clearTimeout(typewriterTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -777,4 +841,42 @@ h1, h2, p {
|
|||||||
28% { transform: scale(1); }
|
28% { transform: scale(1); }
|
||||||
42% { transform: scale(1.1); }
|
42% { transform: scale(1.1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Gemini star cluster positioning */
|
||||||
|
.gemini-stars {
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade transition */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typewriter cursor */
|
||||||
|
.typewriter {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter .cursor {
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 1;
|
||||||
|
animation: cursor-blink 1s ease-in-out infinite;
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter .cursor.cursor-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cursor-blink {
|
||||||
|
0%, 45% { opacity: 1; }
|
||||||
|
50%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -157,14 +157,18 @@ export function useLogoPosition(
|
|||||||
let opacity = 1
|
let opacity = 1
|
||||||
|
|
||||||
if (section === SECTIONS.HOME) {
|
if (section === SECTIONS.HOME) {
|
||||||
transform = 'scale(1.1) translateY(-18vh)'
|
transform = 'scale(1.1) translateY(-12vh)'
|
||||||
opacity = 0.25
|
opacity = 0.25
|
||||||
} else if (section === SECTIONS.CLAUDE) {
|
} else if (section === SECTIONS.CLAUDE) {
|
||||||
transform = isDesktop ? 'translateX(-25vw) scale(1)' : 'translateY(-20vh) scale(0.8)'
|
// Mobile: move logo higher and reduce opacity to avoid blocking content
|
||||||
|
transform = isDesktop ? 'translateX(-25vw) scale(1)' : 'translateY(-32vh) scale(0.6)'
|
||||||
|
opacity = isDesktop ? 1 : 0.2
|
||||||
} else if (section === SECTIONS.CODEX) {
|
} else if (section === SECTIONS.CODEX) {
|
||||||
transform = isDesktop ? 'translateX(25vw) scale(1)' : 'translateY(-20vh) scale(0.8)'
|
transform = isDesktop ? 'translateX(25vw) scale(1)' : 'translateY(-32vh) scale(0.6)'
|
||||||
|
opacity = isDesktop ? 1 : 0.2
|
||||||
} else if (section === SECTIONS.GEMINI) {
|
} else if (section === SECTIONS.GEMINI) {
|
||||||
transform = isDesktop ? 'translateX(-25vw) scale(1)' : 'translateY(-20vh) scale(0.8)'
|
transform = isDesktop ? 'translateX(-25vw) scale(1)' : 'translateY(-32vh) scale(0.6)'
|
||||||
|
opacity = isDesktop ? 1 : 0.2
|
||||||
} else {
|
} else {
|
||||||
transform = isDesktop ? 'translateX(0) scale(1)' : 'translateY(-20vh) scale(0.8)'
|
transform = isDesktop ? 'translateX(0) scale(1)' : 'translateY(-20vh) scale(0.8)'
|
||||||
opacity = 0.15
|
opacity = 0.15
|
||||||
|
|||||||
Reference in New Issue
Block a user