在 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>
相关推荐
又又呢44 分钟前
前端面试题总结——webpack篇
前端·webpack·node.js
dog shit2 小时前
web第十次课后作业--Mybatis的增删改查
android·前端·mybatis
我有一只臭臭2 小时前
el-tabs 切换时数据不更新的问题
前端·vue.js
七灵微2 小时前
【前端】工具链一本通
前端
Nueuis3 小时前
微信小程序前端面经
前端·微信小程序·小程序
_r0bin_5 小时前
前端面试准备-7
开发语言·前端·javascript·fetch·跨域·class
IT瘾君5 小时前
JavaWeb:前端工程化-Vue
前端·javascript·vue.js
zhang98800005 小时前
JavaScript 核心原理深度解析-不停留于表面的VUE等的使用!
开发语言·javascript·vue.js
potender5 小时前
前端框架Vue
前端·vue.js·前端框架
站在风口的猪11086 小时前
《前端面试题:CSS预处理器(Sass、Less等)》
前端·css·html·less·css3·sass·html5