关注

Vue3+TypeScript+Element Plus+el-form+el-table解决校验卡住问题(使用自定义校验validator后出现点击确定按钮触发校验后一直处于加载状态)

问题描述:

代码:

// 资金分配表单实例
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技术社区

原文链接:https://blog.csdn.net/BillKu/article/details/158264824

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--