其实这个项目已经提出来了好久,应该是在今年的一月份的时候,产品就已经提出了这个需求。想当初,小程序刚出来的时候,IT朋友圈经常会被刷屏,估计为了赶潮流,产品也想尝尝鲜,想出来要做一个跟邮箱相关的小程序。然而,要绑定邮箱业务到小程序,也不是想做就能做的,记得当时开评审会的时候,一屋子的人,包括前端、后端在内的各种不能做。经过了几番折腾,后面不知道开了多少次会,最后产品本打算做的两个业务场景,砍掉了其中一个,留了一个,也就是今天我要总结的东西 —- 结合日历实现会议室预订微信小程序产品需求。

需求

首先,看一下需求列表。功能清单大概如下3点:

  • 小程序首页可以根据日期查看会议订满和可预订状态;

  • 可以查看会议室预订情况,在空闲时段可以预订会议室;

  • 预订会议室后,提供集中查看页面,查看自己预订的所有会议室。

此外,还有一个比较重要的功能点也就是登录功能:包括绑定账号登录,以及登录态的维护。当初评审的时候,没有把登录功能加入到工作量里面,事实上,登录功能也一点儿不比其他功能要简单。

需求整体流程图大概如下:

主要流程

项目结构

根据需求以及UI设计,我们把页面分成了登录页、我的(用户中心)、会议室列表页面、会议室预订页面、预订成功页面、我的预订六个模块页面以及其他公共方法模块。

项目结构

主要模块分解

会议室列表模块

会议室列表页

会议室列表页面主要包含两个部分,头部的滑动日历组件,以及内容部分的会议室列表。

预订页面模块

预订页面-可预订 预订页面-已失效 预订页面

预订会议室包含当前会议室预订的列表、预订两个部分。根据底部预订按钮可以分为三种情况,可预订、已订满(截图没有)、已失效;会议室预订的最小粒度为30分钟。

  • 可预订:说明当天该会议室至少有30分钟的时间段是可以预订,提供预订按钮;

  • 已订满:说明当天已经订满或者不可以在预订了,不提供预订按钮;

  • 已失效:比如,我今天打开昨天的会议室,就是已失效会议室,不提供预订按钮。

登录模块

登录页面

登录分为手机号登录和邮箱账号登录。

重点实现

会议室列表日历组件

登录页面

会议室列表页面模块页面重点的部分就是头部这个日历组件,所以有必要重点讲一下这个组件的实现流程。虽然小程序,有很多很好看并且也很好用的组件,但是像头部这种滑动日历组件小程序肯定是没有的,所以只能自己去写一个,实现起来其实也不难,主要用到小程序里面的 touchstart、touchmove、touchend 以及 touchcancel(防止滑动时遇到突然来电话等情况) 事件(当然这4个事件也是 w3c 里面的事件)。关于这个日历组件的实现思路大概如下:

  1. 初始化单元格 这个日历组件总共有15个单元,虽然展示在我们面前的只有5个,实际上在这5个单元格的左边和右边都分别有5个看不到的日历单元格;

  2. 填充单元内容 接下来就是填充着15个单元格里面的日期以及星期,应该如何计算?其实,只需要获取今天00点00分时的时间戳,然后通过加减 n(其他日期与今天的差值) 个 86400000 毫秒即可,这个 86400000 毫秒就是两天之间的时间戳只差,比如3月16日00时00分与3月15日00时00分之间刚好相差 86400000 毫秒;

  3. 计算宽度 每个单元格宽度为五分之一屏幕宽度;

  4. 日历组件居中 为了保证第七个单元格居中,也就是让这中间这个单元格选中,组件容器向左负偏移一个屏幕宽度距离。transiform: translate(-SCREENWIDTHpx, 0)

  5. 向左滑动处理 计算向左滑动的距离,设置组件容器的偏移量;

  6. 向右滑动处理 计算向右滑动距离,设置组件容器的偏移量;

  7. 滑动结束 当滑动结束,根据最终滑动的距离计算向左或向右滑动了多少天,来选中最终的日期,然后根据这个日期重新渲染单元格;

  8. 滑动取消 对于滑动取消这种情况,复位组件,即选中今天。

登录态维护

登录时序图

登录态维护

参考以上的微信提供的登录时序图,以及结合 139 邮箱小程序的登录特征,后台后台提供两个接口:一个是用户第一次登录( sid、rmKey 等登录信息不存在)时所调用的登录接口;另外一个则是,用户之前已经登录过(小程序缓存 Storage 里面已存在 sid、rmKey等登录信息)时所调用的免登录接口。

流程图如下:

登录页面

代码实现如下:

登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
...

doLogin: function () {
var _this = this;
if (_this.data.phoneLogin && !_this.data.phoneNumber.trim()) {
wx.showModal({
title : '139邮箱提示',
content : '请输入手机号',
showCancel: false
});
return;
}

if (_this.data.mailLogin && !_this.data.mailName.trim()) {
wx.showModal({
title : '139邮箱提示',
content : '请输入邮箱账号',
showCancel: false
});
return;
}
// 登录成功,跳转至用户中心
wx.login({
success: function (res) {
var code = res.code;
if (code) {
if (_this.data.phoneLogin) {
_this.loginFunc({
code : code,
userNumber : _this.data.phoneNumber.trim(),
password : _this.data.smsCode
}, function (json) {
if (json.statusCode === 200) {
var data = json.data;
if (typeof data === 'string') {
data = JSON.parse(data);
}
// 登录成功,跳转至用户中心
if (data && data.code === 'S_OK') {
wx.setStorage({
key : 'sid',
data: data.sid
});
// 登录成功,跳转至用户中心
wx.switchTab({
url : '../../pages/conference/meetingRoom',
success: function (res) {
console.log('登录成功!');
}
});
}
}
});
} else if (_this.data.mailLogin) {
_this.loginFunc({
code : code,
userNumber : _this.data.mailName.trim(),
password : _this.data.password
}, function (json) {
if (json.statusCode === 200) {
var data = json.data;
if (typeof data === 'string') {
data = JSON.parse(data);
}
// 登录成功,跳转至用户中心
if (data && data.code === 'S_OK') {
wx.setStorage({
key : 'sid',
data: data.sid
});
wx.setStorage({
key : 'rmKey',
data: data.rmKey
});

// 登录成功,跳转至用户中心
wx.switchTab({
url : '../../pages/conference/meetingRoom',
success: function (res) {
console.log('登录成功!');
}
});
}
}
});
}
}
}
});
},
...

免登陆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
...

/**
* 免登录操作
*/
freeLoginAction: function (callback) {
var sid = wx.getStorageSync('sid');
var code = wx.getStorageSync('code');
var rmKey = wx.getStorageSync('rmKey');
if (sid && code && rmKey && !/MP_USER_####/.test(sid)) {
this.freeLogin({
sid : sid,
code: code
}, rmKey, function (res) {
if (res.statusCode === 200) {
var data = res.data;
// 只有成功的时候才传回调
if (data.code === 'S_OK') {
wx.setStorageSync('sid', data.sid);
data.rmKey && wx.setStorageSync('rmKey', data.rmKey)
callback && callback(res);
}
}
});
} else {
// 如果还没登录,缓存中的 sid 为空或者无效,跳转至登录页
wx.navigateTo({
url: '../../pages/login/login'
});
}
},
/**
* 免登录
* @param {Object} options 请求参数
* @param {String} rmKey RMKEY
* @param {Function} callback 回调
* @return void(0)
*/
freeLogin: function (options, rmKey, callback) {
options = util.json2xml(options);
wx.request({
url : 'https://xxx.cn' + '/weixin/s?func=weixin:freeLoginMiniProgram',
data : options,
method : 'POST',
header : {
'Cookie' : 'RMKEY=' + rmKey,
'content-type': 'application/xml'
},
success: function (res) {
if (res) {
callback && callback(res);
}
},
fail : function (err) {
callback && callback(err);
}
})
},

...

遇到的问题

  • 小程序使用 Mustache 语法(双大括号)将变量包起来的数据绑定,不支持比较复杂的运算,哪怕稍微有点复杂,如:

支持

1
{{ a + b}}

不支持

1
{{ a + b + c }}

因此,如果涉及到数据的计算,最好先在 js 里面计算好了,在绑定到 View 层。

  • 小程序不支持 Cookie。小程序使用框架提供的 wx.request 接口发送 https 请求不会携带 Cookie 信息,传统webserver的会话管理能力 session(比如邮箱会话校验所使用的 RMKEY )在微信小程序无法直接使用,在这点上微信小程序更像CS架构的开发模式,开发者需要自己实现会话管理功能。

我们的解决方法是将 RMKEY 放到请求的头部新建字段带给后台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
subMeetingRoom: function (options, rmKey, callback) {
var sid = wx.getStorageSync('sid');
var rmKey = wx.getStorageSync('rmKey');
options = util.json2xml(options);
wx.request({
url : 'https://xxx.com' + '/calendar/s?func=calendar:subMeetingRoom&sid=' + sid,
data : options,
method : 'POST',
header : {
'content-type': 'application/xml',
'Cookie' : 'RMKEY=' + rmKey // 请求创建一个 Cookie 字段
},
success: function (res) {
if (res) {
callback && callback(res);
}
},
fail : function (err) {
callback && callback(err);
}
})
}
  • 小程序长度单位 rpx 和 px 的转换,有些情况只能到真机里面去看;如果使用微信开发工具的话,建议切换成 iPhone 6 模式。
rpx rpx rpx

以上是在 Windows 下使用的微信开发者工具返回来的像素比,iPhone 4s 的像素比居然的也是 2。

总结

站在开发者的角度看,

  • 小程序入手简单,也正是因为简单,所以不适合做一些场景比较复杂的应用;

  • 很多组件小程序都已经帮你封装好了,简洁、也好看;但是如果,你要自己去实现一些更加个性化的组件还是有点麻烦。