前语
之前写的react组件:
- Affix组件: [react组件库源码+ 单测解析(Affix 固钉组件)]
- Form组件:完成一个比ant-design更好form组件,可用于生产环境!
- GridLayout组件:秒杀ant design布局组件
- Button和ButtonGroup 按钮组件: react组件库源码+ 单测解析(Button和ButtonGroup 按钮组件)
普通的日历组件如下:
这个组件便是ant design的日历组件,咱们一点一点完成它的主要功用,为啥标题写了一个上呢,由于咱们下半部分才会写超越ant功用的部分,便是能够对日期进行拖拽,相似
没有这样交互的日历组件,说实话没啥用,由于这个交互太常见了,比方你在某段时间内去写一些使命,然后定时提醒自己,会议啊,开发使命啊什么的。所以我个人感觉ant 的日历组件实用性十分低。
怎样烘托每个月的数据
如下,咱们怎样烘托每个月,比方下面是2012年,11月15日的款式
代码如下:
<Calendar firstDayOfWeek={3} />
firstDayOfWeek是3,代表日历榜首列是从星期三开端的,所以你写firstDayOfWeek = 2,那么榜首列便是星期二开端,如下
关于日期,咱们运用了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;
};
接着,咱们丰厚一下日历组件的功用,咱们省去什么月视图,年视图的代码,确实没啥好讲的,月和年视图难度太低了。
咱们歇息一下,接着干!
接着,咱们看一下烘托日历的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>;
})()}
cellAppend是针对这个区域
代码如下:
{(() => {
const cellCtx = cellAppend(createCalendarCell(cellData)
return cellAppend && <div className={prefixCls([blockName, 'table-body-cell-content'])}>{cellCtx}</div>;
})()}
好了,到这儿以上代码的逻辑,足以你倒腾一个ant功用相似的日历组件了,咱们下半部分会写超越ant的部分,便是日历能够设置一段日期显现在日历上,如下(参阅了蚂蚁金服同学的完成原理)