在 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>
总结
以上方法各有优缺点,根据项目需求选择:
-
简单场景:直接使用 Element Plus 的 loading 状态
-
全局控制:使用自定义指令
-
组合式 API:使用可组合函数
-
复杂组件:封装高阶组件