Vue3这样子结合hook写弹窗组件更快更高效

news2024/11/27 18:28:10

为什么会有这个想法

在管理后台开发过程中,涉及到太多的弹窗业务弹窗,其中最多的就是“添加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>
复制代码

我们看看现在头部的效果(这里没传入标题,默认为''

现在这个按钮只有样式效果,还没有写上对应的功能 ~

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

<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>
复制代码

再点击下全屏图标看看效果怎么样

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>
复制代码

又搞定了一部分了,就剩下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效果

剩下一些细节处理

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

<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

<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"
      style="max-width: 460px"
      :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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/85763.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DBCO-PEG-NHS,二苯并环辛炔(DBCO)PEG-NHS衍生物,分子量MV 3.4K 5K

1、产品描述&#xff1a; 二苯并环辛炔&#xff08;DBCO&#xff09;PEG-NHS衍生物可以在不需要任何金属催化剂的情况下进行化学反应。菌株促进的环辛炔和叠氮化合物的1,3-偶极环加成反应&#xff0c;也称为无铜点击反应&#xff0c;是一种双正交反应&#xff0c;可使两个分子…

【Java版oj】day05统计回文

目录 一、原题再现 二、问题分析 三、完整代码 一、原题再现 统计回文_牛客题霸_牛客网 描述 “回文串”是一个正读和反读都一样的字符串&#xff0c;比如“level”或者“noon”等等就是回文串。花花非常喜欢这种拥有对称美的回文串&#xff0c;生日的时候她得到两个礼物分别…

ADI Blackfin DSP处理器-BF533的开发详解50:RGBtoGary (图像灰度提取处理)(含源码)

硬件准备 ADSP-EDU-BF533&#xff1a;BF533开发板 AD-HP530ICE&#xff1a;ADI DSP仿真器 软件准备 Visual DSP软件 硬件链接 功能介绍 代码实现了图像灰度提取处理&#xff0c;代码运行时&#xff0c;会通过文件系统打开工程文件根目下" …/ImageView"路径中的…

OPENGL ES 2.0 知识串讲(1)――OPENGL ES 2.0 概括

前言 电脑是做什么用的? 电脑又被称为计算机,那么最重要的工作就是计算。看过三体的同学都知道, 电脑中有无数纳米级别的计算单元,通过 0 和 1 的转换,完成加减乘除的操作。 是什么使得电脑工作? 驱动,驱使着硬件完成工作。 谁来写驱动? 制造电脑的公司自己来写驱动,因…

#芯片# N25Q128A21BSF40F

数据手册下载链接&#xff1a;N25Q128A21BSF40F 0. 指令集 1.基本介绍 SPI兼容串行总线接口。108 MHz&#xff08;最大&#xff09;时钟频率。推荐工作电压&#xff1a;1.8 V。单电源电压1.7 V至2 V。支持传统SPI协议&#xff0c;Quad I/O或Dual I/O SPI协议。四路/双路I/O指…

bug的生命周期你知道吗?一张图带你看懂它!

目录 1、什么是bug 2、bug的生命周期 3、如何描述一个bug 4、bug的级别 1、什么是bug 软件的bug狭义方面可以理解为是指软件程序的漏洞或缺陷&#xff0c;广义方面除找到程序漏洞之外&#xff0c;还包括测试工程师或用户所发现和提出的软件可改进的细节、或与需求文档存在差…

【全网惟一面向软件测试人员的Python基础教程】- 你知道Python代码是怎样运行的吗?

全网惟一面向软件测试人员的Python基础教程 起点&#xff1a;《python软件测试实战宝典》介绍 第一章 为什么软件测试人员要学习Python 第二章 学Python之前要搞懂的道理 第三章 你知道Python代码是怎样运行的吗&#xff1f; 文章目录全网惟一面向软件测试人员的Python基础教程…

如何写好科研论文:Introduction(2)

导读 本系列[1]将切片介绍如何写好科研论文&#xff0c;包含了&#xff1a;摘要&#xff0c;背景介绍&#xff0c;方法&#xff0c;结果&#xff0c;讨论等。 由于翻译能力有限&#xff0c;强力推荐有需要的伙伴&#xff0c;直接在文末找到参考链接&#xff0c;阅读原文&#x…

外汇天眼:Swissquote获得CySEC许可证、BUX收购Ninety Nine

新的一周开始了&#xff0c;那么在过去的一周里备受大家关注的外汇行业新闻都有哪些呢&#xff1f;天眼君现在带大家回顾&#xff0c;比如Swissquote获得CySEC许可证、BUX收购西班牙新交易商Ninety Nine的零售经纪部门、Saxo Bank以“时机不合适”为理由终止与SPAC IPO合作。具…

12.3、后渗透测试--持久化后门

攻击主机&#xff1a; Kali 192.168.11.106靶机&#xff1a;windows server 2008 r2192.168.11.134零、为何要创建后门 当成功获取目标系统的访问权限后&#xff0c;需要寻找方法来恢复与目标主机的连接&#xff0c;而无需再进入目标系统。如果目标用户破坏了该连接&#xff0c…

Metal每日分享,虚假颜色混合滤镜效果

本案例的目的是理解如何用Metal实现虚假颜色效果滤镜&#xff0c;使用图像的亮度在两种用户指定的颜色之间进行混合&#xff1b; Demo HarbethDemo地址 实操代码 // 混合颜色 let filter C7FalseColor.init(fristColor: .blue, secondColor: .green)// 方案1: ImageView.im…

BMVC2022 | HR-VQVAE:用于图像重建和生成的基于Hierarchical Residual Learning的VQVAE

原文标题&#xff1a;Hierarchical Residual Learning Based Vector Quantized Variational Autoencoder for Image Reconstruction and Generation 链接&#xff1a;https://bmvc2022.mpi-inf.mpg.de/0636_poster.pdf paper&#xff1a;https://arxiv.org/abs/2208.04554 一…

新业务势头强劲,晨光股份转型之路走得如何?

近期&#xff0c;据网传的一份文件显示&#xff0c;上海晨光文具股份有限公司&#xff08;以下简称“晨光文具”、“晨光股份”、“晨光”“公司”&#xff09;将于11月29日起&#xff0c;暂停线上线下店面等渠道的A4纸销售。晨光股份&#xff08;603899.SH&#xff09;针对此份…

PreScan快速入门到精通第四十一讲图像分割传感器

图像分割传感器(ISS)是一种生成语义分割图像的工具。它在诸如分类、(深度)学习、行人识别、避免碰撞等应用领域非常有用。ISS通常与相机传感器结合使用,以获得被分割的图像。 摄像机传感器 ISS语义类型颜色 ISS语义目标颜…

Vue3+Vite4+Naive-UI 项目自动导入API和组件

一、Vue3常用API导入 安装依赖 pnpm i -D unplugin-auto-import进行Vite配置 文件名&#xff1a;vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import AutoImport from unplugin-auto-import/vite// https://vitejs.dev/config/ ex…

HDOCK 蛋白蛋白对接软件使用;LZerD网页在线对接

参考&#xff1a; http://hdock.phys.hust.edu.cn/ 注&#xff1a;本文章是取6BOY这蛋白&#xff0c;下载用pymol划分成DDB1-CRBN部分蛋白与BRD4部分蛋白&#xff0c;用这两个蛋白进行软件的对接测试 https://www.rcsb.org/structure/6BOY 一、HDOCK 蛋白蛋白对接软件使用 …

@Enumerated的使用

前言 今天做测试&#xff0c;发现还没有试过实体类中关于枚举项的使用&#xff0c;于是就做了个测试&#xff0c;发现了点问题&#xff08;发现的问题主要是针对我使用的ORM框架是Ebean&#xff0c;它自带了一个比Enumerated更好用的注解和方式&#xff09;。 不多说&#xf…

翻硬币(蓝桥杯C/C++B组真题详解)

目录 题目描述&#xff1a;1208. 翻硬币 - AcWing题库 题目思路&#xff1a; 代码详解&#xff1a; 题目描述&#xff1a;1208. 翻硬币 - AcWing题库 题目思路&#xff1a; 这题我们可以用模拟直接模拟操作 也可以直接推出规律 这里就讲解推出的规律 因为每次必须选择两个…

[附源码]计算机毕业设计二次元信息分享平台的设计及实现Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis MavenVue等等组成&#xff0c;B/S模式…

[操作系统笔记]请求分页管理方式

内容系听课复习所做笔记&#xff0c;图例多来自课程截图 操作系统需要提供两大功能&#xff1a; 请求调页&#xff1a;缺失页面调入内存页面置换&#xff1a;暂时不用的换出外存 页表机制 原先的页式管理使用的页表并不能满足新的需求&#xff0c;因此需要为页表增加新的页表…