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

在 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

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

使用方式:

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

TypeScript 复制代码
/** * 防止重复点击按钮组件(防抖按钮组件) */
<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

TypeScript 复制代码
<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. 复杂组件:封装高阶组件

相关推荐
看到我请叫我铁锤10 小时前
vue3中THINGJS初始化步骤
前端·javascript·vue.js·3d
q***252111 小时前
SpringMVC 请求参数接收
前端·javascript·算法
谢尔登11 小时前
defineProperty如何弥补数组响应式不足的缺陷
前端·javascript·vue.js
涔溪12 小时前
实现将 Vue2 子应用通过无界(Wujie)微前端框架接入到 Vue3 主应用中(即 Vue3 主应用集成 Vue2 子应用)
vue.js·微前端·wujie
T***u33313 小时前
前端框架在性能优化中的实践
javascript·vue.js·前端框架
jingling55514 小时前
vue | 在 Vue 3 项目中集成高德地图(AMap)
前端·javascript·vue.js
油丶酸萝卜别吃14 小时前
Vue3 中如何在 setup 语法糖下,通过 Layer 弹窗组件弹出自定义 Vue 组件?
前端·vue.js·arcgis
J***Q29220 小时前
Vue数据可视化
前端·vue.js·信息可视化
JIngJaneIL21 小时前
社区互助|社区交易|基于springboot+vue的社区互助交易系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·社区互助
ttod_qzstudio1 天前
深入理解 Vue 3 的 h 函数:构建动态 UI 的利器
前端·vue.js