vue |仿寫一個移動端日歷組件


仿寫一個日歷組件,有些粗糙,需要優化的地方歡迎提出!

參考文章:

https://www.jianshu.com/p/67acaaf7d2f7

https://blog.csdn.net/zxb89757/article/details/103579415?ops_request_misc=%7B%22request%5Fid%22%3A%22160359079019195264707225%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=160359079019195264707225&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v28-13-103579415.pc_first_rank_v2_rank_v28&utm_term=%E5%B0%81%E8%A3%85%E6%97%A5%E5%8E%86%E7%BB%84%E4%BB%B6&spm=1018.2118.3001.4187

https://www.jianshu.com/p/612cd47b966d

功能

  • 不展開時,滑動切換周
  • 展開時,滑動切換月
  • 默認選擇當天
  • 切換月份或選中日期后傳遞數據給父組件

導出_222946_2.gif

組件結構

<template>
  <div>
    <!-- 日歷容器 展示月或展示周 -->
    <div class="calendar" :class="[!visible?'hidden':'']" >
        <!-- 列出周一至周日 -->
        <div class="flex_sb cellbox">
            <p v-for="item in weekList" :key="item.id" class="week">{{item}}</p>
        </div>

        <!-- 左右滑動事件 -->
        <v-touch @swipeleft="onSwipeLeft" @swiperight="onSwipeRight" tag="div">
            <!-- 具體日期容器-->
            <div class="flex_sb cellbox border relative"  :class="[visible?'row-1':'row-'+weekRow]">
                <!-- 1.日歷前方的空缺部分 -->
                <p v-for="item in headDays" :key="item.id" class="grey">{{item}}</p>
                <!-- 2.有效日期 -->
                <p v-for="(item,index) in monthDay[this.month-1]" 
                    @click="setDay(index+1)" 
                    :class="index+1===activeDay?'active':''"
                    class="relative"
                    :key="index">
                    {{item}}
                </p>
                <!-- 3.日歷后方的空缺部分 -->
                <p v-for="item in tailDays" :key="item.id" class="grey">{{item}}</p>
            </div>
        </v-touch>
    </div>
    <!-- 控制展開 -->
    <div>
        <van-icon name="arrow-down" v-if="!visible" @click="visible=true"/>
        <van-icon name="arrow-up" @click="visible=false" v-else/>
    </div>
  </div>
</template>

組件數據

data(){
    return{
        year:'',        //年
        month:'',       //月
        day:'',         //日
        weekList:['一','二','三','四','五','六','日'],
        monthDay:[31,'',31,30,31,30,31,31,30,31,30,31],//每月的天數,二月份待定
        
        spaceDay: '',   //當月日期前方的空格數
        headDays:[],    //上個月月尾
        tailDays:[],    //下個月月頭

        selectedDay:'',
        activeDay: '',  //選中的日期
        visible:false,  //判斷日歷是否展開
        weekRow:2,     //當前周 用於按周切換
        rows:''        //當前月的周數
    }
},

要點

  • 左右滑動事件
  • 獲取每月的天數
  • 補前后空格
  • 選中日期樣式
  • 切換周與月
  • 監聽子組件數據變化

1.左右滑動事件

安裝插件

npm install vue-touch@next --save

在main.js 中 引入:

import VueTouch from 'vue-touch'
Vue.use(VueTouch, {name: 'v-touch'})
VueTouch.config.swipe = {
    threshold: 100 //手指左右滑動距離
}

使用

<v-touch @swipeleft="onSwipeLeft" @swiperight="onSwipeRight"  tag="div">
	(你的組件)
</v-touch>

2.獲取每月的天數

判斷2月是否為閏月:

isLeapYear(year){
	return year%4==0&&year%100!==0||year%400==0
},
getFebruary(){
    let February=this.isLeapYear(this.year)?29:28
    this.monthDay.splice(1,1,February)
},

3.補充前后空格

getWholeMonth(){
    //獲取某年某月的第一天
    let firstDay = new Date(this.year,this.month-1,1) //由於new Date的月份是從0開始的,所以this.month要減1
    //獲取前方空格數。getDay()函數判斷是周幾,當getDay()=0表示周日
    if(firstDay.getDay() == 0){
        this.spaceDay = 6
    } else {
        this.spaceDay = firstDay.getDay() - 1
    }
    this.getPrevDays() //補前方空格
    this.getCells()    //補后方空格
},
//補前方空格
//獲取上個月的天數
getPrevDays(){
    //如果當前月為一月份,那么上個月就是十二月份,獲取十二月份的的最后一天的日期(即月份天數)並傳過去。這里傳索引11
    if(this.month==1){
        this.getHeadDays(this.monthDay[11])
    }else{
        this.getHeadDays(this.monthDay[this.month-2])
    }
},

//用具體數字補充前方的空格
getHeadDays(end){
    let headDays=[31,30,29,28,27,26,25,24,23,22]//用於截取的數組 補前方空格
    if(end==31){
        this.headDays=headDays.slice(0,this.spaceDay).reverse()
    }else if(end==30){
        this.headDays=headDays.slice(1,this.spaceDay+1).reverse()
    }else if(end==29){
        this.headDays=headDays.slice(2,this.spaceDay+2).reverse()
    }else if(end==28){
        this.headDays=headDays.slice(3,this.spaceDay+3).reverse()
    }
},
//補后方空格
//獲取方格數與行數
getCells(){
	let cells=this.spaceDay+this.monthDay[this.month-1]
    //余數不能為0(否則就補一行了),cells%7獲取余數
    //一周有7天,假設余數為2,那么后方沒有補的空格就位7-2
	if(7-cells%7!==7){
        this.getTailDays(7-cells%7)
    }else{
        this.tailDays=[]
    }
},
    
//用具體數字補充后方的空格
getTailDays(end){
    let tailDays=[1,2,3,4,5,6,7]//用於截取的數組 補后方空格
    this.tailDays=tailDays.slice(0,end)
},

4.選中日期樣式

//選中日期觸發事件
setDay(day){
    this.day = day
    this.selectedDay=this.year+'-'+this.month+'-'+this.day
    this.$emit('day-change', this.selectedDay);
    //activeDay用於添加選中時的樣式
    this.activeDay=day
},

默認選擇當前日期:由於默認情況下是展示周,所以需要先判斷當日在第幾周,即第幾行:

created(){
    //...
    this.setDay(this.day)
    this.defaultShow()
},
defaultShow(){
    //展示周時,獲取當日的行數並展示
    if(!this.visible){
        this.weekRow=Math.ceil((this.spaceDay+this.day)/7)
    }
},

5.切換月與周

  • 切換月實質上就是改變變量month,讓其動態獲取monthDay中的天數
  • 切換周實質上就是移動日歷的上下位置,當展開時,位置在第一行;當未展開時,動態改變位置,其中weekRow是變量:
//日歷容器,切換周時,通過class綁定位置
<div class="flex_sb cellbox border relative"  :class="[visible?'row-1':'row-'+weekRow]">
.row-1{
    top:0
}
.row-2{
    top:-2.4em
}
.row-3{
    top:-4.8em
}
.row-4{
    top:-7.2em
}
.row-5{
    top:-9.6em
}
.row-6{
    top:-12em
}

以左滑為例

//左滑 下一個
onSwipeLeft(){
    //1.展開的情況下 滑動切換月份
    if(this.visible){
        if(this.month==12){
            this.year++
            this.month=1
        }else{
            this.month++
        }
        this.getWholeMonth()
        this.$emit('month-change', this.month);//只要切換了月,就監聽
    }else{
    //2.未展開的情況下 滑動切換周
        this.getWholeMonth()//先獲取當前行
        //當前周小於行數時,切換下一周
        if(this.weekRow<this.rows){
            this.weekRow++
        }else{
        //當前周等於行數時,切換下一個月份,當前周變成第一周。
        //由於要切到第一周,所以不用獲取下個月的行
            if(this.month==12){
                this.year++
                this.month=1
                this.weekRow=1
            }else{
                this.month++
                this.weekRow=1
            }
            this.getWholeMonth()//由於更換了月,所以調用該函數補空格
            this.$emit('month-change', this.month);
            }
    }
    this.activeDay=0//這樣切換月的時候就不會默認選擇日期
},

6.監聽子組件數據變化

子組件

//點擊日期時:
setDay(day){
    //...
    this.$emit('day-change', this.selectedDay);
},
//切換月時:
onSwipeLeft(){
    //...
    this.$emit('month-change', this.month);
}

父組件

//父組件結構:
<Calendar ref="calendar"  @month-change="updateMonth" @day-change="updateDay"/>
//掛載后先獲取月
mounted(){
	this.current=this.$refs.calendar.month
},
methods:{
	//更新月份
	updateMonth(month){
		console.log('month',month)
	},
	//更新選擇日期
	updateDay(day){
		console.log('day',day)
	}
},

完整源碼

子組件

<template>
  <div>
    <!-- 日歷容器 展示月或展示周 -->
    <div class="calendar" :class="[!visible?'hidden':'']" >
        <!-- 列出周一至周日 -->
        <div class="flex_sb cellbox">
            <p v-for="item in weekList" :key="item.id" class="week">{{item}}</p>
        </div>

        <!-- 左右滑動事件 -->
        <v-touch @swipeleft="onSwipeLeft" @swiperight="onSwipeRight" tag="div">
            <!-- 具體日期容器-->
            <div class="flex_sb cellbox border relative"  :class="[visible?'row-1':'row-'+weekRow]">
                <!-- 1.日歷前方的空缺部分 -->
                <p v-for="item in headDays" :key="item.id" class="grey">{{item}}</p>
                <!-- 2.有效日期 -->
                <p v-for="(item,index) in monthDay[this.month-1]" 
                    @click="setDay(index+1)" 
                    :class="index+1===activeDay?'active':''"
                    class="relative"
                    :key="index">
                    {{item}}
                </p>
                <!-- 3.日歷后方的空缺部分 -->
                <p v-for="item in tailDays" :key="item.id" class="grey">{{item}}</p>
            </div>
        </v-touch>
    </div>
    <!-- 控制展開 -->
    <div>
        <van-icon name="arrow-down" v-if="!visible" @click="visible=true"/>
        <van-icon name="arrow-up" @click="visible=false" v-else/>
    </div>
  </div>
</template>

<script>
export default {
data(){
    return{
        year:'',        //年
        month:'',       //月
        day:'',         //日
        weekList:['一','二','三','四','五','六','日'],
        monthDay:[31,'',31,30,31,30,31,31,30,31,30,31],
        
        spaceDay: '',   //當月日期前方的空格數
        headDays:[],    //上個月月尾
        tailDays:[],    //下個月月頭

        selectedDay:'',
        activeDay: '',  //選中的日期
        visible:false,  //判斷日歷是否展開
        weekRow:2,     //當前周 用於按周切換
        rows:''        //當前月的周數
    }
},

    created(){
        this.getTheCurrentDate() //獲取當前日期(年月日)
		this.getFebruary()//獲取二月份天數
        this.getWholeMonth() //獲取完整月份日歷
        this.defaultShow()
        this.setDay(this.day)
    },
    methods:{
        //判斷是否為閏年
        isLeapYear(year){
            return year%4==0&&year%100!==0||year%400==0
        },

        //獲取當前日期
        getTheCurrentDate(){
            let current=new Date()
            this.year = current.getFullYear()
            this.month = current.getMonth() + 1
            this.day = current.getDate()
        },

        //默認顯示周
        defaultShow(){
            //獲取當日的行數
            if(!this.visible){
                this.weekRow=Math.ceil((this.spaceDay+this.day)/7)
            }
        },

        //獲取空格被填充過的完整的月
        getWholeMonth(){
            let firstDay = new Date(this.year,this.month-1,1) //獲取某年某月的第一天,由於new Date的月份按索引判斷,所以-1
            //獲取前方空格數
            if(firstDay.getDay() == 0){
                this.spaceDay = 6
            } else {
                this.spaceDay = firstDay.getDay() - 1
            }
            this.getPrevDays() //補前方空格
            this.getCells()    //補后方空格
        },
        
        //獲取上個月的天數 並調用函數補充開頭空格
        getPrevDays(){
            //this.month表示的是月份,
            //如果當前月為一月份,獲取十二月份的天數並傳過去。所以傳索引11
            if(this.month==1){
                this.getHeadDays(this.monthDay[11])
            }else{
                this.getHeadDays(this.monthDay[this.month-2])
            }
        },
        //補開頭空格
        getHeadDays(end){
            let headDays=[31,30,29,28,27,26,25,24,23,22]//用於截取的數組 補前方空格
            if(end==31){
                this.headDays=headDays.slice(0,this.spaceDay).reverse()
            }else if(end==30){
                this.headDays=headDays.slice(1,this.spaceDay+1).reverse()
            }else if(end==29){
                this.headDays=headDays.slice(2,this.spaceDay+2).reverse()
            }else if(end==28){
                this.headDays=headDays.slice(3,this.spaceDay+3).reverse()
            }
        },
        //獲取月份方格數,用於補后方空格 並獲取行/重新獲取行
        getCells(){
            let cells=this.spaceDay+this.monthDay[this.month-1]
            //余數不能為0(否則就補一行了),cells%7獲取余數
            //一周有7天,假設余數為2,那么后方沒有補的空格就位7-2
            if(7-cells%7!==7){
                this.getTailDays(7-cells%7)
            }else{
                this.tailDays=[]
            }
            //向上取整
            this.rows=Math.ceil(cells/7)
        },
        //補后方空格
        getTailDays(end){
            let tailDays=[1,2,3,4,5,6,7]//用於截取的數組 補后方空格
            this.tailDays=tailDays.slice(0,end)
        },

        //選取特定日期
        setDay(day){
            this.day = day
            this.selectedDay=this.year+'-'+this.month+'-'+this.day
            this.activeDay=day
            this.$emit('day-change', this.selectedDay);
        },

        //左滑 下一個
        onSwipeLeft(){
            //1.展開的情況下 滑動切換月份
            if(this.visible){
                if(this.month==12){
                    this.year++
                    this.month=1
                }else{
                    this.month++
                }
                this.getWholeMonth()
                this.$emit('month-change', this.month);    
            }else{
            //2.未展開的情況下 滑動切換周
                this.getWholeMonth()//先獲取當前行
                //當前周小於行數時,切換下一周
                if(this.weekRow<this.rows){
                    this.weekRow++
                }else{
                //當前周等於行數時,切換下一個月份,當前周變成第一周。
                //由於要切到第一周,所以不用獲取下個月的行
                    if(this.month==12){
                        this.year++
                        this.month=1
                        this.weekRow=1
                    }else{
                        this.month++
                        this.weekRow=1
                    }
                    this.getWholeMonth()//由於更換了月,所以調用該函數補空格
                    this.$emit('month-change', this.month);
                    }
            }
            this.activeDay=0//這樣切換月的時候就不會默認選擇日期
        },
            
        //右滑 上一個
        onSwipeRight(){
            //1.展開的情況下 滑動切換月份
            if(this.visible){
                if(this.month==1){
                    this.year--
                    this.month=12
                }else{
                    this.month--
                }
                this.getWholeMonth()
                this.$emit('month-change', this.month);
            }else{
            //2.未展開的情況下 滑動切換周
                //當前周大於1時,切換上一周
                if(this.weekRow>1){
                    this.weekRow--
                }else{
                //當前周等於1時,切換上一個月,並把當前周變成上個月的最后一周
                    if(this.month==1){
                        this.year--
                        //成功切換到上個月
                        this.month=12
                        //調用該函數重新獲取行數
                        this.getWholeMonth()
                        this.weekRow=this.rows
                    }else{
                        this.month--
                        this.getWholeMonth()
                        this.weekRow=this.rows
                    }
                    this.$emit('month-change', this.month);  
                }
            }
            this.activeDay=0
        },
    }
}
</script>

<style lang="scss" scoped>
.calendar{
    font-size: .8em;
    width: 80%;
    margin: 0 auto;
    height: auto;
    .flex_sb{
        display: flex;
        justify-content:space-between;
    }
    .grey{
        background-color: rgb(247, 244, 244);
    }
    .relative{
        position: relative;
    }
    &.hidden{
        height: 4.8em;
        overflow: hidden;
    }

    .week{
        z-index: 10;
        background: #fff;
    }
    .cellbox{
        flex-wrap: wrap;
        margin: 0;
        p{
            display: inline-block;
            width:14.28%;
            height:2.4em;
            line-height: 2.4em;
            box-sizing: border-box;
            margin: 0;
            &.active{
                color: #eee;
                background-color: #409EFF;
            }
        }
    }
    .border p{
        border: 1px solid #eee;
    }
    .row-1{
        top:0
    }
    .row-2{
        top:-2.4em
    }
    .row-3{
        top:-4.8em
    }
    .row-4{
        top:-7.2em
    }
    .row-5{
        top:-9.6em
    }
    .row-6{
        top:-12em
    }
}


</style>

優化歷程

選擇其他日期時,當日依舊有樣式

導出_223804_2.gif

綁定樣式:

//用class綁定多個樣式,currentDay表示當日。
<p :class="(index+1===currentDay?'current':'')+(index+1===activeDay?'active':'')">

樣式:

.cellbox{
    p{
        //...
        &.current{
        color: #eee;
        background-color: #409EFF;
        opacity: .6;
        }
    }
}

數據結構:

data(){
    return{
        //...
        currentMonth:'', //當前月,用於后面判斷。格式為'2020-10'
        flag:'',        //在當前月的前提下,指定當日日期;在非當前月的情況下,為0。
        currentDay:'',  //變量
    }
}

數據邏輯:

在獲取當前日期的函數getTheCurrentDate()中(只在created()中調用過一次):保存當前月,並且獲取當前日

getTheCurrentDate(){
  //...
  this.currentMonth=this.year+'-'+this.month//用於判斷是否為當前月 只讀。
  this.flag=this.day//flag只在第一次加載組件時賦值 只讀。
},

當日樣式在選中日期后出現,即在setDay()觸發時出現,所以在這個函數中進行判斷:

setDay(day){
  //...
  //判斷選中的是否為當前年當前月當前日
  this.isCurrent(day)
},
isCurrent(day){
  //先判斷是否為當前月,不是則返回0,這樣就不會有當日樣式了。(因為index+1最小為1)
  if(this.year+'-'+this.month!==this.currentMonth){
      this.currentDay=0
  }else{
      //好了,判斷結果是當前月,接着判斷具體日
      //現在只在選中非當日的時候才有當日樣式,否則樣式沖突,會導致第一次加載組件時兩種樣式都不會出現
      if(day!==this.flag){
          this.currentDay=this.flag
          console.log(this.currentDay)
      }else{
          //如果當日與選中日一致時,設為0,取消當日樣式
          this.currentDay=0
      }
  }
},

在其他有數據關聯的地方進行完善,即左滑和右滑事件觸發導致月份或年份改變時:

onSwipeLeft(){
    //...
    //只需在會改變月份的作用域里調用該函數即可。(可以認准$emit監聽月份變化,放在它后面即可)
    this.$emit('month-change', this.month);
    this.isCurrent()
},

收起月展示周時,顯示選中的那一行

導出_074139_2.gif

//修改前
<van-icon name="arrow-up" @click="visible=false" v-else/>
//修改后
<van-icon name="arrow-up" @click="close" v-else/>
close(){ 
    //如果當前沒有選擇日期,那么收起時顯示在第一行
    if(this.activeDay==0){
        this.weekRow=1
    }else{
    //如果有選擇日期,那么收起時展示選中的那一行
        this.weekRow=Math.ceil((this.spaceDay+this.activeDay)/7)
    }
    this.visible=false//收起
},

當年份改變后,重新獲取二月份天數

this.year發生變化的作用域要重新調用getFebruary()來重新獲取二月份天數。主要出現在切換月和周的觸發函數中:

onSwipeLeft(){
    //...
	this.year++
    this.getFebruary()
}

根據每天的狀態獲取不同顏色的小圓點

image.png

具體思路如下:

  • 子組件注冊一個prop接受父組件傳過來的狀態
  • 該prop是一個數字數組,元素個數為當月天數,根據每個元素的數值來定義狀態
  • 子組件根據prop渲染日歷

首先,給子組件注冊一個prop值存放狀態,這樣就能通過父組件把狀態傳給子組件了。

//子組件
props:{
    monthStatus: Array,
},
//父組件
<Calendar ... @month-change="updateMonth" :monthStatus="monthStatus"/>
//父組件js部分
data(){
    return{
  		monthStatus:[],
	}
}

這個monthStatus是表示狀態數組,里面是純數字,長度為當月的天數,接着根據數字的不同來改變狀態。比如,[0,1,0,3]表示第一天和第三天為無狀態,第二天為狀態1,第四天為狀態3。

接着,父組件要知道當月的天數,所以我對子組件的month-change監聽做了一下修改,在后面多加了一個this.monthDay[this.month-1]參數來傳遞當月天數

//子組件-修改監聽
this.$emit('month-change', this.month,this.monthDay[this.month-1]);

並在created鈎子里添加了該監聽:

created(){
    //...
    this.$emit('month-change', this.month,this.monthDay[this.month-1]);
}

此時父組件就不需要用到ref來獲取當前月了:

image.png

現在在父組件中修改一下updateMonth函數,接受傳過來的天數,:

updateMonth(month,days){
    console.log('month',month)
    this.getStatus(days)
},

然后,通過getStatus函數獲取狀態數組。本意是想根據后台數據動態定義,然額由於沒有后台數據也不知道數據結構,我就先模擬了一下:

getStatus(days){
    //先把當月每天的狀態都調整為0
	let filledArr=new Array(days).fill(0)
	
    //開始定義狀態
    //splice(開始位置,刪除的個數,換成什么)
	filledArr.splice(0,1,1)//表示把第一個位置的值刪掉並換成1
	filledArr.splice(3,1,4)
	filledArr.splice(6,1,3)
	filledArr.splice(9,1,2)
	this.monthStatus=filledArr
},

最后,由於狀態數組傳過來了,現在回到子組件,在有效日期內添加一個span標簽來存放小圓點

<!-- 2.有效日期 -->
<p v-for="(item,index) in monthDay[this.month-1]" ...>
    {{item}}     
    <span :class="['point-'+monthStatus[item-1],'point']"></span>
</p>
//圓
.point::after{
    position: absolute;
    content: ' ';
    width: .3em;
    height: .3em;
    display: block;
    border-radius: .15em;
    top: 80%;
    left: 45%;
}
//無狀態
.point-0::after{
    content: '';
}
//狀態1
.point-1::after{
    background-color:#67C23A;
}
//狀態2
.point-2::after{
    background-color: red;
}
//狀態3
.point-3::after{
    background-color: orange;
}
//狀態4
.point-4::after{
    background-color:#409EFF;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM