Skip to content

基础弹窗组件 - mp-popup

这是一个基于 uni-popup 封装的弹窗组件,相较于 uni-popup 组件,它解决了以下问题:

  1. 支持通过 visible 属性来控制弹窗的显示和隐藏。
  2. 解决了在嵌套弹窗情况下的遮罩定位问题。

因此,如果在项目中需要自定义弹窗,建议始终使用这个封装的组件作为基础组件

基本用法

使用方式与 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 文档

禁止页面滚动

详见:禁止页面滚动

相关文档