vue实现一个炫酷的日历组件
公司业务新开了一个商家管理微信H5移动端项目,日历控件是商家管理员查看通过日程来筛选获取某日用户的订单等数据。 如图: 假设今天为2018-09-02
90天前:
90天后;
产品需求:
- 展示当前日期(服务器时间)前后90天,一共181天的日期。
- 日历可以左右滑动切换月份。
- 当月份的如果不在181天区间的,需要置灰并且不可点击。
- 点击日历绑定的节点的外部,关闭弹窗。
涉及内容:
- 获取服务器时间,渲染日历数据
- vue-touch监听手势滑动事件
- ios日期兼容处理
- clickOutSide自定义指令
- mock模拟数据
开发:
参考了 基于Vue开发一个日历组件 - 掘金 日历的年月日计算方式。 核心思想:假设当前月份是二月份,根据二月和三月的1号是星期几,来对二月进行布局。(如果需要在二月显示一月和三月的日期,还需要知道一月份有多少天)
在项目开发中,为了与后台同事并行开发。项目采用来mock模拟数据来拦截接口。
日历展盘
// calendar.vue <template> <div class="cp-calendar"> <v-touch @swipeleft="handleNextMonth" @swiperight="handlePreMonth" class="calendar"> <div class="calendar-main" > <span class="item-con header-item" v-for="(item, index) in calendarHeader" :key="index">{{item}}</span> <div :class="`item-con ${todayStyle(item.content) && 'item-con-today'} ${item.type === 'disabled' && 'disableStyle'}`" :style="{opacity: isChangeMonth ? 0 : 1}" @click.stop="handleDayClick(item)" v-for="(item, index) in getMonthDays(selectedYear, selectedMonth)" :key="item.type + item.content + `${index}`"> <span :class="`main-content ${selectedDateStyle(item.content) && 'selectedColor'}`"> {{setContent(item.content)}}</span> <span :class="`${selectedDateStyle(item.content) && 'item-con-point'}`" ></span> </div> </div> </v-touch> </div> </template>
初始化数据 针对服务器时间进行初始数据处理
// calendar.vue // 设置初始数据 initData () { this.today = this.currentDate || getDateStr(0) // 如果没有服务器时间,拿本地时间 this.prevDate = getDateStr(-90, this.currentDate) this.nextDate = getDateStr(90, this.currentDate) // 是否有手动选中的日期 let selectedFullDate = this.storeSelectedFullDate if (!this.storeSelectedFullDate) { selectedFullDate = this.currentDate || getDateStr(0) // 如果没有服务器时间,拿本地时间 } this.selectedYear = Number(selectedFullDate.split('-')[0]) this.selectedMonth = Number(selectedFullDate.split('-')[1]) - 1 this.selectedDate = Number(selectedFullDate.split('-')[2]) this.selectedFullDate = `${this.selectedYear}-${this.selectedMonth + 1}-${this.selectedDate}` }, / 渲染日期 getMonthDays(year, month) { // 定义每个月的天数,如果是闰年第二月改为29天 let daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) { daysInMonth[1] = 29; } // 当月第一天为周几 let targetDay = new Date(year, month, 1).getDay(); let calendarDateList = []; let preNum = targetDay; let nextNum = 0; if (targetDay > 0) { // 当前月份1号前的自然周剩余日期,置空 for (let i = 0; i < preNum; i++) { let obj = { type: 'pre', content: '' }; calendarDateList.push(obj); } } // 判断当前年月份 let formatMonth = month + 1 >= 10 ? month + 1 : '0' + (month + 1) this.prevYearMonthBoolean = (`${year}-${formatMonth}` === this.prevYearMonth) this.nextYearMonthBoolean = (`${year}-${formatMonth}` === this.nextYearMonth) for (let i = 0; i < daysInMonth[month]; i++) { // 正常显示的日期 let obj = { type: 'normal', content: i + 1 }; // 判断是否为最往前或者最往后的月份,筛选出不可点击的日期 if (this.prevYearMonthBoolean) { let prevDay = this.prevDate.split('-')[2] if (i + 1 < prevDay) { obj.type = 'disabled' } } else if (this.nextYearMonthBoolean) { let nextDay = this.nextDate.split('-')[2] if (i + 1 > nextDay) { obj.type = 'disabled' } } calendarDateList.push(obj); } nextNum = 6 - new Date(year, month + 1, 0).getDay() // 当前月份最后一天的自然周剩余日期,置空 for (let i = 0; i < nextNum; i++) { let obj = { type: 'next', content: '' }; calendarDateList.push(obj); } return calendarDateList; }, // 设置日期 setContent (content) { if (!content) return '' return `${this.selectedYear}-${this.tf(this.selectedMonth + 1)}-${this.tf(content)}` === this.today ? '今天' : content }, // '今天'样式开关 todayStyle (content) { if (!content) return false // Toast(`${this.selectedYear}-${this.tf(this.selectedMonth + 1)}-${this.tf(content)}`) return `${this.selectedYear}-${this.tf(this.selectedMonth + 1)}-${this.tf(content)}` === this.today }, // 当前选中的日期样式开关 selectedDateStyle (content) { if (!content) return false return `${this.selectedYear}-${this.selectedMonth + 1}-${content}` === this.selectedFullDate },
// src/config/utils.js // 公共方法 /** * @param AddDayCount 必传 今天前后N天的日期 * @param dateStr: 非必传 获取传入日期前后N天的日期:'2018-01-20' * @param type 非必传 'lhRili'类型格式如'2018-7-3' * @return 返回日期'2018/01/20' */ export const getDateStr = (AddDayCount, dateStr, type) => { // console.log('getDateStr', AddDayCount, dateStr, type) var dd if (!dateStr) { dd = new Date() } else { // 判断是否为IOS const isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); let formatDateStr = isIOS ? dateStr.replace(/-/g, '/') : dateStr dd = new Date((formatDateStr.length < 12) ? formatDateStr + ' 00:00:00' : formatDateStr) } dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期 let y = dd.getFullYear() let m let d if (type === 'lhRili') { m = dd.getMonth() + 1 d = dd.getDate() } else { let currentMon = (dd.getMonth() + 1) let getDate = dd.getDate() m = currentMon < 10 ? '0' + currentMon : currentMon // 获取当前月份的日期,不足10补0 d = getDate < 10 ? '0' + getDate : getDate // 获取当前几号,不足10补0 } let time = y + '-' + m + '-' + d return time }
左右触摸滑动事件 判断是否月份还可以继续滑动
// calendar.vue // 上一个月 handlePreMonth() { if (this.prevYearMonthBoolean) { return } if (this.selectedMonth === 0) { this.selectedYear = this.selectedYear - 1 this.selectedMonth = 11 this.selectedDate = 1 } else { this.selectedMonth = this.selectedMonth - 1 this.selectedDate = 1 } }, // 下一个月 handleNextMonth() { if (this.nextYearMonthBoolean) { return } if (this.selectedMonth === 11) { this.selectedYear = this.selectedYear + 1 this.selectedMonth = 0 this.selectedDate = 1 } else { this.selectedMonth = this.selectedMonth + 1 this.selectedDate = 1 } },
vuex存储数据
// src/store/schedule.js const schedule = { state: { selectedDate: '', // 手动点击选中的日期 currentDate: '' // 服务器当前日期 }, getters: { getSelectedDate: state => state.selectedDate, getCurrentDate: state => state.currentDate }, mutations: { SET_SELECTED_DATE: (state, data) => { state.selectedDate = data }, SET_CURRENT_DATE: (state, data) => { state.currentDate = data } }, actions: { setSelectedDate: ({ commit }, data) => commit('SET_SELECTED_DATE', data), setCurrentDate: ({ commit }, data) => commit('SET_CURRENT_DATE', data) } }; export default schedule;
clickOutSide指令 指令方法监听
// src/directive/click-out-side.js export default{ bind (el, binding, vnode) { function documentHandler (e) { if (el.contains(e.target)) { return false; } if (binding.expression) { binding.value(e); } } el.__vueClickOutside__ = documentHandler; document.addEventListener('click', documentHandler); }, unbind (el, binding) { document.removeEventListener('click', el.__vueClickOutside__); delete el.__vueClickOutside__; } }
注册指令
// src/directive/index.js import clickOutSide from './click-out-side' const install = function (Vue) { Vue.directive('click-outside', clickOutSide) } if (window.Vue) { window.clickOutSide = clickOutSide Vue.use(install); // eslint-disable-line } clickOutSide.install = install export default clickOutSide
// src/main.js import clickOutSide from '@/directive/click-out-side/index' Vue.use(clickOutSide)
使用方式:当某节点外部需要触发事件时,挂载到该节点上
// calendar.vue <div class="cp-calendar" v-click-outside="spaceClick"> .... </div>
这里需要使用 fastclick 库来消除解决移动端点击事件300ms延时
// src/mian.js import FastClick from 'fastclick' // 在移动端,手指点击一个元素,会经过:touchstart --> touchmove -> touchend --> click。 FastClick.attach(document.body);
mock数据
// src/mock/index.js // mock数据入口 import Mock from 'mockjs' import currentTime from './currentTime' // 拦截接口请求 Mock.mock(/\/schedule\/getCurrentTime/, 'get', currentTime) export default Mock // src/mock/currentTime.js import Mock from 'mockjs' export default { getList: () => { return { 'status': 'true', 'code': '200', 'msg': null, 'info': { 'currentDate': '2018-09-02' } } } } // src/main.js // 开发环境引入mock if (process.env.NODE_ENV === 'development') { require('./mock') // 需要在这里引入mock数据才可以全局拦截请求 }
坑点
- 在微信内置浏览器中,ios的日期格式跟安卓的日期格式分别是:YY/MM/DD和YY-MM-DD。这里需要对微信内置浏览器User Agent进行判断。
- 获取服务器时间的异步问题,把获取到的服务器时间保存在vuex里面,在calendar.vue页面监听当前日期的变化。及时将日历数据计算渲染出来。
推荐:
感兴趣的朋友可以关注小编的微信公众号【码农那点事儿】,更多网页制作特效源码及学习干货哦!!!
总结
以上所述是小编给大家介绍的vue实现一个炫酷的日历组件,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!
赞 (0)