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