亲自动手实现vue日历控件

所属分类: 网络编程 / JavaScript 阅读数: 163
收藏 0 赞 0 分享

之前项目中有用到日历控件,当时由于时间问题,是在网上找到一个demo,然后二次开发的,从那时就想着自己写一个日历控件。这篇文章说明日历数据的处理,去除月份天数判断以及是否闰年判断。 

设计(以最常用的按月份的日历)

日历其实大家都很熟悉,一切的设计都是从功能出发,这是根本。日历的功能分为两大块。

  • 日历头部:当前年份/月份。
  • 日历主体:当前月份的具体的日期信息。
  • 日历主体的行数:现在我们看到的日历基本上为6行,因为一个月最多为31天,假设当前月的第一天为上一月最后一周的最后一天。如果是五行数据的话则只显示了29天,这也是为什么显示6行数据的原因。

功能点

  • 日历初始渲染日期为当前月份
  • 头部的左右滑动,日历数据需要显示对应月份的信息
  • 点击日期本身可以进行相关数据操作,并且记录操作内容
  • 可以根据调用这设置日历的每周数据以星期*为开始,星期天或者星期一。

首先思考日历的核心问题

如何获取当前日期的年份以及月份

/**
 * 获取日历header内容 格式为:****年 **月
 * @param {*} date
 */
export const getHeaderContent = function (date) {
 let _date = new Date(date)
 
 return dateFormat(_date, 'yyyy年 MM月')
}

如何获取当前月份需要显示的42条数据(6*7),这42条数据是什么呢?

这个问题的核心是:当前月份显示的42条数据的第一天是哪一天?

这个问题的解决思路还要从上面的设计说起,上面提到日历主题的行数时,说到“假设当前月的第一天为上一月最后一周的最后一天”,那么42条数据显示的内容的第一条数据还要根据当前月的第一天是第一天所在周的第几天。

举例:2019-02-01

2月的第一天,星期五,所以当前月日历的第一天为2019-02-01 - 5

var date = new Date()
date.setDate(date.getDate() - date.getDay() + 1) // 获取当前月的第一天为2019-01-28

这里有一问题是什么呢?

date.getDate()的值为0 - 6(0为周日,如果你的日历也是将周日放在日历的第一天,没什么问题,可是在中国是将周日放在最后一天的),这也就意味着前面的实现还需要考虑日历的放置顺序,因为日历是按照普通的周一到周日,还是周日到周一,我们获取的当月日历的第一天是不同的。所以上面的代码还要依赖于日历的排放顺序。

这里的排放顺序将是日历组件的第一个可被调用者控制的参数。这里我的设想是将该参数的传入值与date.getDay()匹配。

  • 0:周日
  • 1:周一
  • .....
  • 5:周五
  • 6:周六

所以上面的公式为

date.setDate(date.getDate() - date.getDay() + x)

但是这里的x值加了之后的日期如果大于当前月份的第一天,那就需要将当前得到的日期数值再减去7天,这个原因就不用说明了吧。

/**
 * 获取当前月日历的第一天
 * @param {*} date
 */
export const getFirstDayOfCalendar = function (date, weekLabelIndex) {
 let _date = new Date(date)
 _date = new Date(_date.setDate(_date.getDate() - _date.getDay() + weekLabelIndex))
 // 如果当前日期大于当前月第一天,则需要减去7天
 if (_date > date) {
 _date = new Date(_date.setDate(_date.getDate() - 7))
 }
 
 return _date
}

接下来就好做了,只需要在当前的日期加上加上1,每次得到下一天的日期。

左右切换月份如何设定

上面设计都是以今天为计算初始值,左右切换的初始值如何设计呢?

第一反应是将当前的日期的月份进行加减1,这样是不行的,因为如果今天是31号,那么碰到下个月只有30的时候,这样就会碰到点击下月,直接切换了两个月。更别说2月这个月份天数不固定的月份。所以这里又是一个问题了。

我的解决思路是:月份点击切换的时候,初始计算值设计为当前月的第一天。

/**
 * 以传入参数作为基准获取下个月的第一天日期
 * @param {*} firstDayOfCurrentMonth
 */
export const getFirstDayOfNextMonth = function (firstDayOfCurrentMonth) {
 return new Date(firstDayOfCurrentMonth.getFullYear(), firstDayOfCurrentMonth.getMonth() + 1, 1)
}
 
/**
 * 以传入参数作为基准获取上个月的第一天日期
 * @param {*} firstDayOfCurrentMonth
 */
export const getFirstDayOfPrevMonth = function (firstDayOfCurrentMonth) {
 return new Date(firstDayOfCurrentMonth.getFullYear(), firstDayOfCurrentMonth.getMonth() - 1, 1)
}

左右切换月份数据传递方式(观察者模式)

因为对于日历组件本身来说,header和body是属于同一个父组件的同级组件,数据传递可以依赖于父组件进行传递,这里我使用的是观察者模式实现。

引入观察者模式代码:

/*
 * Subject
 * 内部创建了三个方法,内部维护了一个ObserverList。
 */
 
// contructor function
export const Subject = function () {
 this.observers = new ObserverList()
}
 
// addObserver: 调用内部维护的ObserverList的add方法
Subject.prototype.addObserver = function (observer) {
 this.observers.add(observer)
}
 
// removeObserver: 调用内部维护的ObserverList的removeat方法
Subject.prototype.removeObserver = function (observer) {
 this.observers.removeAt(this.observers.indexOf(observer, 0))
}
 
// notify: 通知函数,用于通知观察者并且执行update函数,update是一个实现接口的方法,是一个通知的触发方法。
Subject.prototype.notify = function (context) {
 let observerCount = this.observers.count()
 for (let i = 0; i < observerCount; i++) {
 this.observers.get(i).update(context)
 }
}
 
/*
 * ObserverList
 * 内部维护了一个数组,4个方法用于数组的操作,这里相关的内容还是属于subject,因为ObserverList的存在是为了将subject和内部维护的observers分离开来,清晰明了的作用。
 */
function ObserverList () {
 this.observerList = []
}
 
ObserverList.prototype.add = function (obj) {
 return this.observerList.push(obj)
}
 
ObserverList.prototype.count = function () {
 return this.observerList.length
}
 
ObserverList.prototype.get = function (index) {
 if (index > -1 && index < this.observerList.length) {
 return this.observerList[index]
 }
}
 
ObserverList.prototype.indexOf = function (obj, startIndex) {
 let i = startIndex
 
 while (i < this.observerList.length) {
 if (this.observerList[i] === obj) {
  return i
 }
 i++
 }
 
 return -1
}
 
ObserverList.prototype.removeAt = function (index) {
 this.observerList.splice(index, 1)
}
 
/*
 * The Observer
 * 提供更新接口,为想要得到通知消息的主体提供接口。
 */
export const Observer = function () {
 this.update = function () {
 // ...
 }
}

CalendarBody观察者注册:

CalendarHeader通知消息

组件设计以及结构

VueCalendar
 
 Component
 
 CalendarBody.vue
 CalendarHeader.vue
 
 lib
 
 Subject.js 
 Util.js
 index.vue

当前效果

周一为第一天:

周日为第一天

Github地址

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

更多精彩内容其他人还在看

AngularJs IE Compatibility 兼容老版本IE

本文主要介绍AngularJs IE Compatibility 兼容老版本IE的问题及解决办法,有兴趣的小伙伴可以参考下
收藏 0 赞 0 分享

AngularJs Modules详解及示例代码

本文主要介绍AngularJs Modules的相关知识,这里整理了详细的资料及简单示例代码,有兴趣的朋友可以参考下
收藏 0 赞 0 分享

AngularJs Scope详解及示例代码

本文主要介绍AngularJs Scope的知识,这里整理了详细资料及示例代码,有兴趣的小伙伴可以参考下
收藏 0 赞 0 分享

node.js中module.exports与exports用法上的区别

Node.js 引入了模块(Module)概念,一个模块可以通过module.exports 或 exports 将函数、变量等导出,以使其它 JavaScript 脚本通过require() 函数引入并使用。那么node.js中module.exports与exports有什么
收藏 0 赞 0 分享

基于JS实现发送短信验证码后的倒计时功能(无视页面刷新,页面关闭不进行倒计时功能)

这篇文章主要介绍了基于JS实现发送短信验证码后的倒计时功能(无视页面刷新,页面关闭不进行倒计时功能)的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
收藏 0 赞 0 分享

基于jQuery实现发送短信验证码后的倒计时功能(无视页面关闭)

最近做了一个项目,其中有需求要求实现发送短信验证码后倒计时功能,其中有个难点:要求关闭页面也进行倒计时。好吧,下面小编把jquery 发送验证码倒计时的实现代码分享给大家,大家可以参考下
收藏 0 赞 0 分享

js绘制购物车抛物线动画

这篇文章主要为大家详细介绍了js绘制购物车抛物线动画,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
收藏 0 赞 0 分享

vue.js入门教程之绑定class和style样式

小编之前介绍了通过vue.js计算属性,不知道大家都学会了吗。那这篇文章中我们将一起学习vue.js实现绑定class和style样式,有需要的朋友们可以参考借鉴。
收藏 0 赞 0 分享

纯JS实现可拖拽表单的简单实例

下面小编就为大家带来一篇纯JS实现可拖拽表单的简单实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
收藏 0 赞 0 分享

js实现StringBuffer的简单实例

下面小编就为大家带来一篇js实现StringBuffer的简单实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
收藏 0 赞 0 分享
查看更多