Vue.js组件化开发

一、组件基础

1.1 什么是组件

组件是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在 Vue 中,组件本质上是一个拥有预定义选项的一个 Vue 实例。

1.2 组件的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- ChildComponent.vue -->
<template>
<div class="child-component">
<h2>{{ title }}</h2>
<p>{{ content }}</p>
</div>
</template>

<script>
export default {
name: 'ChildComponent',
data() {
return {
title: '子组件标题',
content: '这是子组件的内容'
}
}
}
</script>

<style scoped>
.child-component {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- App.vue -->
<template>
<div id="app">
<ChildComponent />
<ChildComponent />
<ChildComponent />
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
name: 'App',
components: {
ChildComponent
}
}
</script>

1.3 组件注册方式

全局注册

1
2
3
4
5
6
7
8
9
10
11
12
// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 全局注册组件
app.component('MyGlobalComponent', {
template: '<div>全局组件</div>'
})

app.mount('#app')
1
2
3
4
<!-- 在任何组件中都可以使用 -->
<template>
<MyGlobalComponent />
</template>

局部注册

1
2
3
4
5
6
7
8
9
10
11
<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

export default {
components: {
ComponentA,
ComponentB
}
}
</script>

实践练习1: 创建一个全局通知组件,可在应用任何地方使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<!-- Notification.vue -->
<template>
<transition name="fade">
<div v-if="visible" :class="['notification', type]">
<span>{{ message }}</span>
<button @click="close">×</button>
</div>
</transition>
</template>

<script>
export default {
name: 'Notification',
props: {
message: {
type: String,
default: ''
},
type: {
type: String,
default: 'info' // info, success, warning, error
},
duration: {
type: Number,
default: 3000
}
},
data() {
return {
visible: false
}
},
methods: {
show() {
this.visible = true
if (this.duration > 0) {
setTimeout(() => {
this.close()
}, this.duration)
}
},
close() {
this.visible = false
}
}
}
</script>

<style scoped>
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
z-index: 9999;
}

.notification.info { background: #1890ff; color: white; }
.notification.success { background: #52c41a; color: white; }
.notification.warning { background: #faad14; color: white; }
.notification.error { background: #f5222d; color: white; }

.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>

二、Props 组件通信

2.1 Props 基础

Props 用于父组件向子组件传递数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!-- UserProfile.vue -->
<template>
<div class="user-profile">
<img :src="avatar" :alt="name" />
<h3>{{ name }}</h3>
<p>年龄: {{ age }}</p>
<p>职业: {{ job }}</p>
</div>
</template>

<script>
export default {
name: 'UserProfile',
props: {
// 基础的类型检查
name: String,

// 多个可能的类型
age: [Number, String],

// 必填的字符串
job: {
type: String,
required: true
},

// 带有默认值
avatar: {
type: String,
default: '/default-avatar.png'
},

// 对象或数组的默认值
skills: {
type: Array,
default() {
return ['JavaScript', 'Vue']
}
},

// 自定义验证函数
score: {
validator(value) {
return value >= 0 && value <= 100
}
}
}
}
</script>

2.2 Props 的单向数据流

所有的 props 都遵循单向绑定原则:props 因父组件的更新而变化,自然地将新的值传给子组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- ParentComponent.vue -->
<template>
<div>
<button @click="count++">增加计数</button>
<ChildComponent :count="count" @update="handleUpdate" />
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
components: { ChildComponent },
data() {
return {
count: 0
}
},
methods: {
handleUpdate(newValue) {
this.count = newValue
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- ChildComponent.vue -->
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>

<script>
export default {
props: ['count'],
methods: {
increment() {
// 不要直接修改 props
// this.count++ // ❌ 错误

// 应该触发事件通知父组件
this.$emit('update', this.count + 1) // ✅ 正确
}
}
}
</script>

实践练习2: 创建一个商品卡片组件,接收商品信息并展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<!-- ProductCard.vue -->
<template>
<div class="product-card">
<div class="product-image">
<img :src="product.image" :alt="product.name" />
<span v-if="product.discount" class="discount-badge">
{{ product.discount }}% OFF
</span>
</div>
<div class="product-info">
<h3>{{ product.name }}</h3>
<p class="description">{{ product.description }}</p>
<div class="price-row">
<span class="current-price">¥{{ currentPrice }}</span>
<span v-if="product.discount" class="original-price">
¥{{ product.price }}
</span>
</div>
<button @click="addToCart" class="btn-cart">加入购物车</button>
</div>
</div>
</template>

<script>
export default {
name: 'ProductCard',
props: {
product: {
type: Object,
required: true,
validator(value) {
return value.name && value.price
}
}
},
computed: {
currentPrice() {
if (this.product.discount) {
return (this.product.price * (100 - this.product.discount) / 100).toFixed(2)
}
return this.product.price.toFixed(2)
}
},
methods: {
addToCart() {
this.$emit('add-to-cart', this.product)
}
}
}
</script>

<style scoped>
.product-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s;
}

.product-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

.product-image {
position: relative;
height: 200px;
overflow: hidden;
}

.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}

.discount-badge {
position: absolute;
top: 10px;
right: 10px;
background: #f5222d;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}

.product-info {
padding: 15px;
}

.current-price {
font-size: 20px;
font-weight: bold;
color: #f5222d;
}

.original-price {
margin-left: 10px;
color: #999;
text-decoration: line-through;
}

.btn-cart {
width: 100%;
margin-top: 10px;
padding: 8px 16px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

.btn-cart:hover {
background: #40a9ff;
}
</style>

三、自定义事件

3.1 $emit 触发事件

子组件通过 $emit 触发事件,向父组件传递数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- SearchBox.vue -->
<template>
<div class="search-box">
<input
v-model="searchText"
@keyup.enter="handleSearch"
placeholder="搜索..."
/>
<button @click="handleSearch">搜索</button>
</div>
</template>

<script>
export default {
name: 'SearchBox',
data() {
return {
searchText: ''
}
},
methods: {
handleSearch() {
// 触发自定义事件
this.$emit('search', this.searchText)
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- ParentComponent.vue -->
<template>
<div>
<SearchBox @search="handleSearch" />
<p>搜索结果: {{ results }}</p>
</div>
</template>

<script>
import SearchBox from './SearchBox.vue'

export default {
components: { SearchBox },
data() {
return {
results: []
}
},
methods: {
async handleSearch(keyword) {
// 执行搜索逻辑
this.results = await fetchSearchResults(keyword)
}
}
}
</script>

3.2 v-model 实现双向绑定

在组件上使用 v-model 实现双向绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>

<script>
export default {
name: 'CustomInput',
props: ['modelValue'],
emits: ['update:modelValue']
}
</script>
1
2
3
4
<!-- 使用 -->
<template>
<CustomInput v-model="inputText" />
</template>

3.3 多个 v-model 绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- UserForm.vue -->
<template>
<form>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
placeholder="姓"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
placeholder="名"
/>
</form>
</template>

<script>
export default {
props: ['firstName', 'lastName'],
emits: ['update:firstName', 'update:lastName']
}
</script>
1
2
3
4
5
6
7
<!-- 使用 -->
<template>
<UserForm
v-model:first-name="user.firstName"
v-model:last-name="user.lastName"
/>
</template>

实践练习3: 创建一个评分组件,支持 v-model 双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!-- Rating.vue -->
<template>
<div class="rating">
<span
v-for="star in maxStars"
:key="star"
@click="setRating(star)"
@mouseenter="hoverRating = star"
@mouseleave="hoverRating = 0"
:class="['star', { active: star <= (hoverRating || modelValue) }]"
>

</span>
<span class="rating-text">{{ ratingText }}</span>
</div>
</template>

<script>
export default {
name: 'Rating',
props: {
modelValue: {
type: Number,
default: 0
},
maxStars: {
type: Number,
default: 5
},
readonly: {
type: Boolean,
default: false
}
},
emits: ['update:modelValue'],
data() {
return {
hoverRating: 0
}
},
computed: {
ratingText() {
const texts = ['未评分', '很差', '较差', '一般', '较好', '很好']
return texts[this.hoverRating || this.modelValue]
}
},
methods: {
setRating(star) {
if (!this.readonly) {
this.$emit('update:modelValue', star)
}
}
}
}
</script>

<style scoped>
.rating {
display: inline-flex;
align-items: center;
gap: 5px;
}

.star {
font-size: 24px;
color: #ddd;
cursor: pointer;
transition: color 0.2s;
}

.star.active {
color: #fadb14;
}

.star:hover {
transform: scale(1.1);
}

.rating-text {
margin-left: 10px;
color: #666;
}
</style>

四、插槽 Slots

4.1 默认插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Card.vue -->
<template>
<div class="card">
<slot>默认内容</slot>
</div>
</template>

<style scoped>
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
}
</style>
1
2
3
4
5
6
7
<!-- 使用 -->
<template>
<Card>
<h2>卡片标题</h2>
<p>卡片内容</p>
</Card>
</template>

4.2 具名插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Layout.vue -->
<template>
<div class="layout">
<header class="header">
<slot name="header"></slot>
</header>

<main class="main">
<slot></slot>
</main>

<footer class="footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 使用 -->
<template>
<Layout>
<template #header>
<nav>导航栏</nav>
</template>

<template #default>
<p>主要内容</p>
</template>

<template #footer>
<p>页脚信息</p>
</template>
</Layout>
</template>

4.3 作用域插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user" :index="index">
{{ user.name }}
</slot>
</li>
</ul>
</template>

<script>
export default {
props: {
users: Array
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
<!-- 使用 -->
<template>
<UserList :users="userList">
<template #default="{ user, index }">
<div class="user-item">
<span>{{ index + 1 }}. {{ user.name }}</span>
<span>{{ user.email }}</span>
</div>
</template>
</UserList>
</template>

实践练习4: 创建一个表格组件,使用作用域插槽自定义列渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<!-- DataTable.vue -->
<template>
<table class="data-table">
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in data" :key="rowIndex">
<td v-for="col in columns" :key="col.key">
<slot
:name="col.key"
:row="row"
:value="row[col.key]"
:index="rowIndex"
>
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>

<script>
export default {
name: 'DataTable',
props: {
columns: {
type: Array,
required: true
},
data: {
type: Array,
required: true
}
}
}
</script>

<style scoped>
.data-table {
width: 100%;
border-collapse: collapse;
}

.data-table th,
.data-table td {
padding: 12px;
border: 1px solid #e0e0e0;
text-align: left;
}

.data-table th {
background: #f5f5f5;
font-weight: bold;
}

.data-table tr:hover {
background: #fafafa;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 使用示例 -->
<template>
<DataTable :columns="columns" :data="tableData">
<template #status="{ value }">
<span :class="['status', value]">
{{ value === 'active' ? '激活' : '禁用' }}
</span>
</template>

<template #actions="{ row }">
<button @click="editUser(row)">编辑</button>
<button @click="deleteUser(row)">删除</button>
</template>
</DataTable>
</template>

<script>
export default {
data() {
return {
columns: [
{ key: 'id', title: 'ID' },
{ key: 'name', title: '姓名' },
{ key: 'email', title: '邮箱' },
{ key: 'status', title: '状态' },
{ key: 'actions', title: '操作' }
],
tableData: [
{ id: 1, name: '张三', email: 'zhangsan@example.com', status: 'active' },
{ id: 2, name: '李四', email: 'lisi@example.com', status: 'inactive' }
]
}
}
}
</script>

五、组件生命周期

5.1 生命周期钩子

Vue 组件的生命周期分为四个阶段:创建、挂载、更新、销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<template>
<div>
<p>{{ message }}</p>
<button @click="message = '更新了'">更新</button>
</div>
</template>

<script>
export default {
data() {
return {
message: '初始消息'
}
},

// 创建阶段
beforeCreate() {
console.log('beforeCreate: 实例初始化之后,数据观测和事件配置之前')
// 此时无法访问 data、methods
},

created() {
console.log('created: 实例创建完成')
// 可以访问 data、methods
// 常用于:初始化数据、调用API
},

// 挂载阶段
beforeMount() {
console.log('beforeMount: 挂载开始之前')
// 此时 DOM 还未生成
},

mounted() {
console.log('mounted: DOM 挂载完成')
// 可以访问 DOM
// 常用于:DOM操作、第三方库初始化
},

// 更新阶段
beforeUpdate() {
console.log('beforeUpdate: 数据更新,DOM 重新渲染之前')
},

updated() {
console.log('updated: DOM 更新完成')
// 避免在此修改数据,可能导致无限循环
},

// 销毁阶段
beforeUnmount() {
console.log('beforeUnmount: 实例销毁之前')
// 常用于:清理定时器、取消订阅
},

unmounted() {
console.log('unmounted: 实例销毁完成')
}
}
</script>

5.2 实际应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<script>
export default {
data() {
return {
timer: null,
user: null
}
},

async created() {
// 组件创建时获取数据
this.user = await fetchUserInfo()
},

mounted() {
// DOM 挂载后启动定时器
this.timer = setInterval(() => {
this.updateTime()
}, 1000)

// 添加事件监听
window.addEventListener('resize', this.handleResize)
},

beforeUnmount() {
// 清理定时器
if (this.timer) {
clearInterval(this.timer)
}

// 移除事件监听
window.removeEventListener('resize', this.handleResize)
},

methods: {
updateTime() {
// 更新时间逻辑
},
handleResize() {
// 处理窗口大小变化
}
}
}
</script>

实践练习5: 创建一个倒计时组件,正确使用生命周期钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<!-- Countdown.vue -->
<template>
<div class="countdown">
<div class="timer">
<span>{{ hours }}</span>:<span>{{ minutes }}</span>:<span>{{ seconds }}</span>
</div>
<div class="controls">
<button @click="start" v-if="!isRunning">开始</button>
<button @click="pause" v-else>暂停</button>
<button @click="reset">重置</button>
</div>
</div>
</template>

<script>
export default {
name: 'Countdown',
props: {
endTime: {
type: Date,
required: true
}
},

data() {
return {
remaining: 0,
isRunning: false,
timer: null
}
},

computed: {
hours() {
return String(Math.floor(this.remaining / 3600)).padStart(2, '0')
},
minutes() {
return String(Math.floor((this.remaining % 3600) / 60)).padStart(2, '0')
},
seconds() {
return String(this.remaining % 60).padStart(2, '0')
}
},

mounted() {
this.calculateRemaining()
// 从 localStorage 恢复状态
const savedState = localStorage.getItem('countdown_state')
if (savedState) {
const { remaining, isRunning } = JSON.parse(savedState)
this.remaining = remaining
if (isRunning) {
this.start()
}
}
},

beforeUnmount() {
this.pause()
// 保存状态到 localStorage
localStorage.setItem('countdown_state', JSON.stringify({
remaining: this.remaining,
isRunning: this.isRunning
}))
},

methods: {
calculateRemaining() {
const now = new Date()
this.remaining = Math.max(0, Math.floor((this.endTime - now) / 1000))
},

start() {
if (this.remaining <= 0) return

this.isRunning = true
this.timer = setInterval(() => {
this.remaining--

if (this.remaining <= 0) {
this.pause()
this.$emit('finished')
}
}, 1000)
},

pause() {
this.isRunning = false
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},

reset() {
this.pause()
this.calculateRemaining()
}
}
}
</script>

<style scoped>
.countdown {
text-align: center;
padding: 20px;
}

.timer {
font-size: 48px;
font-weight: bold;
color: #333;
font-family: monospace;
margin-bottom: 20px;
}

.controls {
display: flex;
gap: 10px;
justify-content: center;
}

button {
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}

button:first-child {
background: #52c41a;
color: white;
}

button:last-child {
background: #1890ff;
color: white;
}
</style>

六、实战案例:用户管理组件

综合运用组件化开发的所有知识点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
<!-- UserManagement.vue -->
<template>
<div class="user-management">
<!-- 搜索组件 -->
<SearchBox
v-model="searchKeyword"
@search="handleSearch"
placeholder="搜索用户..."
/>

<!-- 用户列表 -->
<DataTable
:columns="columns"
:data="filteredUsers"
@sort="handleSort"
>
<template #avatar="{ value }">
<img :src="value" class="avatar" />
</template>

<template #status="{ value }">
<span :class="['status', value]">
{{ value === 'active' ? '激活' : '禁用' }}
</span>
</template>

<template #actions="{ row }">
<button @click="editUser(row)">编辑</button>
<button @click="deleteUser(row)">删除</button>
</template>
</DataTable>

<!-- 用户编辑弹窗 -->
<UserEditModal
v-if="showEditModal"
:user="editingUser"
@save="handleSave"
@close="showEditModal = false"
>
<template #header>
<h2>编辑用户</h2>
</template>

<template #footer>
<button @click="showEditModal = false">取消</button>
<button @click="saveUser">保存</button>
</template>
</UserEditModal>
</div>
</template>

<script>
import SearchBox from './SearchBox.vue'
import DataTable from './DataTable.vue'
import UserEditModal from './UserEditModal.vue'

export default {
name: 'UserManagement',
components: {
SearchBox,
DataTable,
UserEditModal
},

data() {
return {
searchKeyword: '',
users: [],
columns: [
{ key: 'avatar', title: '头像', sortable: false },
{ key: 'name', title: '姓名', sortable: true },
{ key: 'email', title: '邮箱', sortable: true },
{ key: 'status', title: '状态', sortable: true },
{ key: 'actions', title: '操作', sortable: false }
],
showEditModal: false,
editingUser: null
}
},

computed: {
filteredUsers() {
return this.users.filter(user =>
user.name.includes(this.searchKeyword) ||
user.email.includes(this.searchKeyword)
)
}
},

async created() {
await this.loadUsers()
},

methods: {
async loadUsers() {
const response = await fetch('/api/users')
this.users = await response.json()
},

handleSearch(keyword) {
this.searchKeyword = keyword
},

handleSort(column) {
this.users.sort((a, b) => {
if (a[column] < b[column]) return -1
if (a[column] > b[column]) return 1
return 0
})
},

editUser(user) {
this.editingUser = { ...user }
this.showEditModal = true
},

async handleSave(user) {
await fetch(`/api/users/${user.id}`, {
method: 'PUT',
body: JSON.stringify(user)
})

await this.loadUsers()
this.showEditModal = false
},

async deleteUser(user) {
if (confirm('确定要删除该用户吗?')) {
await fetch(`/api/users/${user.id}`, { method: 'DELETE' })
await this.loadUsers()
}
}
}
}
</script>

<style scoped>
.user-management {
padding: 20px;
}

.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}

.status {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
}

.status.active {
background: #f6ffed;
color: #52c41a;
}

.status.inactive {
background: #fff7e6;
color: #faad14;
}
</style>

总结

组件化开发是 Vue.js 的核心特性之一,掌握以下关键点:

  1. 组件基础: 理解组件的注册和使用方式
  2. Props 通信: 掌握父子组件间的数据传递
  3. 自定义事件: 实现子组件向父组件的通信
  4. 插槽系统: 灵活的组件内容分发机制
  5. 生命周期: 在合适的时机执行正确的逻辑

通过合理使用这些特性,可以构建出可维护、可复用的组件体系。

扩展阅读