Appearance
基础弹窗组件 - mp-popup
这是一个基于 uni-popup 封装的弹窗组件,相较于 uni-popup 组件,它解决了以下问题:
- 支持通过
visible
属性来控制弹窗的显示和隐藏。 - 解决了在嵌套弹窗情况下的遮罩定位问题。
因此,如果在项目中需要自定义弹窗,建议始终使用这个封装的组件作为基础组件。
基本用法
使用方式与 uni-popup 基本相同,同时支持它的所有特性:
vue
<script setup>
import {ref} from 'vue'
const popup = ref(null)
const open = () => {
// 通过组件定义的 ref 调用 mp-popup 的方法
popup.value.open()
}
</script>
<template>
<view>
<button @click="open">打开弹窗</button>
<mp-popup ref="popup" type="bottom">底部弹出 Popup</mp-popup>
</view>
</template>
你也可以使用 visible
属性来控制弹窗的显示和隐藏(支持双向同步):
vue
<script setup>
import {useToggle} from '@vueuse/core'
const [visible, toggleVisible] = useToggle(false)
</script>
<template>
<view>
<button @click="toggleVisible(true)">打开弹窗</button>
<mp-popup v-model:visible="visible" type="bottom">底部弹出 Popup</mp-popup>
</view>
</template>
封装组件
由于 mp-popup
是一个通用的弹窗组件,而每个项目的弹窗风格都可能有所不同。不过,通常情况下,单个项目内的弹窗风格是比较统一的。如果 mp-popup
的弹窗不能直接适用于你所在的项目,为了提高代码复用性,我们更推荐在项目中基于 mp-popup
进行封装,创建一个基础的弹窗组件,以供其它页面使用。这样可以更好地满足项目的特定需求并确保风格的一致性。
下面是一些示例模板,你可以根据需要进行自定义和扩展。
app-modal
这是一种最常见的弹窗,它的特点是整个弹窗相对于屏幕水平和垂直居中,同时在顶部或底部提供了一个关闭弹窗的图标,并且允许自定义弹窗内的内容。
Click me to view the code
vue
<script setup>
defineProps({
/**
* 是否显示关闭按钮
* @type {boolean}
* @default true
*/
showClose: {
type: Boolean,
default: true,
},
})
const emit = defineEmits([
'change',
'close',
'maskClick',
// 因为 uni-app 中 listener 不能通过 v-bind 透传,所以要自行定义
'update:visible',
])
const onChange = e => {
emit('change', e)
emit('update:visible', e.show)
}
const onClose = () => {
onMaskClick()
emit('close')
emit('update:visible', false)
}
const onMaskClick = () => {
emit('maskClick')
}
</script>
<template>
<mp-popup v-bind="$attrs" @change="onChange" @maskClick="onClose">
<slot name="close">
<image v-if="showClose" class="close-icon" src="./icon/close-icon.svg" @click="onClose" />
</slot>
<view class="modal-body">
<slot />
</view>
</mp-popup>
</template>
<style scoped>
.close-icon {
position: absolute;
top: -100rpx;
right: 0;
width: 60rpx;
height: 60rpx;
}
.modal-body {
display: flex;
flex-direction: column;
place-items: center center;
width: 570rpx;
padding: 60rpx 0 48rpx;
background: #fff;
border-radius: 20rpx;
}
</style>
vue
<app-modal v-model:visible="visible">
自定义内容...
</app-modal>
app-bottom-modal
另一种常见的表单弹窗通常从下向上弹出,顶部通常有「取消」和「确定」按钮,「取消」按钮用于重置表单和关闭弹窗,而「确定」按钮用于提交表单。
Click me to view the code
vue
<script setup>
const props = defineProps({
/**
* 确定按钮文案
* @type {string}
* @default '确定'
*/
confirmText: {
type: String,
default: '确定',
},
/**
* 取消按钮文案
* @type {string}
* @default '取消'
*/
cancelText: {
type: String,
default: '取消',
},
// 点击取消时关闭弹窗
cancelClosable: {
type: Boolean,
default: false,
},
/**
* 弹窗高度
* @type {string},
* @default '80vh'
*/
height: {
type: String,
default: '80vh',
},
})
const emit = defineEmits([
'confirm',
'cancel',
'change',
'maskClick',
// 因为 uni-app 中 listener 不能通过 v-bind 透传,所以要自行定义
'update:visible',
])
const onChange = e => {
emit('change', e)
emit('update:visible', e.show)
}
const onConfirm = () => {
emit('confirm')
}
const onCancel = () => {
emit('cancel')
if (props.cancelClosable) {
emit('update:visible', false)
}
}
</script>
<template>
<mp-popup
v-bind="$attrs"
:safeArea="false"
type="bottom"
@change="onChange"
@maskClick="emit('maskClick')"
>
<view class="app-bottom-modal modal" :style="{height: props.height}">
<slot name="header">
<view class="modal-header">
<view class="cancel-btn" @click.stop="onCancel">{{ props.cancelText }}</view>
<view class="confirm-btn" @click.stop="onConfirm">{{ props.confirmText }}</view>
</view>
</slot>
<view class="modal-body">
<slot />
</view>
</view>
</mp-popup>
</template>
<style scoped>
.modal {
width: 100%;
height: 80vh;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
background: #fff;
border-radius: 30px 30px 0 0;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 122rpx;
padding: 0 40rpx;
font-size: 30rpx;
font-weight: 500;
}
.cancel-btn {
color: rgb(51 51 51 / 50%);
}
.confirm-btn {
color: #333;
}
.modal-body {
flex: 1;
width: 100%;
padding: 0 40rpx;
overflow-y: auto;
}
</style>
vue
<script setup>
import AppBottomModal from './app-bottom-modal.vue'
const emit = defineEmits([
'confirm',
'cancel',
// 因为 uni-app 中 v-bind 不会透传事件,所以要触发一遍
'update:visible',
])
const onUpdateVisible = e => {
emit('update:visible', e)
}
const onConfirm = () => {
// 获取表单数据
const data = {}
emit('confirm', {data})
}
const onCancel = () => {
// 重置表单数据
emit('cancel')
onUpdateVisible(false)
}
</script>
<template>
<AppBottomModal
v-bind="$attrs"
height="auto"
:isMaskClick="false"
@cancel="onCancel"
@confirm="onConfirm"
@update:visible="onUpdateVisible"
>
<view class="form-container">
<!-- 一些表单项 -->
<view class="form-item">一些表单数据</view>
<view class="form-item">一些表单数据</view>
<view class="form-item">一些表单数据</view>
<view class="form-item">一些表单数据</view>
<view class="form-item">一些表单数据</view>
<view class="form-item">一些表单数据</view>
<view class="form-item">一些表单数据</view>
</view>
</AppBottomModal>
</template>
<style scoped>
.form-item {
line-height: 80rpx;
}
</style>
vue
<form-modal
v-model:visible="visibleFormModal"
@cancel="onCancel"
@confirm="onConfirm"
/>
常见问题
弹窗顶部距离
在 uni-popup
原生组件中,当 type=top
时,弹窗将从页面顶部开始,然而,这样的设定在实际页面中不够合理,因为通常页面顶部会有导航栏。如果 top=0
,那么弹窗内容可能会被导航栏遮挡。为了解决这个问题,我们对 top
默认值进行了修改。现在,当 type
设置为 top
时,top
属性的默认值会被设置为导航栏的高度 (navBarHeight
),这样弹窗会从导航栏底部开始,避免了内容被遮挡的情况。当然,你也可以通过传入 top
属性来手动调整定位,以满足特定需求。
vue
<template>
<mp-popup ref="popup" type="top" :top="100"></mp-popup>
</template>
弹窗底部距离
与顶部距离类似,当页面底部存在固定的悬浮按钮时,type=bottom
的弹窗也可能被遮挡。然而,解决这种情况的方法与处理顶部距离不同。在这种情况下,你应该在弹窗内容内部增加 padding-bottom
,以确保弹窗内容不被遮挡。我们推荐使用 mp-shadow-element 组件来实现这种效果。
vue
<template>
<view class="page">
<view class="footer-bar"> 悬浮按钮... </view>
<mp-popup ref="popup" type="top" :top="100">
<!-- 可以通过这种方式 -->
<view class="container" :style="{padding-bottom: `${footerHeight}px`}">
弹窗内容...
<!-- 更推荐这种方式 -->
<mp-shadow-element selector=".footer-bar">
</view>
</mp-popup>
</view>
</template>
TIP
对于 type=bottom
的弹窗,uni-popup
默认会在具有安全区域的设备上添加 34rpx
的底部间距。如果你不需要这个间距,你可以通过关闭 safe-area
属性来移除它,具体信息请参考 uni-popup 文档。
禁止页面滚动
详见:禁止页面滚动