<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          用react手寫一個(gè)簡(jiǎn)單的日歷

          共 28052字,需瀏覽 57分鐘

           ·

          2021-07-06 18:41

          設(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 (thisany, 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 }) => {
            // 頁(yè)面綁定數(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);
            };

            /**
             * 頁(yè)面操作
             */

            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 {
            dateDate;
            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>

            );
          };


                               


          -------------- 喜歡的話,關(guān)注一下哦 --------------

          往期推薦




          瀏覽 42
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  豆花无码视频一区二区 | 韩国人免费的吊黑 | 伊人久久五月 | 操进来综合网 | 欧美爆乳一区二区 |