- 小程序日历插件
想问问大家,类似这样选择往返日期的日历,现在有可以直接用的插件吗?需求大致是日历平铺显示,可以选择开始时间和结束时间[图片][图片]
2019-07-05 - 关于微信授权获取昵称含Emoji表情引发的乱码问题总结
做过微信授权的小伙伴都可能会遇到获取用户昵称乱码问题,那是因为微信昵称中的含有SoftBank版本的Emoji表情。 如我的微信昵称: 正常显示为[图片]; 未处理Softbank及微信自定义表情显示为[图片]; 处理Softbank后显示为[图片]。 [图片] Emoji表情有很多种版本,其中包括Unified、DoCoMo、KDDI、SoftBank和Google,不同版本的Unicode代码并不一定相同。经研究,微信昵称中的Emoji表情截止目前(2019.12.10)已知支持三种版本: 1、SoftBank版本(网上一般称之为SB Unicode),如😂为E412; 2、Unified版本,如😂为1F602; 3、自定义表情版本,如[捂脸]。 举个例子,😂(喜极而泣)的各种编码如下: SoftBank:0000E412 Unified:0001F602(U+1F602) DoCoMo:0000E72A KDDI:0000EB64 Google:000FE334 UTF-8:F09F9882(%F0%9F%98%82) UTF-16BE:FEFFD83DDE02(\uD83D\uDE02) UTF-16LE:FFFE3DD802DE UTF-32BE:0000FEFF0001F602 UTF-32LE:FFFE000002F60100 Emoji表情代码表参阅:http://punchdrunker.github.io/iOSEmoji/table_html/index.html 对于SoftBank及微信自家定义的表情,需要做映射处理转换成标准的Unified版本的Emoji表情才能正常显示,否则就可能乱码。具体解决方案参见https://github.com/gzu-liyujiang/UnicodeEmoji SoftBank版本编码与Unified版本编码对应关系 { “E150”: “0001F68F”, “E030”: “0001F338”, “E151”: “0001F6BB”, “E152”: “0001F46E”, “E031”: “0001F531”, “E032”: “0001F339”, “E153”: “0001F3E3”, …省略… } SoftBank版本编码与标准Unicode编码对应关系 { “E150”: “\uD83D\uDE8F”, “E030”: “\uD83C\uDF38”, “E151”: “\uD83D\uDEBB”, “E152”: “\uD83D\uDC6E”, “E031”: “\uD83D\uDD31”, “E032”: “\uD83C\uDF39”, “E153”: “\uD83C\uDFE3”, …省略… } SoftBank版本编码与标准的Emoji字符表情的对应关系 { “E150”: “🚏”, “E030”: “🌸”, “E151”: “🚻”, “E152”: “👮”, “E031”: “🔱”, “E032”: “🌹”, “E153”: “🏣”, …省略… }
2019-12-11 - 输入框全匹配输入事件与输入框全匹配正则校验事件
代码片段: https://developers.weixin.qq.com/s/Ku7Daamt70dh 在代码片段中版本库不能使用2.9.3,这个版本输入框输入事件不会触发,修改2.9.2OK 一个“inpuAll”事件决绝使用输入框的输入事件, 一个“blurAll事件决绝使用输入框的数据校验事件, 就是这么简单 例:index inputtype"text"bindblur"blurAll"bindinput"inpuAll"data-key"name"value"{{user.name}}"></input inputtype"number"maxlength"11"bindblur"blurAll"disabled"true"bindinput"inpuAll"data-key"phone"value"{{user.phone}}"></input 数组型: viewclass"itemData"> textclass"{{!(userRegular.familyMember[idx].phone.regular)?'errorText':''}}">联系电话:</text inputtype"number"bindblur"blurAll"maxlength"11"bindinput"inpuAll"data-key"familyMember[{{idx}}].phone"value"{{item.phone}}"></input </view inpuAll: function(e) { varuser = app.util.inpuAll(this_.data.user, e); //不可传整个data,否则每次返回数据都会刷新整个页面,页面上的组件会发生赋值事件 if(user) { this_.setData({ user: user }); }, blurAll: function(e) { varuserRegular = app.util.blurAll(this_.data.userRegular, e); if(userRegular) { this_.setData({ userRegular: userRegular }); }, user: { name: '', // 姓名 phone: '', array:[] }, userRegular: { name: { type: 'name', //正则表达式类型,请参考regular.js regular: true error: '请正确输入用户名!' }, phone: { type: 'phone' regular: true error: '请正确输入手机号!' array:[] 表单提交前: regularAll_: function(obj) { varregularTem = app.util.regularAll(obj); if(!(regularTem.regular)) { this_.setError(regularTem.error); returnregularTem.regular; }, regularDataAll: function() { try varuserRegular = app.util.regularDataAll(this_.data.userRegular); this_.setData({ userRegular: userRegular }); catch(e){ console.log(e); }, regularAll_: function(obj) { varregularTem = app.util.regularAll(obj); if(!(regularTem.regular)) { this_.setError(regularTem.error); returnregularTem.regular; }, regularAll: function() { returnthis_.regularAll_(this_.data.userRegular); }, 表单提交,请求服务器: submi: function() { this_.regularDataAll(); if(!this_.regularAll()) { console.log(this.data.userRegular); return console.log('数据校验通过'); 使用数据校验组必须包含两个属性 userRegular.cooperative.regular = app.regular.emptyAttr(cooperative, 'name'); userRegular.cooperative.value = cooperative.name; 如果是自定义事件:[图片] app.js import initiResize from 'utils/initiResize.js' import regular from 'utils/regular.js' import util from 'utils/util.js' varthis_; App({ debug:false /** * 根据模板布局大小动态计算用户手机布局相对大小 */ resize: initiResize, /** * 正则表达式 */ regular: regular, userId:'' util: util, onLaunch: function() { this_ = this util.app = this util.initi(); util.js import regular from 'regular.js' let this_; module.exports = { initi:function(){ //在app.js中初始化 this_=this }, /** * 全局输入框通用事件 * 添加以下两个属性 * bindinput="inpuAll" * data-key="user.name" * * obj为Page的data * Page(data:{ * user:{name:''} * }) * e为最初的输入事件参数 * * 最后返回data * * 示例: * * <input data-key="user.name" bindinput="inpuAll" value="{{user.name}}"></input> * Page( * data:{ * user:{name:''} * }, * inpuAll: function (e) { * var data=util.inpuAll(this.data,e); * if (data){ * this.setData(data); * } * } * ) * 示例数组:data-key="user.members[{{idx}}].identity" * data-key="user.members[{{idx}}][{{idx}}].identity"(多维数组没有测试,算法理论上是可以的) * * 假如提高页面渲染效率: * inpuAll: function (e) { * var xx=util.inpuAll(this.data.XX,e); * if (xx){ * this.setData({XX:xx}); * } * } */ inpuAll: function(obj, e) { try vartag = e.currentTarget.dataset.key; vartems = this.keySplit(tag); varobjt = obj; forvari = 0; i < tems.length; i++) { varkey = tems[i]; ifthis.isArray(key)) { varnames = this.getArray(key); varindexs = this.getArrayIndexs(key); obj = obj[names]; forvarp = 0; p < indexs.length; p++) { if(i == (tems.length - 1) && p == indexs.length - 1) { obj[(indexs[p])] = e.detail.value; } else obj = obj[(indexs[p])]; } else if(i == (tems.length - 1)) { obj[key] = e.detail.value; } else obj = obj[key]; returnobjt; } catch(e) { console.log(e); returnobj; }, keySplit: function(e) { returne.split("."); }, isArray: function(e) { vari = e.indexOf("["); returni > 0; }, getArray: function(e) { returne.substring(0, e.indexOf("[")); }, getArrayIndexs: function(e) { vart = []; varl = 0; varks = e.split("["); forvari = 0; i < ks.length; i++) { vark = ks[i].substring(0, ks[i].length - 1); if(regular.number(k)) { t[l++] = newNumber(k).valueOf(); returnt; }, /** * 数据校验组的支持 * <input type="number" bindblur="blurAll" data-key="name" value="{{nameData.name}}"></input> * Page({ * blurAll: function(e) { //失去焦点事件 var userRegular = util.blurAll(this.data.nameData, e); if (userRegular) { this.setData({ userRegular: userRegular }); }) * nameData:{ * name:'', * arrs:[] * } * regularCopy:{ name: { type: 'empty', //正则表达式类型,请参考regular.js regular: true, error: '请正确输入用户名!' }, arrs:[] */ blurAll: function(regularObj, e){ varregularTem = regularObj; vartag = e.currentTarget.dataset.key; vartems = this.keySplit(tag); forvari = 0; i < tems.length; i++) { varkey = tems[i]; ifthis.isArray(key)) { varnames = this.getArray(key); varindexs = this.getArrayIndexs(key); regularTem = regularObj[names]; forvarp = 0; p < indexs.length; p++) { if(i == (tems.length - 1) && p == indexs.length - 1) { vartype = (regularTem[(indexs[p])]).type; varregularVal=(regular[type])(e.detail.value); (regularTem[(indexs[p])]).regular = regularVal; (regularTem[(indexs[p])]).value = e.detail.value; } else regularTem = regularTem[(indexs[p])]; } else if(i == (tems.length - 1)) { vartype = (regularTem[key]).type; varregularVal = (regular[type])(e.detail.value); (regularTem[key]).regular = regularVal; (regularTem[key]).value = e.detail.value; } else regularTem = regularTem[key]; returnregularObj; }, /** * 检验regularObj数据校验组中是否有不通过的数据 * Regular: { name: { type: 'empty', //正则表达式类型,请参考regular.js regular: true, error: '请正确输入用户名!' }, arrs:[] */ regularAll:function(obj){ vartemobj={ regular: true error: '' }; forvarprop inobj){ if(regular.attr(obj[prop],'regular')){ if(!((obj[prop]).regular)){ temobj.regular = false temobj.error = obj[prop].error; returntemobj; continue if(regular.isArray(obj[prop])) { vartem = this.regularArray(obj[prop]); if(!(tem.regular)){ temobj.regular = tem.regular; temobj.error = tem.error; returntemobj; continue vartem = this.regularAll(obj[prop]); if(!(tem.regular)) { temobj.regular = tem.regular; temobj.error = tem.error; returntemobj; returntemobj; }, regularArray: function(obj) { vartemobj = { regular: true error: '' }; forvari = 0; i < obj.length;i++){ varprop = obj[i]; if(regular.isArray(prop)) { vartem = this.regularArray(prop); if(!(tem.regular)) { temobj.regular = tem.regular; temobj.error = tem.error; returntemobj; else vartem = this.regularAll(prop); if(!(tem.regular)) { temobj.regular = tem.regular; temobj.error = tem.error; returntemobj; returntemobj; }, /** * 检验regularObj数据校验组中所有数据 */ regularDataAll: function(obj) { forvarprop inobj) { if(regular.attr(obj[prop], 'regular')) { vartype = (obj[prop]).type; if(regular.attr(obj[prop],'value')){ varregularVal = (regular[type])((obj[prop]).value); (obj[prop]).regular = regularVal; else (obj[prop]).regular = false continue if(regular.isArray(obj[prop])) { vartem = this.regularDataArray(obj[prop]); obj[prop] = tem; else vartem = this.regularDataAll(obj[prop]); obj[prop] = tem; returnobj; }, regularDataArray: function(obj) { forvari = 0; i < obj.length; i++) { varprop = obj[i]; if(regular.isArray(prop)) { obj[i]= this.regularDataArray(prop); } else obj[i] = this.regularDataAll(prop); returnobj; }, /** * 从nameData中初始化regular数据校验组中的数据 * nameData:{ * name:'', * arrs:[] * } * * regularCopy:{ name: { type: 'empty', //正则表达式类型,请参考regular.js regular: true, error: '请正确输入用户名!' }, arrs:[] */ regularDataCopy: function(nameData, regularCopy) { forvarprop inregularCopy) { try if(regular.attr(regularCopy[prop], 'regular')) { regularCopy[prop].value = nameData[prop]; if(regular.empty(nameData[prop])){ vartype = regularCopy[prop].type; varregularVal = (regular[type])(nameData[prop]); regularCopy[prop].regular = regularVal; continue if(regular.isArray(regularCopy[prop])) { vartem = this.regularDataCopyArray(nameData[prop], regularCopy[prop]); regularCopy[prop] = tem; } else vartem = this.regularDataCopy(nameData[prop], regularCopy[prop]); regularCopy[prop] = tem; } catch(error){ console.log(error); returnregularCopy; }, regularDataCopyArray: function(nameData, regularCopy) { vartem; forvari = 0; i < nameData.length; i++) { if(i == 0) { tem = regularCopy[0]; varprop = regularCopy[i]; if(!prop) { prop = tem; if(regular.isArray(prop)) { regularCopy[i] = this.regularDataCopyArray(nameData[i], prop); } else regularCopy[i] = this.regularDataCopy(nameData[i], prop); returnregularCopy; 正则校验:regular.js /** * 小程序所有的正则表达式以及数据正则校验 */ module.exports = { number: function(e) { vart = /^\d+$/; returnt.test(e); }, phone: function(e) { /** * 手机号 */ vart = /^0?1(3|4|5|6|7|8|9)\d{9}$/; returnt.test(e); }, /** * 中文 */ chinese: function(e) { vart = /^[\u4e00-\u9fa5]+$/; returnt.test(e); }, /** * 中文名字 */ name: function(e) { vart = /^[\u4e00-\u9fa5]{2,8}$/; returnt.test(e); }, password: function(e) { /** * 密码 */ vart = /^[A-Za-z0-9]{6,16}$/; returnt.test(e); }, phoneCode: function(e) { /** * 验证码 */ vart = /^(\w|\d){4,4}$/; returnt.test(e); }, /** * 空 */ empty: function(e) { varis = e ? true: false if(is && this.isString(e)) { e = e.trim(); is = e ? true: false returnis; }, emptyAttr: function(e, key) { varis = this.attr(e, key); if(is) { is = this.empty(e[key]); returnis; }, attr: function(e, key) { varis = e ? true: false ifthis.isString(e)) { returnfalse ifthis.isArray(e)) { returnfalse is = this.isObject(e); if(is) { is = key ine; returnis; }, object: function(e) { varis = e ? true: false returnis; }, /** * 身份证号15位与18位 */ idCard: function(e) { vart = /(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}[0-9Xx]$)/; returnt.test(e); }, isPositiveInteger: function(e) { vart = /^[1-9][0-9]*$/; returnt.test(e); }, isString: function(e) { varbool = true try bool = e instanceofString; } catch(e) { bool = false if(!bool) { try varstr = typeofe; bool = ("string"== str); } catch(e) { bool = false returnbool; }, isArray: function(e) { varbool = true try bool = e instanceofArray; } catch(e) { bool = false if(!bool) { try varstr = typeofe; bool = ("array"== str); } catch(e) { bool = false if(bool) { bool = e.length > 0; returnbool; }, isObject: function(e) { varbool = true try bool = e instanceofObject; } catch(e) { bool = false if(!bool) { try varstr = typeofe; bool = ("object"== str); } catch(e) { bool = false returnbool; }, isBoolean: function(e) { varbool = true try bool = e instanceofBoolean; } catch(e) { bool = false if(!bool) { try varstr = typeofe; bool = ("boolean"== str); } catch(e) { bool = false returnbool; }, isNumber: function(e) { varbool = true try bool = e instanceofNumber; } catch(e) { bool = false if(!bool) { try varstr = typeofe; bool = ("number"== str); } catch(e) { bool = false returnbool;
2019-12-30 - 在微信小程序中使用mobx
在微信小程序中使用mobx
2018-08-24 - 微信小程序三种授权登录的方式
经过一段时间对微信小程序的研发后 总结出以下三种授权登录的方式,我给他们命名为‘一次性授权’‘永久授权’‘不授权’ 1.一次性授权 常规写法,需要获取用户公开信息(头像,昵称等)时,判断调取授权登录接口,但是此方法如果不经处理的话 用户如果拒绝授权或者删除该微信小程序后 需要重新调取并获取用户公开信息(头像,昵称等),此方法用户体验较差,不建议使用; 2.永久授权 在不必要使用用户公开信息(头像,昵称等)时,不调取授权登录接口,只有在必要的时候再去判断调取授权登录接口并把获取到的用户公开信息存入数据库,这样在每次登录时直接先运行指定函数从数据库索取需要的用户公开信息(头像,昵称等)即可,此方法在删除小程序后不用再次去授权登录(因为在用户第一次授权登录时已经把用户的公开信息存入数据库了以后直接向数据库索取即可),建议使用; 3.不授权 不需要授权登录获取用户公开信息(头像,昵称等),使用wx.login获取用户code并传入后台,后台可以通过用户的code值向微信要一个值(具体需要问后台,我只是个小前端,后台的东西不是很懂,只是知道一些逻辑而且也已经成功实现)然后通过这个用code换取的值就可以识别到指定用户,如果需要的话,前端要显示的头像、昵称等这些信息可以使用自定义可编辑的功能,当然,也可以通过<open-data type=“userAvatarUrl”></open-data><open-data type=“userNickName”></open-data>小程序提供的这个组件显示用户的头像及昵称(不过这个组件只有显示功能),用户如果想直接使用自己的头像昵称,也可以自行授权(比如添加个引导按钮什么之类的),建议使用; [图片][图片] 文中使用的微信自带接口、组件及函数: <open-data type=“userAvatarUrl”></open-data> <open-data type=“userNickName”></open-data> wx.login({ success(res){ console.log(res.code) } }) 微信授权登录 以上三种方式可以灵活运用,也可以把需要的结合到一起,并不冲突; 当然,大佬很多,我也只是个小前端而已,第一次发表技术方面的帖子,希望互相学习,互相指导,如有说的不对的地方还望大佬们及时指出!!! 谢谢
2019-04-18 - 自定义可配置format的日期时间选择组件DateTimePicker
背景 由于在项目中遇到需要同时选择日期和时间的需求,所以自己写了一个可以动态配置format格式的时间选择器 效果图 [图片] wxml [代码]<view class="view-body"> <text class='item-key'>{{title}}<text style="color:red" wx:if="{{isRequired}}">*</text></text> <view class="item-value"> <picker mode="multiSelector" bindchange="bindPickerChange" bindcolumnchange="bindPickerColumnChange" value="{{multiIndex}}" range="{{multiArray}}"> <input disabled="{{true}}" value='{{value}}' name='{{name}}' /> <image class="img-arrow" src='/images/date_icon.png' /> </picker> </view> </view> [代码] js [代码]Component({ behaviors: ['wx://form-field'], properties: { title: { type: String }, name: { type: String }, isRequired: { type: Boolean }, format: { type: String } }, data: {}, lifetimes: { attached: function() { //当前时间 年月日 时分秒 const date = new Date() const curYear = date.getFullYear() const curMonth = date.getMonth() + 1 const curDay = date.getDate() const curHour = date.getHours() const curMinute = date.getMinutes() const curSecond = date.getSeconds() //记录默认年份 后面加载二月份天数 this.setData({ chooseYear: curYear }) //初始化时间选择轴 this.initColumn(curMonth) //不足两位的前面好补0 因为后面要获取在时间轴上的索引 时间轴初始化的时候都是两位 let showMonth = curMonth < 10 ? ('0' + curMonth) : curMonth let showDay = curDay < 10 ? ('0' + curDay) : curDay let showHour = curHour < 10 ? ('0' + curHour) : curHour let showMinute = curMinute < 10 ? ('0' + curMinute) : curMinute let showSecond = curSecond < 10 ? ('0' + curSecond) : curSecond //当前时间在picker列上面的索引 为了当打开时间选择轴时选中当前的时间 let indexYear = this.data.years.indexOf(curYear + '') let indexMonth = this.data.months.indexOf(showMonth + '') let indexDay = this.data.days.indexOf(showDay + '') let indexHour = this.data.hours.indexOf(showHour + '') let indexMinute = this.data.minutes.indexOf(showMinute + '') let indexSecond = this.data.seconds.indexOf(showSecond + '') let multiIndex = [] let multiArray = [] let value = '' let format = this.properties.format; if (format == 'yyyy-MM-dd') { multiIndex = [indexYear, indexMonth, indexDay] value = `${curYear}-${showMonth}-${showDay}` multiArray = [this.data.years, this.data.months, this.data.days] } if (format == 'HH:mm:ss') { multiIndex = [indexHour, indexMinute, indexSecond] value = `${showHour}:${showMinute}:${showSecond}` multiArray = [this.data.hours, this.data.minutes, this.data.seconds] } if (format == 'yyyy-MM-dd HH:mm') { multiIndex = [indexYear, indexMonth, indexDay, indexHour, indexMinute] value = `${curYear}-${showMonth}-${showDay} ${showHour}:${showMinute}` multiArray = [this.data.years, this.data.months, this.data.days, this.data.hours, this.data.minutes] } if (format == 'yyyy-MM-dd HH:mm:ss') { multiIndex = [indexYear, indexMonth, indexDay, indexHour, indexMinute, indexSecond] value = `${curYear}-${showMonth}-${showDay} ${showHour}:${showMinute}:${showSecond}` multiArray = [this.data.years, this.data.months, this.data.days, this.data.hours, this.data.minutes, this.data.seconds] } this.setData({ value, multiIndex, multiArray, curMonth, chooseYear: curYear, }) } }, /** * 组件的方法列表 */ methods: { //获取时间日期 bindPickerChange: function(e) { this.setData({ multiIndex: e.detail.value }) const index = this.data.multiIndex let format = this.properties.format var showTime = '' if (format == 'yyyy-MM-dd') { const year = this.data.multiArray[0][index[0]] const month = this.data.multiArray[1][index[1]] const day = this.data.multiArray[2][index[2]] showTime = `${year}-${month}-${day}` } if (format == 'HH:mm:ss') { const hour = this.data.multiArray[0][index[0]] const minute = this.data.multiArray[1][index[1]] const second = this.data.multiArray[2][index[2]] showTime = `${hour}:${minute}:${second}` } if (format == 'yyyy-MM-dd HH:mm') { const year = this.data.multiArray[0][index[0]] const month = this.data.multiArray[1][index[1]] const day = this.data.multiArray[2][index[2]] const hour = this.data.multiArray[3][index[3]] const minute = this.data.multiArray[4][index[4]] showTime = `${year}-${month}-${day} ${hour}:${minute}` } if (format == 'yyyy-MM-dd HH:mm:ss') { const year = this.data.multiArray[0][index[0]] const month = this.data.multiArray[1][index[1]] const day = this.data.multiArray[2][index[2]] const hour = this.data.multiArray[3][index[3]] const minute = this.data.multiArray[4][index[4]] const second = this.data.multiArray[5][index[5]] showTime = `${year}-${month}-${day} ${hour}:${minute}:${second}` } this.setData({ value: showTime }) this.triggerEvent('dateTimePicker', showTime) }, //初始化时间选择轴 initColumn(curMonth) { let years = [] let months = [] let days = [] let hours = [] let minutes = [] let seconds = [] for (let i = 1990; i <= 2099; i++) { years.push(i + '') } for (let i = 1; i <= 12; i++) { if (i < 10) { i = "0" + i; } months.push(i + '') } if (curMonth == 1 || curMonth == 3 || curMonth == 5 || curMonth == 7 || curMonth == 8 || curMonth == 10 || curMonth == 12) { for (let i = 1; i <= 31; i++) { if (i < 10) { i = "0" + i; } days.push(i + '') } } if (curMonth == 4 || curMonth == 6 || curMonth == 9 || curMonth == 11) { for (let i = 1; i <= 30; i++) { if (i < 10) { i = "0" + i; } days.push(i + '') } } if (curMonth == 2) { days=this.setFebDays() } for (let i = 0; i <= 23; i++) { if (i < 10) { i = "0" + i; } hours.push(i + '') } for (let i = 0; i <= 59; i++) { if (i < 10) { i = "0" + i; } minutes.push(i + '') } for (let i = 0; i <= 59; i++) { if (i < 10) { i = "0" + i; } seconds.push(i + '') } this.setData({ years, months, days, hours, minutes, seconds }) }, /** * 列改变时触发 */ bindPickerColumnChange: function(e) { //获取年份 用于计算改年的2月份为平年还是闰年 if (e.detail.column == 0 && this.properties.format != 'HH:mm:ss') { let chooseYear = this.data.multiArray[e.detail.column][e.detail.value]; this.setData({ chooseYear }) if (this.data.curMonth == '02' || this.data.chooseMonth == '02') { this.setFebDays() } } //当前第二为月份时需要初始化当月的天数 if (e.detail.column == 1 && this.properties.format != 'HH:mm:ss') { let num = parseInt(this.data.multiArray[e.detail.column][e.detail.value]); let temp = []; if (num == 1 || num == 3 || num == 5 || num == 7 || num == 8 || num == 10 || num == 12) { //31天的月份 for (let i = 1; i <= 31; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp }); } else if (num == 4 || num == 6 || num == 9 || num == 11) { //30天的月份 for (let i = 1; i <= 30; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp }); } else if (num == 2) { //2月份天数 this.setFebDays() } } let data = { multiArray: this.data.multiArray, multiIndex: this.data.multiIndex }; data.multiIndex[e.detail.column] = e.detail.value; this.setData(data); }, //计算二月份天数 setFebDays() { let year = parseInt(this.data.chooseYear); let temp = []; if (year % (year % 100 ? 4 : 400) ? false : true) { for (let i = 1; i <= 29; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp, chooseMonth: '02' }); } else { for (let i = 1; i <= 28; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp, chooseMonth: '02' }); } return temp; } }, }) [代码] wxss [代码].view-body { display: flex; align-items: center; padding: 4% 0%; border-bottom: 1px solid #eee; } .item-key { font-size: 30rpx; color: #666; width: 25%; } .item-value { flex-grow: 1; font-size: 31rpx; position: relative; margin-left: 3%; } .img-arrow { width: 45rpx; height: 45rpx; padding-right: 10rpx; position: absolute; top: 50%; transform: translateY(-50%); right: 5rpx; } [代码] 使用 在你页面中的json中添加引用,路径根据你的实际工程目录来写。 [代码]{ "usingComponents": { "DateTimePicker": "/components/datetimepicker/datetimepicker" } } [代码] wxml中添加 [代码]<DateTimePicker title='选择时间' isRequired='true' bind:dateTimePicker='onDateTimePicker' name='time' format='yyyy-MM-dd HH:mm:ss'/> [代码] 说明 title:表单组件的名称 isRequired:是否必填项 dateTimePicker:为选中确认回调 name:为表单点击 form 表单中 form-type 为 submit 的 button 组件时,会将表单组件中的 value 值进行提交,需要在表单组件中加上 name 来作为 key format:时间格式化参数 支持:‘yyyy-MM-dd HH:mm:ss’,‘HH:mm:ss’,‘yyyy-MM-dd HH:mm’,‘yyyy-MM-dd’ 四种 开发者可以根据自己的需求调整组件样式 最后代码片段如下: https://developers.weixin.qq.com/s/PzmDvcmi79fI
2020-02-17 - 使用 MobX 来管理小程序的跨页面数据
在小程序中,常常有些数据需要在几个页面或组件中共享。对于这样的数据,在 web 开发中,有些朋友使用过 redux 、 vuex 之类的 状态管理 框架。在小程序开发中,也有不少朋友喜欢用 MobX ,说明这类框架在实际开发中非常实用。 小程序团队近期也开源了 MobX 的辅助模块,使用 MobX 也更加方便。那么,在这篇文章中就来介绍一下 MobX 在小程序中的一个简单用例! 在小程序中引入 MobX 在小程序项目中,可以通过 npm 的方式引入 MobX 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 引入 MobX : [代码]npm install --save mobx-miniprogram mobx-miniprogram-bindings [代码] (这里用到了 mobx-miniprogram-bindings 模块,模块说明在这里: https://developers.weixin.qq.com/miniprogram/dev/extended/functional/mobx.html 。) npm 命令执行完后,记得在开发者工具的项目中点一下菜单栏中的 [代码]工具[代码] - [代码]构建 npm[代码] 。 MobX 有什么用呢? 试想这样一个场景:制作一个天气预报资讯小程序,首页是列表,点击列表中的项目可以进入到详情页。 首页如下: [图片] 详情页如下: [图片] 每次进入首页时,需要使用 [代码]wx.request[代码] 获取天气列表数据,之后将数据使用 setData 应用到界面上。进入详情页之后,再次获取指定日期的天气详情数据,展示在详情页中。 这样做的坏处是,进入了详情页之后需要再次通过网络获取一次数据,等待网络返回后才能将数据展示出来。 事实上,可以在首页获取天气列表数据时,就一并将所有的天气详情数据一同获取回来,存放在一个 数据仓库 中,需要的时候从仓库中取出来就可以了。这样,只需要进入首页时获取一次网络数据就可以了。 MobX 可以帮助我们很方便地建立数据仓库。接下来就讲解一下具体怎么建立和使用 MobX 数据仓库。 建立数据仓库 数据仓库通常专门写在一个独立的 js 文件中。 [代码]import { observable, action } from 'mobx-miniprogram' // 数据仓库 export const store = observable({ list: [], // 天气数据(包含列表和详情) // 设置天气列表,从网络上获取到数据之后调用 setList: action(function (list) { this.list = list }), }) [代码] 在上面数据仓库中,包含有数据 [代码]list[代码] (即天气数据),还包括了一个名为 [代码]setList[代码] 的 action ,用于更改数据仓库中的数据。 在首页中使用数据仓库 如果需要在页面中使用数据仓库里的数据,需要调用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中,然后就可以在页面中直接使用仓库数据了。 [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad() { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 actions: ['setList'], // 将 this.setList 绑定为仓库中的 setList action }) // 从服务器端读取数据 wx.showLoading() wx.request({ // 请求网络数据 // ... success: (data) => { wx.hideLoading() // 调用 setList action ,将数据写入 store this.setList(data) } }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,可以在 wxml 中直接使用 list : [代码]<view class="item" wx:for="{{list}}" wx:key="date" data-index="{{index}}"> <!-- 这里可以使用 list 中的数据了! --> <view class="title">{{item.date}} {{item.summary}}</view> <view class="abstract">{{item.temperature}}</view> </view> [代码] 在详情页中使用数据仓库 在详情页中,同样可以使用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中: [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad(args) { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 }) // 页面参数 `index` 表示要展示哪一条天气详情数据,将它用 setData 设置到界面上 this.setData({ index: args.index }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,这个页面 wxml 中也可以直接使用 list : [代码]<view class="title">{{list[index].date}}</view> <view class="content">温度 {{list[index].temperature}}</view> <view class="content">天气 {{list[index].weather}}</view> <view class="content">空气质量 {{list[index].airQuality}}</view> <view class="content">{{list[index].details}}</view> [代码] 完整示例 完整例子可以在这个代码片段中体验: https://developers.weixin.qq.com/s/YhfvpxmN7HcV 这个就是 MobX 在小程序中最基础的玩法了。相关的 npm 模块文档可参考 mobx-miniprogram-bindings 和 mobx-miniprogram 。 MobX 在实际使用时还有很多好的实践经验,感兴趣的话,可以阅读一些其他相关的文章。
2019-11-01 - 小程序多端差异调研报告(微信,支付宝,头条,QQ)
已经使用uni-app开发并发布了一个跨端小程序啦,嘻嘻嘻! 🧐 须知 这是一份详细的小程序各特性各端真机调研对比报告 测试机:iPhone7 plus IOS 12.4.1 客户端:微信7.0.5,支付宝10.1.72,今日头条7.4.0,抖音8.1.0,QQ8.1.5.461 🚫️ 百度小程序只有商户才能注册,个人开发者无法注册,没有appid功能受限(如百度开发者工具无法使用预览功能导致无法真机测试),所以暂时不测百度小程序 用户信息授权 授权方式: 【头条】用户信息授权方式还停留在微信小程序第一版,即直接调用 getUserInfo 弹出授权弹窗,如果用户选择允许,则后续调用不再出弹窗,而是直接走 success 回调。如果用户选择取消,则后续调用也不再出弹窗,而是直接走 fail 回调 【微信】【QQ】【支付宝】则采用 button + 回调事件的方式调起授权弹窗,如果用户选择允许,则后续点击不再出弹窗,直接走回调。如果用户选择取消,则后续点击继续弹窗询问授权 授权信息清除方式: 【微信】删除小程序即可清除授权信息 【支付宝】我的-设置-安全设置-账号授权 【今日头条】我的-系统设置-清除缓存。【抖音】未找到清除方法 【QQ】未找到清除方法(据说开放小程序的QQ版本尚未灰度发布) 小程序登录 【微信】wx.login 【QQ】qq.login 基本同微信 【支付宝】my.getAuthCode 【头条】大致同微信,未找到模型文档 分享 行为: 【微信】直接调起聊天对话列表进行选择 【QQ】调起分享渠道列表: QQ好友 QQ空间 点右上角三个点调起的列表还有微信好友和朋友圈两个项,在微信中打开qq小程序是走中间页 【支付宝】调起分享渠道列表: 支付宝朋友圈 支付宝联系人 微信好友|QQ好友(保存支付宝生成的分享图片后打开支付宝扫码) 钉钉好友(中间页自动打开支付宝小程序,中间页不自动关闭) 新浪微博(中间页自动打开支付宝小程序,和钉钉一个中间页) 【头条】调起分享渠道列列表: 转发到头条 微信好友|微信朋友圈(生成口令,复制口令后打开今日头条弹出识别弹窗) QQ|QQ空间(打开中间页,点击打开(QQ空间点了没反应),出现另一个中间页,自动打开AppStore,再点打开调起今日头条,最后居然没打开那个小程序🥴!!!) 【抖音】调起分享渠道列列表: 多闪好友 微信好友|微信朋友圈|QQ好友|QQ空间(生成抖音码图片,打开抖音扫码识别) 【头条】webview的转发暂未支持: 【今日头条】能转发,但转发的链接点击后总是提示加载失败!也可能是小程序未发布的原因,扫uni-app官方demo进行 webview转发是能正常打开的 【抖音】不支持转发,右上角胶囊只有一个关闭按钮 跳转到其他小程序 【微信】支持(navigateToMiniProgramAppIdList + navigateToMiniProgram) 【QQ】支持 【头条】支持(navigateToMiniProgramAppIdList + navigateToMiniProgram) 【支付宝】支持(后台配置 + navigateToMiniProgram) 🚫️ ️QQ,支付宝和头条未真机验证,因为须要一个其他小程序的appId 客服会话 【微信】支持(button open-type=“contact”) 【QQ】支持,须用户加一个客服机器人为好友 【支付宝】支持(contact-button) 【头条】不支持。 支付 【微信】支持(调起微信支付) 【QQ】支持(调起QQ支付) 【支付宝】支持(调起支付宝支付) 【头条】支持(调起支付宝App进行支付) 🚫 ️QQ,支付宝和头条未真机验证,因为支付接口只有商户才有权限 地理位置 【微信】支持(须在app.json中配置permission字端),用户拒绝授权后再次调用不再出询问弹窗,而是直接走fail回调 【QQ】支持。真机行为同微信。QQ开发者工具上拒绝授权再次调用仍会出询问弹窗 【头条】支持,同微信 【支付宝】支持,用户拒绝授权后再次调用继续出询问弹窗 视频播放 【微信】支持 【QQ】支持 【头条】支持 【支付宝】支持?(uni-app里说支付宝不支持,支付宝文档也没找到video组件,但放在页面里video能正常渲染和播放,难道是昨天刚支持🤔) 复制文字 行为: 【微信】【QQ】复制成功后有一个默认的复制成功toast且无法控制 【支付宝】【头条】复制成功后没有toast 权限: 【支付宝】my.setClipboard 此功能仅支持企业支付宝账号。实际情况是:在IDE上个人账号是可以复制的,但在真机上调用就会报 [代码]ERROR 4: 无权调用该接口[代码] 错误 【微信】【QQ】【头条】无限制 打电话 【微信】【QQ】【支付宝】【头条】都支持 收货地址 【微信】支持 【QQ】不支持 【头条】支持(实测【今日头条】支持,【抖音】不支持) 【支付宝】支持。但仅商户才有使用权限。且目前 my.getAddress 接口暂不支持在开发者工具调试和真机调试,仅支持真机预览 相机/图片相关 拍照/相册选图片 【微信】【QQ】支持 【支付宝】支持。IDE上会弹一个相册授权询问弹窗,真机上并没有弹窗 【头条】支持。但会弹出两个询问弹窗(相机权限,相册权限) 拍摄/相册选视频 【微信】【QQ】支持 【支付宝】支持。IDE上会弹一个相册授权询问弹窗,真机上并没有弹窗。须调用 my.chooseVideo(文档未找到),uni.chooseVideo会报错 【头条】支持。但会弹出两个询问弹窗(相机权限,相册权限) ⚠️chooseVideo的maxDuration选项在【微信】和【支付宝】是只限制拍摄时长,在【头条】是同时限制相册选择视频时长和拍摄时长 图片预览 【微信】【QQ】【支付宝】【头条】都支持 保存图片到相册 【微信】【QQ】【头条】支持,弹窗仅询问一次 【支付宝】tt.saveImageToPhotosAlbum 在IDE上报错 [代码]tt.saveImageToPhotosAlbum is not a function[代码],在真机上报错 [代码]无权调用该接口[代码],文档未提及,猜测是仅商户可用,且不支持在开发者工具调试和真机调试,仅支持真机预览 接口返回值差异 getUserInfo【微信】【QQ】【支付宝 】【头条】 [代码]// 支付宝 { 'nickName': 'test', 'gender': 'm', 'city': '北京市', 'province': '北京' 'countryCode': 'CN', 'avatar': 'https:\/\/tfs.alipayobjects.com\/images\/partner\/T1_38eXnRiXXXXXXXX', 'code': '10000', 'msg': 'Success', } // 微信 { 'nickName': 'test', 'gender': 1, 'city': 'Xinxiang', 'province': 'Henan', 'country': 'China', 'avatarUrl': 'https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJCzUl7llykqrMLicpULvVfkbbL2bVDua4tI8ibjxq5E9ib1oPW3F4QazLIUdS2GsFMAGnrWSYjN05Ew/132' 'language': 'zh_CN', } // QQ { 'nickName': 'test', 'gender': 1, 'city': '新乡', 'province': '河南' 'country': '中国', 'avatarUrl': 'https://thirdqq.qlogo.cn/qqapp/1108100302/D64611B2AE700324589177922EEBA5F4/100', 'language': 'zh_CN', } // 头条系(今日头条,抖音,皮皮虾,西瓜视频分别取各自用户信息) { 'nickName': 'test', 'gender': 1, 'city': '新乡市', 'province': '河南省' 'country': '中国', 'avatarUrl': 'http://wx.qlogo.cn/mmhead/Q3auHgzwzM5uibSytRCXFs0Y3xSpdy12thibjWIoMrBIsf7FiaPp2ibnFg/0', 'language': '', } [代码] getSetting【微信】【支付宝 】【头条】【QQ】 [代码]// 微信 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html [ 'scope.userInfo', // 用户信息 'scope.userLocation', // 地理位置 'scope.address', // 通讯地址 'scope.record', // 录音功能 'scope.camera', // 摄像头 'scope.writePhotosAlbum', // 保存到相册 'scope.userLocationBackground', // 后台定位 'scope.invoiceTitle', // 发票抬头 'scope.invoice', // 获取发票 'scope.werun', // 微信运动步数 ] // 头条 https://developer.toutiao.com/dev/miniapp/uQjMy4CNyIjL0IjM [ 'scope.userInfo', // 用户信息 'scope.userLocation', // 地理位置 'scope.address', // 通讯地址 'scope.record', // 录音功能 'scope.camera', // 摄像头 'scope.album', // *保存到相册* ] // 支付宝 https://docs.alipay.com/mini/api/xmk3ml#-1 [ 'userInfo', // 用户信息 'location', // 地理位置 'audioRecord', // 录音功能 'camera', // 摄像头 'album', // 保存到相册 ] // QQ https://q.qq.com/wiki/develop/game/frame/open-ability/authorize.html [ 'scope.userInfo', // 用户信息 'scope.userLocation', // 地理位置 'scope.qqrun', // QQ运动步数 'scope.writePhotosAlbum', // 保存到相册 'scope.appMsgSubscribed', // 订阅消息 ] [代码] 主要入口 【微信】 首屏对话列表下拉 扫一扫 发现->小程序 搜索 【支付宝】 扫一扫 搜索 首页我的小程序 【今日头条】 我的->扫一扫 搜索 【抖音】 搜索->扫一扫 【QQ】 扫一扫 💣 头条小程序陷阱 目前仅在头条Android版本7.2.9及以上版本支持真机调试功能。iOS暂时不支持真机调试 抖音App的小程序上没有打开调试器选项,右上角胶囊只有一个关闭按钮 💣 支付宝小程序陷阱 my.getOpenUserInfo用于获取支付宝会员基础信息,只能在真机上调试,无法在 IDE 中调试,也就是只要有用户授权的页面都需要推送到真机上开发调试! 支付宝授权平台只返回tocken和uid,由开发者自己维护session有效期,[代码]checkSession[代码] 方法不可用 打开调试的调试器面板在调起用户授权弹窗时会消失,此时须使用真机调试 💣 uni-app 陷阱 uni.getSetting,文档上说【支付宝】支持,调用却报错 [代码]支付宝小程序,暂不支持getSetting[代码],而直接调支付宝的api my.getSetting 确是支持的 uni.chooseVideo,文档上说【支付宝】支持,调用却报错 [代码]支付宝小程序,暂不支持chooseVideo[代码],而直接调支付宝的api my.chooseVideo(文档未找到) 确是支持的 uni.chooseAddress,文档上说【支付宝】不支持,实际上是支持的,只是需要调用 my.getAddress,且仅商户才能使用 uni.getImageInfo,文档上说【头条】支持,调用却报错 [代码]头条小程序,暂不支持getImageInfo[代码],而直接调头条的api tt.getImageInfo 确是支持的 📌 TODO 模版消息 第三方插件 uni-app 跨端小程序风险点 后端接口。不同端的后端接口不一样,需要后端评估一下。举例:模版消息(微信|支付宝|头条);设计用户系统时需注意微信和QQ都有各自的openID和unionID,支付宝只有uid,头条只有openID;接入微信,QQ,支付宝支付时各种传参不一样 分享转发。支付宝,头条小程序分享至微信和QQ的主要方式是生成口令或者生成小程序码图片或者走中间页,导致传播路径较长 某些端重要功能缺失。举例:【头条】不支持客服会话。【抖音】不支持webview转发。【QQ】不支持收货地址 某些端api缺失,可能导致某些功能无法实现 第三方插件支持度
2019-11-02 - 云开发经验总结(展示两种增删改查的方法)
开发工具mpvue官方文档 云开发初始化[代码] [代码] [代码] wx.cloud.init({[代码] [代码] [代码][代码]env: [代码][代码]'wedding-10c111'[代码][代码] })[代码] [代码] [代码] 上面这段代码配置在src目录下的main.js文件中 数据库API(不使用云函数进行增删改查)以下说明均写在对应代码注释里,不清楚的请查看相关注释 查(获取数据)[代码] [代码] [代码] // 获取轮播图列表[代码] [代码] getBannerList () {[代码][代码] [代码][代码]// 获取数据库引用[代码][代码] [代码][代码]const db = wx.cloud.database()[代码][代码] [代码][代码]// 获取名为“banner”的集合引用[代码][代码] [代码][代码]const banner = db.collection([代码][代码]'banner'[代码][代码])[代码][代码] [代码][代码]// 获取集合(Promise 风格)[代码][代码] [代码][代码]banner.get().then(res => {[代码][代码] [代码][代码]this[代码][代码].list = res.data[0].bannerList[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下: [图片] 注意:之所以数据库只有一条数据,而把banner列表当成这条数据的一个字段存储,其目的是为了自己后续换图操作的方便 增(添加数据)[代码][代码][代码] [代码] [代码] // 添加用户[代码] [代码] addUser () {[代码][代码] [代码][代码]// 获取数据库引用[代码][代码] [代码][代码]const db = wx.cloud.database()[代码][代码] [代码][代码]// 获取名为“user”的集合引用[代码][代码] [代码][代码]const user = db.collection([代码][代码]'user'[代码][代码])[代码][代码] [代码][代码]// 向“user”集合中添加一条数据(Promise 风格)[代码][代码] [代码][代码]user.add({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]user: that.userInfo,[代码][代码] [代码][代码]// 构造一个服务端时间的引用,我的项目中都是取自己转化后的时间,[代码][代码] [代码][代码]// 取这个时间更加合理,可用于查询条件、更新字段值或新增记录时的字段值[代码][代码] [代码][代码]time: db.serverDate()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]// 添加成功后重新查询列表[代码][代码] [代码][代码]that.getUserList()[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下: [图片] 注意: 可以看出_id和_openid是添加完自动生成的属性 改(修改数据)[代码] [代码] [代码] // 改变某条留言的显示隐藏[代码] [代码] switchMessage (e) {[代码][代码] [代码][代码]// 获取数据库的引用[代码][代码] [代码][代码]const db = wx.cloud.database()[代码][代码] [代码][代码]// 获取名为“message”的集合的引用[代码][代码] [代码][代码]const message = db.collection([代码][代码]'message'[代码][代码])[代码][代码] [代码][代码]// 这里的id是拿到当前操作项对应的id,[代码][代码] [代码][代码]// 这里的show对应change事件传递过来的值[代码][代码] [代码][代码]message.doc(e.mp.target.dataset.id).update({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]show: e.mp.detail.value[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]console.log(res)[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下: [图片] 注意:这个界面在你们使用的小程序中是看不到的,只有本人才有权限查看 [图片] [代码] [代码] [代码] <[代码][代码]switch[代码] [代码]class[代码][代码]=[代码][代码]"switch"[代码] [代码]:data-id[代码][代码]=[代码][代码]"item._id"[代码] [代码]:checked[代码][代码]=[代码][代码]"item.show"[代码] [代码]@[代码][代码]change[代码][代码]=[代码][代码]"switchMessage"[代码][代码]></[代码][代码]switch[代码][代码]>[代码] [代码] [代码] 注意:上面我们之所以能得到e.mp.target.dataset.id是因为在<switch>标签上加了`:data-id="item._id"`,不然取不到对应id 删(删除数据)正好对应的上图有删除操作 [代码] [代码] [代码] deleteItem (id) {[代码] [代码] [代码][代码]// 记录this指向[代码][代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]// 这里之所以使用wx.showModal是防止误操作[代码][代码] [代码][代码]wx.showModal({[代码][代码] [代码][代码]title: [代码][代码]'提示'[代码][代码],[代码][代码] [代码][代码]content: [代码][代码]'你确定要删除这条留言?'[代码][代码],[代码][代码] [代码][代码]success (res) {[代码][代码] [代码][代码]if[代码] [代码](res.confirm) {[代码][代码] [代码][代码]// 获取数据库的引用[代码][代码] [代码][代码]const db = wx.cloud.database()[代码][代码] [代码][代码]// 获取名为“message”集合的引用[代码][代码] [代码][代码]const message = db.collection([代码][代码]'message'[代码][代码])[代码][代码] [代码][代码]// 删除操作(Promise 风格)[代码][代码] [代码][代码]message.doc(id).remove().then(res => {[代码][代码] [代码][代码]// 删除成功后再次请求列表,达到刷新数据的目的[代码][代码] [代码][代码]if[代码] [代码](res.errMsg === [代码][代码]'document.remove:ok'[代码][代码]) {[代码][代码] [代码][代码]that.getList()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 使用云函数进行增删改查 查(获取数据)[代码] [代码] [代码] // 云函数初始化[代码] [代码] const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码][代码] // 由于文章开始已经讲过初始化步骤,这里init(options)的options可以省略[代码][代码] // options参数定义了云开发的默认配置,该配置会作为之后调用其他所有云 API 的默认配置[代码][代码] cloud.init()[代码][代码] // 获取数据库的引用[代码][代码] const db = cloud.database()[代码][代码] exports.main = async (event, context) => {[代码][代码] [代码][代码]// 将集合名定义成一个变量,方便后续调用[代码][代码] [代码][代码]const dbName = [代码][代码]'message'[代码][代码] [代码][代码]// filter为指定的筛选条件,配合where()使用[代码][代码] [代码][代码]const filter = event.filter ? event.filter : [代码][代码]null[代码][代码] [代码][代码]// pageNum如果小程序端未传入则默认为1[代码][代码] [代码][代码]const pageNum = event.pageNum ? event.pageNum : 1[代码][代码] [代码][代码]// pageSize如果小程序端未传入则默认是10[代码][代码] [代码][代码]const pageSize = event.pageSize ? event.pageSize : 10[代码][代码] [代码][代码]// 数据库满足filter条件的数据总条数[代码][代码] [代码][代码]const countResult = await db.collection(dbName).where(filter).count()[代码][代码] [代码][代码]const total = countResult.total[代码][代码] [代码][代码]// 共多少页[代码][代码] [代码][代码]const totalPage = Math.ceil(total / pageSize)[代码][代码] [代码][代码]// 是否有下一页[代码][代码] [代码][代码]let hasMore[代码][代码] [代码][代码]if[代码] [代码](pageNum >= totalPage) {[代码][代码] [代码][代码]hasMore = [代码][代码]false[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]hasMore = [代码][代码]true[代码][代码] [代码][代码]}[代码][代码] [代码][代码]// 等待所有,orderBy()通过创建时间排序,查询单页数据[代码][代码] [代码][代码]return[代码] [代码]db.collection(dbName).orderBy([代码][代码]'time'[代码][代码], [代码][代码]'desc'[代码][代码]).where(filter).skip((pageNum - 1) * pageSize).limit(pageSize).get().then(res => {[代码][代码] [代码][代码]// 返回结果中顺带注入hasMore和total方便小程序端判断[代码][代码] [代码][代码]res.hasMore = hasMore[代码][代码] [代码][代码]res.total = total[代码][代码] [代码][代码]return[代码] [代码]res[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] [代码] [代码] [代码] getList () {[代码] [代码] [代码][代码]// 记录this指向[代码][代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]// 每次调用getList时重新从第一页开始[代码][代码] [代码][代码]that.pageNum = 1[代码][代码] [代码][代码]// 每次调用getList时,先将authorityList置空[代码][代码] [代码][代码]that.authorityList = [][代码][代码] [代码][代码]wx.cloud.callFunction({[代码][代码] [代码][代码]// 云函数名[代码][代码] [代码][代码]name: [代码][代码]'authorityList'[代码][代码],[代码][代码] [代码][代码]// 传入云函数的参数[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]// 查询的默认筛选条件,这里可以参考下面留言审核对应的两张图来看,左上角有个switch开关[代码][代码] [代码][代码]// 当开关开启时,filter:{show:false}生效[代码][代码] [代码][代码]filter: that.checkFlag ? {[代码][代码] [代码][代码]show: [代码][代码]false[代码][代码] [代码][代码]} : [代码][代码]null[代码][代码],[代码][代码] [代码][代码]// 查询页数[代码][代码] [代码][代码]pageNum: that.pageNum,[代码][代码] [代码][代码]// 每页条数[代码][代码] [代码][代码]pageSize: that.pageSize[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]// 配合下拉刷新使用,作用是停止刷新事件[代码][代码] [代码][代码]wx.stopPullDownRefresh()[代码][代码] [代码][代码]// 以下动作为赋值操作[代码][代码] [代码][代码]const temp = res.result[代码][代码] [代码][代码]that.total = temp.total[代码][代码] [代码][代码]that.hasMore = temp.hasMore[代码][代码] [代码][代码]that.authorityList = temp.data[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 上面代码对应实例如下:1.查询未通过审核的留言;2.查询全部的留言 [图片][图片] 增(添加数据)[代码] [代码] [代码] // 前面讲解过的注释之后的代码将不重复说明[代码] [代码] const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码][代码] cloud.init()[代码][代码] const db = cloud.database()[代码][代码] exports.main = async (event, context) => {[代码][代码] [代码][代码]const dbName = [代码][代码]'message'[代码][代码] [代码][代码]// 添加数据[代码][代码] [代码][代码]return[代码] [代码]db.collection(dbName).doc(event.id).add({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]desc: event.desc,[代码][代码] [代码][代码]type: event.type,[代码][代码] [代码][代码]show: event.show,[代码][代码] [代码][代码]time: event.time,[代码][代码] [代码][代码]url: event.url,[代码][代码] [代码][代码]name: event.name[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] [代码][代码][代码] [代码] [代码] sendMessage () {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]if[代码] [代码](that.desc) {[代码][代码] [代码][代码]wx.cloud.callFunction({[代码][代码] [代码][代码]// 云函数名[代码][代码] [代码][代码]name: [代码][代码]'addMessage'[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]desc: that.desc,[代码][代码] [代码][代码]type: [代码][代码]'message'[代码][代码],[代码][代码] [代码][代码]show: [代码][代码]false[代码][代码],[代码][代码] [代码][代码]time: utils.getNowFormatDate(),[代码][代码] [代码][代码]url: that.userInfo.avatarUrl,[代码][代码] [代码][代码]name: that.userInfo.nickName[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]// 关闭所有页面,打开到应用内的某个页面,跳转到留言列表页[代码][代码] [代码][代码]wx.reLaunch({[代码][代码] [代码][代码]url: [代码][代码]'/pages/message/main'[代码][代码] [代码][代码]})[代码][代码] [代码][代码]})[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]tools.showToast([代码][代码]'说点什么吧~'[代码][代码])[代码][代码] [代码][代码]}[代码][代码] }[代码] [代码] [代码] [代码][代码] 对应实例如下: [图片][图片] 改(修改数据)[代码] [代码] [代码] const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码] [代码] cloud.init()[代码][代码] const db = cloud.database()[代码][代码] exports.main = async (event, context) => {[代码][代码] [代码][代码]const dbName = [代码][代码]'message'[代码][代码] [代码][代码]return[代码] [代码]db.collection(dbName).doc(event.id).update({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]show: event.show[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] [代码] [代码] [代码] switchMessage (e) {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]wx.cloud.callFunction({[代码][代码] [代码][代码]name: [代码][代码]'switchMessage'[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]id: e.mp.target.dataset.id,[代码][代码] [代码][代码]show: e.mp.detail.value[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]if[代码] [代码](res.result.errMsg === [代码][代码]'document.update:ok'[代码][代码]) {[代码][代码] [代码][代码]that.getList()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下:(前面没使用云函数也实现了相同的功能,感兴趣的可以对比查阅) [图片] 删(删除数据)[代码] [代码] [代码] const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码] [代码] cloud.init()[代码][代码] const db = cloud.database()[代码][代码] exports.main = async (event, context) => {[代码][代码] [代码][代码]const dbName = [代码][代码]'message'[代码][代码] [代码][代码]return[代码] [代码]db.collection(dbName).doc(event.id).remove()[代码][代码] }[代码] [代码] [代码] [代码] [代码] [代码] deleteItem (id) {[代码] [代码] [代码][代码]// 记录this指向[代码][代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]// 这里之所以使用wx.showModal是防止误操作[代码][代码] [代码][代码]wx.showModal({[代码][代码] [代码][代码]title: [代码][代码]'提示'[代码][代码],[代码][代码] [代码][代码]content: [代码][代码]'你确定要删除这条留言?'[代码][代码],[代码][代码] [代码][代码]success (res) {[代码][代码] [代码][代码]if[代码] [代码](res.confirm) {[代码][代码] [代码][代码]wx.cloud.callFunction({[代码][代码] [代码][代码]name: [代码][代码]'deleteMessage'[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]id[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]if[代码] [代码](res.result.errMsg === [代码][代码]'document.remove:ok'[代码][代码]) {[代码][代码] [代码][代码]that.getList()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下: [图片] 总结掌握上面两种对应的增删改查后,相信大家对云开发会有一个更清晰的认识,也希望大家多多使用云开发做出更多好玩的小程序作品; 如果觉得看完这篇文章让你有想尝试使用云开发的冲动,请不要吝啬你的赞,有什么问题欢迎留言,一起交流学习。 对应小程序 欢迎大家体验: [图片] 日记小程序 [图片] 小程序订制加本人微信:huangbin910419 可按给定UI图订制
2019-11-06 - async/await 还是同步怎么办?
[代码]get[代码][代码]: async function(e) {[代码][代码] [代码][代码]let[代码] [代码]$that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]let[代码] [代码]$distance = await getDistance($that.data.info._point);[代码][代码] [代码][代码]if[代码] [代码]($distance <= 8000) {[代码][代码] [代码][代码]//step1[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]//step2[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码] [代码]let[代码] [代码]getDistance = async (point) => {[代码][代码] [代码][代码]let[代码] [代码]$geo = [代码][代码]null[代码][代码];[代码][代码] [代码][代码]await wx.getLocation({[代码][代码] [代码][代码]type: [代码][代码]"gcj02"[代码][代码],[代码][代码] [代码][代码]altitude: [代码][代码]true[代码][代码],[代码][代码] [代码][代码]success: res => {[代码][代码] [代码][代码]$geo = [res.longitude, res.latitude];[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: err => {[代码][代码] [代码][代码]$geo = [180, -80];[代码][代码] [代码][代码]},[代码][代码] [代码][代码]complete: () => {[代码][代码] [代码][代码]let[代码] [代码]$pointJson = JSON.stringify(point);[代码][代码] [代码][代码]let[代码] [代码]$pointGeo = JSON.parse($pointJson);[代码][代码] [代码][代码]let[代码] [代码]$point = $pointGeo.coordinates;[代码][代码] [代码][代码]let[代码] [代码]$rad = 6378137;[代码][代码] [代码][代码]let[代码] [代码]$rad1 = parseFloat($geo[1]) * Math.PI / 180.0;[代码][代码] [代码][代码]let[代码] [代码]$rad2 = parseFloat($point[1]) * Math.PI / 180.0;[代码][代码] [代码][代码]let[代码] [代码]$sub1 = $rad1 - $rad2;[代码][代码] [代码][代码]let[代码] [代码]$sub2 = parseFloat($geo[0]) * Math.PI / 180.0 - parseFloat($point[0]) * Math.PI / 180.0;[代码][代码] [代码][代码]let[代码] [代码]$sub = (2 * $rad * Math.asin(Math.sqrt(Math.pow(Math.sin($sub1 / 2), 2) + Math.cos($rad1) * Math.cos($rad2) * Math.pow(Math.sin($sub2 / 2), 2)))).toFixed(0);[代码][代码] [代码][代码]return[代码] [代码]parseInt($sub);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码]}[代码]我在get函数中会直接跑到step2,然后才到return parseInt($sub)
2019-11-04 - 微信小程序开发UI组件样式库推荐
做为微信开发的程序员来说,写一些WXSS页面样式最头疼了。往往做出来的界面虽然功能一个不少,但显示的效果简陋而达不到用户满意。 我们推荐了以下几款微信小程序的组件库,可以让你不用懂WXSS也不用设计感,照样能做出很漂亮的小程序。 一、WeUIWEUI是一套基于样式库weui-wxss开发的小程序扩展组件库,同微信原生视觉体验一致的UI组件库,由微信官方设计团队和小程序团队为微信小程序量身设计,令用户的使用感知更加统一。 [图片] [图片] 官方组件库能够满足基础的界面需求,但是,如果你想要更加饱满的视觉,更加活泼的动效,恐怕 WeUI 就满足不了你的需要了。 GitHub 地址:https://github.com/Tencent/weui 二、ColorUI 组件库ColorUI 是一款高颜值组件库,侧重于视觉交互。比起 WeUI 的低调克制,ColorUI 色彩鲜亮,样式繁多。除了拥有非常丰富的原生组件的自定义样式,它还提供一些常见的页面元素,比如时间轴、步骤条、聊天页、模态窗口等等。 [图片] 这些页面元素通常应用在哪些场景下呢? 如果你想做一款诸如日记类、记账类、博客类、Vlog 类的小程序,这时就需要用到「时间轴」。 如果你想做一款涉及流程的小程序,比如物流跟踪,工作审批等,「步骤条」就可以派上用场了。 如果你想做一款社交类小程序,那么,当然少不得要用到「聊天」的界面。 而「模态窗口」则可以应用于各类小程序中出现弹框、侧边栏的地方。 [图片] 此外,ColorUI 还引入了插件扩展,也就是更为复杂的组件。目前已有的扩展包括索引列表、微动画、全屏抽屉以及垂直导航。引用这几项扩展,只需编写少量代码,就能实现较炫的视觉交互,进一步简化了开发工作。 [图片] 前面我们已经提到,ColorUI 是侧重于视觉交互的组件库,这方面的表现,还在于它为用户提供了色彩的搭配方案。打开「背景」,可以看到深色、淡色、渐变等多种配色。 [图片] ColorUI 还有许多值得推荐的地方。多样化的示例就是其中之一,它详尽地向用户展示了各种情况下,开发者可能需要编写的样式。 比如,打开「头像」,就会看到被一一列举的圆形头像、圆角矩形头像、各种尺寸头像、默认头像、文字头像、彩色头像、头像组、贴标签头像等等。一个这么简单的组件,也可以有许多种不同的呈现方式。 [图片] 又比如,打开「列表」,不仅可以看到宫格列表、菜单列表、消息列表、左滑列表等基本的样式,还可以设置一些可选项,像边框、箭头等,在细节处也有多种可选样式。 [图片] ColorUI 给大家提供了高度自定义的组件,一些比较麻烦的样式,开发者只需调用其组件就能得以实现。不过,ColorUI 也不是万能的,比如,它尚未涉及购物类小程序所需的组件。 GitHub 地址:https://github.com/weilanwl/ColorUI [图片] 三、Vant 组件库演示Vant 是由有赞发布的,轻量的小程序 UI 组件库。如果你想制作一款电商、餐饮、外卖平台、票务预订等购物类小程序,选用 Vant 是较为合适的。为什么这么说呢? [图片] 首先,我们来看「业务组件」这一块。可以看到,「商品卡片」与「提交订单栏」两个组件可以构成一个基本的「购物车」页面;而「商品卡片」与「商品导航」二者又可以组成一个简单的商店页面。 [图片] 我们再看看其他琐碎的组件,比如「表单组件」中的「评分」、「搜索」、「步进器」,都属于购物类小程序需要用到的组件。 [图片] 「导航组件」中的「徽章」与「展示组件」中的「分类选择」,都可以用于商品品类的选择切换。 [图片] 「展示组件」中的「折叠面板」与「面板」可以用作详细介绍商品的组件,「步骤条」则可以用于显示物流跟踪信息。 [图片] 使用 Vant 组件库,除了可以用常用的 Toast 方法,向用户弹出提醒消息,还可以引用「反馈组件」中的「消息通知」以及「展示组件」中的「通告栏」,向用户输出通知信息。 [图片] 除了以上可用于购物类小程序的组件,Vant 组件库当然还有那些比较通用基本元素、弹出层、Transition 动画等。值得一提的是,Vant 还支持自定义 Actionsheet,在「反馈组件」的「上拉菜单」中,有三种不同的自定义 Actionsheet。 [图片] Vant 对开发者非常友好,文档可以说是事无巨细了,而且在文档右侧,还可以预览样式哦。 开发文档:https://youzan.github.io/vant-weapp/#/intro GitHub 地址:https://github.com/youzan/vant-weapp [图片] 四、iViewUIiViewUI 是由 TalkingData 发布的组件库。作为一款好用的组件库,布局、面板、列表、表单、顶部导航栏、底部导航栏等组件当然必不可少,那么 iViewUI 除了具备这些标配的组件,还有哪些亮点呢? [图片] 在「导航」分类下,「分页」、「索引选择器」以及「吸顶容器」都是比较实用的组件。 其中,「索引选择器」与 ColorUI 中的「索引列表」是同类组件,不同的是,ColorUI 的「索引列表」中每一项可以包含图片、名字与描述,且支持搜索,而 iViewUI 的「索引选择器」中每一项只包含名字,且不支持搜索。 而「吸顶容器」在上文中尚未提及,这一组件适合用于分级长列表的显示。 [图片] 在「视图」分类下的「倒计时」一项中,提供了多种倒计时的显示格式。 [图片] iViewUI 同样有详细的文档,但是不支持网页预览,只能打开小程序预览。 开发文档:https://weapp.iviewui.com/docs/guide/start GitHub 地址:https://github.com/TalkingData/iview-weapp [图片] 五、MinUI 组件库MinUI 是由蘑菇街发布的组件库。与其他组件库不同的是,MinUI 更注重一些细节的处理。 [图片] 调用「基础元件」中的「文本截断」,可以控制长文本的显示行数,文本超长的用省略号结尾。「页底提示」可以用在上拉加载中的过程中。而「价格」则提供了各种样式的价格及货币符号。 [图片] 「功能组件」的「异常流展示」为开发者提供了各种异常状态下,向用户展示的界面。「遮罩层」则提供了各种效果的遮罩层,及其显示、隐藏方式。 [图片] 相比其他组件库,MinUI 将各种组件拆分得更细,真正使用时,需要开发者更多的对各个组件进行再次结合,但也因此 MinUI 显得更加通用。 开发文档:https://meili.github.io/min/docs/minui/index.html#README GitHub 地址:https://github.com/meili/min-cli [图片] 六、TaroUITaroUI 是由京东·凹凸实验室发布的多端 UI 组件库。这套组件库,可以在 H5、微信小程序、支付宝小程序、百度小程序多端适配运行。TaroUI 的整体风格简约、清新、统一,适合工具、读书、资讯、教育、商务等类型的小程序。 [图片] 除了拥有上文所提及的组件之外,TaroUI 还有几个特别的组件。在「表单」中有一项「范围选择器」,可以通过滑动条指定数值范围。在「高阶组件」中,可以显示「日历」,并且支持多种日期选择样式。 [图片] TaroUI 同样拥有健全的开发文档,也支持在网页中预览手机效果。 开发文档:https://taro-ui.aotu.io/#/docs/introduction GitHub 地址:https://github.com/NervJS/taro-ui [图片] 七、WuxUI这套组件库所包含的组件最为丰富。不仅我们前文提到的各类组件都可以在 Wux 中找到,而且还有进度环、骨架屏、筛选栏、数字键盘、结果页等实用工具类组件。如果你想开发一款工具类小程序,Wux 是个不错的选择。 [图片] 开发文档:https://wux-weapp.github.io/wux-weapp-docs/#/introduce GitHub 地址:https://github.com/wux-weapp/wux-weapp/ [图片] 这 7 款 UI 组件库各有所长,适合不同的小程序类型,Vant 适合电商类的,TaroUI 与 Wux 适合工具类的,而蘑菇街的 MinUI 当然更适合社区类的了。 大家可以根据自己的需求来选择相应的UI组件库来创建制作微信小程序。有微信小程序开发需求也可以联系云梁网络(https://www.yunliangwang.com)
2019-10-29 - 这文档是敌特派来的人写的吗?目的就是为了整死我们搬砖工吗?
事情是这样的。 我司一小破程序,打开时类似这样,显示一个logo,一个标题 [图片] 经过一个2秒的动画效果,logo和标题就移动到上面部分了,同时渐显出来一个loading组件,这些都是使用小程序的Animation API实现的。 [图片] [图片] 现在需求来了。 我们想在首屏渲染后。在图标往上移的动画执行周期中,将背景色缓慢从蓝色变为白色。 (别问为什么要变背景色,我们准备待会加完班拿上弹弓组团去打设计师家玻璃了) [图片] 有朋友会说了,这不是很简单嘛,弄个定时器去替换class不就行了? 我只想说,no no no。朋友,我们搬砖就要有搬砖的样子嘛。 什么时间搬,搬多少,什么时间停,都要严谨嘛。 天真的我,想当然的就拍着胸脯向BOSS表示小意思啦。 [图片] naive的我心里想着 肯定会有动画执行开始和结束一个callback接口的嘛 然鹅,、翻遍了小程序文档里关于动画的各个段落之后才发现 [图片] 神马?? 我不信!一定是我的眼刚刚瞎了,我要再看一遍。 [图片] [图片] [图片] [图片] [图片] PS 看,多么言简意赅的文档! 在看多了外面那些"妖艳贱货"的文档后,如此小清新的文档,还真让我这老司机虎躯一震。 // TODO 我当即在心里暗暗发誓,我一定要强烈建议我司将此文档规范引进并在我司大范围实践,太他【文明用语】高效了。 END PS 在我不懈的努力下 在某毒找到了一篇关于动画重置的实例 [图片] [图片] 哦也,三七三十一,一定是我聋了才没看见这么大个接口 同事心里还在做自我批判,怎么能轻易的就甩锅给腾讯爸爸。 祭出我的Ctrl+F大法 [图片] 果然。还是我太天真。竟然没有搜到 0/0? 在经过了一番苦苦的某毒搜索之后,猛然意识到,或许是我姿势不对? [图片] 谢天谢地,博客园诚不我欺。确实有这个东东。 我默默的打开了唯一的一条搜索结果学习了起来。你猜怎么着? [图片] 我发现了腾讯爸爸藏起来的彩蛋。 哇,没想到小程序团队这么调皮。 在动画相关的所有文档里,竟然半个字都没提有这几个事件。保密工作做的很到位。表扬。5星好评。 [图片] 根据文档,照猫画虎。 [图片] [图片] 控制台没有任何反应 [图片] 一定是我姿势不对,我换换姿势。 [图片] [图片] 一顿操作猛如虎,然鹅发现并没有什么卵用。 [图片] [图片] [图片] 我盯着这条说明,默默的给自己点上了一根烟后陷入了痛苦的沉思。 期间我尝试了各种姿势,都没有找到关于WXSS animation到底是个什么鬼。 我只知道有Animation这个动画API。或许他俩是一个东西? 但是为什么Animation里没有关于它的只言片语? [图片] 既然Animation里没有写,肯定是另外一套体系吧? 灵光一闪, oh no,别又是腾讯爸爸调皮了把文档藏起来了吧。 [图片] [图片] 经过地毯式的搜索及换遍了各种姿势想要跟我的小程序互动一把后。 [图片] [图片] [图片] 我选择死亡。 [图片] [图片] 我想起那天夕阳下调的微信小程序,那是我逝去的青春。。。 IDE: v1.02.1901230 Library: 2.4.2
2019-01-28 - 小程序开发,必备的学习网站!
官方三件套 小程序官方开发文档 https://developers.weixin.qq.com/miniprogram/dev/ [图片]无论你多么厉害官方文档跑不了,因为你是基于它的标准来做开发的。 小程序开发指南 https://developers.weixin.qq.com/ebook?action=get_post_info&docid=0008aeea9a8978ab0086a685851c0a [图片]当你能够使用微信小程序提供的组件及API完成项目的时候,这只是刚刚开始。 你还要理解底层原理,做到“知其然知其所以然”。 微信开放社区 https://developers.weixin.qq.com/community/ [图片]一个可以讨论技术问题,可以及时了解版本更新,以及一些优秀文章的地方。 前端学习网站 MDN Web 文档 https://developer.mozilla.org/zh-CN/ [图片]在开发小程序的过程中,要用到很多css和js,这些都是小程序官方网站没有的前端开发知识。 在此推荐一个优质的 Web 前端开发文档网站。除了有系统系列的教程,还能看国外优秀的技术博客。 MDN使命:提供给开发者们更轻易构建Web项目的信息。我们致力于记录互联网上的开源技术。 框架推荐 WePY https://github.com/Tencent/wepy [图片]一款让小程序支持组件化开发的框架,通过预编译的手段让开发者可以选择自己喜欢的开发风格去开发小程序。框架的细节优化,Promise,Async Functions的引入都是为了能让开发小程序项目变得更加简单,高效。 比较显著的特点是,类 Vue 开发风格 和 小程序细节优化,如请求列队,事件优化等。 同时WePY也是一款成长中的框架,大量吸收借鉴了一些优化前端工具以及框架的设计理念和思想。 基于WePY的完整开源项目: 满熊阅读 https://github.com/Thunf/wepy-demo-bookmall [图片] 店铺商家版 https://github.com/coolhwm/leshare-seller-wepy [图片] 商城(微店) https://github.com/dyq086/wepy-mall [图片]
2019-09-06 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - 用小程序·云开发轻松构建二手书商城小程序丨实战
导语 很多大学有个普遍现象,毕业或者搬校区的时候,成堆成堆的书都被随便处理掉,作为过来人,每每想到都十分痛心可惜,而导致这种情况发生的原因,我认为主要还是归结学校原因,一方面没有提供靠谱便利的平台,另一方面,宣传不到位,基于此开发了这款小程序。下面挑了些开发过程中遇到的典型来讲解实现过程,感兴趣可以一览… 一:登录注册页 目前小程序有了详细的登录规范,参考官方示例,本程序的登录入口做了以下处理: 在需要涉及用户信息的部分,进行Modal提示进入,比如:游客发布、购买等 个人中心,未登录默认显示”点击登录“按钮 好了,先来看看登录页面效果图吧: [图片] 手机号获取(相关代码): [代码]<button class="phone" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"> <block wx:if="{{phone==''}}"> 请点击获取您的手机号</block> <block wx:if="{{phone!==''}}"> {{phone}}</block> <image wx:if="{{phone==''}}" class="right" src="/images/right.png" /> </button> [代码] [代码] //获取用户手机号 getPhoneNumber: function(e) { let that = this; //判断用户是否授权确认 if (!e.detail.errMsg || e.detail.errMsg != "getPhoneNumber:ok") { wx.showToast({ title: '获取手机号失败', icon: 'none' }) return; } wx.showLoading({ title: '获取手机号中...', }) wx.login({ success(re) { wx.cloud.callFunction({ name: 'regist', // 对应云函数名 data: { $url: "phone", //云函数路由参数 encryptedData: e.detail.encryptedData, iv: e.detail.iv, code: re.code }, success: res => { wx.hideLoading(); //获取成功,设置手机号码 that.setData({ phone: res.result.data.phoneNumber }) }, }) }, }) }, [代码] 此处仅展示前端部分核心代码,手机号获取涉及到解密过程,需要配合云函数实现,具体的请参考完整demo注册页代码 目前该接口针对非个人开发者,且完成了认证的小程序开放(不包含海外主体)。 常用联系方式的校检: [代码]if (!(/^\w+((.\w+)|(-\w+))@[A-Za-z0-9]+((.|-)[A-Za-z0-9]+).[A-Za-z0-9]+$/.test(email))) { wx.showToast({ title: '请输入常用邮箱', icon: 'none' }); return false; } [代码] 同理相关正则: [代码]//手机号 /^[1][3,4,5,6,7,8,9][0-9]{9}$/ //QQ号 /^\s*[.0-9]{5,11}\s*$/ //微信号 /^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$/ [代码] 目前常用手机号,似乎就差10和12字段的没有了。 二:发布信息页 [图片] 步骤条实现 发布页有几个小地方值得留意: 顶部的步骤条,随操作流程一直在变。 步骤改变时,有个横向切换动画 价格设置,使用了步进器 刚刚上面之所以说这几个点,因为他们都是同出一源–vant组件 此组件的使用教程可直接看对应官网 https://youzan.github.io/vant-weapp/ 使用组件开发效率会高很多,避免重复工作,同时可以参考部分组件的写法,还是有很多值得学习的地方的。 textarea小注意 步骤二中备注信息那里使用了层级最高的原生组件textarea,这里有个特别使用注意项:如果下面tabbar是自己写的而非使用的自带原生的tabbar,会出现穿透现象,如下图示例: [图片] 我常用的解决办法,通过动态改变textarea的聚焦状况,当点击该区域时,设置聚焦显示真实textarea,当失焦之后,展示为view层,代码如下: [代码] <view class="beibox"> <view wx:if="{{!focus}}" bindtap="focus" >{{beizhu?beizhu:'请输入信息'}}</view> <textarea wx:if="{{focus}}" focus="{{focus}}" bindblur="loose" bindinput="beiInput" value="{{beizhu}}"></textarea> </view> [代码] [代码] data: { beizhu:'', focus: false //默认不聚焦 } //点击聚焦显示textarea隐藏view focus() { let that = this; that.setData({ focus: true }) }, //失焦隐藏textarea显示view loose() { let that = this; that.setData({ focus: false }) }, [代码] 三:首页 [图片] 上面左图是首页的进入后的初始样式,右图是下滑之后的动态页面,关于页面的样式布局方面,使用flex可以轻松搞定,我们重点说下面这点: 监控屏幕滚动实现动态响应 在上图第二张示例图中,随着页面下滑,顶部分类栏也随之置顶,下方也出现了一个返回顶部按钮,实现原理: 监控屏幕下滑高度,当大于我们设定的某个值时,元素进行渲染 这里我们需要使用页面的一个事件处理函数:onPageScroll [代码]//监测屏幕滚动 onPageScroll: function(e) { this.setData({ scrollTop: (e.scrollTop) * (wx.getSystemInfoSync().pixelRatio) }) }, [代码] 函数获取的是页面在垂直方向已滚动的距离(单位px),但我们页面布局使用了rpx计算,所以后面我们乘以设备像素比获取对应的rpx值 在view视图层中通过wx:if或者hidden进行控制显隐,区别在于:wx:if每次隐藏都是销毁了,而hidden只是不呈现,但依旧渲染到页面,具体的使用效果,可查看视图调试处的效果。 下面给个完整的返回顶部示例 [代码]<view class="totop" bindtap="gotop" hidden="{{ scrollTop<500 }}"> <image lazy-load src="/images/top.png" /> </view> [代码] [代码] data: { scrollTop: 0 //初始滚动高度为0 }, //监测屏幕滚动 onPageScroll: function(e) { this.setData({ scrollTop: parseInt((e.scrollTop) * wx.getSystemInfoSync().pixelRatio) }) }, //返回顶部 gotop() { wx.pageScrollTo({ scrollTop: 0 }) }, [代码] 四:详情页面 [图片] 小程序布局只要掌握一个flex,基本上就够了,所以这里不过多阐述样式问题,到时候如果有疑问可查看完整demo,都有注释的。 因为此小程序的使用对象及功用限制,所以和完整的商城相比少了一个购物车功能,支付购买在商品详情页即完成了,这里涉及到两个点,一是下单购买,二是购买之后的通知问题。 小程序内支付提现 不仅仅是支付包括提现,此程序都借助了tenpay这个模块,详细介绍: https://www.npmjs.com/package/tenpay 在小程序中的实例使用,可以参考之前社区之前发布的文章: 10行代码实现小程序支付功能!丨实战 当然,之前文章是教大家如何实现支付,关于提现流程也一样,先去看看tenpay的商户付款到余额的说明,再看一下此程序的相关代码,读一遍准能懂。 发送通知 此程序通知分为两类:短信通知、邮件通知 使用场景:用户下单后,对卖家进行短信+邮件通知,下单后订单状态改变使用邮件通知。 说一点题外话:小程序有一个自带的模板通知,在用户主动触发后7天内能推送模板信息,之前写这个程序的时候慎重考虑过,最后还是舍弃了,毕竟七天时间,不是每本书都那么畅销的。 邮件只需要有一个账户即可,短信通知却是要成本的,当然效果要比邮件好,配置起来的话,难度都一样,我们就以短信为例: 首先去腾讯云申请短信API: https://cloud.tencent.com/product/sms [图片] 按照提示操作,设置好短信签名,模板等。 配置云函数 新建sms云函数,代码如下: [代码] const cloud = require('wx-server-sdk') const QcloudSms = require("qcloudsms_js") const envid = 'zf-shcud'; //云开发环境id const appid = 140000001 // 替换成您申请的云短信 AppID 以及 AppKey const appkey = "abcdefghijkl123445" const templateId = 1234 // 替换成您所申请模板 ID const smsSign = "腾讯云" // 替换成您所申请的签名 cloud.init({ env: envid, }) // 云函数入口函数 exports.main = async (event, context) => new Promise((resolve, reject) => { /*单发短信示例为完整示例,更多功能请直接替换以下代码*/ var qcloudsms = QcloudSms(appid, appkey); var ssender = qcloudsms.SmsSingleSender(); var params = ["测试内容"]; // 获取发送短信的手机号码 var mobile = event.mobile // 获取手机号国家/地区码 var nationcode = event.nationcode ssender.sendWithParam(nationcode, mobile, templateId, params, smsSign, "", "", (err, res, resData) => { /*设置请求回调处理, 这里只是演示,您需要自定义相应处理逻辑*/ if (err) { console.log("err: ", err); reject({ err }) } else { resolve({ res: res.req, resData }) } } ); }) [代码] 提一个小点:在有多个云环境时候,如果涉及到查询云数据库等和云环境有直接干系的操作时候,最好在cloud.init({env: envid})这里声明一下环境,否则有小几率报错。 五、启动页设计 [图片] 启动页也算本程序一个亮点,首次进入就是一张美美的图给人一种身心愉悦之感,下面我们就详细说说这个怎么做: 哪些元素? 全屏背景图 倒计时跳转 说这个之前,大家注意一下整个页面是全屏了的,所以这里我们要配置一下页面参数: 在此页面的.json中这么配置: [代码]{ "navigationStyle":"custom" } [代码] 这就成功全屏了,接着我们来编写页面样式: [代码]<view class="contain"> <view class="go"> <button bindtap="go">跳过{{count}}s</button> </view> <image class="bg" src="{{bgurl}}"></image> </view> [代码] [代码].contain { width: 100%; height: 100%; position: relative; } .bg { position: absolute; left: 0rpx; top: 0rpx; width: 100%; height: 100%; z-index: -1; } .go { position: absolute; right: 30rpx; top: 150rpx; z-index: 9; } .go button { font-size: 28rpx; letter-spacing: 4rpx; border-radius: 30rpx; color: #000; background: rgba(255, 255, 255, 0.781); display: flex; justify-content: center; align-items: center; text-align: center; width: 160rpx; height: 60rpx; } [代码] 样式快速搞定,再来说说js部分。 倒计时功能: [代码]countDown: function() { let that = this; let total = 3;//倒计时总数3秒 this.interval = setInterval(function() { total > 0 && (total--, that.setData({ count: total })), 0 === total && (that.setData({ count: total }), wx.switchTab({ url: "/pages/index/index" }), clearInterval(that.interval)); }, 1e3); }, [代码] 背景图 实现有两种办法,第一是本地路径,第二是引用远程地址(可通过接口动态改变) 第一种好处是直接使用本地图片,加载速度快,第二种可以随时更换启动图,两种办法都试过了,最终我建议还是采用第一种办法,使用本地图片,如果使用远程地址,首次进入会出现短时间白屏,体验不好,当然,你也可以想办法把图片压缩再压缩,那就不存在加载慢了,但分辨率又成了个问题,所以具体如何使用,还是根据产品需求。 总结 纸上得来终觉浅,绝知此事要躬行,以上总结的是开发此程序中我认为遇到的典型问题,实践过程中肯定会有更多有意思的问题的出现,“面向百度”编程是一个方面,但我更建议“面向官方文档”,很多问题其实官方文档中都有很详细的说明和代码示例,如果阅读文档颇感费力,我建议你该静下心来,先熟悉下html,css,javascript相关内容,到时候再回过头来看你会发现“原来如此”。 如果你想要了解更多关于云开发CloudBase相关的技术故事/技术实战经验,请扫码关注【腾讯云云开发】公众号~ [图片]
2019-09-29 - 小程序云开发攻略,最棘手的问题
背景 最近小程序非常的火,应公司业务发展要求,开发维护了几款小程序,公司开发的小程序都是由后端提供的接口,开发繁琐而复杂,直到小程序出现了云开发,仔细研读了文档之后,欣喜不已,于是我着手开发了本人的第一款小程序 小程序云开发教程地址 点我查看>> 分析 云开发为开发者提供完整的原生云端支持和微信服务支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代,同时这一能力,同开发者已经使用的云服务相互兼容,并不互斥。 优势 无需自建服务器,数据库,无需自建存储和CDN 数据库模型很简单,就是一个json形式的对象格式 调用服务端云函数自动获取openid,再也没有繁琐的授权登陆流程了,只要进入小程序就是登陆状态,体验真的好 开发迅速,只需要前端就能搞定所有开发工作 需要解决的问题 数据库切换问题 使用过云开发的人都发现云开发切换数据库环境是最头疼的,如果手动去切换容易搞错,不小心在当前环境修改了线上数据库数据 直到官方出了这个函数问题也就迎刃而解 [代码]cloud.updateConfig({ env: ENV === 'local' ? 'dev-aqijb' : ENV }); [代码] 我使用的是服务端云开发功能,为什么要这样判断,因为在开发工具中ENV = ‘local’,所以这么判断一下,保证开发工具中使用的是测试环境数据库 使用taro多端开发框架,借助于webpack,还可以通过process.env.NODE_ENV值区分当前代码开发环境 [代码]await Taro.cloud.init({ env: `${process.env.NODE_ENV === 'development' ? 'dev-aqijb' : 'pro-hljv7'}` /* env: 'pro-hljv7' */ }); [代码] 这样可以保证开发环境和线上环境可以使用对应环境的数据库 数据库字段定义问题 因为JS是弱类型语言,不能像typescript那样静态定义变量类型,这样添加到数据库的字段数量和字段类型都无法控制 我不想用typescript,能不能实现这样的功能呢,可以用superstruct库来实现这个功能 superstruct git地址 点我查看>> 详细使用案例见下方代码 函数文件太多的问题 官方和他人教程的例子都是一个文件对应一个云函数,通过开发体验我发现这样做并不好,当项目有多个表的时候,找个函数文件真的太难了 我们可以将一个表的增删改查函数全部写入一个文件中 教程: 首先每个云函数文件中package.json引入superstruct [代码]{ "dependencies": { "wx-server-sdk": "latest", "superstruct": "latest" } } [代码] 以下代码是一个完整的云函数例子 [代码]const cloud = require('wx-server-sdk'); const { struct, superstruct } = require('superstruct'); cloud.init(); //小区信息 const Model = () => { const db = cloud.database(); const _ = db.command; const collection = db.collection('address'); return { async add(data) { try { data = struct({ name: 'string', //名字 phone: 'string', unit: 'number', //楼单元号 doorNumber: 'string', //门号 communityId: 'string', //小区id _openid: 'string' //用户的id //isDefault: 'boolean' //是否默认地址 })(data); } catch (e) { const { path, value, type } = e; const key = path[0]; if (value === undefined) { const error = new Error(`${key}_required`); error.attribute = key; throw error; } if (type === undefined) { const error = new Error(`attribute_${key}_unknown`); error.attribute = key; throw error; } const error = new Error(`${key}_invalid`); error.attribute = key; error.value = value; throw error; } let res = await this.getList({ _openid: data._openid }); if (res.data.length >= 1) { return { msg: '当前只支持保存一个地址' }; } res = await collection.add({ data, createTime: db.serverDate(), updateTime: db.serverDate() }); return res; }, async getAdressById({ _openid, _id }) { const user = await collection .where({ _openid, _id: _.eq(_id) }) .get(); return user; }, //更新指定的id 先判断手机号修改没,没修改直接就改数据,修改过判断一下库中有没有这条数据 async update(data) { //更新表的操作 }, //删除指定id的shop async remove({ _id, _openid }) { //删除表的操作 }, /** * 获取商列表 * @param {*} option {category 类别, pagenum 页码} */ async getList({ _openid }) { const shopList = await collection .where({ _openid }) .get(); return shopList; } }; }; exports.main = async (event, context) => { const { func, data } = event; const { ENV, OPENID } = cloud.getWXContext(); // 更新默认配置,将默认访问环境设为当前云函数所在环境 console.log('ENV', ENV); cloud.updateConfig({ env: ENV === 'local' ? 'dev-aqijb' : ENV }); let res = await Model()[func]({ ...data, _openid: OPENID }); return { ENV, data: res }; }; [代码] 函数使用方式 [代码]wx.cloud.callFunction({ 'address', //云函数文件名 data: { func: 'add', //云函数中定义的方法 data: {} //需要上传的数据 } }); [代码] 图片 视频等文件 直接打开云开发控制台选择存储直接上传文件,复制url地址就可以放到代码中使用了 扫码体验我的小程序: [图片]
2019-09-29 - 分享一个固定头和列的 table 组件的简单实现
本案案例基于 WePY 实现,大家可根据自身需要进行更改扩展。 代码地址>> 演示 [图片] 演示视频地址>> 实现原理 [图片] 橙色和紫色区域组成了横向滚动的 [代码]scroll-view[代码]。 红色虚线区域是纵向滚动的 [代码]scroll-view[代码]。但由于绿色区域设置了 [代码]pointer-events: none;[代码],即实际只能触摸橙色区域。通过在橙色区域绑定的 [代码]scroll[代码] 事件(纵向),实时设置绿色虚线区域的 [代码]scrollTop[代码]。 紫色区域是固定头部,绿色区域是固定列。左上角的绿色区域是横向与纵向共同固定的区域。 实现要点 绑定了 [代码]scroll[代码] 事件的 [代码]scroll-view[代码] 要指定 [代码]throttle: false[代码],否则回调函数有可能取不到最终位置的 [代码]scrollTop[代码] 值。官方文档目前未提及此属性,参考资料>>。 固定列需要设置 [代码]pointer-events: none;[代码],实现点击穿透。使得 [代码]tbody[代码] 能触发 [代码]scroll[代码] 事件,而不是为固定列也绑定 [代码]scroll[代码] 事件。 找出每列的最大单元格作为该列的宽度,当然你也可以显示设置。 peace out!👋 小程序 Bug 2019.09.03 更新 当将该组件至于 Popup 弹框,且该弹框通过 [代码]visibility: hidden/visible[代码] 切换,那么在 iOS 中,会使固定列([代码].table__fixed-columns[代码])的 [代码]pointer-events: none[代码] 失效。
2019-09-03