在 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、明细数量必须大于 0if (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、明细数量必须大于 0if (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>
......<BasePreventReClickButtonclass="header-btn"type="primary"plain:disabled="rkMaster.stage === 1 || !rkMaster.rkNo":onClick="onSaveClick">保存</BasePreventReClickButton><BasePreventReClickButtonclass="header-btn"type="primary"plain:disabled="rkMaster.stage === 1 || !rkMaster.rkNo":onClick="onJzClick":delay="0">记账</BasePreventReClickButton>
......
</template>
总结
以上方法各有优缺点,根据项目需求选择:
-
简单场景:直接使用 Element Plus 的 loading 状态
-
全局控制:使用自定义指令
-
组合式 API:使用可组合函数
-
复杂组件:封装高阶组件