本文正在参与「金石方案 . 瓜分6万现金大奖」

本文有点小长,直接看封装代码和Demo的同学请直接跳到最后边 ~

为什么会有这个主意

在办理后台开发过程中,涉及到太多的弹窗业务弹窗,其间最多的就是“增加XX数据”,“修正XX数据”,“检查XX概况数据”等弹窗类型最多。

这些弹窗组件的代码,许多都是相同的,例如组件状况,表单组件相关的办法…

所以,我简略地对Dialog组件进行的二次封装和hooks,削减了一些重复的代码

要封装什么

假如是普通弹窗运用的话,直接运用el-dialog组件已经足够了

但我仍是一个比较爱折腾的人,咱们先看看官方dialog文档有什么可以增加的功用

大概看了一下,我打算封装一下功用

  • 供给全屏操作按钮(右上角)
  • 默许供给“承认”,“封闭”按钮
  • 内部增加Loading作用

封装Dialog

承认了要封装的功用之后,先来一个简略的dialog组件。

把双向绑定处理一下,这样外部就可以直接经过v-model直接操控弹窗了。

<template>
    <el-dialog :model-value="props.modelValue"></el-dialog>
</template>
<script lang="ts" setup>
interface PropsType {
  modelValue?: boolean;
}
const props = withDefaults(defineProps<PropsType>(), {
  modelValue: false,
});
const emits = defineEmits<{
  (e: "update:modelValue"): void;
}>();
</script>

header

这儿运用到图标库@element-plus/icons-vue

如没有安装,请履行npm install @element-plus/icons-vue

运用el-dialog供给的header插槽,将全屏图表和封闭图标放置到右上角中。给el-dialog传递show-close特点封闭默许图标。

<template>
  <el-dialog :model-value="props.modelValue" :show-close="false">
    <template #header>
      <div>
        <span class="dialog-title">{{ props.title }}</span>
      </div>
      <div class="btns">
        <el-icon><FullScreen /></el-icon>
        <el-icon><Close /></el-icon>
      </div>
    </template>
  </el-dialog>
</template>
<script setup lang="ts">
import { FullScreen, Close } from "@element-plus/icons-vue";
</script>
<style lang="less" scoped>
// 处理款式
:deep(.el-dialog__header) {
  border-bottom: 1px solid #eee;
  display: flex;
  padding: 12px 16px;
  align-items: center;
  justify-content: space-between;
  margin: 0;
}
.dialog-title {
  line-height: 24px;
  font-size: 18px;
  color: #303133;
}
.btns {
  display: flex;
  align-items: center;
  i {
    margin-right: 8px;
    font-size: 16px;
    cursor: pointer;
  }
  i:last-child {
    margin-right: 0;
  }
}
</style>

弹窗的标题文字内容经过props进行传递,默许为空(''

<script lang="ts" setup>
interface PropsType {
  // 疏忽之前的代码
  title?: string;
}
const props = withDefaults(defineProps<PropsType>(), {
  title: "",
});
</script>

咱们看看现在头部的作用(这儿没传入标题,默许为''

Vue3这姿态结合hook写弹窗组件更快更高效

现在这个按钮只要款式作用,还没有写上对应的功用 ~

给他们先绑定上对应的事情和指令

<template>
    <el-dialog
    :model-value="props.modelValue"
    :show-close="false"
    :fullscreen="attrs?.fullscreen ?? isFullscreen"
    >
        <template #header>
        <div>
            <span class="dialog-title">{{ props.title }}</span>
        </div>
        <div class="btns">
            <el-icon v-if="isFullScreenBtn" @click="handleFullscreen"
            ><FullScreen
            /></el-icon>
            <el-icon @click="handleClose"><Close /></el-icon>
        </div>
        </template>
    </el-dialog>
</template>
<script setup lang="ts">
import { FullScreen, Close } from "@element-plus/icons-vue";
interface PropsType {
  title?: string;
  modelValue?: boolean;
  hiddenFullBtn?: boolean;
}
const props = withDefaults(defineProps<PropsType>(), {
  title: "",
  modelValue: false,
  hiddenFullBtn: false,
});
const emits = defineEmits<{
  (e: "update:modelValue"): void;
  (e: "close"): void;
}>();
// 当时是否处于全屏状况
const isFullscreen = ref(false);
// 是否显现全屏作用图标
const isFullScreenBtn = computed(() => {
  if (props.hiddenFullBtn) return false;
  if (attrs?.fullscreen) return false;
  return true;
});
// 敞开、封闭全屏作用
const handleFullscreen = () => {
  if (attrs?.fullscreen) return;
  isFullscreen.value = !isFullscreen.value;
};
// 封闭弹窗时向外部发送close事情
const handleClose = () => {
  emits("close");
};
</script>

再点击下全屏图标看看作用怎么样

Vue3这姿态结合hook写弹窗组件更快更高效

NICE 头部功用也就完成了

Footer

接下来,再处理下底部内容,默许供给两个按钮,分别是“承认”和“封闭”,这个称号也是可以经过props特点修正的。

两个按钮绑定点击事情,向外发送不同的事情。

<template>
  <div class="">
    <el-dialog
      v-bind="attrs"
      :model-value="props.modelValue"
      :show-close="false"
      :fullscreen="attrs?.fullscreen ?? isFullscreen"
    >
      <template #footer>
        <!-- 假如没有供给其他footer插槽,就运用默许的 -->
        <span v-if="!slots.footer" class="dialog-footer">
          <el-button type="primary" @click="handleConfirm">{{
            props.confirmText
          }}</el-button>
          <el-button @click="handleClose">{{ props.cancelText }}</el-button>
        </span>
        <!-- 运用传入进来的插槽 -->
        <slot v-else name="footer"></slot>
      </template>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import { useSlots } from "vue";
// 获取插槽
const slots = useSlots();
interface PropsType {
    title?: string;
    width?: string | number;
    isDraggable?: boolean;
    modelValue?: boolean;
    hiddenFullBtn?: boolean;
    confirmText?: string;
    cancelText?: string;
}
const props = withDefaults(defineProps<PropsType>(), {
    title: "",
    isDraggable: false,
    modelValue: false,
    hiddenFullBtn: false,
    confirmText: "承认",
    cancelText: "封闭",
});
const handleClose = () => {
    emits("close");
};
const handleConfirm = () => {
    emits("confirm");
};
</script>

Vue3这姿态结合hook写弹窗组件更快更高效

又搞定了一部分了,就剩余Content了 ~

Content

弹窗内容经过默许插槽的办法传入进来,在外层的div元素上增加v-loading标签,完成加载态。

假如你想整个弹窗完成loading作用,请把v-loading移到最外层元素即可。 注意不能是el-dialog元素上,不然无法完成 可能是el-dialog运用了teleport组件,导致v-loading无法正常作业。 等有空研究一下 ~

<template>
  <div class="">
    <el-dialog
      v-bind="attrs"
      :model-value="props.modelValue"
      :show-close="false"
      :fullscreen="attrs?.fullscreen ?? isFullscreen"
    >
        <div class="content" v-loading="props.loading">
            <slot></slot>
        </div>
    </el-dialog>
  </div>
</template>
<script lang="ts" setup>
interface PropsType {
  loading?: boolean;
}
const props = withDefaults(defineProps<PropsType>(), {
  loading: false,
});
</script>

试试看中心的loading作用

Vue3这姿态结合hook写弹窗组件更快更高效

剩余一些细节处理

el-dialog组件供给了许多个props特点供用户挑选,但咱们现在封装的dialog组件只运用到了一小部分props特点。当用户想要运用其他的props特点时该怎么办?

例如运用width特点时,莫非要在咱们封装的组件中接纳props.width再传递给<el-dialog :width="props.width" />组件吗?

不不不,还有另外一种办法,还记得刚刚在做全屏操作的时候运用到的useAttrs辅佐函数吗

它可以获取当时组件传递进来的特点。有了这个办法之后,再配兼并即可将外部传递进来的函数再传递到el-dialog组件上面啦

<el-dialog
    v-bind="attrs"
    :model-value="props.modelValue"
    :show-close="false"
    :fullscreen="attrs?.fullscreen ?? isFullscreen"
    :before-close="handleClose"
>
    <!-- 疏忽其他代码 -->
</el-dialog>

为了避免内部传递的props被掩盖掉,v-bind="attrs"需求放在最前面

在运用时,可能会给before-close特点传递一个函数,但到了后边被内部的handleClose办法给掩盖掉了。

解决方案是在handleClose函数中,获取attrs.['before-close']特点,假如类型是函数函数,先履行它。

const handleClose = () => {
  if (
    Reflect.has(attrs, "before-close") &&
    typeof attrs["before-close"] === "function"
  ) {
    attrs["before-close"]();
  }
  emits("close");
};

有关于el-dialog组件的封装就到这儿了

封装hooks

利用Vue composition Api再封装一下在运用el-dialog组件状况的办理hook

useDialog

简略处理显现和加载态开关的hook


import { ref } from "vue";
export default function useDialog() {
  const visible = ref(false);
  const loading = ref(false);
  const openDialog = () => (visible.value = true);
  const closeDialog = () => (visible.value = false);
  const openLoading = () => (loading.value = true);
  const closeLoading = () => (loading.value = false);
  return {
    visible,
    loading,
    openDialog,
    closeDialog,
    openLoading,
    closeLoading,
  };
}

useDialog Demo

Vue3这姿态结合hook写弹窗组件更快更高效

<template>
<el-button @click="openDialog1">普通弹窗</el-button>
<DialogCmp
  title="DialogCmp1"
  :hiddenFullBtn="true"
  v-model="visible1"
  @confirm="handleConfirm"
  @close="handleClose"
>
  <h3>DialogCmp1</h3>
</DialogCmp>
</template>
<script setup lang="ts">
import useDialog from "./components/useDialog";
import DialogCmp from "./components/Dialog.vue";
const {
  visible: visible1,
  openDialog: openDialog1,
  closeDialog: closeDialog1,
} = useDialog();
</script>

useDialogState 和 useDialogWithForm

useDialogState

针对开发办理后台弹窗状况封装的一个hook,调配下面的useDialogWithForm运用。

export enum MODE {
  ADD,
  EDIT,
}
import { ref } from "vue";
import { MODE } from "./types";
export default function useDialogState() {
  const mode = ref<MODE>(MODE.ADD);
  const visible = ref(false);
  const updateMode = (target: MODE) => {
    mode.value = target;
  };
  return { mode, visible, updateMode };
}

useDialogWithForm

针对表单弹窗组件封装的hooks,接纳一个formRef实例,负责操控弹窗内标题及清空表单中的校验成果,削减剩余的代码 ~

import { FormInstance } from "element-plus";
import { Ref, ref } from "vue";
import { MODE } from "./types";
import useDialogState from "./useDialogState";
export default function useDialogFn(
  formInstance: Ref<FormInstance>
) {
  const { visible, mode, updateMode } = useDialogState();
  const closeDialog = () => {
    formInstance.value.resetFields();
    visible.value = false;
  };
  const openDialog = (target: MODE) => {
    updateMode(target);
    visible.value = true;
  };
  return { visible, mode, openDialog, closeDialog };
}

useDialogWithForm Demo

Vue3这姿态结合hook写弹窗组件更快更高效

<template>
  <Dialog
    :before-close="customClose"
    @confirm="confirm"
    v-model="visible"
    :title="mode == MODE.ADD ? '增加数据' : '修正信息'"
    :confirm-text="mode == MODE.ADD ? '增加' : '修正'"
  >
    <el-form
      label-width="100px"
      :model="formData"
      ref="formDataRef"
     
      :rules="rules"
    >
      <el-form-item label="姓名" prop="name">
        <el-input v-model="formData.name" />
      </el-form-item>
      <el-form-item label="年纪" prop="age">
        <el-input v-model="formData.age" />
      </el-form-item>
      <el-form-item label="手机号码" prop="mobile">
        <el-input v-model="formData.mobile" />
      </el-form-item>
    </el-form>
  </Dialog>
</template>
<script setup lang="ts">
import { ElMessage, FormInstance } from "element-plus";
import { Ref, ref } from "vue";
import Dialog from "./Dialog.vue";
import { MODE } from "./types";
import useDialogWithForm from "./useDialogWithForm";
const rules = {
  name: {
    type: "string",
    required: true,
    pattern: /^[a-z]+$/,
    trigger: "change",
    message: "只能是英文称号哦",
    transform(value: string) {
      return value.trim();
    },
  },
  age: {
    type: "string",
    required: true,
    pattern: /^[0-9]+$/,
    trigger: "change",
    message: "年纪只能是数字哦",
    transform(value: string) {
      return value.trim();
    },
  },
  mobile: {
    type: "string",
    required: true,
    pattern:
      /^(?:(?:+|00)86)?1(?:(?:3[d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[d])|(?:9[189]))d{8}$/,
    trigger: "change",
    message: "请输入正确的手机号码",
    transform(value: string) {
      return value.trim();
    },
  },
};
interface FromDataType {
  name: string;
  age: string;
  mobile: string;
}
const formDataRef = ref<FormInstance | null>(null);
let formData = ref<FromDataType>({
  name: "",
  age: "",
  mobile: "",
});
const { visible, closeDialog, openDialog, mode } = useDialogWithForm(
  formDataRef as Ref<FormInstance>
);
const confirm = () => {
  if (!formDataRef.value) return;
  formDataRef.value.validate((valid) => {
    if (valid) {
      console.log("confirm");
      ElMessage({
        message: "提交成功",
        type: "success",
      });
      closeDialog();
    }
  });
};
const customClose = () => {
  ElMessage({
    message: "取消提交",
    type: "info",
  });
  closeDialog();
};
defineExpose({
  closeDialog,
  openDialog,
});
</script>
<style lang="less" scoped></style>

仓库地址

useDialog

Vue3这姿态结合hook写弹窗组件更快更高效

假如您觉得本文对您有协助,请帮帮忙点个star

您的反馈 是我更新的动力!