问题描述:

代码:
// 资金分配表单实例
const allocateFormRef = ref<FormInstance | null>(null);
// 添加资金分配表单校验规则,推荐使用 computed 替换 reactive,每次依赖的验证函数或参数改变时,规则对象会重新生成
const allocateFormRules = computed<FormRules<CapitalAllocateCreateDTO>>(() => ({
deptId: [{ required: true, message: "请选择指标使用部门", trigger: "change" }],
total: [
{ required: true, message: "请输入分配金额", trigger: "change" },
{
// 自定义校验
validator: (rule, value, callback) => {
if (value < 0) {
callback(new Error("金额不能为负数"));
return; // 提前返回,避免继续执行
}
const max = capitalInfoStore.currentSelectedCapitalInfo?.capitalValidTotal || 0;
if (value > max) {
callback(new Error(`不能超过${max}`));
return; // 提前返回,避免继续执行
}
},
trigger: "blur"
}
],
budget: [{ required: true, message: "请选择预算情况", trigger: "change" }],
payType: [{ required: true, message: "请选择支出分类", trigger: "change" }],
payMode: [{ required: true, message: "请选择支出方式", trigger: "change" }]
}));
// 分配对话框 - 确定
const onAllocateConfirmClick = async () => {
if (!capitalInfoStore.currentSelectedCapitalInfo) return;
// 校验资金分配表单规则,校验不通过,退出
if (!(await validateAllocateFormRules())) return;
// 检查
if (!allocateDatas.value || allocateDatas.value.length === 0) {
ElMessage.warning("请增加分配资金");
return;
}
// if (sumAllocateTotal.value > capitalInfoStore.currentSelectedCapitalInfo.capitalValidTotal) {
// ElMessage.warning("合计的指标分配金额不能大于指标可用总额");
// return;
// }
// 永远不要直接比较浮点数是否相等或大小,而是引入一个极小的容差值(epsilon)
// 请始终使用 Math.abs(a - b) < epsilon 检查"两个数是否明显不相等",a - b > epsilon 检查"a是否明显大于b"。
const epsilon = 1e-10;
if (sumAllocateTotal.value - capitalInfoStore.currentSelectedCapitalInfo.capitalValidTotal > epsilon) {
ElMessage.warning("合计的指标分配金额不能大于指标可用总额");
return;
}
await capitalInfoStore.generateCapitalAllocateWorkflow(
capitalInfoStore.currentSelectedCapitalInfo,
allocateDatas.value
);
onAllocateCancelClick();
};
// 校验资金分配表单规则
const validateAllocateFormRules = async () => {
if (!allocateFormRef.value) return true;
// 校验标识,默认校验不通过
let rulesValid = false;
// 表单校验
await allocateFormRef.value.validate((valid, fields) => {
if (valid) {
// 规则校验通过
rulesValid = true;
} else {
// 规则校验不通过
rulesValid = false;
console.warn("规则校验不通过的属性有:", fields);
}
});
return rulesValid;
};
// 分配对话框 - 取消
const onAllocateCancelClick = () => {
allocateDialogVisible.value = false;
};
<!-- 资金分配对话框 -->
<el-dialog
class="allocate-dialog"
title="资金分配"
width="1130px"
top="0vh"
center
style="border-radius: 10px"
v-model="allocateDialogVisible"
:close-on-press-escape="true"
:close-on-click-modal="false"
:show-close="true"
draggable
overflow
@close="onAllocateCancelClick">
<template #default>
<el-form label-width="auto" style="margin: 8px 16px">
<el-row :gutter="10">
<el-col :span="24">
<el-form-item label="资金名称" label-position="right">
<el-input :model-value="capitalInfoStore.currentSelectedCapitalInfo?.capitalName" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="资金序号" label-position="right">
<el-input :model-value="capitalInfoStore.currentSelectedCapitalInfo?.capitalNo" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="指标可用总额" label-position="right">
<el-input :model-value="capitalInfoStore.currentSelectedCapitalInfo?.capitalValidTotal" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 操作栏 -->
<div class="operation">
<el-button type="primary" plain @click="onAddAllocateDetailClick">增加</el-button>
</div>
<el-form ref="allocateFormRef" :rules="allocateFormRules" :model="allocateDatas" label-width="auto">
<!-- 表格 -->
<el-table
class="allocate-table"
:data="allocateDatas"
highlight-current-row
show-summary
:summary-method="getSummaries">
<el-table-column prop="deptName" label="指标使用部门" width="250" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.deptId`" :rules="allocateFormRules.deptId">
<el-select v-model="row.deptId" clearable>
<el-option
v-for="item in departmentList"
:key="item.deptId"
:label="item.deptName"
:value="item.deptId" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="total" label="指标分配金额" width="125" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.total`" :rules="allocateFormRules.total">
<BaseTotalInput v-model="row.total" />
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="budget" label="预算情况" width="125" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.budget`" :rules="allocateFormRules.budget">
<el-select
v-model="row.budget"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option v-for="item in capitalBudgetOptions" :label="item" :value="item" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="payType" label="支出分类" width="125" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.payType`" :rules="allocateFormRules.payType">
<el-select
v-model="row.payType"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option v-for="item in capitalPayTypeOptions" :label="item" :value="item" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="payMode" label="支出方式" width="125" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.payMode`" :rules="allocateFormRules.payMode">
<el-select
v-model="row.payMode"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option v-for="item in capitalPayModeOptions" :label="item" :value="item" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="assistDeptName" label="协助部门" min-width="250" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item>
<el-select v-model="row.assistDeptId" clearable placement="left">
<el-option
v-for="item in departmentList"
:key="item.deptId"
:label="item.deptName"
:value="item.deptId" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="操作" width="90" header-align="center" align="center">
<template #default="{ row }">
<!-- 添加一个空的 el-form-item,不绑定校验规则 -->
<el-form-item>
<el-button
class="table-btn"
type="danger"
size="default"
plain
@click="onDeleteAllocateDetailClick(row)"
>删除</el-button
>
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
</template>
<!-- 模态框底部插槽,就算没有内容,也要写一个空的插槽,否则会影响布局 -->
<template #footer>
<div class="footer-div">
<BasePreventReClickButton class="btn" type="primary" @click="onAllocateConfirmClick"
>确定</BasePreventReClickButton
>
<el-button class="btn" @click="onAllocateCancelClick">取消</el-button>
</div>
</template>
</el-dialog>
请分析以下vue3项目代码:
// 资金分配表单实例
const allocateFormRef = ref<FormInstance | null>(null);
// 添加资金分配表单校验规则,推荐使用 computed 替换 reactive,每次依赖的验证函数或参数改变时,规则对象会重新生成
const allocateFormRules = computed<FormRules<CapitalAllocateCreateDTO>>(() => ({
deptId: [{ required: true, message: "请选择指标使用部门", trigger: "change" }],
total: [
{ required: true, message: "请输入分配金额", trigger: "change" },
{
// 自定义校验
validator: (rule, value, callback) => {
if (value < 0) {
callback(new Error("金额不能为负数"));
return; // 提前返回,避免继续执行
}
const max = capitalInfoStore.currentSelectedCapitalInfo?.capitalValidTotal || 0;
if (value > max) {
callback(new Error(`不能超过${max}`));
return; // 提前返回,避免继续执行
}
},
trigger: "blur"
}
],
budget: [{ required: true, message: "请选择预算情况", trigger: "change" }],
payType: [{ required: true, message: "请选择支出分类", trigger: "change" }],
payMode: [{ required: true, message: "请选择支出方式", trigger: "change" }]
}));
// 分配对话框 - 确定
const onAllocateConfirmClick = async () => {
if (!capitalInfoStore.currentSelectedCapitalInfo) return;
// 校验资金分配表单规则,校验不通过,退出
if (!(await validateAllocateFormRules())) return;
// 检查
if (!allocateDatas.value || allocateDatas.value.length === 0) {
ElMessage.warning("请增加分配资金");
return;
}
// if (sumAllocateTotal.value > capitalInfoStore.currentSelectedCapitalInfo.capitalValidTotal) {
// ElMessage.warning("合计的指标分配金额不能大于指标可用总额");
// return;
// }
// 永远不要直接比较浮点数是否相等或大小,而是引入一个极小的容差值(epsilon)
// 请始终使用 Math.abs(a - b) < epsilon 检查"两个数是否明显不相等",a - b > epsilon 检查"a是否明显大于b"。
const epsilon = 1e-10;
if (sumAllocateTotal.value - capitalInfoStore.currentSelectedCapitalInfo.capitalValidTotal > epsilon) {
ElMessage.warning("合计的指标分配金额不能大于指标可用总额");
return;
}
await capitalInfoStore.generateCapitalAllocateWorkflow(
capitalInfoStore.currentSelectedCapitalInfo,
allocateDatas.value
);
onAllocateCancelClick();
};
// 校验资金分配表单规则
const validateAllocateFormRules = async () => {
if (!allocateFormRef.value) return true;
// 校验标识,默认校验不通过
let rulesValid = false;
// 表单校验
await allocateFormRef.value.validate((valid, fields) => {
if (valid) {
// 规则校验通过
rulesValid = true;
} else {
// 规则校验不通过
rulesValid = false;
console.warn("规则校验不通过的属性有:", fields);
}
});
return rulesValid;
};
// 分配对话框 - 取消
const onAllocateCancelClick = () => {
allocateDialogVisible.value = false;
};
<!-- 资金分配对话框 -->
<el-dialog
class="allocate-dialog"
title="资金分配"
width="1130px"
top="0vh"
center
style="border-radius: 10px"
v-model="allocateDialogVisible"
:close-on-press-escape="true"
:close-on-click-modal="false"
:show-close="true"
draggable
overflow
@close="onAllocateCancelClick">
<template #default>
<el-form label-width="auto" style="margin: 8px 16px">
<el-row :gutter="10">
<el-col :span="24">
<el-form-item label="资金名称" label-position="right">
<el-input :model-value="capitalInfoStore.currentSelectedCapitalInfo?.capitalName" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="资金序号" label-position="right">
<el-input :model-value="capitalInfoStore.currentSelectedCapitalInfo?.capitalNo" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="指标可用总额" label-position="right">
<el-input :model-value="capitalInfoStore.currentSelectedCapitalInfo?.capitalValidTotal" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 操作栏 -->
<div class="operation">
<el-button type="primary" plain @click="onAddAllocateDetailClick">增加</el-button>
</div>
<el-form ref="allocateFormRef" :rules="allocateFormRules" :model="allocateDatas" label-width="auto">
<!-- 表格 -->
<el-table
class="allocate-table"
:data="allocateDatas"
highlight-current-row
show-summary
:summary-method="getSummaries">
<el-table-column prop="deptName" label="指标使用部门" width="250" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.deptId`" :rules="allocateFormRules.deptId">
<el-select v-model="row.deptId" clearable>
<el-option
v-for="item in departmentList"
:key="item.deptId"
:label="item.deptName"
:value="item.deptId" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="total" label="指标分配金额" width="125" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.total`" :rules="allocateFormRules.total">
<BaseTotalInput v-model="row.total" />
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="budget" label="预算情况" width="125" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.budget`" :rules="allocateFormRules.budget">
<el-select
v-model="row.budget"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option v-for="item in capitalBudgetOptions" :label="item" :value="item" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="payType" label="支出分类" width="125" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.payType`" :rules="allocateFormRules.payType">
<el-select
v-model="row.payType"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option v-for="item in capitalPayTypeOptions" :label="item" :value="item" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="payMode" label="支出方式" width="125" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.payMode`" :rules="allocateFormRules.payMode">
<el-select
v-model="row.payMode"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option v-for="item in capitalPayModeOptions" :label="item" :value="item" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="assistDeptName" label="协助部门" min-width="250" show-overflow-tooltip>
<template #default="{ row, $index }">
<el-form-item>
<el-select v-model="row.assistDeptId" clearable placement="left">
<el-option
v-for="item in departmentList"
:key="item.deptId"
:label="item.deptName"
:value="item.deptId" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="操作" width="90" header-align="center" align="center">
<template #default="{ row }">
<!-- 添加一个空的 el-form-item,不绑定校验规则 -->
<el-form-item>
<el-button
class="table-btn"
type="danger"
size="default"
plain
@click="onDeleteAllocateDetailClick(row)"
>删除</el-button
>
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
</template>
<!-- 模态框底部插槽,就算没有内容,也要写一个空的插槽,否则会影响布局 -->
<template #footer>
<div class="footer-div">
<BasePreventReClickButton class="btn" type="primary" @click="onAllocateConfirmClick"
>确定</BasePreventReClickButton
>
<el-button class="btn" @click="onAllocateCancelClick">取消</el-button>
</div>
</template>
</el-dialog>
请解决点击确定按钮触发校验后一直处于加载状态的问题
问题分析:
点击确定按钮后,onAllocateConfirmClick 函数中的 await validateAllocateFormRules() 一直处于 pending 状态,导致按钮加载状态无法结束。根本原因在于 allocateFormRules 中 total 字段的自定义 validator 在校验通过时未调用 callback,使得 Element Plus 的表单校验无法完成,validate 方法返回的 Promise 永远不会 resolve。
详细原因
-
Element Plus 的
validator支持异步校验,无论同步还是异步,都必须调用callback来通知校验结果:-
校验失败:调用
callback(new Error('错误信息'))。 -
校验通过:必须调用
callback()(无参数)。
-
-
当前
total的 validator 只处理了失败场景(负数、超限),遗漏了成功场景的callback()调用,导致校验过程“卡死”。
解决方案
在 validator 末尾添加 callback(),确保所有分支都调用了回调函数。
修改后的代码
javascript
const allocateFormRules = computed<FormRules<CapitalAllocateCreateDTO>>(() => ({
deptId: [{ required: true, message: "请选择指标使用部门", trigger: "change" }],
total: [
{ required: true, message: "请输入分配金额", trigger: "change" },
{
validator: (rule, value, callback) => {
if (value < 0) {
callback(new Error("金额不能为负数"));
return;
}
const max = capitalInfoStore.currentSelectedCapitalInfo?.capitalValidTotal || 0;
if (value > max) {
callback(new Error(`不能超过${max}`));
return;
}
callback(); // 必须调用,表示校验通过
},
trigger: "blur"
}
],
// ... 其余规则保持不变
}));
优化建议
validateAllocateFormRules 函数可以使用更简洁的 Promise 风格,避免手动维护 rulesValid 变量:
javascript
const validateAllocateFormRules = async () => {
if (!allocateFormRef.value) return true;
try {
await allocateFormRef.value.validate();
return true;
} catch (fields) {
console.warn("规则校验不通过的属性有:", fields);
return false;
}
};
补充说明
-
确保
BasePreventReClickButton组件在异步操作完成后能自动恢复按钮状态(通常基于 Promise 的 finally 实现)。 -
浮点数比较使用了 epsilon,做法正确,无需修改。
完成以上修正后,点击确定按钮时的校验将正常完成,按钮加载状态也会随之结束。
修改后的效果:

修改的核心代码:
// 资金分配表单实例
const allocateFormRef = ref<FormInstance | null>(null);
// 添加资金分配表单校验规则,推荐使用 computed 替换 reactive,每次依赖的验证函数或参数改变时,规则对象会重新生成
const allocateFormRules = computed<FormRules<CapitalAllocateCreateDTO>>(() => ({
deptId: [{ required: true, message: "请选择指标使用部门", trigger: "change" }],
total: [
{ required: true, message: "请输入分配金额", trigger: "change" },
{
// 自定义校验,自定义 validator 中必须调用 callback,无论成功还是失败。这是 Element Plus 异步校验的约定,遵守它才能让校验流程正常结束。
validator: (rule, value, callback) => {
if (value < 0) {
callback(new Error("金额不能为负数"));
return; // 提前返回,避免继续执行
}
const max = capitalInfoStore.currentSelectedCapitalInfo?.capitalValidTotal || 0;
if (value > max) {
callback(new Error(`不能超过${max}`));
return; // 提前返回,避免继续执行
}
callback(); // 【必须调用】,表示校验通过。如果不调用 callback(),即使条件都满足,校验也会被认为未完成,表单校验会卡住,导致确定按钮一直处于加载状态。
},
trigger: "blur"
}
],
budget: [{ required: true, message: "请选择预算情况", trigger: "change" }],
payType: [{ required: true, message: "请选择支出分类", trigger: "change" }],
payMode: [{ required: true, message: "请选择支出方式", trigger: "change" }]
}));
转载自CSDN-专业IT技术社区



