在 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>
相关推荐
MiyueFE6 分钟前
14 个逻辑驱动的 UI 设计技巧,助您改善任何界面
前端·设计
啃火龙果的兔子10 分钟前
前端单元测试覆盖率工具有哪些,分别有什么优缺点
前端·单元测试
「、皓子~38 分钟前
后台管理系统的诞生 - 利用AI 1天完成整个后台管理系统的微服务后端+前端
前端·人工智能·微服务·小程序·go·ai编程·ai写作
就改了40 分钟前
Ajax——在OA系统提升性能的局部刷新
前端·javascript·ajax
凌冰_42 分钟前
Ajax 入门
前端·javascript·ajax
京东零售技术1 小时前
京东小程序JS API仓颉改造实践
前端
老A技术联盟1 小时前
从小白入门,基于Cursor开发一个前端小程序之Cursor 编程实践与案例分析
前端·小程序
风铃喵游1 小时前
构建引擎: 打造小程序编译器
前端·小程序·架构
sunbyte1 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | ThemeClock(主题时钟)
前端·javascript·css·vue.js·前端框架·tailwindcss
小飞悟1 小时前
🎯 什么是模块化?CommonJS 和 ES6 Modules 到底有什么区别?小白也能看懂
前端·javascript·设计