1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 28052字,需瀏覽 57分鐘

         ·

        2021-07-06 18:41

        設(shè)計(jì)實(shí)現(xiàn)一個(gè)簡(jiǎn)單版本的日歷。支持定義日歷的排放順序,以周幾作為開(kāi)始。如下圖:

        • 先看效果: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ù)以星期*為開(kāi)始,星期天或者星期一。


        核心問(wèn)題

        如何獲取當(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è)問(wèn)題的核心是:當(dāng)前月份顯示的42條數(shù)據(jù)的第一天是哪一天?

        這個(gè)問(wèn)題的解決思路還要從上面的設(shè)計(jì)說(shuō)起,上面提到日歷主題的行數(shù)時(shí),說(shuō)到“假設(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


        這里有一問(wèn)題是什么呢?

        上面的代碼邏輯是假設(shè)日歷的排列順序是周一圍最開(kāi)始的(如果你的日歷也是將周日放在日歷的第一天,沒(méi)什么問(wèn)題,可是在中國(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è)原因就不用說(shuō)明了吧。

        /**
         * 獲取當(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;
        };

        接下來(lái)就好做了,只需要在當(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è)月。更別說(shuō)2月這個(gè)月份天數(shù)不固定的月份。所以這里又是一個(gè)問(wèn)題了。

        我的解決思路是:月份點(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ì)于日歷組件本身來(lái)說(shuō),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分離開(kāi)來(lái),清晰明了的作用。
         */

        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分離開(kāi)來(lái),清晰明了的作用。
         */

        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 '無(wú)';
          }

          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)前月的第一天,用來(lái)數(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)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产成人久久久精品免费澳门 | 国产亚洲精品久久久久久快乐8 | 丰满人妻一区二区三区色按摩 | 操批视频在线观看 | 国产成人精品一区二区三区四区 | 娇淫青春之放纵h第二部 | 天天操天天插天天 | 婷婷五月天在线观看 | 午夜性色妇淫片 | 一级婬片A片免费播放桃色 |