其实这个项目已经提出来了好久,应该是在今年的一月份的时候,产品就已经提出了这个需求。想当初,小程序刚出来的时候,IT朋友圈经常会被刷屏,估计为了赶潮流,产品也想尝尝鲜,想出来要做一个跟邮箱相关的小程序。然而,要绑定邮箱业务到小程序,也不是想做就能做的,记得当时开评审会的时候,一屋子的人,包括前端、后端在内的各种不能做。经过了几番折腾,后面不知道开了多少次会,最后产品本打算做的两个业务场景,砍掉了其中一个,留了一个,也就是今天我要总结的东西 —- 结合日历实现会议室预订微信小程序产品需求。
需求
首先,看一下需求列表。功能清单大概如下3点:
此外,还有一个比较重要的功能点也就是登录功能:包括绑定账号登录,以及登录态的维护。当初评审的时候,没有把登录功能加入到工作量里面,事实上,登录功能也一点儿不比其他功能要简单。
需求整体流程图大概如下:
项目结构
根据需求以及UI设计,我们把页面分成了登录页、我的(用户中心)、会议室列表页面、会议室预订页面、预订成功页面、我的预订六个模块页面以及其他公共方法模块。
主要模块分解
会议室列表模块
会议室列表页面主要包含两个部分,头部的滑动日历组件,以及内容部分的会议室列表。
预订页面模块
预订会议室包含当前会议室预订的列表、预订两个部分。根据底部预订按钮可以分为三种情况,可预订、已订满(截图没有)、已失效;会议室预订的最小粒度为30分钟。
可预订:说明当天该会议室至少有30分钟的时间段是可以预订,提供预订按钮;
已订满:说明当天已经订满或者不可以在预订了,不提供预订按钮;
已失效:比如,我今天打开昨天的会议室,就是已失效会议室,不提供预订按钮。
登录模块
登录分为手机号登录和邮箱账号登录。
重点实现
会议室列表日历组件
会议室列表页面模块页面重点的部分就是头部这个日历组件,所以有必要重点讲一下这个组件的实现流程。虽然小程序,有很多很好看并且也很好用的组件,但是像头部这种滑动日历组件小程序肯定是没有的,所以只能自己去写一个,实现起来其实也不难,主要用到小程序里面的 touchstart、touchmove、touchend 以及 touchcancel(防止滑动时遇到突然来电话等情况) 事件(当然这4个事件也是 w3c 里面的事件)。关于这个日历组件的实现思路大概如下:
初始化单元格 这个日历组件总共有15个单元,虽然展示在我们面前的只有5个,实际上在这5个单元格的左边和右边都分别有5个看不到的日历单元格;
填充单元内容 接下来就是填充着15个单元格里面的日期以及星期,应该如何计算?其实,只需要获取今天00点00分时的时间戳,然后通过加减 n(其他日期与今天的差值) 个 86400000 毫秒即可,这个 86400000 毫秒就是两天之间的时间戳只差,比如3月16日00时00分与3月15日00时00分之间刚好相差 86400000 毫秒;
计算宽度 每个单元格宽度为五分之一屏幕宽度;
日历组件居中 为了保证第七个单元格居中,也就是让这中间这个单元格选中,组件容器向左负偏移一个屏幕宽度距离。transiform: translate(-SCREENWIDTHpx, 0)
;
向左滑动处理 计算向左滑动的距离,设置组件容器的偏移量;
向右滑动处理 计算向右滑动距离,设置组件容器的偏移量;
滑动结束 当滑动结束,根据最终滑动的距离计算向左或向右滑动了多少天,来选中最终的日期,然后根据这个日期重新渲染单元格;
滑动取消 对于滑动取消这种情况,复位组件,即选中今天。
登录态维护
登录时序图
参考以上的微信提供的登录时序图,以及结合 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 { wx.navigateTo({ url: '../../pages/login/login' }); } },
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 语法(双大括号)将变量包起来的数据绑定,不支持比较复杂的运算,哪怕稍微有点复杂,如:
支持
不支持
因此,如果涉及到数据的计算,最好先在 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 }, success: function (res) { if (res) { callback && callback(res); } }, fail : function (err) { callback && callback(err); } }) }
|
- 小程序长度单位 rpx 和 px 的转换,有些情况只能到真机里面去看;如果使用微信开发工具的话,建议切换成 iPhone 6 模式。
以上是在 Windows 下使用的微信开发者工具返回来的像素比,iPhone 4s 的像素比居然的也是 2。
总结
站在开发者的角度看,