irpas技术客

教你如何写一个符合自己需求的小程序日历组件_分享干货的猫_小程序写日历

网络 2657

1|0?前言

很多时候,我们生活中会有各种打卡的情况,比如 keep 的运动打卡、单词的学习打卡和各种签到打卡或者酒店的入住时间选择,这时候就需要我们书写一个日历组件来处理我们这种需求。

但是更多时候,我们都是网上找一个插件直接套用了,有没有想过自己实现一下呢?如果有,但是感觉不太会的话,接下来跟着我一起实现符合自己需求的日历吧

2|0?准备工作

因为我们是小程序日历嘛,所以必不可少的肯定是微信开发者工具啦。项目目录结构如下:

**

|-- calendar |-- app.js |-- app.json |-- app.wxss |-- project.config.json |-- sitemap.json |-- components | |-- calendar | |-- index.js | |-- index.json | |-- index.wxml | |-- index.wxss |-- pages |-- index |-- index.js |-- index.json |-- index.wxml |-- index.wxss

使用?git?下载空白模板:

**

git clone -b calendar https://gitee.com/gating/demo.git

ps: 下面步骤有点啰嗦,如果看目录结构就能懂的话就不需要跟着步骤啦

新建一个名为calendar的空文件夹

打卡微信开发者工具,新增项目,选中刚刚创建的calendar文件夹,开发模式选中小程序,AppID 使用测试号即可,如图所示:

创建完后,开发者工具会默认帮我们生成默认的代码,我们在当前文件夹新增components文件家,再在components文件夹中新增calendar文件夹,再从当前文件夹新增名为index的组件,如图:

ps:因为开发者工具会默认生成初始代码,所以直接使用他创建组件比较方便

删除一些和本次博文无关的代码,比如app.js中的本地缓存能力,具体参考空白模板 3|0?编写代码

接下来编写代码部分我们直接在VSCode编写,因为微信开发者工具实在太。。。- -所以还是使用VSCode编写比较合适

3|1?思考一下

想要实现日历,我们需要知道几个小知识:

根据常识,我们知道一个月最少有 28 天,最多有 31 天,一周有 7 天,那么就可以有 5 排,但如果有一个月的第一天为星期六且当月有 31 天,那么他就会有 6 排格子才对。比如2020年8月,如图所示:

我们需要知道,当月的第一天是周几我们需要知道,当月有多少天最重要的是小程序没有 DOM 操作概念,所以我们不能动态往当月第一天插入格子,所以只能根据第一天是周几循环插入格子

知道以上四点后,我们就可以编写我们的代码啦

首先,第二第三点是最简单的,我先书写第二第三点,怎么获取当前是周几呢?其实js的Date对象直接有现成的方法,我们直接拿来用就好了

**

console.log("今天是星期" + new Date().getDay());

我想有点小难度的是第三点,获取当月有多少天,因为你想,其他的月份的天数是固定的,唯独 2 月,在平年和闰年的不同年份中,2 月的天数也是不同的,那么有没有简单的方法获取当月有多少天呢,其实也是有的,Date实例中的getDate就可以实现我们想要的效果了

**

// 获取一个月有多少天 const getMonthDays = (year, month) => { let days = new Date(year, month + 1, 0).getDate(); return days; };

我们通过?Date?的第三个参数传 0 就可以获取上个月的最后一天,最后通过?getDate()?获取它的日期就可以对应我们当月的天数,那么就不需要我们自己处理平年和闰年的 2 月有多少天了

是不是又学到了小知识点呢?

解决了 2.3 两个问题,我们就可以往下书写我们的日历了。

众所周知,小程序规定宽度为750rpx(尺寸单位),而我们的一周有 7 天,即 7 格,那么就是每个格子的宽度为107rpx,不建议使用小数,因为 rpx 计算的时候,遇到小数会存在少量偏差。这里我们使用flex布局解决。

所以接下来就可以写我们的布局和生成我们的数据啦,从上面我们分析了,我们有 6 排格子,一排有 7 个,也就是一共 42 个格子。即需要遍历 42 次

先定义一下我们所需要的数据,便于我们后续操作:

**

[ { "dataStr": "2020/06/08", "day": "08", "month": "08", "otherMonth": false, "today": true, "year": 2020 } ]

这里我只定义个几个简单的基本数据,如果有不同的业务场景可以自行添加基础数据

小 tips

IOS 端的日期格式必须为/才可以转化为日期格式,比如2018/07/08,而2018-07-08则返回Invalid Date,所以我们需要把-都替换为/。

不单单是小程序,微信公众号,safari 都是一样的。

3|2?正式开始编写代码

那么就可以写我们的 js 代码了,在?components -> calendar目录下新建utils.js文件,书写我们创建数据的基础方法:

**

/** * 获取当月有多少天 * @param {String | Number} year => 年 * @param {String | Number} month => 月 */ const getMonthDays = (year, month) => { let days = new Date(year, month + 1, 0).getDate(); return days; }; /** * 补0 * @param {String | Number} num */ const toDou = (num) => { return num > 9 ? num : "0" + num; }; /** * 转换为日期格式 * @param {*} date */ const transformDate = (date) => { if (!(date instanceof Date)) { date = new Date(date); } return date; }; /** * 获取当前日期的年月日 * @param {any} date => 日期对象 */ const getDateObj = (date) => { date = transformDate(date); var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); return { year, month, day, dataStr: `${year}/${toDou(month)}/${toDou(day)}`, }; }; /** * 获取当月1号的时间戳 * @param {Date} date => 日期对象 */ const startOfMonth = (date) => { return date.setDate(1); }; // 获取今天,导出供组件作为默认值使用 const { dataStr } = getDateObj(new Date()); /** * 生成日历数据 * @param {Date} date => 日期对象 */ const getDate = (date) => { date = transformDate(date); // 计算需要补的格子 let dist; const { year, month } = getDateObj(date); // 获取当月有多少天 const days = getMonthDays(year, month - 1); // 获取当前日期是星期几 let currentDate = new Date(startOfMonth(date)).getDay(); // 众所周知的原因,一周的第一天时星期天,而我们做的日历星期天是放在最后的,所以我们这里需要改一下值 if (currentDate == 0) { currentDate = 7; } dist = currentDate - 1; currentDate -= 2; const res = []; for (let i = 0; i < 42; i++) { // 是否不是当前月 const otherMonth = i >= dist + days || i <= currentDate; const date = new Date(year, month - 1, -currentDate + i); const dateObj = getDateObj(date); res.push({ ...dateObj, today: dataStr === dateObj.dataStr, otherMonth, }); } return res; }; module.exports = { getMonthDays, toDou, getDateObj, startOfMonth, getDate, dataStr, transformDate, };

这里代码都比较简单,注释也有写,所以就不详细解释了,如有问题就评论,我看到会第一时间回复的。。。。

在?components -> calendar -> index.js?引入一下?utils.js?文件,然后在created这个生命周期打印一下我们的基础数据,看是否符合预期:

?如果你打印的和我打印的一致,那么就可以愉快的写我们组件的界面啦 😄

因为布局大多数都是样式方面的问题,这里就不多讲解啦,我想大家应该都会的,所以这里直接粘贴代码啦,主要部分我就讲解一下

index.wxml代码如下:

**

<view class="calendar-wrapper"> <view class="calendar-controller"> <view class="calendar-picker"> <text class="arrow left" bindtap="prevMonth"></text> <picker mode='date' fields='month' end="2999-12-31" start="1970-01-01" value="{{monthFormat}}" bindchange="dateChange" > <text class="month-format">{{monthFormat}}</text> </picker> <text class="arrow right" bindtap="nextMonth"></text> </view> </view> <view class="calendar-header"> <view class="item" wx:for="{{week}}" wx:key="*this">{{item}}</view> </view> <view class="calendar-container"> <view class="item {{item.today?'today':''}} {{item.otherMonth?'other-month':''}}" wx:for="{{calendar}}" wx:key="dataStr"> <text>{{item.day}}</text> </view> </view> </view>

index.wxss代码如下:

**

.calendar-container, .calendar-controller, .calendar-header, .calendar-picker, .calendar-container .item, .calendar-header .item { display: flex; align-items: center; line-height: normal; } .calendar-container, .calendar-controller, .calendar-header { justify-content: space-around; flex-wrap: wrap; } .calendar-container .item, .calendar-header .item { justify-content: center; width: 107rpx; font-size: 28rpx; height: 80rpx; } .calendar-header .item { color: #666; } .calendar-container .item { color: #111; } .calendar-container .item.other-month { color: #999; } .calendar-container .item.today { color: #6190e8; font-weight: 600; } .calendar-picker { font-size: 30rpx; color: #111; padding: 20rpx 0; } .month-format { margin: 0 30rpx; } .arrow { display: flex; padding: 10rpx 15rpx; background: #f7f8fc; } .arrow::after { content: ""; width: 14rpx; height: 14rpx; border-top: 4rpx solid #ccc; border-left: 4rpx solid #ccc; } .arrow.left::after { transform: rotateY(-45deg) rotate(-47deg) skew(5deg); } .arrow.right::after { transform: rotateY(-135deg) rotate(-47deg) skew(5deg); }

index.js代码如下:

**

// components/calendar/index.js const { getDate, dataStr, getDateObj } = require("./utils"); const getDateStr = (dataStr) => dataStr.slice(0, -3).replace("/", "-"); Component({ /** * 组件的属性列表 */ properties: {}, /** * 组件的初始数据 */ data: { week: ["一", "二", "三", "四", "五", "六", "日"], calendar: getDate(new Date()), monthFormat: getDateStr(dataStr), }, /** * 组件的方法列表 */ methods: { dateChange(e) { const monthFormat = e.detail.value; this.setData({ monthFormat, }); }, // 上个月日期 prevMonth() { const [year, month] = this.data.monthFormat.split("-"); const { dataStr } = getDateObj( new Date(year, month, 1).setMonth(month - 2) ); this.setData({ monthFormat: getDateStr(dataStr), calendar: getDate(new Date(dataStr)), }); }, // 下个月日期 nextMonth() { const [year, month] = this.data.monthFormat.split("-"); const { dataStr } = getDateObj(new Date(year, month, 1)); this.setData({ monthFormat: getDateStr(dataStr), calendar: getDate(new Date(dataStr)), }); }, }, created() {}, });

这里的主要迷惑点就是月份,因为我们得到的月份是转换后的(即月份+1),而js中的月份是从 0 开始的,所以我们获取上个月的时候月份就需要-2才能实现我们要的效果,而获取下个月的时候,因为本身我们月份本身就+1了,所以不需要进行操作。

书写完成布局后,大概会得出下面这个日历:

?写到这里,其实整个日历的雏形已经出来了,我们可以通过picker换,可以通过点击切换,也算一个相对可以使用的日历组件啦 😝

但是其实还是远远不够的,毕竟,我们连手势左右滑动切换日历这个功能都没有,所以接下来就完善我们这个日历吧

3|3?无缝滑动思考

你想,既然要做左右滑动切换了,肯定得无缝吧?既然得无缝,肯定不能生成多份吧?那么怎么才能用最少的 DOM 做到无缝呢?答案是我们只需要在我们可视范围内生成 DOM 结构即可,即我们的可视范围就是三份,如图所示:

既然说到了左右滑动,肯定少不了我们强大的swiper组件啦,我们这次的日历组件就是建立在swiper组件下实现的,既然用到了swiper,那么我们的布局肯定需要进行小改,数据结构也是,需要进行小改动。

刚才说了,我们的可是范围是三份,所以我们的数据结构就变成了长度为三的数组,即:

**

{ "calendarArr": [calendar, calendar, calendar] }

界面也是,我们新增一个swiper组件,然后遍历calendarArr这个数据,

**

<view class="calendar-wrapper"> <view class="calendar-controller"> <view class="calendar-picker"> <text class="arrow left" bindtap="prevMonth"></text> <picker mode='date' fields='month' end="2999-12-31" start="1970-01-01" value="{{monthFormat}}" bindchange="dateChange" > <text class="month-format">{{monthFormat}}</text> </picker> <text class="arrow right" bindtap="nextMonth"></text> </view> </view> <view class="calendar-header"> <view class="item" wx:for="{{week}}" wx:key="*this">{{item}}</view> </view> <swiper circular class="calendar-swiper" current="{{current}}" duration="{{duration}}" vertical="{{isVertical}}" skip-hidden-item-layout bindchange="swiperChange" bindanimationfinish="swiperAnimateFinish" bindtouchstart="swipeTouchStart" bindtouchend="swipeTouchEnd" > <block wx:for="{{calendarArr}}" wx:for-item="calendar" wx:key="index"> <swiper-item> <view class="calendar-container"> <view class="item {{item.today?'today':''}} {{item.otherMonth?'other-month':''}}" wx:for="{{calendar}}" wx:key="dataStr"> <text>{{item.day}}</text> </view> </view> </swiper-item> </block> </swiper> </view>

样式的话,因为swiper组件有默认样式,高度是150px,而我们这里6 * 80rpx,所以我们需要修改下它的默认样式,即添加下面的 css 即可:

**

.calendar-swiper { height: 480rpx; }

之后就是书写我们的逻辑啦,从布局可以看到我们用了touchstart和touchend,本意其实就是判断我们是向左滑还是向右滑(向上划还是向下滑),来切换我们的月份

3|4?如何区分左滑右滑(上滑下滑) 需要定两个变量供我们区分是滑动的方向,一个是swipeStartPoint,一个是isPrevMonth既然我们说到了无缝,那么肯定用户就会滑动多次,那么我们也需要一个值来计算用户滑动的次数,我们定义为changeCount这点也是最重要的一点,我们需用通过当前我们滑动到第几个swiper-item,来修改我们的上个月和下个月的数据,因为我们知道,当前的swiper-item肯定是中间的那个月份,所以我们也需要一个变量来标记我们当前的是第几个,我们定义为currentSwiperIndex,针对于这里的逻辑,我们举个例子:

**

// 假设我们现在是6月,那么数据就是 let calendar = [5, 6, 7]; // 那么我们的 currentSwiperIndex 这时是等于1的 // 假设我滑动了五月,currentSwiperIndex 这时变成0了,我们的月份还是不变 // 但是我们的逻辑就发生改变了 // 这时候的上个月变成了7,下个月变成6,我们需要通过 currentSwiperIndex 的值来动态修改他,即 calendar = [5, 6, 7]; // 半伪代码 const calendarArr = []; const now = getDate(currentDate); const prev = getDate(this.getPrevMonth(dataStr)); const next = getDate(this.getNextMonth(dataStr)); const prevIndex = currentSwiperIndex === 0 ? 2 : currentSwiperIndex - 1; const nextIndex = currentSwiperIndex === 2 ? 0 : currentSwiperIndex + 1; calendarArr[prevIndex] = prev; calendarArr[nextIndex] = next; calendarArr[currentSwiperIndex] = now;

理清楚上面所有的,基本上我们就可以开始重构我们的代码了

3|5?正式书写我们可滑动的日历组件

先定义我们之前的所说的变量,和处理这些变量的方法

**

// 当前的索引值,必须从第一个开始,因为这样我们才能实现视野内的无缝 let currentSwiperIndex = 1, generateDate = dataStr, // 当前时间 swipeStartPoint = 0, // 滑动的坐标 isPrevMonth = false, // 是否向右滑动 changeCount = 0; // 滑动的次数 Component({ // ... methods: { // 设置当前的索引值 swiperChange(e) { const { current, source } = e.detail; if (source === "touch") { currentSwiperIndex = current; changeCount += 1; } }, // 获取手指刚按下的坐标 swipeTouchStart(e) { const { clientY, clientX } = e.changedTouches[0]; swipeStartPoint = this.data.isVertical ? clientY : clientX; }, // 获取手指松开时的坐标 swipeTouchEnd(e) { const { clientY, clientX } = e.changedTouches[0]; isPrevMonth = this.data.isVertical ? clientY - swipeStartPoint > 0 : clientX - swipeStartPoint > 0; }, }, // ... });

然后定义一个处理我们日历数据的方法,因为我们日历方法是每个时间都需要使用的,所以我们定义个公用的方法,

**

Component({ // ... methods: { // 设置上个月的时间 getPrevMonth(monthFormat) { const [year, month] = monthFormat.split(/-|//); const { dataStr } = getDateObj( new Date(year, month, 1).setMonth(month - 2) ); return dataStr; }, // 设置下个月的时间 getNextMonth(monthFormat) { const [year, month] = monthFormat.split(/-|//); const { dataStr } = getDateObj(new Date(year, month, 1)); return dataStr; }, // 生成日历数组 generatorCalendar(date) { const calendarArr = []; // 转换为 Date 实例 const currentDate = transformDate(date); // 获取当前时间的日历数据 const now = getDate(currentDate); // 获取当前时间的字符串 const { dataStr } = getDateObj(currentDate); // 获取上个月的日历数据 const prev = getDate(this.getPrevMonth(dataStr)); // 获取下个月的日历数据 const next = getDate(this.getNextMonth(dataStr)); // 设置日历数据 const prevIndex = currentSwiperIndex === 0 ? 2 : currentSwiperIndex - 1; const nextIndex = currentSwiperIndex === 2 ? 0 : currentSwiperIndex + 1; calendarArr[prevIndex] = prev; calendarArr[nextIndex] = next; calendarArr[currentSwiperIndex] = now; this.setData({ calendarArr, monthFormat: getDateStr(dataStr), }); }, }, // ... });

ps: 因为这里上下月份也可以公用,所以单独提取出来供其他方法使用

最后,我们只需要在动画结束的时候设置日历数据即可,即:

**

Component({ // ... methods: { // 动画结束后让滑动的次数置0 swiperAnimateFinish() { const { year, month } = getDateObj(generateDate); const monthDist = isPrevMonth ? -changeCount : changeCount; generateDate = new Date(year, month + monthDist - 1); // 清空滑动次数 changeCount = 0; this.generatorCalendar(generateDate); }, }, // ... });

整合起来就是:

**

// components/calendar/index.js const { getDate, dataStr, getDateObj, transformDate } = require("./utils"); const getDateStr = (dataStr) => dataStr.slice(0, 7).replace("/", "-"); // 当前的索引值,必须从第一个开始,因为这样我们才能实现视野内的无缝 let currentSwiperIndex = 1, generateDate = dataStr, // 当前时间 swipeStartPoint = 0, // 滑动的坐标 isPrevMonth = false, // 是否向右滑动 changeCount = 0; // 滑动的次数 Component({ /** * 组件的属性列表 */ properties: { duration: { type: String, value: 500, }, isVertical: { type: Boolean, value: false, }, }, /** * 组件的初始数据 */ data: { week: ["一", "二", "三", "四", "五", "六", "日"], current: 1, calendarArr: [], monthFormat: getDateStr(dataStr), }, /** * 组件的方法列表 */ methods: { // 设置上个月的时间 getPrevMonth(monthFormat) { const [year, month] = monthFormat.split(/-|//); const { dataStr } = getDateObj( new Date(year, month, 1).setMonth(month - 2) ); return dataStr; }, // 设置下个月的时间 getNextMonth(monthFormat) { const [year, month] = monthFormat.split(/-|//); const { dataStr } = getDateObj(new Date(year, month, 1)); return dataStr; }, // 生成日历数组 generatorCalendar(date) { const calendarArr = []; // 转换为 Date 实例 const currentDate = transformDate(date); // 获取当前时间的日历数据 const now = getDate(currentDate); // 获取当前时间的字符串 const { dataStr } = getDateObj(currentDate); // 获取上个月的日历数据 const prev = getDate(this.getPrevMonth(dataStr)); // 获取下个月的日历数据 const next = getDate(this.getNextMonth(dataStr)); // 设置日历数据 const prevIndex = currentSwiperIndex === 0 ? 2 : currentSwiperIndex - 1; const nextIndex = currentSwiperIndex === 2 ? 0 : currentSwiperIndex + 1; calendarArr[prevIndex] = prev; calendarArr[nextIndex] = next; calendarArr[currentSwiperIndex] = now; this.setData({ calendarArr, monthFormat: getDateStr(dataStr), }); // 通知父组件 this.triggerEvent("change", this.data.monthFormat); }, // 设置当前的索引值 swiperChange(e) { const { current, source } = e.detail; if (source === "touch") { currentSwiperIndex = current; changeCount += 1; } }, // 动画结束后让滑动的次数置0 swiperAnimateFinish() { const { year, month } = getDateObj(generateDate); const monthDist = isPrevMonth ? -changeCount : changeCount; generateDate = new Date(year, month + monthDist - 1); // 清空滑动次数 changeCount = 0; this.generatorCalendar(generateDate); }, // 获取手指刚按下的坐标 swipeTouchStart(e) { const { clientY, clientX } = e.changedTouches[0]; swipeStartPoint = this.data.isVertical ? clientY : clientX; }, // 获取手指松开时的坐标 swipeTouchEnd(e) { const { clientY, clientX } = e.changedTouches[0]; isPrevMonth = this.data.isVertical ? clientY - swipeStartPoint > 0 : clientX - swipeStartPoint > 0; }, dateChange(e) { const monthFormat = e.detail.value; this.setData({ monthFormat, }); generateDate = getDateStr(monthFormat); this.generatorCalendar(generateDate); }, // 上个月日期 prevMonth() { this.setData({ monthFormat: this.getPrevMonth(this.data.monthFormat), }); this.generatorCalendar(this.data.monthFormat); }, // 下个月日期 nextMonth() { this.setData({ monthFormat: this.getNextMonth(this.data.monthFormat), }); this.generatorCalendar(this.data.monthFormat); }, }, ready() { this.generatorCalendar(generateDate); }, });

页面中使用,

**

<calendar bindchange="calendarChange"></calendar>

**

Page({ calendarChange(e) { console.log(e.detail); }, });


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #小程序写日历 #Keep