Appearance
第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>© {{ 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.vue→UiButton)
示例:
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')">×</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。
