vue |仿写一个移动端日历组件

仿写一个日历组件,有些粗糙,需要优化的地方欢迎提出!

参考文章:

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

https://blog.csdn.net/zxb89757/article/details/103579415?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160359079019195264707225%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&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;
}
posted @ 2020-10-26 13:31  sanhuamao  阅读(3644)  评论(1编辑  收藏  举报