Vue3 + Element Plus 防止按钮重复点击的解决方案

news2025/6/4 13:01:19

在 Vue3 和 Element Plus 项目中,防止按钮重复点击是一个常见的需求,特别是在表单提交、支付等场景下。以下是几种实现方式:

1. 使用 Element Plus 的 loading 状态

Element Plus 的按钮组件本身就支持 loading 状态,这是最简单的方式:

vue

复制

下载

<template>
  <el-button 
    type="primary" 
    :loading="loading" 
    @click="handleSubmit"
  >
    提交
  </el-button>
</template>

<script setup>
import { ref } from 'vue';

const loading = ref(false);

const handleSubmit = async () => {
  loading.value = true;
  try {
    // 执行异步操作
    await submitForm();
  } finally {
    loading.value = false;
  }
};
</script>

2. 自定义指令实现防重复点击

可以创建一个全局指令来实现防重复点击:

javascript

复制

下载

// directives.js
import { Directive } from 'vue';

export const preventReClick: Directive = {
  mounted(el, binding) {
    el.addEventListener('click', () => {
      if (!el.disabled) {
        el.disabled = true;
        setTimeout(() => {
          el.disabled = false;
        }, binding.value || 2000);
      }
    });
  }
};

// main.js
import { preventReClick } from './directives';
app.directive('prevent-reclick', preventReClick);

使用方式:

vue

复制

下载

<el-button v-prevent-reclick="1000" @click="handleClick">提交</el-button>

3. 使用装饰器(适用于组合式 API)

可以创建一个可组合函数来防止重复点击:

javascript

复制

下载

// composables/usePreventReClick.js
import { ref } from 'vue';

export function usePreventReClick() {
  const isClicking = ref(false);
  
  const preventReClick = async (fn) => {
    if (isClicking.value) return;
    isClicking.value = true;
    try {
      await fn();
    } finally {
      isClicking.value = false;
    }
  };
  
  return {
    isClicking,
    preventReClick
  };
}

使用方式:

vue

复制

下载

<script setup>
import { usePreventReClick } from './composables/usePreventReClick';

const { isClicking, preventReClick } = usePreventReClick();

const handleSubmit = () => {
  preventReClick(async () => {
    // 执行提交逻辑
    await submitForm();
  });
};
</script>

<template>
  <el-button :loading="isClicking" @click="handleSubmit">提交</el-button>
</template>

 ts,usePreventReClick.ts

import { ref } from "vue";

type AsyncFunction = () => Promise<void>;

/**
 * 防止重复点击 hook
 * @returns
 */
export function usePreventReClick() {
  const isClicking = ref(false);
  const preventReClick = async (fn: AsyncFunction) => {
    if (isClicking.value) {
      return;
    }
    isClicking.value = true;
    try {
      await fn();
    } finally {
      isClicking.value = false;
    }
  };

  return {
    isClicking,
    preventReClick
  };
}

使用方式:

<script setup lang="ts" name="MaterialOut">
......
import { usePreventReClick } from "@/hooks/usePreventReClick";

// 防止重复点击
const { preventReClick } = usePreventReClick();

// 保存
const onSaveClick = async () => {
  // 防止重复点击
  preventReClick(async () => {
    await store.fetchSaveData();
    ElMessage.success("保存成功!");
  });
};

// 记账
const onJzClick = async () => {
  // 防止重复点击
  preventReClick(async () => {
    // 检查数据合法性
    // 1、领取人员不能为空
    if (!ckMaster.value.llPersonId) {
      ElMessage.error("请选择领取人员!");
      // 模拟点击,调用 el-cascader 的公开方法来展开下拉框
      cascaderRef.value?.togglePopperVisible(true);
      return;
    }
    // 2、必须有出库明细
    if (ckDetail.value.length === 0) {
      ElMessage.error("请点击【新增】,增加出库明细!");
      return;
    }
    // 循环遍历出库明细
    for (let i = 0; i < ckDetail.value.length; i++) {
      // 3、明细数量必须大于 0
      if (ckDetail.value[i].amount <= 0) {
        ElMessage.error("数量必须大于 0!");
        document.getElementById(`input-amount-${ckDetail.value[i].id}`)?.focus();
        (document.getElementById(`input-amount-${ckDetail.value[i].id}`) as HTMLInputElement)?.select();
        return;
      }
      // 4、明细数量不能大于库存数量
      if (ckDetail.value[i].amount > ckDetail.value[i].stockAmount) {
        ElMessage.error("数量不能大于库存数量!");
        document.getElementById(`input-amount-${ckDetail.value[i].id}`)?.focus();
        (document.getElementById(`input-amount-${ckDetail.value[i].id}`) as HTMLInputElement)?.select();
        return;
      }
      // 5、用途不能为空
      if (!ckDetail.value[i].purpose) {
        ElMessage.error("用途不能为空!");
        document.getElementById(`input-purpose-${ckDetail.value[i].id}`)?.focus();
        (<HTMLInputElement>document.getElementById(`input-purpose-${ckDetail.value[i].id}`))?.select();
        return;
      }
    }

    // 记账
    await store.fetchJzData();
    // 重新加载数据
    await store.fetchData(ckMaster.value.ckNo);
    initPageData();
    ElMessage.success("记账成功!");
  });
};
......
</script>

4. 封装高阶组件

如果需要更复杂的控制,可以封装一个高阶组件:

vue

复制

下载

<!-- PreventReClickButton.vue -->
<template>
  <el-button 
    v-bind="$attrs" 
    :loading="loading" 
    @click="handleClick"
  >
    <slot></slot>
  </el-button>
</template>

<script setup>
import { ref } from 'vue';

const props = defineProps({
  onClick: {
    type: Function,
    required: true
  },
  delay: {
    type: Number,
    default: 1000
  }
});

const loading = ref(false);

const handleClick = async () => {
  if (loading.value) return;
  loading.value = true;
  try {
    await props.onClick();
  } finally {
    setTimeout(() => {
      loading.value = false;
    }, props.delay);
  }
};
</script>

使用方式:

vue

复制

下载

<PreventReClickButton 
  type="primary" 
  :onClick="submitForm"
>
  提交
</PreventReClickButton>

ts:BasePreventReClickButton.vue

/** * 防止重复点击按钮组件(防抖按钮组件) */
<script setup lang="ts" name="BasePreventReClickButton">
import { ref } from "vue";

const props = withDefaults(
  defineProps<{
    onClick: () => Promise<void> | void;
    delay?: number;
  }>(),
  {
    delay: 0
  }
);

// 加载标识
const loading = ref(false);

// 点击事件
const handleClick = async (): Promise<void> => {
  if (loading.value) return;
  loading.value = true;
  try {
    await props.onClick();
  } finally {
    let delay = props.delay;
    if (delay < 0) delay = 0;
    setTimeout(() => {
      loading.value = false;
    }, delay);
  }
};
</script>

<template>
  <!-- v-bind="$attrs" 绑定父组件传递的所有属性 -->
  <!-- 设置当前组件的个性属性,可以覆盖父组件属性 :loading="loading" :disabled="loading" @click="handleClick" -->
  <el-button v-bind="$attrs" :loading="loading" :disabled="loading" @click="handleClick">
    <!-- 插槽 -->
    <slot></slot>
  </el-button>
</template>

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

使用方式:MaterialIn.vue

<script setup lang="ts" name="MaterialIn">
......
import BasePreventReClickButton from "@/components/base/BasePreventReClickButton.vue";

// 保存
const onSaveClick = async () => {
  await store.fetchSaveData();
  ElMessage.success("保存成功!");
};

// 记账
const onJzClick = async () => {
  // 检查数据合法性
  // 1、供应厂商不能为空
  if (!rkMaster.value.supplier) {
    ElMessage.error("请选择或输入供应厂商!");
    document.getElementById("input-supplier")?.focus();
    return;
  }
  // 2、必须有入库明细
  if (rkDetail.value.length === 0) {
    ElMessage.error("请点击【新增】,增加入库明细!");
    return;
  }
  // 循环遍历入库明细
  for (let i = 0; i < rkDetail.value.length; i++) {
    // 3、明细数量必须大于 0
    if (rkDetail.value[i].amount <= 0) {
      ElMessage.error("数量必须大于 0!");
      document.getElementById(`input-amount-${rkDetail.value[i].id}`)?.focus();
      (document.getElementById(`input-amount-${rkDetail.value[i].id}`) as HTMLInputElement)?.select();
      return;
    }
    // 4、用途不能为空
    if (!rkDetail.value[i].purpose) {
      ElMessage.error("用途不能为空!");
      document.getElementById(`input-purpose-${rkDetail.value[i].id}`)?.focus();
      (<HTMLInputElement>document.getElementById(`input-purpose-${rkDetail.value[i].id}`))?.select();
      return;
    }
  }

  // 记账
  await store.fetchJzData();
  // 重新加载数据
  await store.fetchData(rkMaster.value.rkNo);
  setInputRMB();
  ElMessage.success("记账成功!");
};
......
</script>

<template>
......
        <BasePreventReClickButton
          class="header-btn"
          type="primary"
          plain
          :disabled="rkMaster.stage === 1 || !rkMaster.rkNo"
          :onClick="onSaveClick">
          保存
        </BasePreventReClickButton>
        <BasePreventReClickButton
          class="header-btn"
          type="primary"
          plain
          :disabled="rkMaster.stage === 1 || !rkMaster.rkNo"
          :onClick="onJzClick"
          :delay="0">
          记账
        </BasePreventReClickButton>
......
</template>

总结

以上方法各有优缺点,根据项目需求选择:

  1. 简单场景:直接使用 Element Plus 的 loading 状态

  2. 全局控制:使用自定义指令

  3. 组合式 API:使用可组合函数

  4. 复杂组件:封装高阶组件

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

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

相关文章

OpenCV计算机视觉实战(9)——阈值化技术详解

OpenCV计算机视觉实战&#xff08;9&#xff09;——阈值化技术详解 0. 前言1. 全局阈值与自适应阈值2. Otsu 算法3. 实战案例&#xff1a;文档扫描中的二值化处理4. 算法对比小结系列链接 0. 前言 在图像处理领域&#xff0c;阈值化 (Binarization) 技术就像一把魔术剪刀&…

【Tauri2】049——upload

前言 这篇就看看一个简单地插件——upload Upload | Taurihttps://tauri.app/plugin/upload/upload的英文意思是“上传&#xff08;程序或信息&#xff09;”。 看来是用来上传文件的。 支持移动端 正文 安装 pnpm tauri add upload 在前后端都会安装&#xff0c;即 .plug…

4、数据标注的武林秘籍:Label-Studio vs CVAT vs Roboflow

开篇痛点&#xff1a;90%的模型效果取决于数据质量 "标注3小时&#xff0c;训练5分钟"——这是很多AI工程师的真实写照。上周有位读者训练YOLOv12时发现&#xff0c;同样的代码&#xff0c;换批数据mAP直接跌了15%&#xff0c;根本原因是标注不规范&#xff01;本文…

Linux 基础IO(上)

目录 前言 重谈文件 文件操作 1.打开和关闭 2.对文件打开之后操作 理解文件fd 1.文件fd的分配规则与重定向 2.理解shell中的重定向 3.关于Linux下一切皆文件 关于缓冲区 1.为什么要有缓冲区 2.缓冲区刷新策略的问题 3.缓冲区的位置 前言 本篇到了我们linux中的文件…

el-tree拖拽事件,限制同级拖拽,获取拖拽后节点的前后节点,同级拖拽合并父节点name且子节点加入目标节点里

node-drag-start:开始拖拽节点时触发​​(按下鼠标按钮),无论是否允许放置,此事件都会触发。 allow-drop 返回 true 才能触发@node-drag-end="handleDragend"、@node-drop="handleDrop"; (1)allow-drop:动态控制​​是否允许放置; (2)node-dr…

day62—DFS—太平洋大西洋水流问题(LeetCode-417)

题目描述 有一个 m n 的矩形岛屿&#xff0c;与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界&#xff0c;而 “大西洋” 处于大陆的右边界和下边界。 这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights &#xff0c; hei…

《Python基础》第2期:环境搭建

在开始编写 Python 代码前&#xff0c;还需要搭建 Python 的开发环境。 电脑是没办法直接读懂 Python 代码的&#xff0c;而是需要一个解释器&#xff0c;实时把代码翻译成字节码&#xff0c;字节码再转换成 0 和 1&#xff0c;电脑就能读懂了。 Python 的运行过程就是翻译一行…

WSL 安装 Debian 12 后,Linux 如何安装 curl , quickjs ?

在 WSL 的 Debian 12 系统中安装 curl 非常简单&#xff0c;你可以直接使用 APT 包管理器从官方仓库安装。以下是详细步骤&#xff1a; 1. 更新软件包索引 首先确保系统的包索引是最新的&#xff1a; sudo apt update2. 安装 curl 执行以下命令安装 curl&#xff1a; sudo…

[CSS3]vw/vh移动适配

vw/vh 目标: 能够使用vw单位设置网页元素的尺寸 相对单位相对视口的尺寸计算结果.vw全称viewport width; 1vw1/100视口宽度 vh全称viewport height; 1vh1/100视口高度 体验vw和vh单位 <!DOCTYPE html> <html lang"en"> <head><meta charset…

YOLOX 的动态标签分类(如 SimOTA)与 Anchor-free 机制解析2025.5.29

YOLOX 的动态标签分类&#xff08;如 SimOTA&#xff09;与 Anchor-free 机制是其核心改进中的两个关键部分&#xff0c;它们在目标检测中的作用和实现方式存在显著差异。以下从原理、实现细节及效果三个方面进行详细对比&#xff1a; 一、核心原理与目标 1. Anchor-free 机制…

724.寻找数组的中心下标前缀和

题目链接&#xff1a; https://leetcode.cn/problems/find-pivot-index/ 这道题目我们可以使用暴力解法&#xff0c;就一个下标前数组之和&#xff0c;再求一个下标后数组之和&#xff0c;时间复杂度达到n方&#xff0c;我们来写一下&#xff1a; int pivotIndex(vector<in…

软考-系统架构设计师-第十六章 层次式架构设计理论与实践

层次式架构设计理论与实践 16.2 表现层框架设计16.3 中间层框架设计16.4 数据访问层设计16.5 数据架构规划与设计16.6 物联网层次架构设计 软件体系结构为软件系统提供了结构、行为和属性的高级抽象&#xff0c;由构成系统的元素描述这些元素的相互作用、指导元素集成的模式以及…

Docker学习笔记:基础知识

本文是自己的学习笔记 1、什么是Docker2、Docker的架构设计2.1、镜像&#xff08;Image&#xff09;2.2、容器&#xff08;Container&#xff09;2.3、仓库&#xff08;Repository)2.4、Docker使用场景案例 1、什么是Docker Docker是基于Go语言实现的云开源项目。它的角色是作…

5.2 初识Spark Streaming

在本节实战中&#xff0c;我们初步探索了Spark Streaming&#xff0c;它是Spark的流式数据处理子框架&#xff0c;具备高吞吐量、可伸缩性和强容错能力。我们了解了Spark Streaming的基本概念和运行原理&#xff0c;并通过两个案例演示了如何利用Spark Streaming实现词频统计。…

Python趣学篇:交互式词云生成器(jieba + Tkinter + WordCloud等)

名人说&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。—— 屈原《离骚》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、为什么要做词云&#xff1f;让文字"活"起来&#xff01;二、核心…

理解解释器架构:原理、组成与运行机制全解析

目录 前言1. 什么是解释器架构2. 解释器的基本组成2.1 被解释执行的程序2.2 解释器引擎2.3 解释器内部状态2.4 程序执行的当前状态2.5 存储器模型 3. 解释器的工作原理3.1 解析源代码3.2 初始化运行环境3.3 逐条执行语法结构3.4 维护程序状态3.5 内存管理与变量作用域 4. 举例&…

2025华为OD机试真题+全流程解析+备考攻略+经验分享+Java/python/JavaScript/C++/C/GO六种语言最佳实现

华为OD全流程解析&#xff0c;备考攻略 快捷目录 华为OD全流程解析&#xff0c;备考攻略一、什么是华为OD&#xff1f;二、什么是华为OD机试&#xff1f;三、华为OD面试流程四、华为OD薪资待遇及职级体系五、ABCDE卷类型及特点六、题型与考点七、机试备考策略八、薪资与转正九、…

设计模式——桥接设计模式(结构型)

摘要 桥接设计模式是一种结构型设计模式&#xff0c;用于将抽象与实现解耦&#xff0c;使二者可以独立变化。它通过将一个类拆分为“抽象”和“实现”两部分&#xff0c;并通过桥接关系组合&#xff0c;避免了类继承层次结构过于庞大。桥接模式包含抽象类、扩充抽象类、实现类…

LLaDa——基于 Diffusion 的大语言模型 打平 LLama 3

这里分享一篇文章《Large Language Diffusion Models》&#xff0c;来自人民大学高领人工智能学院&#xff0c;一篇尝试改变传统自回归范&#xff08;预测下一个token&#xff09; LLM 架构&#xff0c;探索扩散模型在 LLM 上的作用&#xff0c;通过随机掩码-预测逆向思维&…

2. 数据结构基本概念 (2)

本文部分ppt、视频截图来自&#xff1a;[青岛大学-王卓老师的个人空间-王卓老师个人主页-哔哩哔哩视频] 1. 数据结构基本概念 1.1 数据类型和抽象数据类型 (1) 数据类型(Data Type) 概念 数据类型是一组性质相同的值的集合以及定义于这个值集合上的一组操作的总称。 在使用…