自定义组件开发 - 封装你自己的组件
🧩 组件化是现代前端开发的核心思想,今天我们来学习如何创建自己的组件
1. 创建基础组件
简单的按钮组件
vue
<!-- components/MyButton.vue -->
<template>
<button
class="my-button"
:class="[`my-button--\${type}`, `my-button--\${size}`, { 'my-button--disabled': disabled }]"
:disabled="disabled"
@click="handleClick"
>
<text v-if="loading" class="loading-icon">⏳</text>
<slot v-else></slot>
</button>
</template>
<script>
export default {
name: 'MyButton',
props: {
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
},
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
}
},
methods: {
handleClick(e) {
if (this.disabled || this.loading) {
return
}
this.$emit('click', e)
}
}
}
</script>
<style lang="scss">
.my-button {
border: none;
border-radius: 6rpx;
font-size: 28rpx;
text-align: center;
transition: all 0.3s ease;
// 尺寸
&--small {
padding: 16rpx 24rpx;
font-size: 24rpx;
}
&--medium {
padding: 20rpx 32rpx;
font-size: 28rpx;
}
&--large {
padding: 24rpx 40rpx;
font-size: 32rpx;
}
// 类型
&--default {
background: #f5f5f5;
color: #333;
&:active {
background: #e0e0e0;
}
}
&--primary {
background: #007aff;
color: white;
&:active {
background: #0056cc;
}
}
&--success {
background: #4cd964;
color: white;
&:active {
background: #2ac845;
}
}
&--warning {
background: #ff9500;
color: white;
&:active {
background: #cc7700;
}
}
&--danger {
background: #ff3b30;
color: white;
&:active {
background: #cc1f17;
}
}
// 禁用状态
&--disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.loading-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
使用自定义按钮
vue
<template>
<view class="button-demo">
<MyButton @click="handleClick">默认按钮</MyButton>
<MyButton type="primary" @click="handleClick">主要按钮</MyButton>
<MyButton type="success" size="large" @click="handleClick">成功按钮</MyButton>
<MyButton type="warning" :loading="isLoading" @click="handleAsyncAction">
\{\{ isLoading ? '加载中...' : '异步操作' \}\}
</MyButton>
<MyButton type="danger" disabled>禁用按钮</MyButton>
</view>
</template>
<script>
import MyButton from '@/components/MyButton.vue'
export default {
components: {
MyButton
},
data() {
return {
isLoading: false
}
},
methods: {
handleClick() {
uni.showToast({
title: '按钮被点击',
icon: 'success'
})
},
async handleAsyncAction() {
this.isLoading = true
try {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 2000))
uni.showToast({
title: '操作成功',
icon: 'success'
})
} catch (error) {
uni.showToast({
title: '操作失败',
icon: 'error'
})
} finally {
this.isLoading = false
}
}
}
}
</script>
2. 复杂组件:卡片组件
vue
<!-- components/Card.vue -->
<template>
<view class="card" :class="{ 'card--shadow': shadow }">
<!-- 头部 -->
<view v-if="$slots.header || title" class="card__header">
<slot name="header">
<text class="card__title">\{\{ title \}\}</text>
<text v-if="subtitle" class="card__subtitle">\{\{ subtitle \}\}</text>
</slot>
</view>
<!-- 内容 -->
<view class="card__body" :style="{ padding: bodyPadding }">
<slot></slot>
</view>
<!-- 底部 -->
<view v-if="$slots.footer" class="card__footer">
<slot name="footer"></slot>
</view>
</view>
</template>
<script>
export default {
name: 'Card',
props: {
title: {
type: String,
default: ''
},
subtitle: {
type: String,
default: ''
},
shadow: {
type: Boolean,
default: true
},
bodyPadding: {
type: String,
default: '30rpx'
}
}
}
</script>
<style lang="scss">
.card {
background: white;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 20rpx;
&--shadow {
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
&__header {
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
&__title {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
}
&__subtitle {
font-size: 26rpx;
color: #666;
margin-top: 8rpx;
display: block;
}
&__body {
padding: 30rpx;
}
&__footer {
padding: 20rpx 30rpx;
background: #f8f9fa;
border-top: 1rpx solid #f0f0f0;
}
}
</style>
3. 组件通信
父子组件通信
vue
<!-- components/Counter.vue -->
<template>
<view class="counter">
<button @click="decrement" :disabled="value <= min">-</button>
<text class="counter__value">\{\{ value \}\}</text>
<button @click="increment" :disabled="value >= max">+</button>
</view>
</template>
<script>
export default {
name: 'Counter',
props: {
value: {
type: Number,
default: 0
},
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 100
},
step: {
type: Number,
default: 1
}
},
methods: {
increment() {
if (this.value < this.max) {
this.$emit('input', this.value + this.step)
this.$emit('change', this.value + this.step)
}
},
decrement() {
if (this.value > this.min) {
this.$emit('input', this.value - this.step)
this.$emit('change', this.value - this.step)
}
}
}
}
</script>
<style lang="scss">
.counter {
display: flex;
align-items: center;
gap: 20rpx;
button {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
border: 1rpx solid #ddd;
background: white;
font-size: 24rpx;
&:disabled {
opacity: 0.5;
}
}
&__value {
min-width: 60rpx;
text-align: center;
font-size: 32rpx;
font-weight: bold;
}
}
</style>
使用计数器组件
vue
<template>
<view class="counter-demo">
<Card title="商品数量">
<view class="product-item">
<image src="/static/product.jpg" class="product-image" />
<view class="product-info">
<text class="product-name">iPhone 15 Pro</text>
<text class="product-price">¥8999</text>
</view>
<Counter
v-model="quantity"
:min="1"
:max="10"
@change="onQuantityChange"
/>
</view>
<template #footer>
<view class="total">
<text>总价:¥\{\{ totalPrice \}\}</text>
</view>
</template>
</Card>
</view>
</template>
<script>
import Card from '@/components/Card.vue'
import Counter from '@/components/Counter.vue'
export default {
components: {
Card,
Counter
},
data() {
return {
quantity: 1,
price: 8999
}
},
computed: {
totalPrice() {
return this.quantity * this.price
}
},
methods: {
onQuantityChange(newQuantity) {
console.log('数量变化:', newQuantity)
uni.showToast({
title: `数量:\${newQuantity}`,
icon: 'none'
})
}
}
}
</script>
<style>
.counter-demo {
padding: 20rpx;
}
.product-item {
display: flex;
align-items: center;
gap: 20rpx;
}
.product-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 28rpx;
font-weight: bold;
display: block;
margin-bottom: 8rpx;
}
.product-price {
color: #ff6b6b;
font-size: 32rpx;
font-weight: bold;
}
.total {
text-align: right;
}
.total text {
font-size: 32rpx;
font-weight: bold;
color: #ff6b6b;
}
</style>
4. 高级组件:模态框
vue
<!-- components/Modal.vue -->
<template>
<view v-if="visible" class="modal" @click="handleMaskClick">
<view class="modal__content" :style="contentStyle" @click.stop>
<!-- 头部 -->
<view v-if="showHeader" class="modal__header">
<slot name="header">
<text class="modal__title">\{\{ title \}\}</text>
</slot>
<view v-if="showClose" class="modal__close" @click="handleClose">
<text>✕</text>
</view>
</view>
<!-- 内容 -->
<view class="modal__body">
<slot></slot>
</view>
<!-- 底部 -->
<view v-if="$slots.footer || showFooter" class="modal__footer">
<slot name="footer">
<button v-if="showCancel" @click="handleCancel" class="modal__btn modal__btn--cancel">
\{\{ cancelText \}\}
</button>
<button v-if="showConfirm" @click="handleConfirm" class="modal__btn modal__btn--confirm">
\{\{ confirmText \}\}
</button>
</slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'Modal',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '提示'
},
width: {
type: String,
default: '600rpx'
},
maskClosable: {
type: Boolean,
default: true
},
showHeader: {
type: Boolean,
default: true
},
showClose: {
type: Boolean,
default: true
},
showFooter: {
type: Boolean,
default: true
},
showCancel: {
type: Boolean,
default: true
},
showConfirm: {
type: Boolean,
default: true
},
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
}
},
computed: {
contentStyle() {
return {
width: this.width
}
}
},
methods: {
handleMaskClick() {
if (this.maskClosable) {
this.handleClose()
}
},
handleClose() {
this.$emit('close')
},
handleCancel() {
this.$emit('cancel')
this.handleClose()
},
handleConfirm() {
this.$emit('confirm')
}
}
}
</script>
<style lang="scss">
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
&__content {
background: white;
border-radius: 16rpx;
max-height: 80vh;
overflow: hidden;
animation: modalSlideIn 0.3s ease-out;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
&__title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
&__close {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f5f5f5;
color: #666;
font-size: 24rpx;
}
&__body {
padding: 30rpx;
max-height: 60vh;
overflow-y: auto;
}
&__footer {
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx;
border-top: 1rpx solid #f0f0f0;
}
&__btn {
flex: 1;
height: 80rpx;
border: none;
border-radius: 8rpx;
font-size: 28rpx;
&--cancel {
background: #f5f5f5;
color: #666;
}
&--confirm {
background: #007aff;
color: white;
}
}
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.8) translateY(-50rpx);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
</style>
使用模态框组件
vue
<template>
<view class="modal-demo">
<button @click="showBasicModal">基础模态框</button>
<button @click="showCustomModal">自定义模态框</button>
<button @click="showConfirmModal">确认对话框</button>
<!-- 基础模态框 -->
<Modal
:visible="basicVisible"
title="基础模态框"
@close="basicVisible = false"
@confirm="handleBasicConfirm"
>
<text>这是一个基础的模态框内容。</text>
</Modal>
<!-- 自定义模态框 -->
<Modal
:visible="customVisible"
:show-footer="false"
width="700rpx"
@close="customVisible = false"
>
<template #header>
<text style="color: #007aff; font-size: 36rpx;">自定义标题</text>
</template>
<view class="custom-content">
<image src="/static/success.png" class="success-icon" />
<text class="success-text">操作成功!</text>
<button @click="customVisible = false" class="custom-btn">知道了</button>
</view>
</Modal>
<!-- 确认对话框 -->
<Modal
:visible="confirmVisible"
title="确认删除"
@close="confirmVisible = false"
@confirm="handleDelete"
@cancel="confirmVisible = false"
>
<text>确定要删除这个项目吗?删除后无法恢复。</text>
</Modal>
</view>
</template>
<script>
import Modal from '@/components/Modal.vue'
export default {
components: {
Modal
},
data() {
return {
basicVisible: false,
customVisible: false,
confirmVisible: false
}
},
methods: {
showBasicModal() {
this.basicVisible = true
},
showCustomModal() {
this.customVisible = true
},
showConfirmModal() {
this.confirmVisible = true
},
handleBasicConfirm() {
uni.showToast({
title: '确认操作',
icon: 'success'
})
this.basicVisible = false
},
handleDelete() {
uni.showToast({
title: '删除成功',
icon: 'success'
})
this.confirmVisible = false
}
}
}
</script>
<style>
.modal-demo {
padding: 20rpx;
}
.modal-demo button {
margin-bottom: 20rpx;
width: 100%;
}
.custom-content {
text-align: center;
padding: 40rpx 0;
}
.success-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
}
.success-text {
font-size: 32rpx;
color: #333;
display: block;
margin-bottom: 40rpx;
}
.custom-btn {
background: #007aff;
color: white;
border: none;
border-radius: 8rpx;
padding: 20rpx 60rpx;
}
</style>
小结
今天我们学习了:
- ✅ 创建基础自定义组件
- ✅ 组件的props和事件通信
- ✅ 插槽的使用和自定义内容
- ✅ 复杂组件的设计和实现
- ✅ 组件的生命周期和最佳实践
组件开发要点:
- 单一职责原则,每个组件只做一件事
- 合理设计props和事件接口
- 使用插槽提供灵活性
- 注意组件的可复用性和可维护性
下一篇预告
下一篇我们将学习《状态管理 - Vuex在UniApp中的使用》,学习如何管理复杂应用的状态。
组件化是现代前端开发的精髓,好的组件设计能让开发事半功倍!