Article
🎯 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 APIconst 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 备案信息展示
- ✅ 社交图标链接组
- ✅ 暗黑模式切换动画
- ✅ 响应式布局适配
下一步计划-开发主体内容区域,实现核心业务功能