likes
comments
collection
share

数形结合谈日期选择器对时分秒的动态禁用

作者站长头像
站长
· 阅读数 11

场景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. 根据实际不可选日期区间(第1步得到的区间)、最早日期、最晚日期在时间上的先后关系,获取实际禁用区间
  3. 根据选中的日期(天)去实际不可选日期区间(第2步得到的区间)找出和选中日期同一天的日期,这些日期的时分秒就是需要禁用的,根据先后关系去禁用时(hour)
  4. 根据选中的时(hour),选中的日期(天)找出哪些分是需要禁用的,根据先后关系去禁用分(minute)
  5. 根据选中的时(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
评论
请登录