從零開始, 用原生js寫一個類似elementUI的日歷

image.png

最近工作中經(jīng)常用到日歷這個組件, 以前大多是用moment.js或者dayjs去做的, 其實(shí)使用原生js也并不復(fù)雜.

1.首先我們來認(rèn)識幾個這個組件中使用的new Date常用的方法

      this.lastDay = new Date(y, m + 1, 0).getDate() // 獲取指定月的最后一天也即每月多少天

      this.lastMonthDay = new Date(y, m, 0).getDate() // 獲取上個月多少天
      this.firstDayisWhat = new Date(y, m, 1).getDay() // 第一天星期幾0-6(星期日到星期六)

      this.lastDayisWhat = new Date(y, m + 1, 0).getDay() // 最后一天星期幾0-6(星期日到星期六)

      date.getFullYear() // 指定日期的年
      date.getMonth() // 指定日期的月
      date.getDate() // 指定日期的天
      new Date().getFullYear() + 1 // 指定年加1年
這幾個方法就可以寫出我們需要的組件. 其中需要注意的是 星期是 0 - 6 星期日到星期六, 月份是 0 - 11 一月到十二月.

2.需求整理

依照ElementUI的日歷, 我們需要可動態(tài)渲染的slot, 默認(rèn)的選中, 上月下月翻頁, 除此之外,我又加了動態(tài)的起始周

3.開始代碼

  props: {
    activeDay: {
      type: Array,
      default: () => []
    }, // 首先我們接收activeDay 默認(rèn)選中的日期 后面我們將點(diǎn)擊事件暴露給父節(jié)點(diǎn), 在父節(jié)點(diǎn)中動態(tài)修改選中的日期
    startingweek: {
      type: [Number, String],
      default: () => 0
    }, // 起始周 默認(rèn)為 0 即周日開始 參數(shù) 0 - 6 , 星期日到星期六
    filtWeekTab: {
      type: Function
    }, // 渲染week的方法, 這個方法暴露出三個參數(shù) 當(dāng)前的星期, 當(dāng)前年, 當(dāng)前月
    activeCurrentDay: {
      type: Boolean,
      default: () => true
    }, // 是否默認(rèn)選中當(dāng)前天 
    width: {
      type: [Number, String],
      default: () => 400
    } // 寬度
  },

核心方法,因?yàn)閯討B(tài)起始周的關(guān)系,所以前后填充的邏輯比較繞

(7 - this.startingweek + this.firstDayisWhat) % 7
(6 - this.lastDayisWhat + this.startingweek) % 7
能看懂這倆為什么這么寫,基本上算是無壓力寫這個組件了

此處拿到所有需要展示的日期

    dateInit (year = new Date().getFullYear(), month = new Date().getMonth()) {
      const y = year // 獲取傳入的年  默認(rèn) new Date().getFullYear()
      const m = month // 獲取傳入的月 默認(rèn) new Date().getMonth()
      this.panelYear = year // 全局展示的年
      this.panelMonth = month // 全局展示的月
      this.lastDay = new Date(y, m + 1, 0).getDate() // 每月最后一天也即每月多少天

      this.lastMonthDay = new Date(y, m, 0).getDate() // 獲取上個月多少天
      this.firstDayisWhat = new Date(y, m, 1).getDay() // 第一天星期幾0-6(星期日到星期六)

      this.lastDayisWhat = new Date(y, m + 1, 0).getDay() // 最后一天星期幾0-6(星期日到星期六)

      let beginTmp = [] // 初始化頭部填充 非當(dāng)前月的數(shù)據(jù)都是day開頭
      // 頭部拿到 firstDayisWhat 當(dāng)月第一天是周幾 去循環(huán) 用lastMonthDay上個月的總天數(shù)
      for (let i = 0; i < (7 - this.startingweek + this.firstDayisWhat) % 7; i++) {
        beginTmp.push(`upday-${this.lastMonthDay - ((7 - this.startingweek + this.firstDayisWhat) % 7 - i - 1)}`)
      }

      // 初始化完整的月的每天格式為年月日
      const lastFullTmp = []
      // 拿到lastDay本月的天數(shù)循環(huán)拼接完整年月日 panelYear panelMonth i addPreZero方法補(bǔ)零
      for (let i = 1; i <= this.lastDay; i++) {
        lastFullTmp.push(`${this.panelYear}-${this.addPreZero(this.panelMonth + 1)}-${this.addPreZero(i)}`)
      }

      // 尾部填充
      let lastinTmp = []
      // 獲取最后一天是周幾
      for (let i = 0; i < (6 - this.lastDayisWhat + this.startingweek) % 7; i++) {
        lastinTmp.push(`downday-0${i + 1}`)
      }

      // 拼接數(shù)組拿到當(dāng)前月完整的數(shù)據(jù)
      this.dayFullList = [...beginTmp, ...lastFullTmp, ...lastinTmp]
    },
此處做選中, 如果activeCurrentDay為false則不選中當(dāng)前天, 很簡單的邏輯控制
    // 設(shè)置選中的日期
    getActiveDay (day) {
      if (this.activeCurrentDay && this.formatDate().includes(day)) {
        return true
      }
      return this.activeDay.includes(day)
    },
此處對應(yīng)的 filtWeekTab以及起始周的控制,filtWeekTab接收一個方法, 暴露出三個參數(shù), 如下圖

Math.abs(value - 1 + this.startingweek) % 7 // 這個是關(guān)鍵

    getStartingDay (value) {
      let week
      let key
      key = Math.abs(value - 1 + this.startingweek) % 7
      switch (key) {
        case 0:
          week = '日'
          break;
        case 1:
          week = '一'
          break;
        case 2:
          week = '二'
          break;
        case 3:
          week = '三'
          break;
        case 4:
          week = '四'
          break;
        case 5:
          week = '五'
          break;
        case 6:
          week = '六'
          break;
      }
      if (typeof this.filtWeekTab === 'function') {
        return this.filtWeekTab(week, this.panelYear, this.panelMonth)
      }
      return week
    },

使用方法

    filtWeekTab (value, year, month) {
      console.log(value, year, month);
      return value
    },
image.png
最關(guān)鍵的翻頁邏輯及slot插槽處理 // 屬于內(nèi)置方法,
    updateDays (year = this.panelYear, month = this.panelMonth) {
      this.dateInit(year, month)
      this.panelYear = year
      this.panelMonth = month
    }
    <div class="date-tools" :style="`width: ${width}px`">
        <div class="date-years">
            <span>{{ panelYear }}年{{ panelMonth + 1 }}月</span>
            <slot name="buttons"></slot>
        </div>
        <div class="date-weeks">
            <span v-for="i in 7" :key="i">{{ getStartingDay(i) }}</span>
        </div>
        <div class="date-days">
            <template v-for="(day, index) in dayFullList">
                <div @click="getClickDay(day)" v-if="day.includes('day-')" :key="index" class="dayButton">
                    <slot :days="day">
                        <span class="othermonths">{{ filterDay(day) }}</span>
                    </slot>
                </div>
                <div @click="getClickDay(day)" v-else :key="index" :class="{ active: getActiveDay(day) }" class="dayButton">
                    <slot :days="day">
                        <span class="currentmonth">{{ filterDay(day) }}</span>
                    </slot>
                </div>
            </template>
        </div>
    </div>
這個組件總共設(shè)置了兩個插槽一個具名插槽buttons默認(rèn)為空, 因?yàn)橛X的翻頁按鈕每個人放的位置都不一樣,而且翻頁按鈕有些定制化的需求如翻到指定年月日選中日期等,所以通過具名插槽及updateDays方法可以動態(tài)設(shè)置翻頁.使用案列如下
        <demo ref="demos" :activeDay="activeDay" :activeCurrentDay="true" :startingweek="0" :filtWeekTab="filtWeekTab" @getClickDay="getClickDay">
            <template slot="buttons">
                <div>
                    <el-button @click="down">上個月</el-button>
                    <el-button @click="up">下個月</el-button>
                </div>
            </template>
            <template v-slot="{ days }">
                <span v-if="days.includes('day-')" class="othermonths">{{ getDays(days) }}</span>
                <span v-if="!days.includes('day-')" class="currentmonth">{{ getDays(days) }} {{ activeDay.includes(days) ? '??' : '' }}</span>
            </template>
        </demo>
    up () {
      let t = this.$refs.demos.getCurrentYearMonth()
      let year = t[0]
      let month
      if (Number(t[1]) >= 11) {
        month = 0
        year = new Date(`${t[0]}-01-01`).getFullYear() + 1
      } else {
        month = Number(t[1]) + 1
      }
      this.$refs.demos.updateDays(year, month)
    },
    down () {
      let t = this.$refs.demos.getCurrentYearMonth()
      let year = t[0]
      let month
      if (Number(t[1]) <= 0) {
        month = 11
        year = new Date(`${t[0]}-01-01`).getFullYear() - 1
      } else {
        month = Number(t[1]) - 1
      }
      this.$refs.demos.updateDays(year, month)
    }
說一下第二個匿名插槽, 匿名插槽暴露出來的參數(shù)是days及當(dāng)前月的日期 可以通過判斷條件渲染不同的樣式及頁面, 和element的日歷是一致的

最后補(bǔ)充上完整代碼和使用案列, 使用上有什么問題歡迎和我說

<template>
    <div class="date-tools" :style="`width: ${width}px`">
        <div class="date-years">
            <span>{{ panelYear }}年{{ panelMonth + 1 }}月</span>
            <slot name="buttons"></slot>
        </div>
        <div class="date-weeks">
            <span v-for="i in 7" :key="i">{{ getStartingDay(i) }}</span>
        </div>
        <div class="date-days">
            <template v-for="(day, index) in dayFullList">
                <div @click="getClickDay(day)" v-if="day.includes('day-')" :key="index" class="dayButton">
                    <slot :days="day">
                        <span class="othermonths">{{ filterDay(day) }}</span>
                    </slot>
                </div>
                <div @click="getClickDay(day)" v-else :key="index" :class="{ active: getActiveDay(day) }" class="dayButton">
                    <slot :days="day">
                        <span class="currentmonth">{{ filterDay(day) }}</span>
                    </slot>
                </div>
            </template>
        </div>
    </div>
</template>

<script>
export default {
  props: {
    activeDay: {
      type: Array,
      default: () => []
    },
    startingweek: {
      type: [Number, String],
      default: () => 0
    },
    filtWeekTab: {
      type: Function
    },
    activeCurrentDay: {
      type: Boolean,
      default: () => true
    },
    width: {
      type: [Number, String],
      default: () => 400
    }
  },
  data () {
    return {
      dayFullList: [], // 所有的天數(shù)列表珍坊,前面空位補(bǔ)0
      panelYear: '', // 全局展示的年
      panelMonth: '', // 全局展示的月(從0開始)

      lastDay: '', // 每月最后一天也即每月多少天

      lastMonthDay: '', // 獲取上個月多少天
      firstDayisWhat: '', // 第一天星期幾0-6(星期日到星期六)

      lastDayisWhat: ''  // 最后一天星期幾0-6(星期日到星期六)
    }
  },
  methods: {
    dateInit (year = new Date().getFullYear(), month = new Date().getMonth()) {
      const y = year // 獲取傳入的年  默認(rèn) new Date().getFullYear()
      const m = month // 獲取傳入的月 默認(rèn) new Date().getMonth()
      this.panelYear = year // 全局展示的年
      this.panelMonth = month // 全局展示的月
      this.lastDay = new Date(y, m + 1, 0).getDate() // 每月最后一天也即每月多少天

      this.lastMonthDay = new Date(y, m, 0).getDate() // 獲取上個月多少天
      this.firstDayisWhat = new Date(y, m, 1).getDay() // 第一天星期幾0-6(星期日到星期六)

      this.lastDayisWhat = new Date(y, m + 1, 0).getDay() // 最后一天星期幾0-6(星期日到星期六)

      let beginTmp = [] // 初始化頭部填充 非當(dāng)前月的數(shù)據(jù)都是day開頭
      // 頭部拿到 firstDayisWhat 當(dāng)月第一天是周幾 去循環(huán) 用lastMonthDay上個月的總天數(shù)
      for (let i = 0; i < (7 - this.startingweek + this.firstDayisWhat) % 7; i++) {
        beginTmp.push(`upday-${this.lastMonthDay - ((7 - this.startingweek + this.firstDayisWhat) % 7 - i - 1)}`)
      }

      // 初始化完整的月的每天格式為年月日
      const lastFullTmp = []
      // 拿到lastDay本月的天數(shù)循環(huán)拼接完整年月日 panelYear panelMonth i addPreZero方法補(bǔ)零
      for (let i = 1; i <= this.lastDay; i++) {
        lastFullTmp.push(`${this.panelYear}-${this.addPreZero(this.panelMonth + 1)}-${this.addPreZero(i)}`)
      }

      // 尾部填充
      let lastinTmp = []
      // 獲取最后一天是周幾
      for (let i = 0; i < (6 - this.lastDayisWhat + this.startingweek) % 7; i++) {
        lastinTmp.push(`downday-0${i + 1}`)
      }

      // 拼接數(shù)組拿到當(dāng)前月完整的數(shù)據(jù)
      this.dayFullList = [...beginTmp, ...lastFullTmp, ...lastinTmp]
    },
    // 小于9的需要添加0前綴
    addPreZero (num) {
      return num > 9 ? num : '0' + num
    },
    // 設(shè)置選中的日期
    getActiveDay (day) {
      if (this.activeCurrentDay && this.formatDate().includes(day)) {
        return true
      }
      return this.activeDay.includes(day)
    },
    getStartingDay (value) {
      let week
      let key
      key = Math.abs(value - 1 + this.startingweek) % 7
      switch (key) {
        case 0:
          week = '日'
          break;
        case 1:
          week = '一'
          break;
        case 2:
          week = '二'
          break;
        case 3:
          week = '三'
          break;
        case 4:
          week = '四'
          break;
        case 5:
          week = '五'
          break;
        case 6:
          week = '六'
          break;
      }
      if (typeof this.filtWeekTab === 'function') {
        return this.filtWeekTab(week, this.panelYear, this.panelMonth)
      }
      return week
    },
    filterDay (value) {
      return parseInt(value.slice(-2))
    },
    getClickDay (day) {
      this.$emit('getClickDay', day)
    },
    updateDays (year = this.panelYear, month = this.panelMonth) {
      this.dateInit(year, month)
      this.panelYear = year
      this.panelMonth = month
    },
    getCurrentYearMonth () {
      return [this.panelYear, this.panelMonth]
    },
    formatDate (date = new Date()) {
      let y = date.getFullYear()

      let m = date.getMonth() + 1

      m = m < 10 ? '0' + m : m

      let d = date.getDate()

      d = d < 10 ? ('0' + d) : d

      return y + '-' + m + '-' + d

    }
  },
  mounted () {
    this.dateInit()
  }
}
</script>
<style lang="scss" scoped>
.date-tools {
    min-width: 400px;
    .date-years {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-bottom: 15px;
        font-size: 13px;
    }
    .date-weeks {
        display: flex;
        border-bottom: 1px solid #d7dce5;
        padding-bottom: 10px;
        span {
            width: calc(100% / 7);
            text-align: center;
            color: #929aac;
            font-size: 13px;
        }
    }
    .date-days {
        display: flex;
        flex-wrap: wrap;
        .dayButton {
            width: calc(100% / 7);
            min-height: 50px;
            background-color: #fff;
            display: flex;
            align-items: center;
            justify-content: center;
            &:hover {
                background-color: #f2f8fe;
                span:not(.othermonths) {
                    color: #1989fa;
                }
            }
            span {
                width: 20px;
                height: 20px;
                line-height: 20px;
                border-radius: 50%;
                font-size: 12px;
                text-align: center;
                cursor: pointer;
            }
            .othermonths {
                color: #e3e3e3;
            }
            .currentmonth {
                color: #000;
            }
        }
        .active {
            background-color: #f2f8fe;
            cursor: pointer;
            span.currentmonth  {
                color: #1989fa;
            }
        }
    }
}
</style>
<template>
    <div>
        <demo ref="demos" :activeDay="activeDay" :activeCurrentDay="true" :startingweek="0" :filtWeekTab="filtWeekTab" @getClickDay="getClickDay">
            <template slot="buttons">
                <div>
                    <el-button @click="down">上個月</el-button>
                    <el-button @click="up">下個月</el-button>
                </div>
            </template>
            <template v-slot="{ days }">
                <span v-if="days.includes('day-')" class="othermonths">{{ getDays(days) }}</span>
                <span v-if="!days.includes('day-')" class="currentmonth">{{ getDays(days) }} {{ activeDay.includes(days) ? '??' : '' }}</span>
            </template>
        </demo>
    </div>
</template>

<script>
import demo from './demo'
export default {
  components: {
    demo
  },
  data () {
    return {
      activeDay: ['2020-11-20']
    }
  },
  mounted () {
    setTimeout(() => {
      this.activeDay = ['2020-11-20', '2020-11-10']
    }, 3000);
  },
  methods: {
    filtWeekTab (value, year, month) {
      console.log(value, year, month);
      return value
    },
    getDays (days) {
      //   console.log(days);
      return parseInt(days.slice(-2))
    },
    getClickDay (day) {
      console.log(day)
    },
    up () {
      let t = this.$refs.demos.getCurrentYearMonth()
      let year = t[0]
      let month
      if (Number(t[1]) >= 11) {
        month = 0
        year = new Date(`${t[0]}-01-01`).getFullYear() + 1
      } else {
        month = Number(t[1]) + 1
      }
      this.$refs.demos.updateDays(year, month)
    },
    down () {
      let t = this.$refs.demos.getCurrentYearMonth()
      let year = t[0]
      let month
      if (Number(t[1]) <= 0) {
        month = 11
        year = new Date(`${t[0]}-01-01`).getFullYear() - 1
      } else {
        month = Number(t[1]) - 1
      }
      this.$refs.demos.updateDays(year, month)
    }
  },
}
</script>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瞎领,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子功咒,更是在濱河造成了極大的恐慌旁赊,老刑警劉巖桦踊,帶你破解...
    沈念sama閱讀 222,865評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異终畅,居然都是意外死亡籍胯,警方通過查閱死者的電腦和手機(jī)鳄橘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芒炼,“玉大人瘫怜,你說我怎么就攤上這事”竟簦” “怎么了鲸湃?”我有些...
    開封第一講書人閱讀 169,631評論 0 364
  • 文/不壞的土叔 我叫張陵,是天一觀的道長子寓。 經(jīng)常有香客問我暗挑,道長,這世上最難降的妖魔是什么斜友? 我笑而不...
    開封第一講書人閱讀 60,199評論 1 300
  • 正文 為了忘掉前任炸裆,我火速辦了婚禮,結(jié)果婚禮上鲜屏,老公的妹妹穿的比我還像新娘烹看。我一直安慰自己,他們只是感情好洛史,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,196評論 6 398
  • 文/花漫 我一把揭開白布惯殊。 她就那樣靜靜地躺著,像睡著了一般也殖。 火紅的嫁衣襯著肌膚如雪土思。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,793評論 1 314
  • 那天忆嗜,我揣著相機(jī)與錄音己儒,去河邊找鬼。 笑死捆毫,一個胖子當(dāng)著我的面吹牛闪湾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冻璃,決...
    沈念sama閱讀 41,221評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼响谓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了省艳?” 一聲冷哼從身側(cè)響起娘纷,我...
    開封第一講書人閱讀 40,174評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎跋炕,沒想到半個月后赖晶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,699評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,770評論 3 343
  • 正文 我和宋清朗相戀三年遏插,在試婚紗的時候發(fā)現(xiàn)自己被綠了捂贿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,918評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡胳嘲,死狀恐怖厂僧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情了牛,我是刑警寧澤颜屠,帶...
    沈念sama閱讀 36,573評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站鹰祸,受9級特大地震影響甫窟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蛙婴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,255評論 3 336
  • 文/蒙蒙 一粗井、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧街图,春花似錦浇衬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至颤介,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赞赖,已是汗流浹背滚朵。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留前域,地道東北人辕近。 一個月前我還...
    沈念sama閱讀 49,364評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像匿垄,于是被迫代替她去往敵國和親移宅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,926評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 一椿疗、了解Vue.js 1.1.1 Vue.js是什么漏峰? 簡單小巧、漸進(jìn)式届榄、功能強(qiáng)大的技術(shù)棧 1.1.2 為什么學(xué)習(xí)...
    蔡華鵬閱讀 3,336評論 0 3
  • 組件名 該組件名就是 Vue.component 的第一個參數(shù)浅乔。 自定義組件名 (字母全小寫且必須包含一個連字符)...
    Sunshinga閱讀 421評論 0 0
  • 此文章僅記錄學(xué)習(xí)Vue中一些平常自己沒有去學(xué)習(xí)到知識,很多東西都是基于自我的認(rèn)知去寫的。文中可能會有理解錯誤的地方...
    Mstian閱讀 781評論 0 10
  • 本章內(nèi)容:表單 與 v-model靖苇、組件席噩、自定義指令 六、表單 與 v-model 6.1贤壁、基本用法 Vue.js...
    了凡和纖風(fēng)閱讀 913評論 1 2
  • vue基礎(chǔ)知識部分 扯淡前言 這個筆記是關(guān)于vue的所有重點(diǎn)基礎(chǔ)知識,大部分配的有實(shí)例,這些實(shí)例都是我自己一個個敲...
    stephenoo閱讀 184評論 0 0