🎯 Header 头部布局
📋 需求分析
- 左侧区域-放置网站 Logo,点击可返回首页
- 右侧区域-功能区域,包含以下组件-
- 🔍 全局搜索框-支持内容快速检索
- 👤 用户认证-登录/注册按钮或用户头像
- 🌓 主题切换-明暗模式切换按钮
- 📱 移动菜单-移动端的汉堡菜单
- 响应式设计-适配 PC 端和移动端不同布局
- 交互体验-流畅的动画效果和用户反馈
准备工作-提前准备好 Logo、用户头像占位图和网站 favicon.ico 图标
🏗️ Layouts 布局基础
Nuxt 提供了一个布局框架,用于将常见的 UI 模式提取为可重用的布局。
1. 启用布局系统
在 app.vue 中添加 <NuxtLayout>-
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>2. 安装必要依赖
pnpm add @nuxt/image nuxt-icons -D在 nuxt.config.ts 文件中启用-
export default defineNuxtConfig({
modules: ['@nuxt/image', 'nuxt-icons']
})🔍 全局搜索组件
创建 components/AppSearch.vue
<script setup lang="ts">
const searchQuery = ref('')
const isSearchOpen = ref(false)
// 搜索功能
const handleSearch = () => {
if (searchQuery.value.trim()) {
// 执行搜索逻辑
navigateTo(`/search?q=${encodeURIComponent(searchQuery.value)}`)
}
}
// 键盘快捷键支持
const handleKeydown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
event.preventDefault()
isSearchOpen.value = !isSearchOpen.value
}
}
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
</script>
<template>
<div class="relative">
<!-- 搜索触发按钮 -->
<UButton
icon="i-heroicons-magnifying-glass"
size="sm"
variant="ghost"
class="text-gray-700 dark:text-gray-200"
@click="isSearchOpen = !isSearchOpen"
/>
<!-- 搜索框 -->
<div v-if="isSearchOpen" class="absolute right-0 top-full mt-2 w-80 z-50">
<UInput
v-model="searchQuery"
placeholder="搜索内容... (Ctrl+K)"
icon="i-heroicons-magnifying-glass"
size="md"
class="w-full"
@keyup.enter="handleSearch"
@keyup.escape="isSearchOpen = false"
/>
</div>
</div>
</template>👤 用户认证组件
创建 components/AppAuth.vue
<script setup lang="ts">
// 模拟用户状态
const user = ref(null) // 实际项目中从状态管理获取
const isLoggedIn = computed(() => !!user.value)
// 登录/注册弹窗状态
const showAuthModal = ref(false)
const authMode = ref<'login' | 'register'>('login')
const handleLogin = () => {
authMode.value = 'login'
showAuthModal.value = true
}
const handleRegister = () => {
authMode.value = 'register'
showAuthModal.value = true
}
const handleLogout = () => {
user.value = null
// 执行登出逻辑
}
</script>
<template>
<div class="flex items-center gap-2">
<!-- 未登录状态 -->
<template v-if="!isLoggedIn">
<UButton
size="sm"
variant="ghost"
@click="handleLogin"
>
登录
</UButton>
<UButton
size="sm"
color="primary"
@click="handleRegister"
>
注册
</UButton>
</template>
<!-- 已登录状态 -->
<UDropdown v-else>
<UAvatar
:src="user?.avatar || '/default-avatar.png'"
:alt="user?.name || '用户'"
size="sm"
class="cursor-pointer"
/>
<template #content>
<UDropdownItem
icon="i-heroicons-user"
label="个人资料"
@click="navigateTo('/profile')"
/>
<UDropdownItem
icon="i-heroicons-cog-6-tooth"
label="设置"
@click="navigateTo('/settings')"
/>
<UDropdownItem
icon="i-heroicons-arrow-right-on-rectangle"
label="退出登录"
@click="handleLogout"
/>
</template>
</UDropdown>
</div>
</template>🌓 主题切换组件
创建 components/AppColorMode.vue
<script setup lang="ts">
const colorMode = useColorMode()
function toggleDark() {
colorMode.value = colorMode.value === 'dark' ? 'light' : 'dark'
}
</script>
<template>
<UTooltip :text="`切换${$colorMode.value === 'dark' ? '白天' : '黑夜'}模式`">
<UButton
:icon="$colorMode.value === 'dark' ? 'i-heroicons-moon-solid' : 'i-heroicons-sun-solid'"
size="sm"
variant="ghost"
class="text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800"
@click="toggleDark"
/>
</UTooltip>
</template>📱 移动端菜单组件
创建 components/AppMobileMenu.vue
<script setup lang="ts">
const isMenuOpen = ref(false)
const menuItems = [
{ label: '首页', to: '/', icon: 'i-heroicons-home' },
{ label: '文章', to: '/blog', icon: 'i-heroicons-document-text' },
{ label: '项目', to: '/projects', icon: 'i-heroicons-code-bracket' },
{ label: '关于', to: '/about', icon: 'i-heroicons-user' }
]
</script>
<template>
<div class="md:hidden">
<!-- 汉堡菜单按钮 -->
<UButton
:icon="isMenuOpen ? 'i-heroicons-x-mark' : 'i-heroicons-bars-3'"
size="sm"
variant="ghost"
@click="isMenuOpen = !isMenuOpen"
/>
<!-- 移动端菜单 -->
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-1 opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y-1 opacity-0"
>
<div
v-if="isMenuOpen"
class="absolute right-0 top-full mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50"
>
<nav class="p-2">
<NuxtLink
v-for="item in menuItems"
:key="item.to"
:to="item.to"
class="flex items-center gap-3 px-3 py-2 rounded-md text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
@click="isMenuOpen = false"
>
<UIcon :name="item.icon" class="w-4 h-4" />
{{ item.label }}
</NuxtLink>
</nav>
</div>
</Transition>
</div>
</template>🏗️ 头部组件整合
创建 components/AppHeader.vue
<template>
<header class="sticky top-0 z-40 w-full border-b border-gray-200 dark:border-gray-800 bg-white/75 dark:bg-gray-900/75 backdrop-blur supports-[backdrop-filter]:bg-white/60">
<div class="container flex h-16 items-center justify-between px-4 md:px-8 lg:px-32">
<!-- 左侧 Logo 区域 -->
<div class="flex items-center">
<NuxtLink to="/" class="flex items-center space-x-2">
<NuxtImg
src="/logo.svg"
alt="网站Logo"
class="h-8 w-8"
/>
<span class="hidden font-bold sm:inline-block">
游戏社区
</span>
</NuxtLink>
</div>
<!-- 桌面端导航 -->
<nav class="hidden md:flex items-center space-x-6 text-sm font-medium">
<NuxtLink
to="/"
class="transition-colors hover:text-foreground/80 text-foreground/60"
>
首页
</NuxtLink>
<NuxtLink
to="/blog"
class="transition-colors hover:text-foreground/80 text-foreground/60"
>
文章
</NuxtLink>
<NuxtLink
to="/projects"
class="transition-colors hover:text-foreground/80 text-foreground/60"
>
项目
</NuxtLink>
<NuxtLink
to="/about"
class="transition-colors hover:text-foreground/80 text-foreground/60"
>
关于
</NuxtLink>
</nav>
<!-- 右侧功能区域 -->
<div class="flex items-center space-x-2">
<!-- 搜索组件 -->
<AppSearch class="hidden sm:block" />
<!-- 用户认证组件 -->
<AppAuth />
<!-- 主题切换 -->
<AppColorMode />
<!-- 移动端菜单 -->
<AppMobileMenu />
</div>
</div>
</header>
</template>创建 components/HeaderLogo.vue
<template>
<NuxtLink
to="/"
class="flex items-center space-x-2 hover:opacity-80 transition-opacity"
>
<NuxtImg
src="/logo.svg"
alt="网站Logo"
class="h-8 w-8"
/>
<span class="hidden font-bold sm:inline-block text-lg">
游戏社区
</span>
</NuxtLink>
</template>🏠 默认布局组件
创建 layouts/default.vue
<template>
<div>
<AppHeader />
<slot />
</div>
</template>🎨 最终效果展示
PC 端效果
- ✅ 完整导航栏-Logo + 导航菜单 + 功能区域
- ✅ 全局搜索-支持键盘快捷键 (Ctrl+K)
- ✅ 用户认证-登录/注册按钮或用户头像下拉菜单
- ✅ 主题切换-流畅的明暗模式切换
- ✅ 响应式布局-适配不同屏幕尺寸
移动端效果
- ✅ 紧凑布局-Logo + 汉堡菜单
- ✅ 滑出菜单-包含所有导航项
- ✅ 触摸优化-适合移动设备的交互
- ✅ 功能完整-保留所有核心功能
交互特性
- 🎯 键盘支持-搜索快捷键、ESC 关闭等
- 🌊 流畅动画-菜单展开、主题切换动画
- 📱 响应式-自适应不同设备和屏幕
- 🎨 视觉反馈-悬停效果、状态变化
📝 阶段总结
本章节完成的核心功能-
- ✅ 完整的 Header 布局-包含 Logo、导航、功能区域
- ✅ 全局搜索功能-支持快捷键和实时搜索
- ✅ 用户认证系统-登录/注册/用户菜单
- ✅ 主题切换功能-明暗模式无缝切换
- ✅ 移动端适配-汉堡菜单和响应式布局
- ✅ 组件化架构-模块化、可复用的组件设计
技术亮点
- 🔧 TypeScript 支持-完整的类型定义
- 🎨 Tailwind CSS-原子化 CSS 样式
- 📦 Nuxt UI 组件-现代化的 UI 组件库
- 🚀 性能优化-懒加载和代码分割
下一步计划-
- 实现搜索功能的后端逻辑
- 完善用户认证流程
- 开发主体内容区域
- 添加更多交互动画
项目资源-
🦶 Footer 底部布局
🎯 设计参考
参考 Nuxt UI 官网 的布局设计,实现简洁美观的底部布局。
📋 需求分析
- ICP 备案信息-左侧显示备案号和图标
- 社交链接-右侧展示各种社交平台图标
- 响应式设计-移动端优化布局顺序
- 视觉分割-使用分割线和 Logo 装饰
准备工作-准备好域名备案号和 ICP 图标
🏗️ Footer 组件实现
1. 创建 components/AppFooter.vue
<template>
<footer class="fixed bottom-0 w-full">
<!-- 分割线 -->
<UDivider :avatar="{ src: '/logo.svg' }" />
<div class="flex justify-between items-center px-4 md:px-8 lg:px-32 py-3 max-sm:flex-col -mt-2.5">
<!-- ICP 备案信息 -->
<ULink
to="https://beian.miit.gov.cn/#/Integrated/index"
target="_blank"
active-class="text-primary"
inactive-class="text-sm text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<div class="flex items-center gap-2">
<NuxtImg
src="/icp.png"
alt="备案号"
class="w-4"
/>
备案号
</div>
</ULink>
</div>
</footer>
</template>2. 更新 layouts/default.vue
<template>
<div>
<!-- 头部 -->
<AppHeader />
<!-- 主体内容 -->
<slot />
<!-- 底部 -->
<AppFooter />
</div>
</template>✨ 暗黑模式切换动画优化
为了提升用户体验,我们为暗黑模式切换添加流畅的过渡动画-
更新 components/AppColorMode.vue
<script setup lang="ts">
const colorMode = useColorMode()
// 切换模式
const setColorMode = () => {
colorMode.value = colorMode.value === 'dark' ? 'light' : 'dark'
}
// 判断是否支持 View Transition API
const enableTransitions = () =>
'startViewTransition' in document &&
window.matchMedia('(prefers-reduced-motion: no-preference)').matches
// 带动画的切换函数
async function toggleDark({ clientX: x, clientY: y }: MouseEvent) {
const isDark = colorMode.value === 'dark'
// 如果不支持动画,直接切换
if (!enableTransitions()) {
setColorMode()
return
}
// 计算动画路径
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
)}px at ${x}px ${y}px)`
]
// 执行视图过渡动画
await document.startViewTransition(async () => {
setColorMode()
await nextTick()
}).ready
// 应用圆形扩散动画
document.documentElement.animate(
{ clipPath: !isDark ? clipPath.reverse() : clipPath },
{
duration: 300,
easing: 'ease-in',
pseudoElement: `::view-transition-${!isDark ? 'old' : 'new'}(root)`
}
)
}
</script>
<template>
<UTooltip :text="`切换${$colorMode.value === 'dark' ? '白天' : '黑夜'}模式`">
<UButton
:icon="$colorMode.value === 'dark' ? 'i-heroicons-moon-solid' : 'i-heroicons-sun-solid'"
size="sm"
variant="ghost"
class="text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800"
@click="toggleDark"
/>
</UTooltip>
</template>
<style>
/* View Transition 动画样式 */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root),
.dark::view-transition-new(root) {
z-index: 1;
}
::view-transition-new(root),
.dark::view-transition-old(root) {
z-index: 9999;
}
</style>🎨 最终效果
功能特性
- ✅ 固定底部布局-始终显示在页面底部
- ✅ ICP 备案链接-点击跳转到备案查询页面
- ✅ 社交图标组-支持多个社交平台链接
- ✅ 响应式设计-移动端优化布局顺序
- ✅ 暗黑模式动画-流畅的圆形扩散切换效果
交互体验
- 🎯 悬停效果-图标和链接的悬停状态
- 🌓 主题切换-支持明暗主题无缝切换
- 📱 移动适配-小屏幕下的布局重排
📝 章节总结
本章节完成的功能-
- ✅ Footer 底部布局组件
- ✅ ICP 备案信息展示
- ✅ 社交图标链接组
- ✅ 暗黑模式切换动画
- ✅ 响应式布局适配
下一步计划-开发主体内容区域,实现核心业务功能