仿寫一個日歷組件,有些粗糙,需要優化的地方歡迎提出!
參考文章:
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;
}