第十五集: 從零開始實現一套pc端vue的ui組件庫( 日曆組件 )

第十五集: 從零開始實現一套pc端vue的ui組件庫( 日曆組件 )

1. 本集定位

'日曆組件'在後臺管理系統裏面是十分常見的, 在pc端的展現方式基本都爲一個方方的表格, 別看功能單一, 這個組件作起來仍是有點意思的, 本次我來實現的組件只包含最核心的功能,也就是日期的選擇, Element-ui裏面的日期組件功能不少有興趣的同窗能夠去看看他的思想.css

效果展現
圖片描述vue

2. 需求分析

  1. 一個輸入框用來展現以及點擊彈出'日曆組件'.
  2. 展現日期選擇使用6*7的矩形.
  3. 能夠按年份與月份進行翻頁.
  4. 當本月第一天不是週日的時候, 要顯示上一個月的最後幾天.
  5. 能夠選擇一個日期.
  6. 我的不太喜歡手動輸入日期這個操做, 因此本次是禁止手動輸入的.

3. 基礎的搭建

vue-cc-ui/src/components/DatePicker/index.jsnode

import DatePicker from './main/datePicker.vue'

DatePicker.install = function(Vue) {
  Vue.component(DatePicker.name, DatePicker);
};

export default DatePicker

vue-cc-ui/src/components/DatePicker/main/datePicker.vuegit

<template>
  <div class="cc-date" ref='popover'>
   // 用來展現日期的那個輸入框
    <input readonly
           type="text"
           class="cc-date-input"
           // 這是個頗有用的指令, 接下來我講一下他
           v-clickoutside='hide'
           :value='formatDare'
           // 每次聚焦都會呼出日曆
           @focus='isShowPanel = true'>
    // 接下來的'日曆'就在它裏面作了.
    <div v-show='isShowPanel'
         class="cc-date-pannel"
         ref='content'
         :style="{
               top:top+'px',
               left:left+'px'
           }">
    </div>
  </div>
</template>
export default {
  name: "ccDatePicker",
  props: {
    value: {
      type: Date, // 指定類型不準是日期類型
      default: () => new Date() // 你不傳我就取當前時間唄
    }
  },
data() {
    return {
      top: 0,
      left: 0,
      isShowPanel: false,
    };
  },
//...

v-clickoutside : 判斷點擊的是否是自身
這個方法必定要掛在組件內部的指令上, 不要污染全局.github

const Clickoutside = {
  bind(el, bindings, vnode) {
   // 單獨抽出來是爲了最後好把它移除
    const handleClick = function(e) {
      // 若是點擊的元素不在目標元素的包裹內, 那就說明點擊了與元素無關的位置.
      if (!el.contains(e.target)) {
       // 虛擬dom的context屬性能夠找到這個實例, 調用他的hide方法能夠隱藏這個dom
        vnode.context[bindings.expression]();
      }
    };
    el.handleClick = handleClick;
    document.addEventListener('click', handleClick);
  },
  unbind(el) {
    document.removeEventListener('click', el.handleClick);
  }
};

export default Clickoutside;

創給指令的hide方法express

methods: {
 hide() {
      this.isShowPanel = false;
    },
//...

給他定個位把, 具體出如今哪裏
其實這個咱們上一個組件已經封裝好了方法
咱們先觀察這個isShowPanel, 若是他出現, 那咱們就計算出現的位置segmentfault

watch: {
    isShowPanel(val) {
      if (val) {
        this.$nextTick(() => {
          this.setPosion(); // 這個方法是真正獲取位置的
        });
      }
    }
  },

setPosion設計模式

setPosion() {
      let { popover, content } = this.$refs;
      let { left, top } = getPopoverPosition( // 這個函數上一集有說明, 不贅述了.
        popover,
        content,
        "bottom-start",
        3
      );
      this.top = top;
      this.left = left;
    }

上面的步驟咱們作到了點擊input彈出日期選擇, 點擊其餘地方讓其消失數組

4.樣式很重要

  1. 首先要有header展現具體的年月日以及前進與後退.
  2. 其次是一個title展現'週一''週二'...這種.
  3. 具體的顯示框來顯示具體的day.

展現一下結構代碼
首先是第一排dom

<div class="pannel-nav">
    <span><</span>
    <span>←</span>
    <div class="pannel-selected">
    // 像這種結構有人用v-for生成...
    // 其實有時候直接寫出來更直觀, 仁者見仁吧.
      <span>{{formatDare.split('-')[0]}}年 </span>
      <span>{{formatDare.split('-')[1]}}月 </span>
      <span>{{formatDare.split('-')[2]}}日</span>
    </div>
    <span>→</span>
    <span>></span>
</div>

formatDare: 是用來展現時間的 --> '年-月-日'

computed: {
    formatDare() {
      let { year, month, day } = getYMD(this.value),
        result = `${year}-${month + 1}-${day}`;
      return result;
    },
  // ...

展現星期

<div class="pannel-content">
    <ul class="pannel-content__title">
      <li v-for="i in weeksList"
          :key="i">{{i}}</li>
    </ul>
//...
data() {
    return {
      top: 0,
      left: 0,
      isShowPanel: false,
      weeksList: ["日", "一", "二", "三", "四", "五", "六"]
    };
  },

重頭戲: 展現day天
思路: 例如當前是' x年n月 ';

  1. 計算出x年n月有多少天.
  2. 計算出x年n月的第一天是星期幾.
  3. 若是是星期日, 那就不用添加上一個月的日期, 直接開頭就顯示本月的1日.
  4. 若是不是星期日, 須要上個月的日期來補全.
  5. 求出x年n-1月有多少天, 這裏要注意, 極可能-1致使跨年了, 因此要判斷好邊界.
  6. 在當前日期好比有31天展現完畢, 須要用下個月的日期來填補全部剩下來的格子.

template

<ul class="pannel-content__item"
    v-for="i in 6"
    :key="i">
  <li v-for="j in 7"
    :key="j">{{getVisibeDaysIndex(i,j).day}}</li>
</ul>

計算當前有多少天

getVisibeDaysIndex(i, j) {
      i = i - 1;
      j = j - 1;
      let index = i * 7 + j; // 當前第幾個格子
      return this.visibeDays[index];
    },

visibeDays: 它是比較核心的方法

visibeDays() {
      let result = [],
        { year, month } = getYMD(this.value),
        // 傳入年,月,日,就會返回相應的date實例, 用getDay取得星期幾;
        dayOffset = new Date(year, month, 1).getDay(),
        // 傳入年月, 求出本月幾天, 這個方法下面會講.
        dateCountOfMonth = getDayCountOfMonth(year, month),
        // 這個是求得上一個月
        previousMonth = month - 1;
        // 沒有0月, 因此須要變爲12月, 年份-1;
      if (previousMonth === 0) {
        year--;
        previousMonth = 12;
      }
      // 取得上個月有多少天, 這樣才能知道現實上個月的最後一天是否是31;
      let dateCountOfLastMonth = getDayCountOfMonth(year, previousMonth);
      // 把取得完畢的數據傳給專門把它們作成數組用於展現的函數;
      this.getDayList(
        dayOffset,
        dateCountOfMonth,
        dateCountOfLastMonth,
        result
      );
      // 這個結果直接返回出去就行
      return result;
    }

vue-cc-ui/src/assets/js/handelDate.js
這裏面就是對日期相關的處理

export function getYMD(date){
  let day = date.getDate();
  let month = date.getMonth();
  let year = date.getFullYear();
  return {
    year, month, day
  }
}

export const getDayCountOfMonth = function(year, month) {
    if (month === 3 || month === 5 || month === 8 || month === 10) {
      return 30;
    }
  
    if (month === 1) {
      if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
        return 29;
      } else {
        return 28;
      }
    }
  
    return 31;
  };

把日期整理爲使用的數組
getDayList

  1. readOnly爲真, 顯示爲灰色不可選, 爲假就是正常的黑色可選
  2. activate爲真, 則顯示高亮, 表示被選中
getDayList(dayOffset, dateCountOfMonth, dateCountOfLastMonth, result) {
    // 處理上個月的日期, 沒有的話固然就不走這個循環
      for (let i = 0; i < dayOffset; i++) {
        result.unshift({ readOnly: true, day: dateCountOfLastMonth - i });
      }
    // 處理當前月的天數
      let day = getYMD(this.value).day;
      for (let i = 1; i <= dateCountOfMonth; i++) {
        let obj = { day: i, activate: true };
        if (day !== i) {
          obj.activate = false;
        }
        result.push(obj);
      }
     // 總個數減去已使用的數, 把剩餘空間填滿
      let len = 42 - result.length;
      for (let i = 1; i <= len; i++) {
        result.push({ readOnly: true, day: i });
      }
      // 這個函數處理好了也不必有返回值
    },

上面的步驟作完其實就已經能夠正常顯示當前月了

5.選中日期 與 切換月年

其實隨着核心代碼的完成, 周邊的功能都是很好添加的, 這也就是爲何寫代碼必定要符合設計模式;
選中某一天

<li v-for="j in 7"
  @click="handlerActiveDay(getVisibeDaysIndex(i,j,true))"
  :class="{
     'active-date': getVisibeDaysIndex(i,j).activate,
     'read-only':getVisibeDaysIndex(i,j).readOnly
  }"
  :key="j">{{getVisibeDaysIndex(i,j).day}}</li>

handlerActiveDay: 這裏我在getVisibeDaysIndex傳了第三個參數
由於這裏我只須要他返回給我具體的序號就好了, 而不是具體哪天.

getVisibeDaysIndex(i, j, type) {
  i = i - 1;
  j = j - 1;
  let index = i * 7 + j;
  return type ? index : this.visibeDays[index];
},
handlerActiveDay(index) {
  let result = this.visibeDays[index],
    { year, month } = getYMD(this.value);
  if (!result.readOnly) {
   // 這一步實際上是與用戶的 v-model相結合的.
    this.$emit("input", new Date(year, month, result.day));
  }
},

前進與後退

<span @click="handlerChangeYear(-1)"><</span>
<span @click="handlerChangeMonth(-1)">←</span>
// ...
<span @click="handlerChangeMonth(1)">→</span>
<span @click="handlerChangeYear(1)">></span>

月份的
handlerChangeMonth
注意不要超出邊界

handlerChangeMonth(n) {
  let { year, month } = getYMD(this.value);
  month += n;
  if (month === 0) {
    month = 12;
    year += n;
  } else if (month === 13) {
    month = 1;
    year += n;
  }
  this.$emit("input", new Date(year, month, 1));
},

年份
handlerChangeYear
不必判斷負數了, 畢竟選一個公元前的時間這種狀況太極端了, 不必浪費性能去判斷了.

handlerChangeYear(n) {
  let { year, month } = getYMD(this.value);
  year += n;
  this.$emit("input", new Date(year, month, 1));
},

6. 具體的scss樣式

@import './common/var.scss';
@import './common/mixin.scss';
@import './common/extend.scss';

@include b(date) {
    position: relative;
    display: inline-block;
    @include b(date-input){
      border: 1px solid $--color-disabled;
      // 輸入框的outline根據需求來判斷到底要不要清理吧.
      outline: 0px;
      padding: 8px;
      font-size: 16px;
      border-radius:7px; 
    }
    @include b(date-pannel){
       // 這種彈出框確定是要針對視口定位的
        position: fixed;
        border: 1px solid $--color-disabled;
        background-color: $--color-white;
        width: 280px;
        padding: 8px;
        border-radius:7px; 
        .pannel-nav{
            display: flex;
            align-items: center;
            // 總體有一個環繞效果
            justify-content: space-around;
            // 外圈的輪廓
            box-shadow: 0px 2px 2px 2px $--color-difference;
            padding: 6px 0;
            margin-bottom: 10px;
            .pannel-selected{
              width: 160px;
              text-align: center;
            }
            &>span{
                &:hover{
                    cursor: pointer;
                    color: $--color-nomal
                }
            }
        }
        .pannel-content{
            box-shadow: 0px 2px 2px 2px $--color-difference;
            ul{
                display: flex;
            }
            li{
                text-align: center;
                flex: 1;
                height: 35px;
                line-height: 35px;
            }
            .read-only{
               color: $--color-disabled;
            }
            .active-date{
                @extend .active-item;
            }
            .pannel-content__item{
                cursor: pointer;
                border: 1px solid $--color-difference;
                // li標籤中, 沒有.read-only class的標籤;
                li:not(.read-only){
                  // 平時是處於縮小狀態的
                    transition: all .2s;
                    transform: scale(.8);
                    &:hover{
                        transform: scale(1.3);
                        @extend .active-item;
                    }
                }
            }
        }
      }
}

end

你們均可以一塊兒交流, 共同窗習,共同進步, 早日實現自我價值!!
下一集聊聊'tree組件'
做者對tree組件有些不同的理解, 因此作出來的組件也比較怪異吧,可是我挺喜歡個人想法, 下一期與你們分享一下另類的tree.

github:github
我的技術博客(組件的官網):博客
仿寫Vue項目(這個項目裏面也有不少有趣的想法): 項目地址
相關文章:連接描述

相關文章
相關標籤/搜索