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

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

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

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

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

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

DatePicker中的至今功能-蚊子的前端博客

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

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

COPYJAVASCRIPT

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 样式:

COPYCSS

.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 属性如何转成「至今」:

COPYJAVASCRIPT

/** * 若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 标签即可:

COPYJAVASCRIPT

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

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

1.2 日期变化 onChange

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

  • DatePicker 组件本身的切换,如点击某个具体日期,或者清除日期等;

  • 点击「至今」按钮;至今并不等同于选择的今日的日期;

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

COPYJAVASCRIPT

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

当点击至今的按钮时:

COPYJAVASCRIPT

// 点击至今按钮 const handleClickSoFar = () => { const day = dayjs(); day.tillNow = true; // 使用 tillNow 属性标记为「至今」 // 让 DatePicker 失去焦点,即关闭日期选择下拉框 ref.current?.blur(); props?.onChange(day); };

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

1.3 外层 Form 表单

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

COPYJAVASCRIPT

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 标签,也需要定位到结束日期的位置。

COPYJAVASCRIPT

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 样式:

COPYCSS

.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 属性的日期对象,进行特殊处理。

COPYJAVASCRIPT

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 次点击的那个日期的。

COPYJAVASCRIPT

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

标签:
阅读(393)

公众号:

qrcode

微信公众号:前端小茶馆