用react手寫一個(gè)簡(jiǎn)單的日歷
共 29126字,需瀏覽 59分鐘
·
2024-07-26 08:45
大廠技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
設(shè)計(jì)實(shí)現(xiàn)一個(gè)簡(jiǎn)單版本的日歷。支持定義日歷的排放順序,以周幾作為開始。如下圖:
-
先看效果:https://rodchen-king.github.io/react-calendar/components/calendar -
源代碼:https://github.com/rodchen-king/react-calendar
設(shè)計(jì)(以最常用的按月份的日歷)
日歷其實(shí)大家都很熟悉,一切的設(shè)計(jì)都是從功能出發(fā),這是根本。日歷的功能分為兩大塊。
-
日歷頭部:當(dāng)前年份/月份。 -
日歷主體:當(dāng)前月份的具體的日期信息。 -
日歷主體的行數(shù):現(xiàn)在我們看到的日歷基本上為6行,因?yàn)橐粋€(gè)月最多為31天,假設(shè)當(dāng)前月的第一天為上一月最后一周的最后一天。如果是五行數(shù)據(jù)的話則只顯示了29天,這也是為什么顯示6行數(shù)據(jù)的原因。
功能點(diǎn)
-
日歷初始渲染日期為當(dāng)前月份 -
頭部的左右滑動(dòng),日歷數(shù)據(jù)需要顯示對(duì)應(yīng)月份的信息 -
可以根據(jù)調(diào)用設(shè)置日歷的每周數(shù)據(jù)以星期*為開始,星期天或者星期一。
核心問題
如何獲取當(dāng)前日期的年份以及月份
// Calender/lib/utils.ts
/**
* 獲取日歷header內(nèi)容 格式為:****年 **月
* @param {*} date
*/
export const getHeaderContent = function (date: Date) {
let _date = new Date(date);
return dateFormat(_date, 'yyyy年 MM月');
};
如何獲取當(dāng)前月份需要顯示的42條數(shù)據(jù)(6*7),這42條數(shù)據(jù)是什么?
這個(gè)問題的核心是:當(dāng)前月份顯示的42條數(shù)據(jù)的第一天是哪一天?
這個(gè)問題的解決思路還要從上面的設(shè)計(jì)說起,上面提到日歷主題的行數(shù)時(shí),說到“假設(shè)當(dāng)前月的第一天為上一月最后一周的最后一天”,那么42條數(shù)據(jù)顯示的內(nèi)容的第一條數(shù)據(jù)還要根據(jù)當(dāng)前月的第一天是第一天所在周的第幾天。
舉例:2019-02-01
2月的第一天,星期五,所以當(dāng)前月日歷的第一天為
var date = new Date()
date.setDate(date.getDate() - date.getDay() + 1) // 獲取當(dāng)前月的第一天為2019-01-28
這里有一問題是什么呢?
上面的代碼邏輯是假設(shè)日歷的排列順序是周一圍最開始的(如果你的日歷也是將周日放在日歷的第一天,沒什么問題,可是在中國(guó)是將周日放在最后一天的),這也就意味著前面的實(shí)現(xiàn)還需要考慮日歷的放置順序,因?yàn)槿諝v是按照普通的周一到周日,還是周日到周一,我們獲取的當(dāng)月日歷的第一天是不同的。所以上面的代碼還要依賴于日歷的排放順序。
這里的排放順序?qū)⑹侨諝v組件的第一個(gè)可被調(diào)用者控制的參數(shù)。這里我的設(shè)想是將該參數(shù)的傳入值與date.getDay()匹配。
-
0:周日 -
1:周一 -
..... -
5:周五 -
6:周六
所以上面的公式為:
date.setDate(date.getDate() - date.getDay() + x)
但是這里的x值加了之后的日期如果大于當(dāng)前月份的第一天,那就需要將當(dāng)前得到的日期數(shù)值再減去7天,這個(gè)原因就不用說明了吧。
/**
* 獲取當(dāng)前月日歷的第一天
* @param {*} date
*/
export const getFirstDayOfCalendar = function (
date: Date,
weekLabelIndex: number,
) {
let _date = new Date(date);
_date = new Date(
_date.setDate(_date.getDate() - _date.getDay() + weekLabelIndex),
);
// 如果當(dāng)前日期大于當(dāng)前月第一天,則需要減去7天
if (_date > date) {
_date = new Date(_date.setDate(_date.getDate() - 7));
}
return _date;
};
接下來就好做了,只需要在當(dāng)前的日期加上加上1,每次得到下一天的日期。
左右切換月份如何設(shè)定
上面設(shè)計(jì)都是以今天為計(jì)算初始值,左右切換的初始值如何設(shè)計(jì)呢?
第一反應(yīng)是將當(dāng)前的日期的月份進(jìn)行加減1,這樣是不行的,因?yàn)槿绻裉焓?1號(hào),那么碰到下個(gè)月只有30的時(shí)候,這樣就會(huì)碰到點(diǎn)擊下月,直接切換了兩個(gè)月。更別說2月這個(gè)月份天數(shù)不固定的月份。所以這里又是一個(gè)問題了。
我的解決思路是:月份點(diǎn)擊切換的時(shí)候,初始計(jì)算值設(shè)計(jì)為當(dāng)前月的第一天。
/**
* 以傳入?yún)?shù)作為基準(zhǔn)獲取下個(gè)月的第一天日期
* @param {*} firstDayOfCurrentMonth
*/
export const getFirstDayOfNextMonth = function (firstDayOfCurrentMonth: Date) {
return new Date(
firstDayOfCurrentMonth.getFullYear(),
firstDayOfCurrentMonth.getMonth() + 1,
1,
);
};
/**
* 以傳入?yún)?shù)作為基準(zhǔn)獲取上個(gè)月的第一天日期
* @param {*} firstDayOfCurrentMonth
*/
export const getFirstDayOfPrevMonth = function (firstDayOfCurrentMonth: Date) {
return new Date(
firstDayOfCurrentMonth.getFullYear(),
firstDayOfCurrentMonth.getMonth() - 1,
1,
);
};
左右切換月份數(shù)據(jù)傳遞方式(觀察者模式)
因?yàn)閷?duì)于日歷組件本身來說,header和body是屬于同一個(gè)父組件的同級(jí)組件,數(shù)據(jù)傳遞可以依賴于父組件進(jìn)行傳遞,這里我使用的是觀察者模式實(shí)現(xiàn)。
/*
* Subject
* 內(nèi)部創(chuàng)建了三個(gè)方法,內(nèi)部維護(hù)了一個(gè)ObserverList。
*/
export class Subject {
private _observers = new ObserverList();
// addObserver: 調(diào)用內(nèi)部維護(hù)的ObserverList的add方法
public addObserver(observer: Observer) {
this._observers.add(observer);
}
// removeObserver: 調(diào)用內(nèi)部維護(hù)的ObserverList的removeat方法
public removeObserver(observer: Observer) {
this._observers.removeAt(this._observers.indexOf(observer, 0));
}
// notify: 通知函數(shù),用于通知觀察者并且執(zhí)行update函數(shù),update是一個(gè)實(shí)現(xiàn)接口的方法,是一個(gè)通知的觸發(fā)方法。
public notify(context: any) {
let observerCount = this._observers.count();
for (let i = 0; i < observerCount; i++) {
(<Observer>this._observers.get(i)).update(context);
}
}
}
/*
* ObserverList
* 內(nèi)部維護(hù)了一個(gè)數(shù)組,4個(gè)方法用于數(shù)組的操作,這里相關(guān)的內(nèi)容還是屬于subject,因?yàn)镺bserverList的存在是為了將subject和內(nèi)部維護(hù)的observers分離開來,清晰明了的作用。
*/
class ObserverList {
private _observerList: Observer[] = [];
public add(obj: Observer) {
return this._observerList.push(obj);
}
public count() {
return this._observerList.length;
}
public get(index: number) {
if (index > -1 && index < this._observerList.length) {
return this._observerList[index];
}
throw new Error(`_observerList ${index} 未知為null`);
}
public indexOf(obj: Observer, startIndex: number) {
let i = startIndex;
while (i < this._observerList.length) {
if (this._observerList[i] === obj) {
return i;
}
i++;
}
return -1;
}
public removeAt(index: number) {
this._observerList.splice(index, 1);
}
}
export class Observer {
public update: Function = () => {};
}
CalendarBody觀察者注冊(cè)
CalendarHeader通知消息
文件結(jié)構(gòu)
Calendar
├─ Components
│ ├─ CalendarBody.tsx
│ ├─ CalendarHeader.tsx
│ ├─ calenderBody.less
│ └─ calenderHeader.less
├─ lib
│ ├─ subject.ts
│ └─ utils.ts
└─ index.tsx
所有代碼文件
// index.ts
import React from 'react';
import CalendarBody from './components/CalendarBody';
import CalendarHeader from './components/CalendarHeader';
import { initObserver } from './lib/utils';
import { Subject } from './lib/subject';
export default ({ weekLabelIndex = 1 }: { weekLabelIndex: number }) => {
let calendarObserver: Subject = initObserver();
return (
<div>
<CalendarHeader observer={calendarObserver} />
<CalendarBody
observer={calendarObserver}
weekLabelIndex={weekLabelIndex}
/>
</div>
);
};
// lib/subject.ts
export class Subject {
private _observers = new ObserverList();
// addObserver: 調(diào)用內(nèi)部維護(hù)的ObserverList的add方法
public addObserver(observer: Observer) {
this._observers.add(observer);
}
// removeObserver: 調(diào)用內(nèi)部維護(hù)的ObserverList的removeat方法
public removeObserver(observer: Observer) {
this._observers.removeAt(this._observers.indexOf(observer, 0));
}
// notify: 通知函數(shù),用于通知觀察者并且執(zhí)行update函數(shù),update是一個(gè)實(shí)現(xiàn)接口的方法,是一個(gè)通知的觸發(fā)方法。
public notify(context: any) {
let observerCount = this._observers.count();
for (let i = 0; i < observerCount; i++) {
(<Observer>this._observers.get(i)).update(context);
}
}
}
/*
* ObserverList
* 內(nèi)部維護(hù)了一個(gè)數(shù)組,4個(gè)方法用于數(shù)組的操作,這里相關(guān)的內(nèi)容還是屬于subject,因?yàn)镺bserverList的存在是為了將subject和內(nèi)部維護(hù)的observers分離開來,清晰明了的作用。
*/
class ObserverList {
private _observerList: Observer[] = [];
public add(obj: Observer) {
return this._observerList.push(obj);
}
public count() {
return this._observerList.length;
}
public get(index: number) {
if (index > -1 && index < this._observerList.length) {
return this._observerList[index];
}
throw new Error(`_observerList ${index} 未知為null`);
}
public indexOf(obj: Observer, startIndex: number) {
let i = startIndex;
while (i < this._observerList.length) {
if (this._observerList[i] === obj) {
return i;
}
i++;
}
return -1;
}
public removeAt(index: number) {
this._observerList.splice(index, 1);
}
}
export class Observer {
public update: Function = () => {};
}
// lib/utils.ts
import { Subject } from './Subject';
let transfer = function (this: any, fmt: string) {
let o: {
[k: string]: string | number;
} = {
'M+': this.getMonth() + 1, // 月份
'd+': this.getDate(), // 日
'h+': this.getHours(), // 小時(shí)
'm+': this.getMinutes(), // 分
's+': this.getSeconds(), // 秒
'q+': Math.floor((this.getMonth() + 3) / 3), // 季度
S: this.getMilliseconds(), // 毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
(this.getFullYear() + '').substr(4 - RegExp.$1.length),
);
}
for (let k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1
? o[k] + ''
: ('00' + o[k]).substr(('' + o[k]).length),
);
}
}
return fmt;
};
/**
* 用于format日期格式
* @param {*} timeSpan
* @param {*} fmt
* @param {*} formatDateNullValue
*/
export const dateFormat = function (
timeSpan: Date,
fmt: string,
formatDateNullValue?: string,
) {
if (!timeSpan) {
if (formatDateNullValue) {
return formatDateNullValue;
}
return '無';
}
let date = new Date(timeSpan);
return transfer.call(date, fmt);
};
/**
* 獲取日歷header內(nèi)容 格式為:****年 **月
* @param {*} date
*/
export const getHeaderContent = function (date: Date) {
let _date = new Date(date);
return dateFormat(_date, 'yyyy年 MM月');
};
/**
* 獲取當(dāng)前月的第一天
* @param {*} date
*/
export const getFirstDayOfMonth = function (date: Date) {
let _date = new Date(date);
_date.setDate(1);
return _date;
};
/**
* 獲取當(dāng)前月日歷的第一天
* @param {*} date
*/
export const getFirstDayOfCalendar = function (
date: Date,
weekLabelIndex: number,
) {
let _date = new Date(date);
_date = new Date(
_date.setDate(_date.getDate() - _date.getDay() + weekLabelIndex),
);
// 如果當(dāng)前日期大于當(dāng)前月第一天,則需要減去7天
if (_date > date) {
_date = new Date(_date.setDate(_date.getDate() - 7));
}
return _date;
};
/**
* 根據(jù)傳入index確認(rèn)weeklabel的順序
* @param {*} weekIndexOfFirstWeekDay
*/
export const getWeekLabelList = function (weekIndexOfFirstWeekDay: number) {
let weekLabelArray: string[] = [
'周日',
'周一',
'周二',
'周三',
'周四',
'周五',
'周六',
];
for (let index = 0; index < weekIndexOfFirstWeekDay; index++) {
let weekLabel = weekLabelArray.shift() || '';
weekLabelArray.push(weekLabel);
}
return weekLabelArray;
};
/**
* 啟動(dòng)觀察者模式,并且初始化
*/
export const initObserver = function () {
let subject = new Subject();
return subject;
};
/**
* 格式化日期為兩個(gè)單詞,例如:‘1’號(hào) 格式為 ‘01’
* @param {*} dateNumber
*/
export const formatDayWithTwoWords = function (dateNumber: number) {
if (dateNumber < 10) {
return '0' + dateNumber;
}
return dateNumber;
};
/**
* 比較當(dāng)前日期是否為本月日期,用于進(jìn)行本月數(shù)據(jù)高亮顯示
* @param {*} firstDayOfMonth
* @param {*} date
*/
export const isCurrentMonth = function (firstDayOfMonth: Date, date: Date) {
return firstDayOfMonth.getMonth() === date.getMonth();
};
/**
* 比較當(dāng)前日期是否為系統(tǒng)當(dāng)前日期
* @param {*} date
*/
export const isCurrentDay = function (date: Date) {
let _date = new Date();
return (
date.getFullYear() === _date.getFullYear() &&
date.getMonth() === _date.getMonth() &&
date.getDate() === _date.getDate()
);
};
/**
* 以傳入?yún)?shù)作為基準(zhǔn)獲取下個(gè)月的第一天日期
* @param {*} firstDayOfCurrentMonth
*/
export const getFirstDayOfNextMonth = function (firstDayOfCurrentMonth: Date) {
return new Date(
firstDayOfCurrentMonth.getFullYear(),
firstDayOfCurrentMonth.getMonth() + 1,
1,
);
};
/**
* 以傳入?yún)?shù)作為基準(zhǔn)獲取上個(gè)月的第一天日期
* @param {*} firstDayOfCurrentMonth
*/
export const getFirstDayOfPrevMonth = function (firstDayOfCurrentMonth: Date) {
return new Date(
firstDayOfCurrentMonth.getFullYear(),
firstDayOfCurrentMonth.getMonth() - 1,
1,
);
};
// Components/CalendarHeader.tsx
import React, { useEffect, useCallback, useState } from 'react';
import { Subject } from '../lib/subject';
import {
getHeaderContent,
getFirstDayOfMonth,
getFirstDayOfNextMonth,
getFirstDayOfPrevMonth,
} from '../lib/utils';
import './calenderHeader.less';
export default ({ observer }: { observer: Subject }) => {
// 頁面綁定數(shù)據(jù)
const [headerContent, setHeaderContent] = useState<string>('');
const [firstDayOfMonth, setFirstDayOfMonth] = useState<Date>(new Date());
let leftArrow = '<';
let rightArrow = '>';
useEffect(() => {
setHeaderContent(getHeaderContent(new Date()));
setFirstDayOfMonth(new Date());
}, []);
/**
* 主題發(fā)布信息,通知觀察者
*/
const observerNotify = (currentFirstDayOfMonth: Date) => {
setHeaderContent(getHeaderContent(currentFirstDayOfMonth));
observer.notify(currentFirstDayOfMonth);
};
/**
* 頁面操作
*/
const goPrev = () => {
const preFirstDayOfMonth = getFirstDayOfPrevMonth(firstDayOfMonth);
setFirstDayOfMonth(preFirstDayOfMonth);
observerNotify(preFirstDayOfMonth);
};
const goNext = () => {
const nextFirstDayOfMonth = getFirstDayOfNextMonth(firstDayOfMonth);
setFirstDayOfMonth(nextFirstDayOfMonth);
observerNotify(nextFirstDayOfMonth);
};
return (
<div className="calendar-header">
<div className="header-center">
<span className="prev-month" onClick={goPrev}>
{leftArrow}
</span>
<span className="title">{headerContent}</span>
<span className="next-month" onClick={goNext}>
{rightArrow}
</span>
</div>
</div>
);
};
// Components/CalendarBody.tsx
import React, { useEffect, useCallback, useState } from 'react';
import { Subject } from '../lib/subject';
import {
getFirstDayOfMonth,
getFirstDayOfCalendar,
formatDayWithTwoWords,
isCurrentMonth,
isCurrentDay,
getWeekLabelList,
} from '../lib/utils';
import './calenderBody.less';
interface DayItem {
date: Date;
monthDay: number | string;
isCurrentMonth: boolean;
isCurrentDay: boolean;
}
export default ({
observer,
weekLabelIndex = 1,
}: {
observer: Subject;
weekLabelIndex?: number;
}) => {
const [firstDayOfMonth, setFirstDayOfMonth] = useState(new Date());
const [weekList, setWeekList] = useState<DayItem[][]>([]);
const [weekLabelArray, setWeekLabelArray] = useState<string[]>([]);
useEffect(() => {
// 注冊(cè)觀察者對(duì)象
observer.addObserver({
update: update,
});
// 設(shè)置當(dāng)前月的第一天,用來數(shù)據(jù)初始話以及進(jìn)行日期是否為當(dāng)前月判斷
setFirstDayOfMonth(getFirstDayOfMonth(new Date()));
// 設(shè)置每周label標(biāo)識(shí)數(shù)據(jù)
setWeekLabelArray(getWeekLabelList(weekLabelIndex));
// 初始設(shè)置當(dāng)前月日歷數(shù)據(jù)
setWeekListValue(getFirstDayOfMonth(new Date()));
}, []);
/**
* 日歷方法
*/
// 點(diǎn)擊日歷
const onClickDay = (dayItem: DayItem) => {
// this.$emit('dayClick', dayItem)
};
// 設(shè)置weekList值
const setWeekListValue = (firstDayOfmonth: Date) => {
let newWeekList = [];
let dayOfCalendar = getFirstDayOfCalendar(firstDayOfmonth, weekLabelIndex);
// 遍歷層數(shù)為6,因?yàn)槿諝v顯示當(dāng)前月數(shù)據(jù)為6行
for (let weekIndex = 0; weekIndex < 6; weekIndex++) {
let weekItem = [];
// 每一周為7天
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
let dayItem: DayItem = {
date: dayOfCalendar,
monthDay: formatDayWithTwoWords(dayOfCalendar.getDate()),
isCurrentMonth: isCurrentMonth(firstDayOfMonth, dayOfCalendar),
isCurrentDay: isCurrentDay(dayOfCalendar),
};
weekItem.push(dayItem);
// 當(dāng)前日期加1,以此類推得到42條記錄
dayOfCalendar.setDate(dayOfCalendar.getDate() + 1);
}
newWeekList.push(weekItem);
setWeekList(newWeekList);
}
};
/**
* 觀察者模式相關(guān)方法
*/
// 切換月份更新body數(shù)據(jù)
const update = (content: Date) => {
setFirstDayOfMonth(content);
setWeekListValue(content);
};
/**
* 工具方法
*/
// 周六/周日標(biāo)識(shí)紅色字體
const isShowRedColorForWeekLable = (index: number) => {
return (
index + weekLabelIndex === 6 ||
index + weekLabelIndex === 7 ||
(index === 0 && weekLabelIndex === 0)
);
};
return (
<div className="calendar-body">
{/* <!-- 日歷周label標(biāo)識(shí) --> */}
<div className="calendar-body-week-label">
{weekLabelArray.map((item, index) => (
<div
className={`calendar-body-week-label-day ${
isShowRedColorForWeekLable(index) ? 'red-font' : ''
}`}
>
<span>{item}</span>
</div>
))}
</div>
{/* <!-- 日歷數(shù)據(jù),遍歷日歷二位數(shù)組,得到每一周數(shù)據(jù) --> */}
{weekList.map((weekItem: DayItem[]) => (
<div className="calendar-body-week">
{/* <!-- 遍歷每一周數(shù)據(jù) --> */}
{weekItem.map((dayItem: DayItem, index: number) => (
<div
className={`calendar-body-week-day ${
dayItem.isCurrentMonth ? 'calendar-body-current-month' : ''
} ${dayItem.isCurrentDay ? 'calendar-body-current-day' : ''} ${
isShowRedColorForWeekLable(index) ? 'red-font' : ''
}`}
onClick={() => onClickDay(dayItem)}
>
<span>{dayItem.monthDay}</span>
</div>
))}
</div>
))}
</div>
);
};
Node 社群
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點(diǎn)贊、在看” 支持一波??
