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

React Material UI出生日期输入组件的日期验证实现求助

实现React Material UI出生日期组件的日期验证功能

我开发了一个基于React Material UI的用户出生日期注册组件,拆分为日、月、年三个独立输入框。需求是:当用户输入无效日期(例如31-Feb-3000)并点击提交按钮时,在对应的日和年输入框显示红色错误边框,并展示“无效日期/无效年份”类的提示文字。以下是实现该功能的具体方案:

实现步骤

1. 添加错误状态管理

在组件中新增状态变量,跟踪日、年输入框的错误状态及是否触发过提交验证:

const [errors, setErrors] = useState({
  day: '',
  year: ''
});
const [isSubmitted, setIsSubmitted] = useState(false);

2. 编写日期验证函数

创建验证函数,检查年份有效性(范围、数值合理性)和日期有效性(避免Date自动修正无效日期):

const validateDate = () => {
  const newErrors = { day: '', year: '' };
  const currentYear = new Date().getFullYear();

  // 验证年份:非空、不早于1900、不晚于当前年份
  if (!year || year < 1900 || year > currentYear) {
    newErrors.year = intl.formatMessage({ defaultMessage: '无效年份' });
  }

  // 验证日期:确保构造的日期与输入值完全匹配,避免Date自动修正
  if (day && month && year) {
    const testDate = new Date(year, month - 1, day);
    if (
      testDate.getFullYear() !== year ||
      testDate.getMonth() !== month - 1 ||
      testDate.getDate() !== day
    ) {
      newErrors.day = intl.formatMessage({ defaultMessage: '无效日期' });
    }
  } else if (day) {
    newErrors.day = intl.formatMessage({ defaultMessage: '请选择完整日期' });
  }

  setErrors(newErrors);
  return !newErrors.day && !newErrors.year;
};

3. 修改提交逻辑

在提交函数中先执行验证,验证通过后再调用接口更新数据:

const editDateOfBirth = () => {
  setIsSubmitted(true);
  const isValid = validateDate();
  
  // 验证不通过则终止提交
  if (!isValid) return;

  // 原有提交逻辑保持不变
  let newYear;
  let newMonth;
  let newDate;

  const oldYear = dob?.getFullYear() || null;
  const oldMonth = dob?.getMonth() + 1 || null;
  const oldDate = dob?.getDate() || null;

  if (oldMonth !== month) {
    newMonth = month;
  }

  if (oldYear !== year) {
    newYear = year;
  }

  if (oldDate !== day) {
    newDate = day;
  }

  const oldDob = `${oldYear}-${oldMonth}-${oldDate}`;
  const newDob = `${newYear || oldYear}-${newMonth || oldMonth}-${
    newDate || oldDate
  }`;

  const oldDobFormatted = !oldDob.includes('null')
    ? format(new Date(oldDob), 'yyyy-MM-dd')
    : null;
  const newDobFormatted = format(new Date(newDob), 'yyyy-MM-dd');

  onEdit(oldDobFormatted, newDobFormatted);
};

4. 更新输入框组件

给日、年输入框添加错误状态绑定,输入时自动清除对应错误提示:

// 日输入框
<TextField
  sx={{ width: 100 }}
  value={day}
  onChange={(e) => {
    setDay(Number(e.target.value));
    if (isSubmitted) {
      setErrors(prev => ({ ...prev, day: '' }));
    }
  }}
  placeholder={intl.formatMessage({ defaultMessage: 'DD' })}
  label={intl.formatMessage({ defaultMessage: 'Day' })}
  inputProps={{
    inputMode: 'numeric',
    pattern: '[0-9]*',
    min: 1,
    max: maxDay || 31,
  }}
  type="number"
  error={isSubmitted && !!errors.day}
  helperText={isSubmitted && errors.day}
/>

// 年输入框
<TextField
  sx={{ width: 100 }}
  value={year}
  onChange={(e) => {
    setYear(Number(e.target.value));
    if (isSubmitted) {
      setErrors(prev => ({ ...prev, year: '' }));
    }
  }}
  placeholder={intl.formatMessage({ defaultMessage: 'YYYY' })}
  label={intl.formatMessage({ defaultMessage: 'Year' })}
  inputProps={{
    inputMode: 'numeric',
    pattern: '[0-9]*',
    max: new Date().getFullYear(),
  }}
  type="number"
  error={isSubmitted && !!errors.year}
  helperText={isSubmitted && errors.year}
/>

5. 月份切换时清除日期错误

当月度变化时,之前的日期可能失效,自动清除日期错误提示:

<Select
  sx={{ width: 100 }}
  labelId="month-select"
  value={month}
  label={intl.formatMessage({ defaultMessage: 'Month' })}
  onChange={(e) => {
    setMonth(Number(e.target.value));
    if (isSubmitted) {
      setErrors(prev => ({ ...prev, day: '' }));
    }
  }}
>
  {months.map(m => (
    <MenuItem key={m.monthString} value={m.monthNumber}>
      {m.monthString}
    </MenuItem>
  ))}
</Select>

修改后的完整组件代码

import { format } from '@lib/date-util';
import {
  Box,
  FormControl,
  InputLabel,
  MenuItem,
  Select,
  Stack,
  TextField
} from '@mui/material';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { calculateAge, formatOnlyDateToLocale, monthsList } from '../../utils/date-util';
import { EditableDetailTemplate } from './EditableDetailTemplate';

export const DateOfBirth = ({ dateOfBirth, onEdit }) => {
  const intl = useIntl();
  const [editValue, setEditValue] = useState(dateOfBirth);
  const [errors, setErrors] = useState({ day: '', year: '' });
  const [isSubmitted, setIsSubmitted] = useState(false);

  const dob = dateOfBirth ? new Date(dateOfBirth) : null;

  const [month, setMonth] = useState(dob?.getMonth() + 1);
  const [year, setYear] = useState(dob?.getFullYear());
  const [day, setDay] = useState(dob?.getDate());

  const fullDate =
    Boolean(year) && Boolean(month) ? new Date(`${year}-${month}-01`) : null;

  const maxDay = fullDate
    ? new Date(fullDate.getFullYear(), fullDate.getMonth() + 1, 0).getDate()
    : null;

  const validateDate = () => {
    const newErrors = { day: '', year: '' };
    const currentYear = new Date().getFullYear();

    // 验证年份
    if (!year || year < 1900 || year > currentYear) {
      newErrors.year = intl.formatMessage({ defaultMessage: '无效年份' });
    }

    // 验证日期(需确保构造的日期与输入一致,避免Date自动修正)
    if (day && month && year) {
      const testDate = new Date(year, month - 1, day);
      if (
        testDate.getFullYear() !== year ||
        testDate.getMonth() !== month - 1 ||
        testDate.getDate() !== day
      ) {
        newErrors.day = intl.formatMessage({ defaultMessage: '无效日期' });
      }
    } else if (day) {
      newErrors.day = intl.formatMessage({ defaultMessage: '请选择完整日期' });
    }

    setErrors(newErrors);
    return !newErrors.day && !newErrors.year;
  };

  const editDateOfBirth = () => {
    setIsSubmitted(true);
    const isValid = validateDate();
    
    if (!isValid) return;

    let newYear;
    let newMonth;
    let newDate;

    const oldYear = dob?.getFullYear() || null;
    const oldMonth = dob?.getMonth() + 1 || null;
    const oldDate = dob?.getDate() || null;

    if (oldMonth !== month) {
      newMonth = month;
    }

    if (oldYear !== year) {
      newYear = year;
    }

    if (oldDate !== day) {
      newDate = day;
    }

    const oldDob = `${oldYear}-${oldMonth}-${oldDate}`;
    const newDob = `${newYear || oldYear}-${newMonth || oldMonth}-${
      newDate || oldDate
    }`;

    const oldDobFormatted = !oldDob.includes('null')
      ? format(new Date(oldDob), 'yyyy-MM-dd')
      : null;
    const newDobFormatted = format(new Date(newDob), 'yyyy-MM-dd');

    onEdit(oldDobFormatted, newDobFormatted);
  };

  useEffect(() => {
    setEditValue(dateOfBirth);
  }, [dateOfBirth]);

  const months = monthsList();

  return (
    <EditableDetailTemplate
      onStartEditing={() => {
        setEditValue(editValue);
        // 进入编辑模式时重置错误状态
        setIsSubmitted(false);
        setErrors({ day: '', year: '' });
      }}
      onDoneEditing={() => {
        editDateOfBirth(month, year, day);
      }}
      displayComponent={
        <Box sx={{ display: 'flex', alignItems: 'center' }}>
          <DobAndAge dateOfBirth={editValue} />
        </Box>
      }
      editComponent={
        <Stack
          direction="row"
          justifyContent="flex-start"
          alignItems="flex-start"
          spacing={3}
        >
          <TextField
            sx={{ width: 100 }}
            value={day}
            onChange={(e) => {
              setDay(Number(e.target.value));
              if (isSubmitted) {
                setErrors(prev => ({ ...prev, day: '' }));
              }
            }}
            placeholder={intl.formatMessage({ defaultMessage: 'DD' })}
            label={intl.formatMessage({ defaultMessage: 'Day' })}
            inputProps={{
              inputMode: 'numeric',
              pattern: '[0-9]*',
              min: 1,
              max: maxDay || 31,
            }}
            type="number"
            error={isSubmitted && !!errors.day}
            helperText={isSubmitted && errors.day}
          />
          <FormControl>
            <InputLabel id="month-select">{intl.formatMessage({ defaultMessage: 'Month' })}</InputLabel>
            <Select
              sx={{ width: 100 }}
              labelId="month-select"
              value={month}
              label={intl.formatMessage({ defaultMessage: 'Month' })}
              onChange={(e) => {
                setMonth(Number(e.target.value));
                if (isSubmitted) {
                  setErrors(prev => ({ ...prev, day: '' }));
                }
              }}
            >
              {months.map(m => (
                <MenuItem key={m.monthString} value={m.monthNumber}>
                  {m.monthString}
                </MenuItem>
              ))}
            </Select>
          </FormControl>
          <TextField
            sx={{ width: 100 }}
            value={year}
            onChange={(e) => {
              setYear(Number(e.target.value));
              if (isSubmitted) {
                setErrors(prev => ({ ...prev, year: '' }));
              }
            }}
            placeholder={intl.formatMessage({ defaultMessage: 'YYYY' })}
            label={intl.formatMessage({ defaultMessage: 'Year' })}
            inputProps={{
              inputMode: 'numeric',
              pattern: '[0-9]*',
              max: new Date().getFullYear(),
            }}
            type="number"
            error={isSubmitted && !!errors.year}
            helperText={isSubmitted && errors.year}
          />
        </Stack>
      }
    />
  );
};

const DobAndAge = ({ dateOfBirth }) =>
  dateOfBirth
    ? `${formatOnlyDateToLocale(dateOfBirth)} 
(age: ${calculateAge(dateOfBirth)})`
    : '';

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 15:06:29