仿写一个日历组件,有些粗糙,需要优化的地方欢迎提出!
参考文章:
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
功能
- 不展开时,滑动切换周
- 展开时,滑动切换月
- 默认选择当天
- 切换月份或选中日期后传递数据给父组件
组件结构
<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>
优化历程
选择其他日期时,当日依旧有样式
绑定样式:
//用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()
},
收起月展示周时,显示选中的那一行
//修改前
<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()
}
根据每天的状态获取不同颜色的小圆点
具体思路如下:
- 子组件注册一个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来获取当前月了:
现在在父组件中修改一下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;
}