uniApp 命令式确认框组件

背景与问题

在 uniApp 开发过程中, Android 原生应用环境下存在以下限制:

动态 DOM 操作限制
Android 原生应用(非 Web 端)中无法通过 JavaScript 直接动态生成和添加 DOM 节点。这导致传统的通过 document.createElement 创建弹窗的方式在原生应用中不可用。

解决方案

核心思路

通过 Vue3 的组合式 API 结合 ref 引用对象实现组件挂载:

  1. Ref 挂载机制:将组件挂载到预先定义的 ref 节点上
  2. 平台适配:根据运行环境自动切换挂载策略
    • Web 端:动态创建 div 节点
    • Android 端:使用传入的 ref 对象进行挂载

功能

  • 实现真正的命令式调用(通过函数调用触发弹窗)
  • 支持异步加载状态和二次确认功能
  • 保持跨平台兼容性(Web/Android/iOS)

使用说明

参数说明

参数类型必填说明
optionsOptions / string配置对象或直接字符串
mountRefany必须传入已定义的 ref 对象
configCallbackFunction确认回调
cancelCallbackFunction取消回调

组件特性

1. 多形态配置支持
// 简写形式
configDialog('确认删除吗?', this.$refs.myRef)

// 完整配置
configDialog({
  msg: '确认提交?',
  isLoading: true,
  confirmAgain: '请再次确认',
  style: { width: '300px' }
}, this.$refs.myRef, handleConfirm)

使用指南

基础用法

<template>
  <!-- 定义挂载节点 -->
  <view ref="dialogRef"></view>
  
  <button @click="showDialog">打开弹窗</button>
</template>

<script setup>
const dialogRef = ref(null)

const showDialog = () => {
  configDialog(
    {
      msg: '确认执行操作?',
      configText: '确认',
      cancelText: '关闭'
    },
    dialogRef,  // 必须传入 ref 对象
    () => console.log('Confirmed!')
  )
}
</script>

二次确认实现

configDialog({
  msg: '删除后不可恢复',
  confirmAgain: '请再次确认删除'
}, c, handleDelete)
异步操作处理
const dialogRef = ref<any>()
const popupConfig = () => {
  const closeDiglog = configDialog(
    {
      msg: '确定要提交数据吗...\n调用接口',
      isLoading: true,
    },
    dialogRef,
    () => {
      setTimeout(() => {
        closeDiglog()
      }, 5000)
    },
  )
}

源代码

核心函数 configDialog

import { createApp, defineComponent, h } from 'vue'
import config from './config.vue'

interface Options {
  msg: string // 提示信息
  isLoading?: boolean // 是否显示loading
  confirmAgain?: string // 是否再次确认
  configText?: string // 确认按钮文字
  cancelText?: string // 取消按钮文字
  style?: any // 自定义样式
}

/**
 * @description 生成一个确认弹窗,支持异步加载状态和二次确认功能
 * @param {Options | string} options - 弹窗配置对象,或者直接传入字符串作为 `msg` 提示信息
 *   @property {string} msg - 必填,弹窗提示信息
 *   @property {boolean} [isLoading=false] - 选填,点击确认后按钮是否进入 loading 状态,isLoading开启时,必须根据return主动关闭弹窗
 *   @property {string} [confirmAgain=false] - 选填,是否需要二次确认,默认 false
 *   @property {string} [configText='确定'] - 选填,确认按钮文字,默认“确定”
 *   @property {string} [cancelText='取消'] - 选填,取消按钮文字,默认“取消”
 *   @property {any} [style] - 选填,自定义弹窗样式
 * @param {any} mountRef - 需要挂载弹窗的目标 `ref`(需传入一个 `ref` 对象)
 * @param {Function} [configCallback] - 选填,点击“确认”按钮时执行的回调函数
 * @param {Function} [cancelCallback] - 选填,点击“取消”按钮时执行的回调函数
 * @returns {Function} - 返回一个 `destroy` 方法,可手动调用以销毁弹窗
 */
export function configDialog(
  options: Options | string,
  mountRef: any, // 传入的 ref 对象
  configCallback?: Function,
  cancelCallback?: Function,
) {
  const popupApp = createApp(
    defineComponent({
      render() {
        return h(config, {
          options,
          configCallback,
          cancelCallback,
          destroy: this.destroy,
        })
      },
      methods: {
        destroy() {
          destroyApp()
        },
      },
    }),
  )

  const systemInfo = uni.getSystemInfoSync()
  if (systemInfo.uniPlatform === 'web') {
    const div = document.createElement('div')
    document.body.appendChild(div)
    popupApp.mount(div)
  } else {
    popupApp.mount(mountRef.value)
  }

  // 销毁实例
  const destroyApp = () => {
    popupApp.unmount()
  }

  return destroyApp
}

生成组件config

<template>
  <popupPlus :isShow="isShow" :popupStyle="options.style">
    <view class="content">{{ options.msg }}</view>
    <view class="bottom">
      <view class="btn-view" style="width: 70%">
        <nut-button type="primary" shape="square" @click="cancel">
          {{ options.cancelText }}
        </nut-button>
        <nut-button
          type="default"
          shape="square"
          :loading="loading"
          @click="handleConfirm"
        >
          {{ options.configText }}
        </nut-button>
      </view>
    </view>
    <view ref="confirmAgainRef"></view>
  </popupPlus>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
import popupPlus from '@/components/popup-plus.vue'
import { configDialog } from './index'

const popupStyle = {
  width: '400px',
}

interface Options {
  msg: string // 提示信息
  isLoading?: boolean // 是否显示loading
  confirmAgain?: string // 是否再次确认
  configText?: string // 确认按钮文字
  cancelText?: string // 取消按钮文字
  style?: any // 自定义样式
}

export default defineComponent({
  props: {
    options: {
      type: [Object, String],
      required: true,
      validator(value: any) {
        if (typeof value === 'string') return true
        if (typeof value === 'object' && value?.msg) return true
        return false
      },
    },
    configCallback: {
      type: Function,
      default: () => {},
    },
    cancelCallback: {
      type: Function,
      default: () => {},
    },
    destroy: {
      type: Function,
      required: true, // 销毁函数
    },
  },
  components: {
    popupPlus,
  },
  setup(props) {
    const loading = ref<boolean>(false)
    const isShow = ref<boolean>(true)
    const confirmAgainRef = ref<any>(null)

    const options = computed<Options>(() => {
      // 如果传入的是字符串,封装为对象并添加默认值
      if (typeof props.options === 'string') {
        return {
          msg: props.options,
          isLoading: false,
          confirmAgain: '',
          configText: '确定',
          cancelText: '取消',
          style: popupStyle,
        }
      }
      return {
        msg: props.options.msg || '',
        isLoading: props.options.isLoading ?? false,
        confirmAgain: props.options.confirmAgain ?? '',
        configText: props.options.configText ?? '确定',
        cancelText: props.options.cancelText ?? '取消',
        style: props.options.style ?? popupStyle,
      }
    })

    const cancel = () => {
      props.cancelCallback()
      closePopup()
    }

    const handleConfirm = () => {
      if (options.value.confirmAgain) {
        configDialog(
          {
            msg: options.value.confirmAgain,
            style: {
              width: '300px',
              padding: '10px',
            },
          },
          confirmAgainRef,
          executeConfirmAction,
        )
        return
      }
      executeConfirmAction()
    }

    const executeConfirmAction = () => {
      if (options.value.isLoading) {
        loading.value = true
        props.configCallback()
      } else {
        props.configCallback()
        closePopup()
      }
    }

    const closePopup = () => {
      isShow.value = false
      setTimeout(() => {
        props.destroy()
      }, 500) // 延迟销毁
    }

    return {
      loading,
      isShow,
      options,
      confirmAgainRef,
      cancel,
      handleConfirm,
    }
  },
})
</script>

<style lang="scss" scoped>
.content {
  padding: 20px 10px 30px 10px;
  text-align: center;
  font-size: 18px;
  white-space: pre-line;
}
</style>
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐