Article

Nuxt实战(2)-Layout布局实现

更新于:2025-04-26

🎯 Header 头部布局

📋 需求分析

  • 左侧区域-放置网站 Logo,点击可返回首页
  • 右侧区域-功能区域,包含以下组件-
    • 🔍 全局搜索框-支持内容快速检索
    • 👤 用户认证-登录/注册按钮或用户头像
    • 🌓 主题切换-明暗模式切换按钮
    • 📱 移动菜单-移动端的汉堡菜单
  • 响应式设计-适配 PC 端和移动端不同布局
  • 交互体验-流畅的动画效果和用户反馈

准备工作-提前准备好 Logo、用户头像占位图和网站 favicon.ico 图标

🏗️ Layouts 布局基础

Nuxt 提供了一个布局框架,用于将常见的 UI 模式提取为可重用的布局。

1. 启用布局系统

app.vue 中添加 <NuxtLayout>-

<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

2. 安装必要依赖

Terminal window
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 组件库
  • 🚀 性能优化-懒加载和代码分割

下一步计划-

  1. 实现搜索功能的后端逻辑
  2. 完善用户认证流程
  3. 开发主体内容区域
  4. 添加更多交互动画

项目资源-

🎯 设计参考

参考 Nuxt UI 官网 的布局设计,实现简洁美观的底部布局。

📋 需求分析

  • ICP 备案信息-左侧显示备案号和图标
  • 社交链接-右侧展示各种社交平台图标
  • 响应式设计-移动端优化布局顺序
  • 视觉分割-使用分割线和 Logo 装饰

准备工作-准备好域名备案号和 ICP 图标

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 备案信息展示
  • ✅ 社交图标链接组
  • ✅ 暗黑模式切换动画
  • ✅ 响应式布局适配

下一步计划-开发主体内容区域,实现核心业务功能