在一些比如选填教育经历、工作经历等场景的表单时,经常会遇到当前阶段是在职的情况,那这时应该选择「至今」。但目前 <DatePicker />
组件并不支持这一功能,需要我们自己来实现。
<DatePicker />
组件不支持直接设置中文,否则会显示Invalid Date
。因此这里我们用 <Input />
标签来日期和「至今」的文案。
选择「至今」的场景,一般是在日期区间中,在第 2 个日期选择时使用。我们先从单个的日期选择,一步步深入。
我们先看下最终的效果,然后再看实现过程。
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 在前面,然后把里面的显示标签置为透明。这样做的好处是:
能正常使用日期组件的功能,比如呼起日期面板、清除输入等;
让后置的 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。