前语

之前写的react组件:

  • Affix组件: [react组件库源码+ 单测解析(Affix 固钉组件)]
  • Form组件:完成一个比ant-design更好form组件,可用于生产环境!
  • GridLayout组件:秒杀ant design布局组件
  • Button和ButtonGroup 按钮组件: react组件库源码+ 单测解析(Button和ButtonGroup 按钮组件)

普通的日历组件如下:

写一个比ant Design 的 日历组件(Calender)更好的组件(上)

这个组件便是ant design的日历组件,咱们一点一点完成它的主要功用,为啥标题写了一个上呢,由于咱们下半部分才会写超越ant功用的部分,便是能够对日期进行拖拽,相似

写一个比ant Design 的 日历组件(Calender)更好的组件(上)

没有这样交互的日历组件,说实话没啥用,由于这个交互太常见了,比方你在某段时间内去写一些使命,然后定时提醒自己,会议啊,开发使命啊什么的。所以我个人感觉ant 的日历组件实用性十分低。

怎样烘托每个月的数据

如下,咱们怎样烘托每个月,比方下面是2012年,11月15日的款式

写一个比ant Design 的 日历组件(Calender)更好的组件(上)

代码如下:

<Calendar firstDayOfWeek={3} />

firstDayOfWeek是3,代表日历榜首列是从星期三开端的,所以你写firstDayOfWeek = 2,那么榜首列便是星期二开端,如下

写一个比ant Design 的 日历组件(Calender)更好的组件(上)

关于日期,咱们运用了dayjs组件,每个月有多少天,直接运用daysInMonth() API即可,不用去背什么1,3,5,7…

先上代码,咱们把dom结构先看一下

            <tbody>
               // dateList便是每个月的数据
              {dateList.map((dateRow, dateRowIndex) => (
                    // dateRow是日历的每一行数据,后面会解说
                </tr>
              ))}
            </tbody>

所以这儿最重要的便是dateList是什么

  // mode为 'month' 时,构造日历列表
  const dateList = useMemo(
    () => createDateList(year, month, firstDayOfWeek, value, format),
    [year, month, firstDayOfWeek, format, value],
  );

然后咱们看一下createDateList办法

咱们简略说下思路,然后下方附上代码完成。

首先数据结构是二维数组,如下:

[[01,02,03,04,05,06,07],[ 08, 09, 10,11,12,13,14]….]

代表日期的1号,2号,3号。。。。每行烘托7个日期。

假设单纯是展现这个月,比方这个月有30天,那就很简略了,直接push从01到30即可,问题就来自,一般咱们默认榜首列是周一,那么咱们这个月的1号不一定是周一,对吧。

那么咱们就需求把上一个月的周一到这个月1号的日期填进来,同理月末,也需求填进去一些下个月的日期,由于30号不一定便是日历当月的结束星期天,对吧。

所以中心思路便是这个,咱们能够经过以下的公式求得月初1号的星期数跟榜首列之间的差多少天

// 思路:你想核算两个数之间的间隔,z为一个周期,x为已知的日期,y为方针的日期,核算方法便是 (x-y + z) %z
const lastMonthDaysCount = (这个月1号的星期数 - firstDayOfWeek(榜首列的星期数) + 7) % 7;

为什么这个公式成立,咱们能够自己比划思考一下。

然后这个月1号的星期数怎样求呢,咱们先写一个获取传入日期是星期几的函数。

/**
 * 获取一个日期是周几(1~7)
 */
export const getDay = (dt: Date): number => {
  // 这是dayjs供给的现成的办法,可是dayjs会把星期天返回0,根据咱们中国人习惯还是星期7比较合适
  let day = dayjs(dt).day();
  if (day === 0) {
    day = 7;
  }
  return day;
};

其中可经过dayjs(${year}-${month})求得当时月的1号是什么(有点废话啊,1号便是1号呗,这儿代码写的有点多此一举)

然后getDay(dayjs(${year}-${month}).toDate()),获取到这个月的榜首天是星期几了。

最终,咱们的思路便是,二维数组先push上一个月进到本月日历的日期有哪些。然后在push这个月的日期,最终再push下个月的进到本月日历的日期。

// 声明一个装载二维数组的变量
const rowList = [];
// 声明二维数组里的一维数组,用来装载日历每一行的日期
let list = [];
// 记录这是第几周
let weekCount = 1;
// 这个月的榜首个日子是什么(dayjs的格局)
const monthFirstDay = dayjs(`${year}-${month}`);
// lastMonthDaysCount是咱们上面核算的上一个月有多少日期要进到日历来
 for (let i = 0; i < lastMonthDaysCount; i++) {
    // 获取月份中榜首个日子的日期减一天,subtract是dayjs的办法
    const dayObj = monthFirstDay.subtract(i + 1, 'day');
    list.unshift(dayObj);
  }

上面的dayObj其实需求包装一下,为了不增加复杂度,咱们暂时理解为list放入的是dayjs的日期

接着,咱们增加本月的数据

// 增加本月日期
// monthDaysCount  获取当时月份包括的天数
// endOf('month')获取某月的最终一天,daysInMonth 获取当时月份包括的天数
const monthDaysCount = dayjs(`${year}-${month}`).daysInMonth();
for (let i = 0; i < monthDaysCount; i++) {
    // 获取月份中榜首个日子的日期加一天
    const dayObj = monthFirstDay.add(i, 'day');
    list.push(dayObj);
    // 由于一周有7天,list数据每次装载7个元素,所以,假如list的长度是7的话,就要新建一个list重新装数据
    if (list.length === 7) {
      rowList.push(list);
      list = [];
      weekCount += 1;
    }
  }

最终,咱们添下个月的数据

  // 增加下月日期
  if (list.length) {
   // 获取到本月最终一天的日期
    const monthLastDay = dayjs(`${year}-${month}`).endOf('month');
    // 获取到到日历结束,下一个月还需求增加多少日期进来
    const nextMonthDaysCount = 7 - list.length;
    for (let i = 0; i < nextMonthDaysCount; i++) {
      const dayObj = monthLastDay.add(i + 1, 'day');
      list.push(dayObj));
    }
    rowList.push(list);
  }

咱们能够想一下为啥上个月和下个月没有判别 list.length === 7呢,由于不可能上个月和下个月装载的数组超过7。

有了上面的逻辑,你切换年月,然后再改写视图就行了。

下面是完好核算当月日历显现多少天的代码。

/**
 * 创建日历单元格数据
 * @param year 日历年份
 * @param month 日历月份
 * @param firstDayOfWeek 周起始日(1~7)
 * @param currentValue 当时日期
 * @param format 日期格局
 */
export const createDateList = (
  year: number,
  month: number,
  firstDayOfWeek: number,
  currentValue: dayjs.Dayjs,
  format: string,
): CalendarCell[][] => {
  const createCellData = (belongTo: number, isCurrent: boolean, date: Date, weekOrder: number): CalendarCell => {
    // 获取一个日期是周几(1~7)
    const day = getDay(date);
    return {
      mode: 'month',
      belongTo,
      isCurrent,
      day,
      weekOrder,
      date,
      formattedDate: dayjs(date).format(format),
      filterDate: null,
      formattedFilterDate: null,
      isShowWeekend: true,
    };
  };
  // 获取月份中榜首个日子的日期,例如:'2022-11-01'
  const monthFirstDay = dayjs(`${year}-${month}`);
  const rowList = [] as CalendarCell[][];
  let list = [] as CalendarCell[];
  let weekCount = 1;
  // 增加上个月中会在本月显现的最终几天日期
  // getDay(monthFirstDay.toDate()) 获取到获取月份中榜首个日子是星期几
  // firstDayOfWeek 榜首天从星期几开端,仅在日历展现维度为月份时(mode = month)有用。默以为 1。可选项:1/2/3/4/5/6/7
  // lastMonthDaysCount获取当时跟你想要展现的firstDayOfWeek的间隔,思路是你想核算两个数之间的间隔,z为一个周期,x为已知的日期,y为方针的日期,核算方法便是 (x-y + z) %z
  const lastMonthDaysCount = (getDay(monthFirstDay.toDate()) - firstDayOfWeek + 7) % 7;
  for (let i = 0; i < lastMonthDaysCount; i++) {
    // 获取月份中榜首个日子的日期减一天
    const dayObj = monthFirstDay.subtract(i + 1, 'day');
    list.unshift(createCellData(-1, false, dayObj.toDate(), weekCount));
  }
  // 增加本月日期
  // monthDaysCount  获取当时月份包括的天数
  // endOf('month')获取某月的最终一天,daysInMonth 获取当时月份包括的天数
  const monthDaysCount = monthFirstDay.endOf('month').daysInMonth();
  for (let i = 0; i < monthDaysCount; i++) {
    const dayObj = monthFirstDay.add(i, 'day');
    list.push(createCellData(0, currentValue.isSame(dayObj), dayObj.toDate(), weekCount));
    if (list.length === 7) {
      rowList.push(list);
      list = [];
      weekCount += 1;
    }
  }
  // 增加下月日期
  if (list.length) {
    const monthLastDay = dayjs(`${year}-${month}`).endOf('month');
    const nextMonthDaysCount = 7 - list.length;
    for (let i = 0; i < nextMonthDaysCount; i++) {
      const dayObj = monthLastDay.add(i + 1, 'day');
      list.push(createCellData(1, false, dayObj.toDate(), weekCount));
    }
    rowList.push(list);
  }
  return rowList;
};

接着,咱们丰厚一下日历组件的功用,咱们省去什么月视图,年视图的代码,确实没啥好讲的,月和年视图难度太低了。

咱们歇息一下,接着干!

写一个比ant Design 的 日历组件(Calender)更好的组件(上)

接着,咱们看一下烘托日历的dom,有哪些需求丰厚的功用点。下面的dateList,咱们在上面已经求出来了。


 <tbody>
            {dateList.map((dateRow, dateRowIndex) => (
              <tr key={String(dateRowIndex)}>
                {dateRow.map((dateCell, dateCellIndex) => {
                  // dateCell包括哪些信息呢,咱们下面有阐明
                  // 若不显现周末,隐藏 day 为 6 或 7 的元素
                  if (!isShowWeekend && [6, 7].indexOf(dateCell.day) >= 0) return null;
                  // 其余日期正常显现
                  const isNow = dateCell.formattedDate === currentDate;
                  return (
                    <CalendarCellComp
                      key={dateCellIndex}
                      mode={mode}
                      theme={theme}
                      cell={cell}
                      cellData={dateCell}
                      cellAppend={cellAppend}
                      fillWithZero={fillWithZero}
                      isCurrent={dateCell.isCurrent}
                      isNow={isNow}
                      isDisabled={dateCell.belongTo !== 0}
                      createCalendarCell={createCalendarCell}
                      onCellClick={(event) => clickCell(event, dateCell)}
                      onCellDoubleClick={(event) => doubleClickCell(event, dateCell)}
                      onCellRightClick={(event) => rightClickCell(event, dateCell)}
                    />
                  );
                })}
              </tr>
            ))}
          </tbody>

dataCell的interface如下:

export interface CalendarCell extends ControllerOptions {
  /**
   * 用于表明日期单元格归于哪一个月份。值为 0 表明是当时日历显现的月份中的日期,值为 -1 表明是上个月的,值为 1 表明是下个月的(日历展现维度是“月”时有值)
   */
  belongTo?: number;
  /**
   * 日历单元格日期
   */
  date?: Date;
  /**
   * 日期单元格对应的星期,值为 1~7,表明周一到周日。(日历展现维度是“月”时有值)
   */
  day?: number;
  /**
   * 日历单元格日期字符串(输出日期的格局和 format 有关)
   * @default ''
   */
  formattedDate?: string;
  /**
   * 日期单元格是否为当时高亮日期或高亮月份
   */
  isCurrent?: boolean;
  /**
   * 日期在本月的第几周(日历展现维度是“月”时有值)
   */
  weekOrder?: number;
}

咱们有一个隐藏周末的功用,只要判别 day特点是否是6,7即可,如下

 if (!isShowWeekend && [6, 7].indexOf(dateCell.day) >= 0) return null;

咱们怎样判别当时日期,如下:

const isNow = dateCell.formattedDate ===  dayjs().format('YYYY-MM-DD');

接着咱们要看烘托具体日期的组件CalendarCellComp的完成了。

我把代码粘贴一下

import React, { MouseEvent } from 'react';
import { CalendarCell, TdCalendarProps } from './type';
import useConfig from '../hooks/useConfig';
import usePrefixClass from './hooks/usePrefixClass';
import { useLocaleReceiver } from '../locale/LocalReceiver';
import { blockName } from './_util';
const CalendarCellComp: React.FC<CalendarCellProps> = (props) => {
  const {
    mode, // 这个特点疏忽,咱们这儿以为是'month'即可
    cell, // 单元格插槽
    cellAppend, // 单元格插槽,在原来的内容之后追加
    theme, // 这个特点疏忽,以为是'full'即可
    isDisabled = false, // isDisabled 等于 dateCell.belongTo !== 0,belongTo是0代表本月日期,是能点击的,上个月这个值是-1,下个月是1,所以不能点击
    cellData, // cellData上面已经介绍过了
    isCurrent, // 日期单元格是否为当时高亮日期
    isNow, // 是否是今日
    fillWithZero, // 是否日期填0,比方1号,填0便是,01号
    createCalendarCell, 
    onCellClick, // 单击日历中的一个日期事情
    onCellDoubleClick, // 双击事情
    onCellRightClick, // 右击事情
  } = props;
  // 这儿会判别是否要主动补0
  const fix0 = (num: number) => {
    const fillZero = num < 10 && (fillWithZero ?? true);
    return fillZero ? `0${num}` : num;
  };
  return (
    <td
      onClick={onCellClick}
      onDoubleClick={onCellDoubleClick}
      onContextMenu={onCellRightClick}
    >
      {(() => {
        // 假如要自定义cell的话,能够传入function
        if (cell && typeof cell === 'function') return cell( createCalendarCell(cellData));
        let cellCtx = fix0(cellData.date.getDate());
        return <div>{cellCtx}</div>;
      })()}
      {(() => {
        const cellCtx =  cellAppend(createCalendarCell(cellData)
        return cellAppend && <div className={prefixCls([blockName, 'table-body-cell-content'])}>{cellCtx}</div>;
      })()}
    </td>
  );
};
export default CalendarCellComp;

上面的代码我这儿解说一下,这儿是自定义数字的

if (cell && typeof cell === 'function') return cell( createCalendarCell(cellData));
        let cellCtx = fix0(cellData.date.getDate());
        return <div>{cellCtx}</div>;
      })()}

写一个比ant Design 的 日历组件(Calender)更好的组件(上)
cellAppend是针对这个区域

写一个比ant Design 的 日历组件(Calender)更好的组件(上)

代码如下:

{(() => {
        const cellCtx =  cellAppend(createCalendarCell(cellData)
        return cellAppend && <div className={prefixCls([blockName, 'table-body-cell-content'])}>{cellCtx}</div>;
      })()}

好了,到这儿以上代码的逻辑,足以你倒腾一个ant功用相似的日历组件了,咱们下半部分会写超越ant的部分,便是日历能够设置一段日期显现在日历上,如下(参阅了蚂蚁金服同学的完成原理)

写一个比ant Design 的 日历组件(Calender)更好的组件(上)