1|0 前言
很多时候,我们生活中会有各种打卡的情况,比如 keep 的运动打卡、单词的学习打卡和各种签到打卡或者酒店的入住时间选择,这时候就需要我们书写一个日历组件来处理我们这种需求。
但是更多时候,我们都是网上找一个插件直接套用了,有没有想过自己实现一下呢?如果有,但是感觉不太会的话,接下来跟着我一起实现符合自己需求的日历吧
2|0 准备工作
因为我们是小程序日历嘛,所以必不可少的肯定是微信开发者工具啦。项目目录结构如下:
**文章来源:https://www.toymoban.com/news/detail-497501.html
|-- 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>
**文章来源地址https://www.toymoban.com/news/detail-497501.html
Page({
calendarChange(e) {
console.log(e.detail);
},
});
到了这里,关于教你如何写一个符合自己需求的小程序日历组件的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!