You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Hook Form结合Zod实现时间输入框的条件验证问题

解决React Hook Form + Zod实现条件时间验证的问题

核心问题分析

你当前的superRefine逻辑在勾选复选框后立即触发验证,不符合"仅在两个输入框有填写动作后才显示错误提示"的需求。需要调整校验触发条件,只有当复选框勾选,且用户已经输入了部分时间内容(from或to不为空)时,才执行必填和时间逻辑校验。

调整后的Zod Schema实现

修改hoursSchema的superRefine逻辑,增加判断条件,避免勾选后立刻报错:

import { z } from "zod";

const hoursSchema = z
  .object({
    day: z.string(),
    checked: z.boolean(),
    from: z.tuple([z.number(), z.number()]).nullish(),
    to: z.tuple([z.number(), z.number()]).nullish(),
  })
  .superRefine(({ from, to, checked }, ctx) => {
    // 未勾选复选框,直接跳过校验
    if (!checked) return;

    // 关键:只有当from或to至少有一个被填写时,才触发后续校验
    const hasTimeInput = from !== null || to !== null;
    if (!hasTimeInput) return;

    // 必填项校验
    if (!from && !to) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "请填写开始和结束时间",
        path: ["from"], // 指定错误关联到from输入框,方便RHF定位展示
      });
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "请填写开始和结束时间",
        path: ["to"],
      });
      return;
    }

    if (!from) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "请填写开始时间",
        path: ["from"],
      });
      return;
    }

    if (!to) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "请填写结束时间",
        path: ["to"],
      });
      return;
    }

    // 时间先后逻辑校验
    const [fromHour, fromMinute] = from;
    const [toHour, toMinute] = to;

    if (fromHour > toHour || (fromHour === toHour && fromMinute > toMinute)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "结束时间不能早于开始时间",
        path: ["to"],
      });
      return;
    }

    if (fromHour === toHour && fromMinute === toMinute) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "开始和结束时间不能相同",
        path: ["to"],
      });
    }
  });

export const availableHoursSchema = z.object({
  availability: z.array(hoursSchema),
});

React Hook Form组件配合实现

结合RHF的Controller和状态控制,实现输入框禁用、错误展示的完整逻辑:

import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { availableHoursSchema } from "./your-schema-path";
import type { z } from "zod";

type FormData = z.infer<typeof availableHoursSchema>;

// 初始化一周七天的默认数据
const initialAvailability = [
  { day: "周一", checked: false, from: null, to: null },
  { day: "周二", checked: false, from: null, to: null },
  { day: "周三", checked: false, from: null, to: null },
  { day: "周四", checked: false, from: null, to: null },
  { day: "周五", checked: false, from: null, to: null },
  { day: "周六", checked: false, from: null, to: null },
  { day: "周日", checked: false, from: null, to: null },
];

export default function AvailabilityForm() {
  const { control, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(availableHoursSchema),
    defaultValues: {
      availability: initialAvailability,
    },
    mode: "onChange", // 输入变化时触发校验,符合"填写后显示错误"的需求
  });

  const onSubmit = (data: FormData) => {
    console.log("提交的可用时间:", data);
    // 这里添加提交逻辑
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="availability-form">
      {initialAvailability.map((dayItem, index) => (
        <div key={dayItem.day} className="day-time-group">
          {/* 复选框Controller */}
          <Controller
            name={`availability[${index}].checked`}
            control={control}
            render={({ field: { value, onChange } }) => (
              <label className="day-checkbox">
                <input
                  type="checkbox"
                  checked={value}
                  onChange={(e) => onChange(e.target.checked)}
                />
                <span>{dayItem.day}</span>
              </label>
            )}
          />

          {/* 开始时间输入框 */}
          <Controller
            name={`availability[${index}].from`}
            control={control}
            render={({ field: { value, onChange } }) => {
              const isChecked = control._formValues.availability[index].checked;
              return (
                <div className="time-input-group">
                  <span>开始时间:</span>
                  <input
                    type="number"
                    placeholder="时"
                    disabled={!isChecked}
                    value={value ? value[0] : ""}
                    onChange={(e) => {
                      const hour = Math.max(0, Math.min(23, parseInt(e.target.value) || 0));
                      onChange(value ? [hour, value[1]] : [hour, 0]);
                    }}
                    min={0}
                    max={23}
                  />
                  <span>:</span>
                  <input
                    type="number"
                    placeholder="分"
                    disabled={!isChecked}
                    value={value ? value[1] : ""}
                    onChange={(e) => {
                      const minute = Math.max(0, Math.min(59, parseInt(e.target.value) || 0));
                      onChange(value ? [value[0], minute] : [0, minute]);
                    }}
                    min={0}
                    max={59}
                  />
                  {/* 展示开始时间的错误 */}
                  {errors.availability?.[index]?.from && (
                    <p className="error-text">{errors.availability[index].from.message}</p>
                  )}
                </div>
              );
            }}
          />

          {/* 结束时间输入框 */}
          <Controller
            name={`availability[${index}].to`}
            control={control}
            render={({ field: { value, onChange } }) => {
              const isChecked = control._formValues.availability[index].checked;
              return (
                <div className="time-input-group">
                  <span>结束时间:</span>
                  <input
                    type="number"
                    placeholder="时"
                    disabled={!isChecked}
                    value={value ? value[0] : ""}
                    onChange={(e) => {
                      const hour = Math.max(0, Math.min(23, parseInt(e.target.value) || 0));
                      onChange(value ? [hour, value[1]] : [hour, 0]);
                    }}
                    min={0}
                    max={23}
                  />
                  <span>:</span>
                  <input
                    type="number"
                    placeholder="分"
                    disabled={!isChecked}
                    value={value ? value[1] : ""}
                    onChange={(e) => {
                      const minute = Math.max(0, Math.min(59, parseInt(e.target.value) || 0));
                      onChange(value ? [value[0], minute] : [0, minute]);
                    }}
                    min={0}
                    max={59}
                  />
                  {/* 展示结束时间的错误 */}
                  {errors.availability?.[index]?.to && (
                    <p className="error-text">{errors.availability[index].to.message}</p>
                  )}
                </div>
              );
            }}
          />
        </div>
      ))}

      <button type="submit" className="submit-btn">提交可用时间</button>
    </form>
  );
}

关键优化点说明

  1. 校验触发条件调整:通过hasTimeInput判断,只有用户填写了至少一个时间输入框时才触发校验,避免勾选复选框后立刻报错。
  2. 错误路径绑定:在Zod的addIssue中指定path,让错误信息精准关联到对应的输入框,方便RHF正确展示。
  3. 输入状态控制:根据复选框状态控制时间输入框的禁用状态,初始全部禁用,勾选后才允许输入。
  4. 输入值边界处理:对时间输入做了0-23(小时)、0-59(分钟)的边界限制,避免无效值进入校验逻辑。

内容的提问来源于stack exchange,提问作者rudzky

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 23:48:13