数形结合谈日期选择器对时分秒的动态禁用
场景1:日期选择器(antd的DatePicker)选择不同的日期类型(年、年-季度、年-月、年-月-日、年-月-日 时、年-月-日 时:分、年-月-日 时:分:秒),并且支持日期范围限制(包括不可选日期范围区间、最早日期、最晚日期)
场景2:时间选择器(antd的TimePicker)选择不同的日期类型(时、时:分、时:分:秒),并且支持时间范围限制(包括不可选时间范围区间、最早时间、最晚时间)

注意:当处于最早日期和最晚日期之内的某个日期在不可选日期区间内,则该日期为不可选(即不可选日期优先级高),处于最早日期和最晚日期之外的日期均为不可选(时间选择同理),不可选日期区间、最早日期、最晚日期均非必填
如果是你,会怎么思考呢?
这还不好办吗,DatePicker禁用日期是根据disabledDate属性来的,返回true表示禁用该日期,既然不可选日期优先级更高,禁用日期的时候就先去遍历不可选日期区间,如果该日期不在里面,就继续和最早、最晚日期比较,这样就可以禁用日期了
这样确实可以禁用年月日,但是我们忽略了时分秒了,时分秒是需要根据你选中的日期来动态禁用的,例如最早日期是2022-12-23 18:10:10,当你选中了2022-12-23后,你只能选18:10:10之后的时间,那么时分秒该如何禁用呢?
当日期组件DatePicker需要禁用时分秒时,需要借助disabledTime属性,disabledTime的类型如下:
type DisabledTime = (now: Dayjs) => {
disabledHours?: () => number[];
disabledMinutes?: (selectedHour: number) => number[];
disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[];
};
禁用时间是分别对时分秒进行处理,返回需要禁用的数字数组(比如你要禁用10点之前的小时,则disabledHours:[0,1,2,3,4,5,6,7,8,9])
我们已经知道了禁用时分秒的规则,那么我们具体该如何实现上述需求?其实里面要抠的细节还挺多的呢!接下来让我们一步步来思考这个问题,我们从局部到整体,就以“年-月-日 时:分:秒”这个日期格式来进行思考
思考:
- 我们先把日期给禁用了(年-月-日)
- 只有选中了临界日期才需要去考虑时分秒的禁用问题,选中的不是临界日期时分秒是不需要禁用的,我们需要判断选中的日期是否是临界日期
- 应该要找出所有禁用日期的日期区间,且这些区间是有序的且不会重叠
通过以上思考,总结出下面几个步骤:
- 获取没有重叠的不可选日期区间(为什么有这一步,因为不可选区间没有限制,完全有可能会出现重叠),就是数学上的取不可选日期区间的并集
- 根据实际不可选日期区间(第1步得到的区间)、最早日期、最晚日期在时间上的先后关系,获取实际禁用区间
- 根据选中的日期(天)去实际不可选日期区间(第2步得到的区间)找出和选中日期同一天的日期,这些日期的时分秒就是需要禁用的,根据先后关系去禁用时(hour)
- 根据选中的时(hour),选中的日期(天)找出哪些分是需要禁用的,根据先后关系去禁用分(minute)
- 根据选中的时(hour)和分(minute),选中的日期(天)找出哪些秒是需要禁用的,根据先后关系去禁用秒(second)
思路还是很清晰的,我们就用代码来一一实现吧!
获取不可选日期区间的并集

const disRange = [
['2022-10-10 10:10:10', '2022-12-10 10:10:10'],
['2022-12-09 10:10:10', '2022-12-12 10:10:10'],
['2022-12-13 10:10:10', '2022-12-20 10:10:10']
]
变为:
const disRange = [
['2022-10-10 10:10:10', '2022-12-12 10:10:10'],
['2022-12-13 10:10:10', '2022-12-20 10:10:10']
]
这里我的做法是对disRange进行排序,按照每一项的开始时间进行排序,然后取出前一项的结束时间和后一项的开始时间比,如果前一项的结束时间小于后一项的开始时间,表示这两项没有交集,如果前一项的结束时间大于等于后一项的开始时间表示有交集,则需要比较前一项的结束时间是否比后一项的结束时间小,如果大,则前一项完全包含后一项,否则取出他们时间并集
// 取多个时间范围的并集
export cosnt compose = (arr: [string, string][]) => {
arr = arr.sort((a, b) => {
let startA = moment(a[0]).valueOf();
let startB = moment(b[0]).valueOf();
return startA - startB;
});
let res = [];
let temp = [arr[0]];
let len = arr.length;
let i = 0;
if (len < 2) {
return arr;
}
while (i < len - 1) {
temp = compare(temp[temp.length - 1], arr[i + 1])!;
if (temp.length === 2) {
// 无交集
res.push(temp[0]);
}
i += 1;
}
res.push(temp[temp.length - 1]);
return res;
};
// 比较时间
export const compare = (first: [string, string], second: [string, string]) => {
const left = [new Date(first[0]), new Date(first[1])];
const right = [new Date(second[0]), new Date(second[1])];
if (left[1] < right[0]) {
// 没有交集
return [first, second];
} else { // 有交集
return left[1] < right[1]
? [
first.startTime,
second.endTime
]
: [
first.startTime,
first.endTime
];
};
获取实际禁用区间
这里需要思考返回的数组结构是怎么样的
- 需要区分是开始时间还是结束时间节点,因为开始时间和结束时间点是不需要禁用的,用数学术语来说的话就是开区间,而不可选区间的临界点是需要禁用的,即闭区间
- 对于区间来说,当无限早或者无限晚的时间我们也需要表示出来,这里我想到的是Number.POSITIVE_INFINITY表示无限大,Number.NEGATIVE_INFINITY表示无限小,且在时间轴上他们也是开区间
所以最终需要得到的数据格式为:[formatTimeObjType, formatTimeObjType][]
export enum DisabledTypeEnum {
AFTER = 'after',
BEFORE = 'before',
SAME_AND_AFTER = 'sameAndAfter',
SAME_AND_BEFORE = 'sameAndBefore',
}
export type formatTimeObjType = {
time: string | typeof Number.NEGATIVE_INFINITY | typeof Number.POSITIVE_INFINITY;
type: DisabledTypeEnum;
};
我们举几个例子,尝试着获取实际禁用区间
- 最早时间、最晚时间在同一个disRange内,全禁用

[
[
{ time: Number.NEGATIVE_INFINITY, type: DisabledTypeEnum.AFTER },
{ time: Number.POSITIVE_INFINITY, type: DisabledTypeEnum.BEFORE },
]
];
- 最早时间、最晚时间在disRange外的同一区间

[
[
{ time: Number.NEGATIVE_INFINITY, type: DisabledTypeEnum.AFTER },
{ time: startTime, type: DisabledTypeEnum.BEFORE },
],
[
{ time: endTime, type: DisabledTypeEnum.AFTER },
{ time: Number.POSITIVE_INFINITY, type: DisabledTypeEnum.BEFORE },
],
]
- 最早时间、最晚时间一个在disRange内,一个在disRange外

[
[
{ time: Number.NEGATIVE_INFINITY, type: DisabledTypeEnum.AFTER },
{ time: disRange[startIndex][1], type: DisabledTypeEnum.SAME_AND_BEFORE },
],
...sliceDisRange,
[
{ time: endTime, type: DisabledTypeEnum.AFTER },
{ time: Number.POSITIVE_INFINITY, type: DisabledTypeEnum.BEFORE },
],
];
- 最早时间、最晚时间在不同区间disRange内

[
[
{ time: Number.NEGATIVE_INFINITY, type: DisabledTypeEnum.AFTER },
{ time: disRange[startIndex][1], type: DisabledTypeEnum.SAME_AND_BEFORE },
],
...sliceDisRange,
[
{ time: disRange[endIndex][0], type: DisabledTypeEnum.SAME_AND_AFTER },
{ time: Number.POSITIVE_INFINITY, type: DisabledTypeEnum.BEFORE },
],
];
获取实际禁用区间需要非常有耐心,按照数形结合的方式去找
禁用时(hour)
1、根据实际禁用区间找出和选中日期同一天的日期集合dateArray 2、遍历dateArray里每一项的type来获取要禁用的小时
- type为这样的日期A:DisabledTypeEnum.BEFORE,需要把A之前的小时禁用
- type为这样的日期A:DisabledTypeEnum.AFTER,需要把A之后的小时禁用
- type为这样的日期A:DisabledTypeEnum.SAME_AND_AFTER,需要结合该日期的下一个时间点的小时数来一起禁用(这里有一个细节:当该时间在整时上,即分秒都是0,需要把该hour禁用)
disabledHours: () => {
let disHours: number[] = [];
for (let i = 0; i < dateArray.length; i++) {
const item = dateArray[i];
const next = dateArray?.[i + 1];
const hour = getTimeByType(item.time, 'hour');
const minute = getTimeByType(item.time, 'minute');
const second = getTimeByType(item.time, 'second');
const nextHour = getTimeByType(next?.time, 'hour');
// [-Infinity, A] 需要把A之前的小时禁用
if (item.type === DisabledTypeEnum.BEFORE) {
disHours.push(...this.range(0, hour));
} else if (item.type === DisabledTypeEnum.AFTER) {
// [A,Infinity] 需要把A之后的小时禁用
disHours.push(...this.range(hour + 1, 24));
} else if (item.type === DisabledTypeEnum.SAME_AND_AFTER) {
if (next) {
// 当该时间在整时上,即分秒都是0,需要把该hour禁用
if (minute === 0 && second === 0) {
disHours.push(...this.range(hour, nextHour + 1));
} else {
// 当该时间不在整时上,不需要把该hour禁用
disHours.push(...this.range(hour + 1, nextHour + 1));
}
} else {
if (minute === 0 && second === 0) {
disHours.push(...this.range(hour, 24));
} else {
disHours.push(...this.range(hour + 1, 24));
}
break;
}
}
}
return disHours;
},
禁用分(minute)
和禁用时类似,日期集合dateArray为根据实际禁用区间找出和选中日期同一天,和选中小时同一小时的日期
禁用秒(second)
和禁用时类似,日期集合dateArray为根据实际禁用区间找出和选中日期同一天,和选中小时同一小时,和选中分钟同一分钟的日期
转载自:https://juejin.cn/post/7180280361300983868