zoukankan      html  css  js  c++  java
  • 用vuejs仿写一个移动端日历组件

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

    参考文章:

    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) 
        //获取前方空格数。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;
         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;
                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: ' ';
         .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;
    }
    
  • 相关阅读:
    浏览器屏蔽百度搜索右侧热搜推荐脚本,收拾流氓头子
    js简单代码实现大banner轮播
    jquery回到顶部代码
    jquery实现隔行换色及移入移出效果
    利用:before和:after给段落添加效果的应用实例
    nginx配置文件应对网站攻击采集垃圾蜘蛛的方法总结
    Cygwin统计日志常用代码,欢迎各位大神补全
    原生js鼠标拖拽div左右滑动
    Day 82 VUE——基础
    Day 81 ES6
  • 原文地址:https://www.cnblogs.com/sanhuamao/p/13878015.html
Copyright © 2011-2022 走看看