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

2025年4月26日
约 5 分钟阅读时间
By 麦兜九天 & Tianyang Wang

目录

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

下一步计划-

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

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