f4684f694e
✨ 新增功能: - Work 和 News 页面实现筛选功能 - Contact 页面联系表单提交功能(加载状态、成功/失败反馈) - Newsletter 订阅功能(邮箱验证、状态反馈) - 移动端汉堡菜单功能(全屏菜单、动画效果) - 用户登录/注册系统(支持账号/邮箱/手机号三合一) - 用户状态管理(Pinia store + localStorage 持久化) - 登录和注册弹窗组件(表单验证、错误提示) - 用户头像和下拉菜单(桌面端和移动端) 🎨 优化改进: - Footer 显示问题修复(多重备用机制) - 联系信息更新(邮箱改为 chenchun@virtheart.com,手机改为微信) - 完整的国际化支持(中英文双语) - 响应式设计优化 🔧 技术实现: - GSAP 动画系统 - 表单验证和反馈 - 状态持久化 - 自动生成用户头像 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
586 lines
18 KiB
Vue
Executable File
586 lines
18 KiB
Vue
Executable File
<script setup lang="ts">
|
||
import { onBeforeUnmount, onMounted, ref, computed, watch } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { useI18n } from 'vue-i18n'
|
||
import gsap from 'gsap'
|
||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||
import { useAppStore } from '~/stores/app'
|
||
import { useUserStore } from '~/stores/user'
|
||
import AppLogo from '~/components/common/AppLogo.vue'
|
||
import LoginModal from '~/components/common/LoginModal.vue'
|
||
import RegisterModal from '~/components/common/RegisterModal.vue'
|
||
|
||
gsap.registerPlugin(ScrollTrigger)
|
||
|
||
const { t } = useI18n()
|
||
const route = useRoute()
|
||
const appStore = useAppStore()
|
||
const userStore = useUserStore()
|
||
|
||
// 恢复用户状态
|
||
onMounted(() => {
|
||
userStore.restoreUser()
|
||
})
|
||
|
||
// 导航菜单数据配置
|
||
const navItems = computed<Array<{ name: string, path: string }>>(() => [
|
||
{ name: t('nav.home'), path: '/' },
|
||
{ name: t('nav.studio'), path: '/studio' },
|
||
{ name: t('nav.work'), path: '/work' },
|
||
{ name: t('nav.news'), path: '/news' },
|
||
{ name: t('nav.tools'), path: '/tools' },
|
||
{ name: t('nav.contact'), path: '/contact' }
|
||
])
|
||
|
||
const topHeaderRef = ref<HTMLElement | null>(null)
|
||
const bottomHeaderRef = ref<HTMLElement | null>(null)
|
||
const mobileMenuRef = ref<HTMLElement | null>(null)
|
||
const isMobileMenuOpen = ref(false)
|
||
const isLoginModalOpen = ref(false)
|
||
const isRegisterModalOpen = ref(false)
|
||
const isUserMenuOpen = ref(false)
|
||
let topHeaderTween: gsap.core.Tween | null = null
|
||
let bottomHeaderScrollTrigger: ReturnType<typeof ScrollTrigger.create> | null = null
|
||
let isBottomHeaderVisible = false
|
||
let fallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
// 打开登录弹窗
|
||
const openLoginModal = () => {
|
||
isLoginModalOpen.value = true
|
||
isRegisterModalOpen.value = false
|
||
}
|
||
|
||
// 关闭登录弹窗
|
||
const closeLoginModal = () => {
|
||
isLoginModalOpen.value = false
|
||
}
|
||
|
||
// 打开注册弹窗
|
||
const openRegisterModal = () => {
|
||
isRegisterModalOpen.value = true
|
||
isLoginModalOpen.value = false
|
||
}
|
||
|
||
// 关闭注册弹窗
|
||
const closeRegisterModal = () => {
|
||
isRegisterModalOpen.value = false
|
||
}
|
||
|
||
// 从登录切换到注册
|
||
const switchToRegister = () => {
|
||
isLoginModalOpen.value = false
|
||
isRegisterModalOpen.value = true
|
||
}
|
||
|
||
// 从注册切换到登录
|
||
const switchToLogin = () => {
|
||
isRegisterModalOpen.value = false
|
||
isLoginModalOpen.value = true
|
||
}
|
||
|
||
// 切换用户菜单
|
||
const toggleUserMenu = () => {
|
||
isUserMenuOpen.value = !isUserMenuOpen.value
|
||
}
|
||
|
||
// 关闭用户菜单
|
||
const closeUserMenu = () => {
|
||
isUserMenuOpen.value = false
|
||
}
|
||
|
||
// 处理登出
|
||
const handleLogout = () => {
|
||
userStore.logout()
|
||
closeUserMenu()
|
||
}
|
||
|
||
const setBottomHeaderVisible = (visible: boolean) => {
|
||
if (!bottomHeaderRef.value || isBottomHeaderVisible === visible) {
|
||
return
|
||
}
|
||
|
||
isBottomHeaderVisible = visible
|
||
|
||
gsap.to(bottomHeaderRef.value, {
|
||
yPercent: visible ? 0 : 150,
|
||
autoAlpha: visible ? 1 : 0,
|
||
duration: 0.45,
|
||
ease: 'power2.out',
|
||
overwrite: 'auto',
|
||
force3D: true,
|
||
})
|
||
}
|
||
|
||
// 定义一个供外部或延迟调用的入场动画方法
|
||
const playHeaderIntro = () => {
|
||
if (!topHeaderRef.value) return
|
||
|
||
// 如果已经触发过,清理可能存在的保底定时器
|
||
if (fallbackTimer) {
|
||
clearTimeout(fallbackTimer)
|
||
fallbackTimer = null
|
||
}
|
||
|
||
gsap.fromTo(topHeaderRef.value,
|
||
{
|
||
y: '100vh',
|
||
opacity: 0 // 恢复初始透明度为0,让它在屏幕最底部时是隐形的
|
||
},
|
||
{
|
||
y: 0,
|
||
opacity: 1, // 伴随飞入的过程逐渐完全显示
|
||
duration: 1.4,
|
||
ease: 'power4.out',
|
||
delay: 0,
|
||
overwrite: 'auto',
|
||
clearProps: 'transform',
|
||
onComplete: () => {
|
||
// 动画完成时,强制把 style 的 opacity 移除,防止被内联样式锁死
|
||
if (topHeaderRef.value) {
|
||
topHeaderRef.value.style.opacity = ''
|
||
}
|
||
// 进场动画完毕后,再将滚动消失动画挂载到 ScrollTrigger
|
||
if (topHeaderTween) {
|
||
topHeaderTween.scrollTrigger?.kill()
|
||
ScrollTrigger.create({
|
||
animation: topHeaderTween,
|
||
start: 0,
|
||
end: 200,
|
||
scrub: true,
|
||
})
|
||
ScrollTrigger.refresh()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 监听自定义事件,当首页的 Hero 动画完成或到达特定阶段时触发导航栏入场
|
||
const handleHeroReady = () => {
|
||
playHeaderIntro()
|
||
}
|
||
|
||
const cleanupHeroReadyHook = () => {
|
||
if (fallbackTimer) {
|
||
clearTimeout(fallbackTimer)
|
||
fallbackTimer = null
|
||
}
|
||
if (typeof window !== 'undefined') {
|
||
window.removeEventListener('hero-intro-done', handleHeroReady)
|
||
}
|
||
}
|
||
|
||
const hideTopHeader = () => {
|
||
if (!topHeaderRef.value) return
|
||
gsap.set(topHeaderRef.value, { y: '100vh', opacity: 0 })
|
||
}
|
||
|
||
const showTopHeaderInstant = () => {
|
||
if (!topHeaderRef.value) return
|
||
|
||
gsap.set(topHeaderRef.value, { y: 0, opacity: 1 })
|
||
topHeaderRef.value.style.opacity = ''
|
||
|
||
if (topHeaderTween) {
|
||
topHeaderTween.scrollTrigger?.kill()
|
||
ScrollTrigger.create({
|
||
animation: topHeaderTween,
|
||
start: 0,
|
||
end: 200,
|
||
scrub: true,
|
||
})
|
||
ScrollTrigger.refresh()
|
||
}
|
||
}
|
||
|
||
// 切换移动端菜单
|
||
const toggleMobileMenu = () => {
|
||
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||
|
||
if (!mobileMenuRef.value) return
|
||
|
||
if (isMobileMenuOpen.value) {
|
||
// 打开菜单
|
||
gsap.set(mobileMenuRef.value, { display: 'flex' })
|
||
gsap.fromTo(mobileMenuRef.value,
|
||
{ opacity: 0, backdropFilter: 'blur(0px)' },
|
||
{ opacity: 1, backdropFilter: 'blur(12px)', duration: 0.3, ease: 'power2.out' }
|
||
)
|
||
|
||
// 菜单项依次出现
|
||
const menuItems = gsap.utils.toArray<HTMLElement>('.mobile-menu-item', mobileMenuRef.value)
|
||
gsap.fromTo(menuItems,
|
||
{ y: 30, opacity: 0 },
|
||
{ y: 0, opacity: 1, duration: 0.4, stagger: 0.05, ease: 'power3.out', delay: 0.1 }
|
||
)
|
||
|
||
// 禁止背景滚动
|
||
document.body.style.overflow = 'hidden'
|
||
} else {
|
||
// 关闭菜单
|
||
gsap.to(mobileMenuRef.value,
|
||
{
|
||
opacity: 0,
|
||
backdropFilter: 'blur(0px)',
|
||
duration: 0.25,
|
||
ease: 'power2.in',
|
||
onComplete: () => {
|
||
if (mobileMenuRef.value) {
|
||
gsap.set(mobileMenuRef.value, { display: 'none' })
|
||
}
|
||
}
|
||
}
|
||
)
|
||
|
||
// 恢复滚动
|
||
document.body.style.overflow = ''
|
||
}
|
||
}
|
||
|
||
// 关闭移动端菜单
|
||
const closeMobileMenu = () => {
|
||
if (isMobileMenuOpen.value) {
|
||
toggleMobileMenu()
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 顶部导航在页面刚开始滚动的前 200px 内逐渐上移并隐藏。
|
||
topHeaderTween = gsap.to(topHeaderRef.value, {
|
||
yPercent: -140,
|
||
autoAlpha: 0,
|
||
ease: 'none',
|
||
paused: true, // 初始不执行,等进场动画完毕再绑定到 ScrollTrigger
|
||
})
|
||
|
||
// 底部导航默认藏在视口下方,只有滚动到深处并向上滚时才出现。
|
||
gsap.set(bottomHeaderRef.value, {
|
||
xPercent: -50,
|
||
yPercent: 150,
|
||
autoAlpha: 0,
|
||
force3D: true
|
||
})
|
||
|
||
bottomHeaderScrollTrigger = ScrollTrigger.create({
|
||
start: 0,
|
||
end: 'max',
|
||
onUpdate: (self) => {
|
||
const scrollY = self.scroll()
|
||
|
||
// 深度超过 1000px 后向上滚,显示底部导航。
|
||
if (scrollY > 1000 && self.direction === -1) {
|
||
setBottomHeaderVisible(true)
|
||
return
|
||
}
|
||
|
||
// 回到 1000px 以内,或继续向下滚时,底部导航收回。
|
||
if (scrollY <= 1000 || self.direction === 1) {
|
||
setBottomHeaderVisible(false)
|
||
}
|
||
},
|
||
})
|
||
|
||
ScrollTrigger.refresh()
|
||
|
||
if (!appStore.isAppLoading && route.path !== '/') {
|
||
showTopHeaderInstant()
|
||
}
|
||
})
|
||
|
||
watch([() => appStore.isAppLoading, () => route.path], ([isLoading, path]) => {
|
||
if (isLoading || typeof window === 'undefined') return
|
||
|
||
cleanupHeroReadyHook()
|
||
|
||
if (path === '/') {
|
||
hideTopHeader()
|
||
|
||
window.addEventListener('hero-intro-done', handleHeroReady, { once: true })
|
||
|
||
fallbackTimer = setTimeout(() => {
|
||
window.removeEventListener('hero-intro-done', handleHeroReady)
|
||
playHeaderIntro()
|
||
}, 5000)
|
||
return
|
||
}
|
||
|
||
showTopHeaderInstant()
|
||
}, { immediate: true })
|
||
|
||
onBeforeUnmount(() => {
|
||
cleanupHeroReadyHook()
|
||
topHeaderTween?.scrollTrigger?.kill()
|
||
topHeaderTween?.kill()
|
||
bottomHeaderScrollTrigger?.kill()
|
||
topHeaderTween = null
|
||
bottomHeaderScrollTrigger = null
|
||
isBottomHeaderVisible = false
|
||
if (topHeaderRef.value && bottomHeaderRef.value) {
|
||
gsap.killTweensOf([topHeaderRef.value, bottomHeaderRef.value])
|
||
}
|
||
// 清理移动端菜单
|
||
if (isMobileMenuOpen.value) {
|
||
document.body.style.overflow = ''
|
||
}
|
||
})
|
||
|
||
// 监听路由变化,关闭移动端菜单
|
||
watch(() => route.path, () => {
|
||
closeMobileMenu()
|
||
closeUserMenu()
|
||
})
|
||
|
||
// 点击外部关闭用户菜单
|
||
if (typeof window !== 'undefined') {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
const target = event.target as HTMLElement
|
||
if (isUserMenuOpen.value && !target.closest('.relative')) {
|
||
closeUserMenu()
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
document.addEventListener('click', handleClickOutside)
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
document.removeEventListener('click', handleClickOutside)
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<header ref="topHeaderRef"
|
||
class="site-header-top fixed left-0 right-0 top-0 z-50 mx-auto flex h-[100px] w-page-container max-w-[calc(100%-2rem)] items-center justify-between px-4 py-5"
|
||
style="opacity: 0; transform: translateY(100vh);">
|
||
<!-- 左侧:Logo 和 标题 -->
|
||
<AppLogo />
|
||
|
||
<!-- 中间:导航链接 -->
|
||
<nav class="hidden md:flex items-center gap-10 lg:gap-16">
|
||
<NuxtLink v-for="item in navItems" :key="item.name" :to="item.path"
|
||
class="nav-link-item group flex items-center overflow-hidden font-semibold tracking-[-0.7px] text-black"
|
||
style="font-size: 1rem; justify-content: flex-start; position: relative;">
|
||
<div
|
||
class="flex items-center transition-transform duration-300 group-hover:translate-x-0 -translate-x-[18px] pr-[18px]">
|
||
<span class="mr-2 h-2.5 w-2.5 rounded-full bg-black shrink-0"></span>
|
||
<span>{{ item.name }}</span>
|
||
</div>
|
||
</NuxtLink>
|
||
</nav>
|
||
|
||
<!-- 右侧:登录按钮 / 用户信息(桌面端) -->
|
||
<div class="hidden md:flex items-center gap-4">
|
||
<!-- 未登录:显示登录按钮 -->
|
||
<button
|
||
v-if="!userStore.isAuthenticated"
|
||
@click="openLoginModal"
|
||
class="px-5 py-2.5 rounded-full bg-black text-white text-sm font-semibold hover:bg-gray-800 transition-all duration-300 shadow-md hover:shadow-lg"
|
||
>
|
||
{{ t('auth.login') }}
|
||
</button>
|
||
|
||
<!-- 已登录:显示用户头像和菜单 -->
|
||
<div v-else class="relative">
|
||
<button
|
||
@click="toggleUserMenu"
|
||
class="flex items-center gap-3 px-4 py-2 rounded-full hover:bg-gray-100 transition-all duration-300"
|
||
>
|
||
<img
|
||
:src="userStore.user?.avatar"
|
||
:alt="userStore.user?.name"
|
||
class="w-9 h-9 rounded-full object-cover border-2 border-gray-200"
|
||
/>
|
||
<span class="text-sm font-semibold text-black">{{ userStore.user?.name }}</span>
|
||
<Icon
|
||
name="ph:caret-down"
|
||
class="w-4 h-4 text-gray-600 transition-transform duration-300"
|
||
:class="{ 'rotate-180': isUserMenuOpen }"
|
||
/>
|
||
</button>
|
||
|
||
<!-- 用户下拉菜单 -->
|
||
<Transition name="dropdown">
|
||
<div
|
||
v-if="isUserMenuOpen"
|
||
class="absolute top-full right-0 mt-2 w-48 bg-white rounded-xl shadow-xl border border-gray-100 py-2 z-50"
|
||
@click="closeUserMenu"
|
||
>
|
||
<div class="px-4 py-3 border-b border-gray-100">
|
||
<p class="text-sm font-semibold text-black">{{ userStore.user?.name }}</p>
|
||
<p class="text-xs text-gray-500 truncate">{{ userStore.user?.email }}</p>
|
||
</div>
|
||
<button
|
||
@click="handleLogout"
|
||
class="w-full px-4 py-2.5 text-left text-sm font-medium text-red-600 hover:bg-red-50 transition-colors duration-200 flex items-center gap-2"
|
||
>
|
||
<Icon name="ph:sign-out" class="w-4 h-4" />
|
||
{{ t('auth.logout') }}
|
||
</button>
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:菜单图标(移动端) -->
|
||
<button
|
||
@click="toggleMobileMenu"
|
||
class="menu-icon group flex h-12 w-12 cursor-pointer flex-col items-end justify-center gap-[8px] md:hidden relative z-50"
|
||
:class="{ 'menu-open': isMobileMenuOpen }"
|
||
aria-label="Toggle menu"
|
||
>
|
||
<div
|
||
class="h-[2px] w-10 bg-black transition-all duration-300"
|
||
:class="isMobileMenuOpen ? 'rotate-45 translate-y-[5px]' : 'group-hover:translate-y-[2.5px]'"
|
||
></div>
|
||
<div
|
||
class="h-[2px] w-10 bg-black transition-all duration-300"
|
||
:class="isMobileMenuOpen ? '-rotate-45 -translate-y-[5px]' : 'group-hover:-translate-y-[2.5px]'"
|
||
></div>
|
||
</button>
|
||
</header>
|
||
|
||
<!-- 移动端全屏菜单 -->
|
||
<div
|
||
ref="mobileMenuRef"
|
||
class="mobile-menu fixed inset-0 z-40 hidden md:hidden flex-col items-center justify-center bg-white/95 backdrop-blur-md"
|
||
style="opacity: 0;"
|
||
@click.self="closeMobileMenu"
|
||
>
|
||
<nav class="flex flex-col items-center gap-8 px-8">
|
||
<NuxtLink
|
||
v-for="item in navItems"
|
||
:key="'mobile-' + item.name"
|
||
:to="item.path"
|
||
class="mobile-menu-item text-3xl font-bold tracking-tight text-black hover:text-gray-600 transition-colors duration-300"
|
||
>
|
||
{{ item.name }}
|
||
</NuxtLink>
|
||
|
||
<!-- 移动端登录/用户信息 -->
|
||
<div class="mobile-menu-item mt-8 pt-8 border-t border-gray-200 w-full flex flex-col items-center gap-4">
|
||
<!-- 未登录 -->
|
||
<button
|
||
v-if="!userStore.isAuthenticated"
|
||
@click="openLoginModal"
|
||
class="px-8 py-3 rounded-full bg-black text-white text-base font-semibold hover:bg-gray-800 transition-all duration-300"
|
||
>
|
||
{{ t('auth.login') }}
|
||
</button>
|
||
|
||
<!-- 已登录 -->
|
||
<div v-else class="flex flex-col items-center gap-4">
|
||
<div class="flex items-center gap-3">
|
||
<img
|
||
:src="userStore.user?.avatar"
|
||
:alt="userStore.user?.name"
|
||
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200"
|
||
/>
|
||
<div class="text-left">
|
||
<p class="text-lg font-bold text-black">{{ userStore.user?.name }}</p>
|
||
<p class="text-sm text-gray-500">{{ userStore.user?.email }}</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
@click="handleLogout"
|
||
class="px-6 py-2.5 rounded-full border-2 border-red-600 text-red-600 text-sm font-semibold hover:bg-red-50 transition-all duration-300 flex items-center gap-2"
|
||
>
|
||
<Icon name="ph:sign-out" class="w-4 h-4" />
|
||
{{ t('auth.logout') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
</div>
|
||
|
||
<!-- 登录弹窗 -->
|
||
<LoginModal
|
||
:is-open="isLoginModalOpen"
|
||
@close="closeLoginModal"
|
||
@switch-to-register="switchToRegister"
|
||
/>
|
||
|
||
<!-- 注册弹窗 -->
|
||
<RegisterModal
|
||
:is-open="isRegisterModalOpen"
|
||
@close="closeRegisterModal"
|
||
@switch-to-login="switchToLogin"
|
||
/>
|
||
|
||
<header ref="bottomHeaderRef" class="site-header-bottom z-50 bg-white/80 backdrop-blur-md">
|
||
<!-- 左侧菜单 (前两个菜单项) -->
|
||
<nav class="flex items-center justify-evenly md:justify-end gap-3 md:gap-4 w-full">
|
||
<NuxtLink v-for="item in navItems.slice(0, 2)" :key="'bottom-' + item.name" :to="item.path"
|
||
class="group flex items-center font-semibold tracking-[-0.7px] text-black text-sm md:text-base whitespace-nowrap md:overflow-hidden"
|
||
style="justify-content: flex-start; position: relative;">
|
||
<div
|
||
class="flex items-center transition-transform duration-300 md:group-hover:translate-x-0 md:-translate-x-[18px] md:pr-[18px]">
|
||
<span class="hidden md:block mr-1 md:mr-2 h-1.5 w-1.5 md:h-2.5 md:w-2.5 rounded-full bg-black shrink-0"></span>
|
||
<span>{{ item.name }}</span>
|
||
</div>
|
||
</NuxtLink>
|
||
</nav>
|
||
|
||
<!-- 中间 Logo (组件内部自带跳转和动画) -->
|
||
<div class="flex items-center justify-center px-1 md:px-6">
|
||
<AppLogo compact click-action="top" />
|
||
</div>
|
||
|
||
<!-- 右侧菜单 (去除左侧两个和最后contact菜单后的所有菜单) -->
|
||
<nav class="flex items-center justify-evenly md:justify-start gap-3 md:gap-4 w-full">
|
||
<NuxtLink v-for="item in navItems.slice(2, navItems.length - 1)" :key="'bottom-' + item.name" :to="item.path"
|
||
class="group flex items-center font-semibold tracking-[-0.7px] text-black text-sm md:text-base whitespace-nowrap md:overflow-hidden"
|
||
style="justify-content: flex-start; position: relative;">
|
||
<div
|
||
class="flex items-center transition-transform duration-300 md:group-hover:translate-x-0 md:-translate-x-[18px] md:pr-[18px]">
|
||
<span class="hidden md:block mr-1 md:mr-2 h-1.5 w-1.5 md:h-2.5 md:w-2.5 rounded-full bg-black shrink-0"></span>
|
||
<span>{{ item.name }}</span>
|
||
</div>
|
||
</NuxtLink>
|
||
</nav>
|
||
</header>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.site-header-bottom {
|
||
border: 1px solid #ffffff47;
|
||
border-radius: 12px;
|
||
grid-template-rows: auto;
|
||
/* 移动端恢复三列,靠 flex 让菜单均匀铺开 */
|
||
grid-template-columns: 1fr auto 1fr;
|
||
grid-auto-columns: 1fr;
|
||
place-items: center;
|
||
width: calc(100% - 1rem);
|
||
max-width: 570px;
|
||
padding: 10px 10px;
|
||
display: grid;
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 15px;
|
||
overflow: hidden;
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
box-shadow: 0 -1px #0000001f, 0 12px 30px #0000001a, inset 0 1px #ffffff96;
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
.site-header-bottom {
|
||
grid-template-columns: auto minmax(50px, auto) auto;
|
||
gap: 0;
|
||
width: fit-content;
|
||
padding: 15px 20px;
|
||
}
|
||
}
|
||
|
||
/* 用户下拉菜单动画 */
|
||
.dropdown-enter-active,
|
||
.dropdown-leave-active {
|
||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||
}
|
||
|
||
.dropdown-enter-from,
|
||
.dropdown-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
</style>
|