vue寫一個炫酷的日曆元件

Kellercai發表於2019-03-01

專案:

公司業務新開了一個商家管理微信H5移動端專案,日曆控制元件是商家管理員檢視通過日程來篩選獲取某日使用者的訂單等資料。
如圖:
假設今天為2018-09-02

日曆

90天前:

日曆

90天后;

日曆

產品需求:

  • 展示當前日期(伺服器時間)前後90天,一共181天的日期。
  • 日曆可以左右滑動切換月份。
  • 當月份的如果不在181天區間的,需要置灰並且不可點選。
  • 點選日曆繫結的節點的外部,關閉彈窗。

涉及內容:

  1. 獲取伺服器時間,渲染日曆資料
  2. vue-touch監聽手勢滑動事件
  3. ios日期相容處理
  4. clickOutSide自定義指令
  5. mock模擬資料

開發:

參考了基於Vue開發一個日曆元件 – 掘金日曆的年月日計算方式。
核心思想:假設當前月份是二月份,根據二月和三月的1號是星期幾,來對二月進行佈局。(如果需要在二月顯示一月和三月的日期,還需要知道一月份有多少天)

在專案開發中,為了與後臺同事並行開發。專案採用來mock模擬資料來攔截介面。

  • 日曆展盤
// calendar.vue
<template>
  <div class="cp-calendar">
    <v-touch
      @swipeleft="handleNextMonth"
      @swiperight="handlePreMonth"
      class="calendar">
      
      <div class="calendar-main" >
        <span class="item-con header-item"
              v-for="(item, index) in calendarHeader"
              :key="index">{{item}}</span>

        <div :class="`item-con ${todayStyle(item.content) && `item-con-today`} ${item.type === `disabled` && `disableStyle`}`"
             :style="{opacity: isChangeMonth ? 0 : 1}"
             @click.stop="handleDayClick(item)"
             v-for="(item, index) in getMonthDays(selectedYear, selectedMonth)"
             :key="item.type + item.content + `${index}`">
          <span
            :class="`main-content ${selectedDateStyle(item.content) && `selectedColor`}`">
            {{setContent(item.content)}}</span>
          <span :class="`${selectedDateStyle(item.content) && `item-con-point`}`" ></span>
        </div>
      </div>
      
    </v-touch>
  </div>
</template>
複製程式碼
  • 初始化資料
    針對伺服器時間進行初始資料處理
// calendar.vue
// 設定初始資料
      initData () {
        this.today = this.currentDate || getDateStr(0) // 如果沒有伺服器時間,拿本地時間
        this.prevDate = getDateStr(-90, this.currentDate)
        this.nextDate = getDateStr(90, this.currentDate)
        // 是否有手動選中的日期
        let selectedFullDate = this.storeSelectedFullDate
        if (!this.storeSelectedFullDate) {
          selectedFullDate = this.currentDate || getDateStr(0) // 如果沒有伺服器時間,拿本地時間
        }
        this.selectedYear = Number(selectedFullDate.split(`-`)[0])
        this.selectedMonth = Number(selectedFullDate.split(`-`)[1]) - 1
        this.selectedDate = Number(selectedFullDate.split(`-`)[2])
        this.selectedFullDate = `${this.selectedYear}-${this.selectedMonth + 1}-${this.selectedDate}`
      },
      / 渲染日期
      getMonthDays(year, month) {
        // 定義每個月的天數,如果是閏年第二月改為29天
        let daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

        if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) {
          daysInMonth[1] = 29;
        }
        // 當月第一天為周幾
        let targetDay = new Date(year, month, 1).getDay();
        let calendarDateList = [];
        let preNum = targetDay;
        let nextNum = 0;
        if (targetDay > 0) {
          // 當前月份1號前的自然周剩餘日期,置空
          for (let i = 0; i < preNum; i++) {
            let obj = {
              type: `pre`,
              content: ``
            };
            calendarDateList.push(obj);
          }
        }
        // 判斷當前年月份
        let formatMonth = month + 1 >= 10 ? month + 1 : `0` + (month + 1)
        this.prevYearMonthBoolean = (`${year}-${formatMonth}` === this.prevYearMonth)
        this.nextYearMonthBoolean = (`${year}-${formatMonth}` === this.nextYearMonth)
        for (let i = 0; i < daysInMonth[month]; i++) {
          // 正常顯示的日期
          let obj = {
            type: `normal`,
            content: i + 1
          };
          // 判斷是否為最往前或者最往後的月份,篩選出不可點選的日期
          if (this.prevYearMonthBoolean) {
            let prevDay  = this.prevDate.split(`-`)[2]
            if (i + 1 < prevDay) {
              obj.type = `disabled`
            }
          } else if (this.nextYearMonthBoolean) {
            let nextDay  = this.nextDate.split(`-`)[2]
            if (i + 1 > nextDay) {
              obj.type = `disabled`
            }
          }
          calendarDateList.push(obj);
        }

        nextNum = 6 - new Date(year, month + 1, 0).getDay()

        // 當前月份最後一天的自然周剩餘日期,置空
        for (let i = 0; i < nextNum; i++) {
          let obj = {
            type: `next`,
            content: ``
          };
          calendarDateList.push(obj);
        }
        return calendarDateList;
      },
      // 設定日期
      setContent (content) {
        if (!content) return ``
        return `${this.selectedYear}-${this.tf(this.selectedMonth + 1)}-${this.tf(content)}` === this.today ? `今天` : content
      },
      // `今天`樣式開關
      todayStyle (content) {
        if (!content) return false
        // Toast(`${this.selectedYear}-${this.tf(this.selectedMonth + 1)}-${this.tf(content)}`)
        return `${this.selectedYear}-${this.tf(this.selectedMonth + 1)}-${this.tf(content)}` === this.today
      },
      // 當前選中的日期樣式開關
      selectedDateStyle (content) {
        if (!content) return false
        return `${this.selectedYear}-${this.selectedMonth + 1}-${content}` === this.selectedFullDate
      },
複製程式碼
// src/config/utils.js
// 公共方法
/**
 * @param  AddDayCount 必傳   今天前後N天的日期
 * @param  dateStr:   非必傳  獲取傳入日期前後N天的日期:`2018-01-20`
 * @param  type        非必傳  `lhRili`型別格式如`2018-7-3`
 * @return 返回日期`2018/01/20`
 */
export const getDateStr = (AddDayCount, dateStr, type) => {
  // console.log(`getDateStr`, AddDayCount, dateStr, type)
  var dd
  if (!dateStr) {
    dd = new Date()
  } else {
    // 判斷是否為IOS
    const isIOS = !!navigator.userAgent.match(/(i[^;]+;( U;)? CPU.+Mac OS X/);

    let formatDateStr = isIOS ? dateStr.replace(/-/g, `/`) : dateStr
    dd = new Date((formatDateStr.length < 12) ? formatDateStr + ` 00:00:00` : formatDateStr)
  }
  dd.setDate(dd.getDate() + AddDayCount) // 獲取AddDayCount天后的日期

  let y = dd.getFullYear()
  let m
  let d
  if (type === `lhRili`) {
    m = dd.getMonth() + 1
    d = dd.getDate()
  } else {
    let currentMon = (dd.getMonth() + 1)
    let getDate = dd.getDate()
    m = currentMon < 10 ? `0` + currentMon : currentMon // 獲取當前月份的日期,不足10補0
    d = getDate < 10 ? `0` + getDate : getDate // 獲取當前幾號,不足10補0
  }

  let time = y + `-` + m + `-` + d
  return time
}
複製程式碼
  • 左右觸控滑動事件
    判斷是否月份還可以繼續滑動
// calendar.vue
// 上一個月
      handlePreMonth() {
        if (this.prevYearMonthBoolean) {
          return
        }
        if (this.selectedMonth === 0) {
          this.selectedYear = this.selectedYear - 1
          this.selectedMonth = 11
          this.selectedDate = 1
        } else {
          this.selectedMonth = this.selectedMonth - 1
          this.selectedDate = 1
        }
      },
      // 下一個月
      handleNextMonth() {
        if (this.nextYearMonthBoolean) {
          return
        }
        if (this.selectedMonth === 11) {
          this.selectedYear = this.selectedYear + 1
          this.selectedMonth = 0
          this.selectedDate = 1
        } else {
          this.selectedMonth = this.selectedMonth + 1
          this.selectedDate = 1
        }
      },
複製程式碼
  • vuex儲存資料
// src/store/schedule.js
const schedule = {
  state: {
    selectedDate: ``, // 手動點選選中的日期
    currentDate: `` // 伺服器當前日期
  },

  getters: {
    getSelectedDate: state => state.selectedDate,
    getCurrentDate: state => state.currentDate
  },

  mutations: {
    SET_SELECTED_DATE: (state, data) => {
      state.selectedDate = data
    },
    SET_CURRENT_DATE: (state, data) => {
      state.currentDate = data
    }
  },

  actions: {
    setSelectedDate: ({ commit }, data) => commit(`SET_SELECTED_DATE`, data),
    setCurrentDate: ({ commit }, data) => commit(`SET_CURRENT_DATE`, data)
  }
};

export default schedule;
複製程式碼
  • clickOutSide指令
    指令方法監聽
// src/directive/click-out-side.js
export default{
  bind (el, binding, vnode) {
    function documentHandler (e) {
      if (el.contains(e.target)) {
        return false;
      }
      if (binding.expression) {
        binding.value(e);
      }
    }
    el.__vueClickOutside__ = documentHandler;
    document.addEventListener(`click`, documentHandler);
  },
  unbind (el, binding) {
    document.removeEventListener(`click`, el.__vueClickOutside__);
    delete el.__vueClickOutside__;
  }
}

複製程式碼

註冊指令

// src/directive/index.js
import clickOutSide from `./click-out-side`

const install = function (Vue) {
  Vue.directive(`click-outside`, clickOutSide)
}

if (window.Vue) {
  window.clickOutSide = clickOutSide
  Vue.use(install); // eslint-disable-line
}

clickOutSide.install = install
export default clickOutSide

複製程式碼
// src/main.js
import clickOutSide from `@/directive/click-out-side/index`

Vue.use(clickOutSide)
複製程式碼

使用方式:當某節點外部需要觸發事件時,掛載到該節點上

// calendar.vue
<div class="cp-calendar" v-click-outside="spaceClick">
....
</div>
複製程式碼

這裡需要使用fastclick庫來消除解決移動端點選事件300ms延時

// src/mian.js
import FastClick from `fastclick` // 在移動端,手指點選一個元素,會經過:touchstart --> touchmove -> touchend --> click。

FastClick.attach(document.body);
複製程式碼
  • mock資料
// src/mock/index.js
// mock資料入口
import Mock from `mockjs`
import currentTime from `./currentTime`

// 攔截介面請求
Mock.mock(//schedule/getCurrentTime/, `get`, currentTime)

export default Mock

複製程式碼
// src/mock/currentTime.js
import Mock from `mockjs`

export default {
  getList: () => {
    return {
      `status`: `true`,
      `code`: `200`,
      `msg`: null,
      `info`: {
        `currentDate`: `2018-09-02`
      }
    }
  }
}

複製程式碼
// src/main.js
// 開發環境引入mock
if (process.env.NODE_ENV === `development`) {
  require(`./mock`) // 需要在這裡引入mock資料才可以全域性攔截請求
}
複製程式碼

坑點

  • 在微信內建瀏覽器中,ios的日期格式跟安卓的日期格式分別是:YYYY/MM/DD和YYYY-MM-DD。這裡需要對微信內建瀏覽器User Agent進行判斷。
  • 獲取伺服器時間的非同步問題,把獲取到的伺服器時間儲存在vuex裡面,在calendar.vue頁面監聽當前日期的變化。及時將日曆資料計算渲染出來。

相關文章