Wenzi

给 Antd 的 DatePicker 组件实现带有至今的功能

蚊子前端博客
发布于 2023/11/07 00:09
如何给 Antd 中的 DatePicker 添加「至今」功能

在一些比如选填教育经历、工作经历等场景的表单时,经常会遇到当前阶段是在职的情况,那这时应该选择「至今」。但目前 <DatePicker /> 组件并不支持这一功能,需要我们自己来实现。

<DatePicker />组件不支持直接设置中文,否则会显示Invalid Date。因此这里我们用 <Input /> 标签来日期和「至今」的文案。

选择「至今」的场景,一般是在日期区间中,在第 2 个日期选择时使用。我们先从单个的日期选择,一步步深入。

我们先看下最终的效果,然后再看实现过程。

DatePicker中的至今功能

1. 单个日期选择中的页脚 #

我们可以使用 <DatePicker />组件中的 renderExtraFooter 属性来设置「至今」按钮,然后添加点击事件。

const tillNowConfig = {
  text: "至今",
};

const DatePickerTillNow = (props) => {
  return (
    <div className="datepicker-till-now">
      <Input placeholder="结束日期" value={value} />
      <DatePicker
        showToday={false}
        {...props}
        onChange={handleChange}
        ref={ref}
        renderExtraFooter={() => (
          <div className="tillnow-btn" style={{ textAlign: "center" }}>
            <Button type="link" onClick={handleClickSoFar}>
              {tillNowConfig.text}
            </Button>
          </div>
        )}
      />
    </div>
  );
};

我们这里把 DatePicker 和 Input 重叠排布,并且让 DatePicker 在前面,然后把里面的显示标签置为透明。这样做的好处是:

  1. 能正常使用日期组件的功能,比如呼起日期面板、清除输入等;
  2. 让后置的 Input 标签进行展示,日期和中文都可以正常展示;

CSS 样式:

.datepicker-till-now {
  position: relative;

  .ant-picker {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;

    // 只隐藏最内层的显示区域
    .ant-picker-input input {
      opacity: 0;
    }
  }
}

上面还有两个变量还需要我们来实现,value 和 onChange。

1.1 日期的 value #

我们接收到 props.value 后,需要判断下是否有「至今」的标识。我们在这里约定显示「至今」的标识是:

value 对象中的 tillNow 属性为 true。

关于这个标识的问题,在最开始实现时,后端接口只能接收 datetime 的字符串格式(数据库中的格式已固定)。然后我们前后端约定是2099年表示是至今的功能。这个年份导致我在实现时,陷入了误区。当时就想着封装的组件,在选择至今时能直接得到 2099 年,但存在的一个问题是:打开日期面板重新选择时,年份会自动跳转到 2099 年,而不是今日。为了解决这个问题,又用了很多临时变量来进行存储,才实现这样的功能。

后来在写这篇文章时才想到,value 本身就是 dayjs 或者 moment 的对象,我们可以给该对象添加一个自定义属性 tillNow;这样既不影响 antd 原来组件的使用,也能标识出「至今」来。只是在调用该组件或者通过该组件得到数据提交接口时,需要把 2099 年和 tillNow 属性 互相转换一下。

先看下 tillNow 属性如何转成「至今」:

/**
 * 若value有值,则判断是否是否有 tillNow 属性;
 * 若value无值,则返回undefined;
 */
const value = useMemo(() => {
  if (props?.value) {
    if (!dayjs.isDayjs(props.value)) {
      throw new Error("DatepickerTillNow's value is not dayjs");
    }
    if (props.value.tillNow) {
      return tillNowConfig.text;
    }
    const format = props.format || "YYYY-MM-DD";
    return dayjs(props.value).format(format);
  }
  return undefined;
}, [props?.value]);

然后将该 value 给到 Input 标签即可:

<Input placeholder="结束日期" value={value} />

组件 DatePicker 中的 value 无需转换,直接使用 props.value 即可。tillNow属性对该组件也没任何影响。

1.2 日期变化 onChange #

日期切换,主要考虑两种情况:

  • DatePicker 组件本身的切换,如点击某个具体日期,或者清除日期等;
  • 点击「至今」按钮;至今并不等同于选择的今日的日期;

首先来看下 DatePicker 组件自己的 onChange 事件:

const handleChange = (value: dayjs.Dayjs) => {
  if (value) {
    // 点击某个具体日期时,清除 tillNow 属性
    value.tillNow = undefined;
  }
  props?.onChange(value);
};

当点击至今的按钮时:

// 点击至今按钮
const handleClickSoFar = () => {
  const day = dayjs();
  day.tillNow = true; // 使用 tillNow 属性标记为「至今」

  // 让 DatePicker 失去焦点,即关闭日期选择下拉框
  ref.current?.blur();
  props?.onChange(day);
};

对日期下拉面板的选择和至今按钮的点击进行处理后,告知外层组件 value 有发生变化。外层的 Form 组件会将变化后的 value 再重新告知到当前组件。

1.3 外层 Form 表单 #

外层组件在提交表单时,需要根据当前日期是否有 tillNow 属性,再转换为接口需要的格式。

const App = () => {
  const [form] = Form.useForm();

  const handleClick = () => {
    const values = form.getFieldsValue();
    if (values?.date) {
      // 若有 tillNow 属性,则将其设置为接口需要的格式
      values.date = dayjs(values.date.tillNow ? "2099-12-31" : values.date).format("YYYY-MM-DD");
    }
    console.log(values);
  };

  return (
    <Form form={form} labelCol={{ span: 5 }}>
      <Form.Item name="date" label="日期选择">
        <DatepickerTillNow />
      </Form.Item>
      <Button onClick={handleClick}>提交</Button>
    </Form>
  );
};

2. 区间日期中的「至今」页脚 #

区间日期中对「至今」的处理,与单个日期中的处理很像。只不过在区间日期中,value 是一个有两个日期的数组。我们需要对后一个日期进行处理。

用于显示中文的 Input 标签,也需要定位到结束日期的位置。

const RangePicker = () => {
  return (
    <div className="datepicker-till-now datepicker-till-now-range">
      <Input placeholder="结束日期" value={value?.[1]} />
      <DatePicker.RangePicker
        showToday={false}
        {...props}
        onCalendarChange={console.log}
        ref={ref}
        renderExtraFooter={() => (
          <div
            className="sofar-btn"
            style={{
              textAlign: "center",
              width: "50%",
              transform: "translateX(100%)",
            }}
          >
            <Button type="link" onClick={handleClickSoFar}>
              {tillNowConfig.text}
            </Button>
          </div>
        )}
      />
    </div>
  );
};

// 把有至今选项的 RangePicker 挂载到 DatepickerTillNow 上
DatepickerTillNow.RangePicker = RangePicker;

对应的 CSS 样式:

.datepicker-till-now {
  position: relative;

  .ant-picker {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0.5;

    .ant-picker-input input {
      opacity: 0;
    }
  }

  &.datepicker-till-now-range {
    .ant-input {
      text-indent: 50%;
      padding-left: 24px;
    }
    .ant-picker-range {
      .ant-picker-input input {
        opacity: 1;
      }
      & > div:nth-child(3) {
        input {
          opacity: 0;
        }
      }
    }
  }
}

2.1 value 的处理 #

区间日期的 value 是一个有两个日期的数组,我们需要对有 tillNow 属性的日期对象,进行特殊处理。

const value = useMemo(() => {
  if (Array.isArray(props.value)) {
    return props.value.map((item: dayjs.Dayjs) => {
      if (item) {
        if (item.tillNow) {
          // 有 tillNow 属性,则显示至今的文案
          return tillNowConfig.text;
        }
        // 格式化
        return dayjs(item).format(props?.format || "YYYY-MM-DD");
      }
      return item;
    });
  }
  return [];
}, [props?.value, props?.format]);

然后 Input 标签只展示 value?.[1] 的值。

2.2 日期的变化 onCalendarChange #

我们在这里使用了 onCalendarChange 属性,而不是 onChange,是因为需要用点击的第 1 个日期和「至今」按钮的日期进行拼接。而在 onChange 事件里,是拿不到刚才第 1 次点击的那个日期的。

const RangePicker = () => {
  const ref = useRef(null);
  const firstDateRef = useRef(null);

  // 点击至今按钮
  const handleClickSoFar = () => {
    const dd = dayjs();
    dd.tillNow = true;
    props?.onChange([firstDateRef.current, dd]);
    ref.current?.blur();
  };

  return (
    <div className="datepicker-till-now datepicker-till-now-range">
      <Input placeholder="结束日期" value={value?.[1]} />
      <DatePicker.RangePicker
        showToday={false}
        {...props}
        onCalendarChange={(value) => {
          if (Array.isArray(value)) {
            // 将刚才点击的第1个日期存储起来
            firstDateRef.current = value[0];
          }
        }}
        ref={ref}
        renderExtraFooter={() => (
          <div
            className="sofar-btn"
            style={{
              textAlign: "center",
              width: "50%",
              transform: "translateX(100%)",
            }}
          >
            <Button type="link" onClick={handleClickSoFar}>
              {tillNowConfig.text}
            </Button>
          </div>
        )}
      />
    </div>
  );
};

至此,两种日期中,添加「至今」按钮的功能已经实现了。

3. 总结 #

我们在上面的实现中,有很多,其实并未完全处理 props 中的参数,只是为了更快地演示下至今功能的实现。

我把代码放到 GitHub 上了:antd-datepicker-tillnow

标签:reactantd
阅读(1058)
Simple Empty
No data