鸿蒙OS&UniApp 制作自定义弹窗与模态框组件#三方框架 #Uniapp

发布于:2025-05-16 ⋅ 阅读:(20) ⋅ 点赞:(0)

UniApp 制作自定义弹窗与模态框组件

前言

在移动应用开发中,弹窗和模态框是用户交互的重要组成部分,它们用于显示提示信息、收集用户输入或确认用户操作。尽管 UniApp 提供了基础的交互组件如 uni.showModal()uni.showToast(),但这些原生组件的样式和交互方式往往难以满足设计师的要求和复杂的业务需求。

本文将详细介绍如何在 UniApp 中实现自定义弹窗和模态框组件,从基础弹窗到复杂的可交互模态框,全面提升你的应用交互体验。通过本文,你将学会如何打造灵活、美观且功能强大的弹窗组件。

弹窗组件设计思路

在开始编码前,我们需要明确自定义弹窗组件的设计目标和核心功能:

  1. 灵活性:支持不同的弹窗类型(提示、确认、输入等)
  2. 可配置性:可自定义样式、动画效果和内容
  3. 易用性:简单的调用方式,丰富的事件回调
  4. 性能优化:合理的组件复用机制,避免重复创建

基于以上设计思路,我们将实现两个核心组件:

  1. 基础弹窗组件(BasePopup):处理弹窗的显示、隐藏、动画等基础功能
  2. 高级模态框组件(Modal):在基础弹窗基础上,实现更复杂的交互和布局

基础弹窗组件实现

首先,我们来实现一个基础的弹窗组件(BasePopup),它将作为所有弹窗类型的基础:

<!-- components/base-popup/base-popup.vue -->
<template>
  <view 
    class="popup-mask" 
    :class="{ 'popup-show': showPopup }" 
    :style="{ backgroundColor: maskColor }"
    @tap="handleMaskClick"
    @touchmove.stop.prevent="preventTouchMove"
  >
    <view 
      class="popup-container" 
      :class="[
        `popup-${position}`, 
        showContent ? 'popup-content-show' : '',
        customClass
      ]"
      :style="customStyle"
      @tap.stop="() => {}"
    >
      <slot></slot>
    </view>
  </view>
</template>

<script>
export default {
  name: 'BasePopup',
  props: {
    // 是否显示弹窗
    show: {
      type: Boolean,
      default: false
    },
    // 弹窗位置:center, top, bottom, left, right
    position: {
      type: String,
      default: 'center'
    },
    // 是否允许点击遮罩关闭
    maskClosable: {
      type: Boolean,
      default: true
    },
    // 遮罩背景色
    maskColor: {
      type: String,
      default: 'rgba(0, 0, 0, 0.5)'
    },
    // 弹窗内容动画时长(ms)
    duration: {
      type: Number,
      default: 300
    },
    // 自定义弹窗样式
    customStyle: {
      type: Object,
      default: () => ({})
    },
    // 自定义弹窗类
    customClass: {
      type: String,
      default: ''
    },
    // 是否阻止页面滚动
    preventScroll: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      showPopup: false,
      showContent: false
    }
  },
  watch: {
    show: {
      handler(val) {
        if (val) {
          this.open();
        } else {
          this.close();
        }
      },
      immediate: true
    }
  },
  methods: {
    // 打开弹窗
    open() {
      this.showPopup = true;
      // 渐入动画
      setTimeout(() => {
        this.showContent = true;
      }, 50);
    },
    
    // 关闭弹窗
    close() {
      this.showContent = false;
      // 等待内容关闭动画结束后再关闭整个弹窗
      setTimeout(() => {
        this.showPopup = false;
        this.$emit('close');
      }, this.duration);
    },
    
    // 处理遮罩点击
    handleMaskClick() {
      if (this.maskClosable) {
        this.$emit('update:show', false);
      }
    },
    
    // 阻止页面滚动
    preventTouchMove() {
      if (this.preventScroll) {
        return;
      }
    }
  }
}
</script>

<style scoped>
.popup-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 999;
  opacity: 0;
  visibility: hidden;
  transition: all 0.3s ease-in-out;
  display: flex;
  justify-content: center;
  align-items: center;
}

.popup-show {
  opacity: 1;
  visibility: visible;
}

.popup-container {
  position: relative;
  opacity: 0;
  transform: scale(0.9);
  transition: all 0.3s ease-in-out;
  max-width: 90%;
  max-height: 90%;
  overflow: auto;
  background-color: #fff;
  border-radius: 12rpx;
  box-shadow: 0 0 20rpx rgba(0, 0, 0, 0.1);
}

.popup-content-show {
  opacity: 1;
  transform: scale(1);
}

/* 不同位置的弹窗样式 */
.popup-center {
  /* 默认已居中 */
}

.popup-top {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  transform: translateY(-100%);
  border-radius: 0 0 12rpx 12rpx;
}

.popup-bottom {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  transform: translateY(100%);
  border-radius: 12rpx 12rpx 0 0;
}

.popup-top.popup-content-show,
.popup-bottom.popup-content-show {
  transform: translateY(0);
}

.popup-left {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  transform: translateX(-100%);
  border-radius: 0 12rpx 12rpx 0;
}

.popup-right {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  transform: translateX(100%);
  border-radius: 12rpx 0 0 12rpx;
}

.popup-left.popup-content-show,
.popup-right.popup-content-show {
  transform: translateX(0);
}
</style>

这个基础弹窗组件具有以下特点:

  1. 支持多种弹出位置(中间、顶部、底部、左侧、右侧)
  2. 支持自定义背景色、样式和过渡动画
  3. 可配置点击遮罩是否关闭
  4. 阻止页面滚动,提升用户体验

高级模态框组件实现

在基础弹窗组件之上,我们可以实现一个功能更完善的模态框组件:

<!-- components/modal/modal.vue -->
<template>
  <base-popup
    :show="show"
    :position="position"
    :mask-closable="maskClosable"
    :mask-color="maskColor"
    :duration="duration"
    :custom-style="customStyle"
    :custom-class="customClass"
    @update:show="$emit('update:show', $event)"
    @close="$emit('close')"
  >
    <view class="modal-content">
      <!-- 标题区域 -->
      <view v-if="showHeader" class="modal-header">
        <text class="modal-title">{{ title }}</text>
        <view 
          v-if="showClose" 
          class="modal-close" 
          @tap.stop="$emit('update:show', false)"
        >✕</view>
      </view>
      
      <!-- 内容区域 -->
      <view class="modal-body" :style="bodyStyle">
        <!-- 使用默认插槽或展示传入的内容 -->
        <slot>
          <text class="modal-message">{{ message }}</text>
        </slot>
      </view>
      
      <!-- 按钮区域 -->
      <view v-if="showFooter" class="modal-footer">
        <slot name="footer">
          <view 
            v-if="showCancel" 
            class="modal-btn modal-cancel-btn" 
            :style="cancelBtnStyle"
            @tap.stop="handleCancel"
          >{{ cancelText }}</view>
          
          <view 
            v-if="showConfirm" 
            class="modal-btn modal-confirm-btn" 
            :style="confirmBtnStyle"
            @tap.stop="handleConfirm"
          >{{ confirmText }}</view>
        </slot>
      </view>
    </view>
  </base-popup>
</template>

<script>
import BasePopup from '../base-popup/base-popup.vue';

export default {
  name: 'Modal',
  components: {
    BasePopup
  },
  props: {
    // 是否显示模态框
    show: {
      type: Boolean,
      default: false
    },
    // 标题
    title: {
      type: String,
      default: '提示'
    },
    // 内容文本
    message: {
      type: String,
      default: ''
    },
    // 是否显示头部
    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: '确认'
    },
    // 弹窗位置
    position: {
      type: String,
      default: 'center'
    },
    // 是否允许点击遮罩关闭
    maskClosable: {
      type: Boolean,
      default: true
    },
    // 遮罩背景色
    maskColor: {
      type: String,
      default: 'rgba(0, 0, 0, 0.5)'
    },
    // 动画时长
    duration: {
      type: Number,
      default: 300
    },
    // 自定义样式
    customStyle: {
      type: Object,
      default: () => ({})
    },
    // 自定义内容区域样式
    bodyStyle: {
      type: Object,
      default: () => ({})
    },
    // 自定义取消按钮样式
    cancelBtnStyle: {
      type: Object,
      default: () => ({})
    },
    // 自定义确认按钮样式
    confirmBtnStyle: {
      type: Object,
      default: () => ({})
    },
    // 自定义类名
    customClass: {
      type: String,
      default: ''
    }
  },
  methods: {
    // 处理取消事件
    handleCancel() {
      this.$emit('update:show', false);
      this.$emit('cancel');
    },
    
    // 处理确认事件
    handleConfirm() {
      this.$emit('update:show', false);
      this.$emit('confirm');
    }
  }
}
</script>

<style scoped>
.modal-content {
  min-width: 560rpx;
  box-sizing: border-box;
}

.modal-header {
  position: relative;
  padding: 30rpx 30rpx 20rpx;
  border-bottom: 1rpx solid #f0f0f0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
  text-align: center;
  flex: 1;
}

.modal-close {
  position: absolute;
  right: 30rpx;
  top: 30rpx;
  width: 40rpx;
  height: 40rpx;
  font-size: 32rpx;
  color: #999;
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-body {
  padding: 40rpx 30rpx;
  max-height: 60vh;
  overflow-y: auto;
}

.modal-message {
  font-size: 28rpx;
  color: #333;
  line-height: 1.5;
  text-align: center;
  word-break: break-all;
}

.modal-footer {
  display: flex;
  padding: 20rpx 30rpx 30rpx;
  border-top: 1rpx solid #f0f0f0;
}

.modal-btn {
  flex: 1;
  height: 80rpx;
  line-height: 80rpx;
  text-align: center;
  font-size: 30rpx;
  border-radius: 80rpx;
  margin: 0 20rpx;
}

.modal-cancel-btn {
  background-color: #f5f5f5;
  color: #666;
}

.modal-confirm-btn {
  background-color: #07c160;
  color: #fff;
}
</style>

这个模态框组件在基础弹窗之上增加了以下功能:

  1. 标题栏和关闭按钮
  2. 自定义内容区域(支持插槽或文本)
  3. 底部按钮区域(支持自定义样式和文本)
  4. 完善的事件处理(确认、取消、关闭)

使用示例

1. 基础弹窗示例

<template>
  <view class="container">
    <button @tap="showBasicPopup = true">显示基础弹窗</button>
    
    <base-popup :show.sync="showBasicPopup" position="bottom">
      <view style="padding: 30rpx;">
        <text>这是一个基础弹窗</text>
      </view>
    </base-popup>
  </view>
</template>

<script>
import BasePopup from '@/components/base-popup/base-popup.vue';

export default {
  components: {
    BasePopup
  },
  data() {
    return {
      showBasicPopup: false
    }
  }
}
</script>

2. 模态框组件示例

<template>
  <view class="container">
    <button @tap="showConfirmModal">显示确认模态框</button>
    <button @tap="showCustomModal">显示自定义模态框</button>
    
    <!-- 确认模态框 -->
    <modal
      :show.sync="confirmModal"
      title="操作确认"
      message="确定要删除这个项目吗?此操作不可恢复。"
      @confirm="handleConfirm"
      @cancel="handleCancel"
    ></modal>
    
    <!-- 自定义模态框 -->
    <modal
      :show.sync="customModal"
      title="自定义内容"
      :show-footer="false"
    >
      <view class="custom-content">
        <image src="/static/images/success.png" class="success-icon"></image>
        <text class="success-text">操作成功</text>
        <text class="success-tip">将在3秒后自动关闭</text>
      </view>
    </modal>
  </view>
</template>

<script>
import Modal from '@/components/modal/modal.vue';

export default {
  components: {
    Modal
  },
  data() {
    return {
      confirmModal: false,
      customModal: false
    }
  },
  methods: {
    showConfirmModal() {
      this.confirmModal = true;
    },
    
    showCustomModal() {
      this.customModal = true;
      
      // 3秒后自动关闭
      setTimeout(() => {
        this.customModal = false;
      }, 3000);
    },
    
    handleConfirm() {
      console.log('用户点击了确认');
      // 执行确认操作
      uni.showToast({
        title: '已确认操作',
        icon: 'success'
      });
    },
    
    handleCancel() {
      console.log('用户点击了取消');
    }
  }
}
</script>

<style>
.container {
  padding: 40rpx;
}

button {
  margin-bottom: 30rpx;
}

.custom-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 30rpx;
}

.success-icon {
  width: 120rpx;
  height: 120rpx;
  margin-bottom: 20rpx;
}

.success-text {
  font-size: 32rpx;
  color: #07c160;
  font-weight: bold;
  margin-bottom: 10rpx;
}

.success-tip {
  font-size: 24rpx;
  color: #999;
}
</style>

高级应用:表单输入模态框

在实际应用中,我们经常需要通过模态框收集用户输入。下面是一个表单输入模态框的示例:

<template>
  <modal
    :show.sync="show"
    title="用户信息"
    :mask-closable="false"
    :confirm-text="'保存'"
    @confirm="handleSave"
  >
    <view class="form-container">
      <view class="form-item">
        <text class="form-label">姓名</text>
        <input class="form-input" v-model="form.name" placeholder="请输入姓名" />
      </view>
      
      <view class="form-item">
        <text class="form-label">手机号</text>
        <input class="form-input" v-model="form.phone" type="number" placeholder="请输入手机号" />
      </view>
      
      <view class="form-item">
        <text class="form-label">地址</text>
        <textarea class="form-textarea" v-model="form.address" placeholder="请输入地址"></textarea>
      </view>
    </view>
  </modal>
</template>

<script>
import Modal from '@/components/modal/modal.vue';

export default {
  name: 'FormModal',
  components: {
    Modal
  },
  props: {
    show: {
      type: Boolean,
      default: false
    },
    // 初始表单数据
    initData: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      form: {
        name: '',
        phone: '',
        address: ''
      }
    }
  },
  watch: {
    show(val) {
      if (val) {
        // 打开模态框时,初始化表单数据
        this.form = {
          name: this.initData.name || '',
          phone: this.initData.phone || '',
          address: this.initData.address || ''
        };
      }
    }
  },
  methods: {
    // 保存表单数据
    handleSave() {
      // 表单验证
      if (!this.form.name) {
        uni.showToast({
          title: '请输入姓名',
          icon: 'none'
        });
        return;
      }
      
      if (!this.form.phone) {
        uni.showToast({
          title: '请输入手机号',
          icon: 'none'
        });
        return;
      }
      
      // 验证通过,发出保存事件
      this.$emit('save', {...this.form});
    }
  }
}
</script>

<style scoped>
.form-container {
  padding: 10rpx 0;
}

.form-item {
  margin-bottom: 30rpx;
}

.form-label {
  display: block;
  font-size: 28rpx;
  color: #333;
  margin-bottom: 10rpx;
}

.form-input {
  width: 100%;
  height: 80rpx;
  border: 1rpx solid #eee;
  border-radius: 8rpx;
  padding: 0 20rpx;
  box-sizing: border-box;
  font-size: 28rpx;
}

.form-textarea {
  width: 100%;
  height: 160rpx;
  border: 1rpx solid #eee;
  border-radius: 8rpx;
  padding: 20rpx;
  box-sizing: border-box;
  font-size: 28rpx;
}
</style>

在页面中使用表单模态框:

<template>
  <view class="container">
    <button @tap="showFormModal">编辑用户信息</button>
    
    <form-modal 
      :show.sync="formModalVisible" 
      :init-data="userData"
      @save="handleSaveUserData"
    ></form-modal>
  </view>
</template>

<script>
import FormModal from '@/components/form-modal/form-modal.vue';

export default {
  components: {
    FormModal
  },
  data() {
    return {
      formModalVisible: false,
      userData: {
        name: '张三',
        phone: '13800138000',
        address: '北京市朝阳区'
      }
    }
  },
  methods: {
    showFormModal() {
      this.formModalVisible = true;
    },
    
    handleSaveUserData(data) {
      console.log('保存的用户数据:', data);
      this.userData = {...data};
      
      uni.showToast({
        title: '保存成功',
        icon: 'success'
      });
    }
  }
}
</script>

实现一个全局弹窗管理器

为了更方便地调用弹窗,我们可以实现一个全局弹窗管理器,让弹窗的使用更像系统函数调用:

// utils/popup-manager.js
import Vue from 'vue';
import Modal from '@/components/modal/modal.vue';

class PopupManager {
  constructor() {
    // 创建一个Vue实例来管理模态框
    this.modalInstance = null;
    this.initModalInstance();
  }
  
  // 初始化模态框实例
  initModalInstance() {
    const ModalConstructor = Vue.extend(Modal);
    this.modalInstance = new ModalConstructor({
      el: document.createElement('div')
    });
    document.body.appendChild(this.modalInstance.$el);
  }
  
  // 显示提示框
  alert(options = {}) {
    return new Promise(resolve => {
      const defaultOptions = {
        title: '提示',
        message: '',
        confirmText: '确定',
        showCancel: false,
        maskClosable: false
      };
      
      const mergedOptions = {...defaultOptions, ...options};
      
      this.modalInstance.title = mergedOptions.title;
      this.modalInstance.message = mergedOptions.message;
      this.modalInstance.confirmText = mergedOptions.confirmText;
      this.modalInstance.showCancel = mergedOptions.showCancel;
      this.modalInstance.maskClosable = mergedOptions.maskClosable;
      
      this.modalInstance.show = true;
      
      // 监听确认事件
      this.modalInstance.$once('confirm', () => {
        resolve(true);
      });
      
      // 处理取消事件
      this.modalInstance.$once('cancel', () => {
        resolve(false);
      });
    });
  }
  
  // 显示确认框
  confirm(options = {}) {
    const defaultOptions = {
      title: '确认',
      showCancel: true,
      cancelText: '取消',
      confirmText: '确认'
    };
    
    return this.alert({...defaultOptions, ...options});
  }
  
  // 关闭所有弹窗
  closeAll() {
    if (this.modalInstance) {
      this.modalInstance.show = false;
    }
  }
}

export default new PopupManager();

在页面中使用弹窗管理器:

import popupManager from '@/utils/popup-manager.js';

// 显示一个提示框
popupManager.alert({
  title: '操作成功',
  message: '您的操作已完成'
}).then(() => {
  console.log('用户点击了确定');
});

// 显示一个确认框
popupManager.confirm({
  title: '删除确认',
  message: '确定要删除这条记录吗?',
  confirmText: '删除'
}).then(result => {
  if (result) {
    console.log('用户确认了删除');
    // 执行删除操作
  } else {
    console.log('用户取消了删除');
  }
});

注意:上面的全局弹窗管理器代码主要适用于H5环境,在App和小程序环境中可能需要不同的实现方式。

最佳实践与优化建议

  1. 避免过多嵌套:弹窗内部尽量避免再打开多层弹窗,这会导致用户体验下降。

  2. 合理使用动画:动画可以提升用户体验,但过多或过于复杂的动画可能导致性能问题。

  3. 适配不同设备:确保弹窗在不同设备上都有良好的表现,尤其是在小屏幕设备上。

  4. 考虑无障碍访问:为弹窗添加适当的ARIA属性,提高无障碍体验。

  5. 性能优化

    • 避免在弹窗中放置过多复杂内容
    • 使用条件渲染(v-if)而不是隐藏显示(v-show)来完全移除不显示的弹窗DOM
    • 在关闭弹窗时及时清理资源
  6. 错误处理:添加适当的错误处理机制,确保弹窗显示和关闭的稳定性。

总结

本文详细介绍了如何在UniApp中实现自定义弹窗和模态框组件,从基础的弹窗到功能丰富的模态框,再到实用的表单输入模态框,全面覆盖了移动应用中常见的弹窗交互需求。通过这些组件,你可以大大提升应用的交互体验和美观度。

在实际项目中,你可以根据具体业务需求对这些组件进行扩展和优化,例如添加更多的动画效果、支持更复杂的布局、实现特定的交互逻辑等。希望本文对你的UniApp开发有所帮助!

在面对移动应用开发中各种弹窗交互的挑战时,拥有一套灵活、可定制的弹窗组件库将是你的得力助手。愿你能基于本文提供的思路和代码,打造出更加出色的用户体验。


网站公告

今日签到

点亮在社区的每一天
去签到