在 Vue3 中优雅封装 Element Plus Dialog:打造实用的 UseDialog Hooks

前言

在 Vue3 项目开发中,对话框是一个使用频率极高的组件。Element Plus 提供了功能强大的 Dialog 组件,但在实际业务中,痛点太多,维护弹窗变量,关闭弹窗清除数据状态,固定代码量等等,本文将分享如何将 Element Plus 的 Dialog 组件封装成一个灵活易用的 Hooks,大幅提升开发效率。

1.实现内容

  1. 函数式调用
  2. 自定义且异步加载dialog组件内容并支持骨架屏
  3. 统一维护visible变量和loading状态
  4. 统一操作方法并异步回调
  5. 弹窗每次开启都是新建的dom 不会遗留上次打开留下的数据状态

2.使用示例代码 (写法基本都是固定的)

hooks使用示例:

javascript 复制代码
import { useDialog } from '@/components/UseDialog';
useDialog({
  title: "title",
  width: "60%", // 或者 '1200px'
  dialogID: 'demo-id', // 同时有多个弹窗打开需要用到,否则只会打开一个弹窗
  component: defineAsyncComponent(() => import("xxx.vue")),
  demo1: '2', // 参数名自己定义
  actions: [
    {type: "default", label: "关闭", key: "close"},
    {type: "primary", label: "保存", key: "save"}
  ]
}).then((...args) => console.log(...args, '我是异步执行的'))

引入组件示例

xml 复制代码
<template>
  <div>
    我是你自定义引入的组件,
  </div>
</template>

<script setup lang="ts">
defineExpose({ handleAction });
const props = defineProps({}) // 自定义传入
const emits = defineEmits(["close", "loading"]);

// 这是固定的方法,用来处理弹窗的操作 与 接口交互相关
async function handleAction() {
  try {
    emits("loading", true);
    const data = {};
    await xxx(data);
    ElMessage.success("操作成功");
  } finally {
    emits("loading", false);
    emits("close", {我是传到外面的参数});
  }
}
</script>

<style lang="scss" scoped></style>

3.封装的hooks代码

代码目录

UseDialog

  • index.js
  • index.vue

index.js代码:

javascript 复制代码
import FinderDialog from "./index.vue";
import { render, createVNode } from "vue";
import { app } from "@/main"; // 需要再main.ts文件中将app暴漏出来

export function useDialog(params) {
  return new Promise((resolve) => {
    const id = params.dialogID
      ? "__demo_dialog_id__" + params.dialogID
      : "__demo_dialog_id__";
    let el;
    if (!(el = document.getElementById(id))) {
      el = document.createElement("div");
      el.id = id;
      document.body.appendChild(el);
    }

    // 创建VNode
    const vnode = createVNode(FinderDialog, {
      attrs: params,
      data: params.data,
      component: params.component,
      state: true,
      onDestroy: (...args) => {
        resolve(...args);
        // 清理DOM
        render(null, el);
        if (el.parentNode) {
          el.parentNode.removeChild(el);
        }
      },
    });

    // 使用应用实例的上下文
    vnode.appContext = app._context;

    // 渲染到DOM
    render(vnode, el);
  });
}

index.vue代码:

ini 复制代码
<template>
  <el-dialog
    ref="dialog"
    v-bind="attrs"
    append-to-body
    :close-on-click-modal="false"
    :show-close="!loading"
    :model-value="visible"
    @close="handleClose"
  >
    <el-config-provider :locale="locale">
      <Suspense>
        <template #default>
          <component
            :is="component"
            ref="commonDialog"
            v-loading="loading"
            v-bind="attrs"
            :data="data"
            :element-loading-text="loadingText"
            @close="handleClose"
            @loading="setLoading"
          />
        </template>
        <template #fallback>
          <el-skeleton :rows="3" animated />
        </template>
      </Suspense>
    </el-config-provider>
    <template v-if="attrs.actions" #footer>
      <div>
        <el-button
          v-for="(item, index) in attrs.actions"
          :key="index"
          :loading="loading"
          v-bind="item"
          @click="handleAction(item)"
        >
          {{ item.label }}
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script setup>
import { useAppStore } from "@/store";  // 这是多语言相关的 不用可以去掉
defineOptions({
  name: "FinderDialog",
  inheritAttrs: false,
});

const props = defineProps({
  data: {
    type: Array,
  },
  loadingText: {
    type: String,
    default: "正在处理中...",
  },
  component: {
    type: [Function, Object],
    required: true,
    default: null,
  },
  state: {
    type: Boolean,
  },
  attrs: {
    type: Object,
    default: () => ({}),
  },
});

const appStore = useAppStore();
const emit = defineEmits(["destroy"]);
const visible = ref(false);
const loading = ref(false);
const commonDialog = ref(null);
const dialog = ref(null);
const locale = computed(() => appStore.locale);

onMounted(() => {
  visible.value = props.state;
});

async function handleAction(item) {
  if (item.key === "close") {
    handleClose();
    return;
  }
  const actionHandler = commonDialog.value?.handleAction;
  actionHandler && actionHandler(item);
}

function handleClose(...args) {
  visible.value = false;
  emit("destroy", ...args);
}

function setLoading(state) {
  loading.value = state;
}
</script>
相关推荐
风清扬雨7 分钟前
Vue3具名插槽用法全解——从零到一的详细指南
前端·javascript·vue.js
海盗强32 分钟前
Vue 3 常见的通信方式
javascript·vue.js·ecmascript
大熊猫今天吃什么35 分钟前
【一天一坑】空数组,使用 allMatch 默认返回true
前端·数据库
!win !1 小时前
Tailwind CSS一些你需要记住的原子类
前端·tailwindcss
前端极客探险家1 小时前
打造一个 AI 面试助手:输入岗位 + 技术栈 → 自动生成面试问题 + 标准答案 + 技术考点图谱
前端·人工智能·面试·职场和发展·vue
橘子味的冰淇淋~2 小时前
【解决】Vue + Vite + TS 配置路径别名成功仍爆红
前端·javascript·vue.js
利刃之灵2 小时前
03-HTML常见元素
前端·html
kidding7232 小时前
gitee新的仓库,Vscode创建新的分支详细步骤
前端·gitee·在仓库创建新的分支
听风吹等浪起2 小时前
基于html实现的课题随机点名
前端·html
leluckys2 小时前
flutter 专题 六十三 Flutter入门与实战作者:xiangzhihong8Fluter 应用调试
前端·javascript·flutter