Skip to content

第7章:布局与组件化开发

1. 布局系统(layouts 目录使用,全局布局、局部布局切换)

1.1 基础布局

创建布局组件:在 layouts 目录下创建 .vue 文件。

默认布局layouts/default.vue 是默认布局,会应用到所有页面。

示例

vue
<!-- layouts/default.vue -->
<template>
  <div class="layout">
    <header class="header">
      <h1>My Nuxt App</h1>
      <nav>
        <NuxtLink to="/">Home</NuxtLink>
        <NuxtLink to="/about">About</NuxtLink>
        <NuxtLink to="/contact">Contact</NuxtLink>
      </nav>
    </header>
    <main class="main">
      <slot />
    </main>
    <footer class="footer">
      <p>&copy; {{ new Date().getFullYear() }} My Nuxt App</p>
    </footer>
  </div>
</template>

<style scoped>
.layout {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.header {
  background-color: #333;
  color: white;
  padding: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.header nav a {
  color: white;
  margin-left: 1rem;
  text-decoration: none;
}

.main {
  flex: 1;
  padding: 2rem;
}

.footer {
  background-color: #333;
  color: white;
  padding: 1rem;
  text-align: center;
}
</style>

1.2 自定义布局

创建自定义布局:在 layouts 目录下创建其他布局文件。

示例

vue
<!-- layouts/auth.vue -->
<template>
  <div class="auth-layout">
    <div class="auth-container">
      <slot />
    </div>
  </div>
</template>

<style scoped>
.auth-layout {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background-color: #f5f5f5;
}

.auth-container {
  background-color: white;
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  width: 100%;
  max-width: 400px;
}
</style>

1.3 在页面中使用布局

使用默认布局:不需要特别指定,默认使用 layouts/default.vue

使用自定义布局:在页面组件中使用 layout 属性指定布局。

示例

vue
<!-- pages/login.vue -->
<template>
  <div class="login-page">
    <h1>Login</h1>
    <form @submit.prevent="handleSubmit">
      <div>
        <label for="email">Email</label>
        <input type="email" id="email" v-model="email" />
      </div>
      <div>
        <label for="password">Password</label>
        <input type="password" id="password" v-model="password" />
      </div>
      <button type="submit">Login</button>
    </form>
  </div>
</template>

<script setup>
const email = ref('')
const password = ref('')

function handleSubmit() {
  // 登录逻辑
}
</script>

<style scoped>
.login-page h1 {
  margin-bottom: 1.5rem;
}

form div {
  margin-bottom: 1rem;
}

label {
  display: block;
  margin-bottom: 0.5rem;
}

input {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
}

button {
  width: 100%;
  padding: 0.75rem;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

<script>
export default {
  layout: 'auth'
}
</script>

1.4 动态切换布局

使用 useRoute 动态切换布局

vue
<!-- pages/index.vue -->
<template>
  <div>
    <h1>Home Page</h1>
    <button @click="toggleLayout">Toggle Layout</button>
  </div>
</template>

<script setup>
const route = useRoute()
const currentLayout = ref('default')

function toggleLayout() {
  currentLayout.value = currentLayout.value === 'default' ? 'auth' : 'default'
  // 注意:这种方式只能在客户端切换,服务端不会生效
}
</script>

<script>
export default {
  layout: (ctx) => {
    // 可以根据路由参数、用户状态等动态返回布局
    return ctx.route.params.layout || 'default'
  }
}
</script>

2. 自定义组件(组件命名、props 传递、emit 事件)

2.1 组件命名

命名规范

  • 组件名使用 PascalCase(如 Button.vue
  • 嵌套目录中的组件会自动添加目录名作为前缀(如 components/ui/Button.vueUiButton

示例

vue
<!-- components/Button.vue -->
<template>
  <button 
    class="btn" 
    :class="{ 'btn-primary': primary, 'btn-secondary': !primary }"
    @click="$emit('click')"
  >
    <slot />
  </button>
</template>

<script setup>
defineProps({
  primary: {
    type: Boolean,
    default: false
  }
})

defineEmits(['click'])
</script>

<style scoped>
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
}
</style>

2.2 Props 传递

基本用法

vue
<template>
  <div class="card">
    <div v-if="title" class="card-header">
      {{ title }}
    </div>
    <div class="card-body">
      <slot />
    </div>
    <div v-if="footer" class="card-footer">
      {{ footer }}
    </div>
  </div>
</template>

<script setup>
defineProps({
  title: {
    type: String,
    default: ''
  },
  footer: {
    type: String,
    default: ''
  }
})
</script>

<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 1rem;
}

.card-header {
  background-color: #f8f9fa;
  padding: 1rem;
  border-bottom: 1px solid #ddd;
  font-weight: bold;
}

.card-body {
  padding: 1rem;
}

.card-footer {
  background-color: #f8f9fa;
  padding: 1rem;
  border-top: 1px solid #ddd;
  text-align: right;
}
</style>

使用组件

vue
<template>
  <div>
    <Card title="Card Title" footer="Card Footer">
      <p>Card content goes here</p>
    </Card>
  </div>
</template>

<script setup>
// 无需导入 Card 组件,自动导入
</script>

2.3 Emit 事件

基本用法

vue
<template>
  <div class="todo-item">
    <input 
      type="checkbox" 
      :checked="completed" 
      @change="$emit('update:completed', !completed)"
    />
    <span :class="{ 'completed': completed }">{{ text }}</span>
    <button @click="$emit('remove')">Remove</button>
  </div>
</template>

<script setup>
defineProps({
  text: {
    type: String,
    required: true
  },
  completed: {
    type: Boolean,
    default: false
  }
})

defineEmits(['update:completed', 'remove'])
</script>

<style scoped>
.todo-item {
  display: flex;
  align-items: center;
  padding: 0.5rem;
  border-bottom: 1px solid #ddd;
}

.todo-item input {
  margin-right: 0.5rem;
}

.todo-item span {
  flex: 1;
}

.todo-item .completed {
  text-decoration: line-through;
  color: #6c757d;
}

.todo-item button {
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
}
</style>

使用组件

vue
<template>
  <div>
    <h1>Todo List</h1>
    <TodoItem 
      v-for="todo in todos" 
      :key="todo.id"
      :text="todo.text"
      :completed="todo.completed"
      @update:completed="(value) => updateTodo(todo.id, value)"
      @remove="() => removeTodo(todo.id)"
    />
  </div>
</template>

<script setup>
const todos = ref([
  { id: 1, text: 'Learn Nuxt.js', completed: false },
  { id: 2, text: 'Build a project', completed: true }
])

function updateTodo(id, completed) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) {
    todo.completed = completed
  }
}

function removeTodo(id) {
  todos.value = todos.value.filter(t => t.id !== id)
}
</script>

3. 插槽使用(默认插槽、具名插槽、作用域插槽)

3.1 默认插槽

基本用法

vue
<template>
  <div class="container">
    <slot />
  </div>
</template>

<style scoped>
.container {
  padding: 2rem;
  background-color: #f8f9fa;
  border-radius: 8px;
}
</style>

使用

vue
<template>
  <Container>
    <h1>Hello World</h1>
    <p>This is content inside the container</p>
  </Container>
</template>

3.2 具名插槽

基本用法

vue
<template>
  <div class="modal">
    <div class="modal-header">
      <slot name="header">Default Header</slot>
    </div>
    <div class="modal-body">
      <slot></slot>
    </div>
    <div class="modal-footer">
      <slot name="footer">
        <button @click="$emit('close')">Close</button>
      </slot>
    </div>
  </div>
</template>

<script setup>
defineEmits(['close'])
</script>

<style scoped>
.modal {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  width: 100%;
  max-width: 500px;
}

.modal-header {
  padding: 1rem;
  border-bottom: 1px solid #ddd;
}

.modal-body {
  padding: 1rem;
}

.modal-footer {
  padding: 1rem;
  border-top: 1px solid #ddd;
  text-align: right;
}

button {
  padding: 0.5rem 1rem;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

使用

vue
<template>
  <Modal @close="closeModal">
    <template #header>
      <h2>Custom Header</h2>
    </template>
    <p>Modal content goes here</p>
    <template #footer>
      <button @click="save">Save</button>
      <button @click="closeModal">Cancel</button>
    </template>
  </Modal>
</template>

<script setup>
const closeModal = () => {
  // 关闭模态框
}

const save = () => {
  // 保存数据
  closeModal()
}
</script>

3.3 作用域插槽

基本用法

vue
<template>
  <div class="list">
    <div v-for="item in items" :key="item.id" class="list-item">
      <slot name="item" :item="item" :index="index">
        {{ item.name }}
      </slot>
    </div>
  </div>
</template>

<script setup>
defineProps({
  items: {
    type: Array,
    required: true
  }
})
</script>

<style scoped>
.list {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.list-item {
  padding: 1rem;
  border-bottom: 1px solid #ddd;
}

.list-item:last-child {
  border-bottom: none;
}
</style>

使用

vue
<template>
  <List :items="users">
    <template #item="{ item, index }">
      <div>
        <span>{{ index + 1 }}. {{ item.name }}</span>
        <button @click="editUser(item.id)">Edit</button>
      </div>
    </template>
  </List>
</template>

<script setup>
const users = ref([
  { id: 1, name: 'John Doe' },
  { id: 2, name: 'Jane Smith' }
])

function editUser(id) {
  // 编辑用户
}
</script>

4. 第三方 UI 库集成(Element Plus、Ant Design Vue、Naive UI 等)

4.1 集成 Element Plus

安装

bash
# 使用 npm
npm install element-plus

# 使用 pnpm
pnpm add element-plus

# 使用 yarn
yarn add element-plus

配置

创建 plugins/element-plus.ts

typescript
// plugins/element-plus.ts
import { defineNuxtPlugin } from '#app'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(ElementPlus)
})

使用

vue
<template>
  <div>
    <el-button type="primary">Primary Button</el-button>
    <el-input v-model="input" placeholder="Please input" />
    <el-checkbox v-model="checked">Checkbox</el-checkbox>
  </div>
</template>

<script setup>
const input = ref('')
const checked = ref(false)
</script>

4.2 集成 Ant Design Vue

安装

bash
# 使用 npm
npm install ant-design-vue

# 使用 pnpm
pnpm add ant-design-vue

# 使用 yarn
yarn add ant-design-vue

配置

创建 plugins/ant-design.ts

typescript
// plugins/ant-design.ts
import { defineNuxtPlugin } from '#app'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(Antd)
})

使用

vue
<template>
  <div>
    <a-button type="primary">Primary Button</a-button>
    <a-input v-model:value="input" placeholder="Please input" />
    <a-checkbox v-model:checked="checked">Checkbox</a-checkbox>
  </div>
</template>

<script setup>
const input = ref('')
const checked = ref(false)
</script>

4.3 集成 Naive UI

安装

bash
# 使用 npm
npm install naive-ui

# 使用 pnpm
pnpm add naive-ui

# 使用 yarn
yarn add naive-ui

使用

vue
<template>
  <div>
    <n-button type="primary">Primary Button</n-button>
    <n-input v-model:value="input" placeholder="Please input" />
    <n-checkbox v-model:checked="checked">Checkbox</n-checkbox>
  </div>
</template>

<script setup>
import { NButton, NInput, NCheckbox } from 'naive-ui'

const input = ref('')
const checked = ref(false)
</script>

5. 通用组件封装(按钮、弹窗、分页、表单等通用组件实战)

5.1 封装按钮组件

创建 components/ui/Button.vue

vue
<template>
  <button 
    class="btn" 
    :class="[
      `btn-${type}`,
      { 'btn-block': block },
      { 'btn-lg': size === 'large' },
      { 'btn-sm': size === 'small' }
    ]"
    :disabled="disabled"
    @click="$emit('click')"
  >
    <slot />
  </button>
</template>

<script setup>
defineProps({
  type: {
    type: String,
    default: 'default',
    validator: (value) => ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
  },
  size: {
    type: String,
    default: 'default',
    validator: (value) => ['default', 'large', 'small'].includes(value)
  },
  block: {
    type: Boolean,
    default: false
  },
  disabled: {
    type: Boolean,
    default: false
  }
})

defineEmits(['click'])
</script>

<style scoped>
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s;
}

.btn-default {
  background-color: #f8f9fa;
  color: #333;
  border: 1px solid #ddd;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}

.btn-success {
  background-color: #28a745;
  color: white;
}

.btn-warning {
  background-color: #ffc107;
  color: #333;
}

.btn-danger {
  background-color: #dc3545;
  color: white;
}

.btn-block {
  width: 100%;
}

.btn-lg {
  padding: 12px 24px;
  font-size: 16px;
}

.btn-sm {
  padding: 4px 8px;
  font-size: 12px;
}

.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
</style>

使用

vue
<template>
  <div>
    <UiButton>Default Button</UiButton>
    <UiButton type="primary">Primary Button</UiButton>
    <UiButton type="success" size="large">Large Success Button</UiButton>
    <UiButton type="danger" size="small" block>Block Danger Button</UiButton>
  </div>
</template>

5.2 封装弹窗组件

创建 components/ui/Modal.vue

vue
<template>
  <div v-if="visible" class="modal-overlay" @click="handleOverlayClick">
    <div class="modal-content" @click.stop>
      <div class="modal-header">
        <h3>{{ title }}</h3>
        <button class="modal-close" @click="$emit('close')">&times;</button>
      </div>
      <div class="modal-body">
        <slot />
      </div>
      <div class="modal-footer">
        <slot name="footer">
          <UiButton @click="$emit('close')">Cancel</UiButton>
          <UiButton type="primary" @click="$emit('confirm')">Confirm</UiButton>
        </slot>
      </div>
    </div>
  </div>
</template>

<script setup>
defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  title: {
    type: String,
    default: 'Modal Title'
  }
})

defineEmits(['close', 'confirm'])

function handleOverlayClick() {
  $emit('close')
}
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal-content {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  width: 100%;
  max-width: 500px;
  animation: modal-fade-in 0.3s;
}

.modal-header {
  padding: 1rem;
  border-bottom: 1px solid #ddd;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.modal-header h3 {
  margin: 0;
}

.modal-close {
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  color: #6c757d;
}

.modal-body {
  padding: 1rem;
}

.modal-footer {
  padding: 1rem;
  border-top: 1px solid #ddd;
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}

@keyframes modal-fade-in {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
</style>

使用

vue
<template>
  <div>
    <UiButton type="primary" @click="showModal = true">Open Modal</UiButton>
    <UiModal 
      v-model:visible="showModal" 
      title="Modal Title"
      @close="showModal = false"
      @confirm="handleConfirm"
    >
      <p>Modal content goes here</p>
    </UiModal>
  </div>
</template>

<script setup>
const showModal = ref(false)

function handleConfirm() {
  // 确认逻辑
  showModal.value = false
}
</script>

小结

本章介绍了 Nuxt.js 的布局系统、自定义组件、插槽使用和第三方 UI 库集成等内容。通过本章的学习,你应该已经掌握了如何创建和使用布局组件,如何封装和使用自定义组件,以及如何集成第三方 UI 库。

在接下来的章节中,我们将学习 Nuxt.js 的 Composables 与组合式逻辑复用、服务端渲染与静态站点生成等核心特性,帮助你更深入地理解和使用 Nuxt.js。

© 2026 编程马·菜鸟教程 版权所有