- [开盖即食]小程序那点事(2)--- 各种格式文件的存储分享和NPM引入那些坑
[图片] 一、如何下载和存储ZIP,RAR,PSB等各类文件 存储非图片、PDF文件,对于很多IOS用户是很痛苦的时候,但是可以通过文件分享的连招来解决存储的文件,微信自带的文件助手就是一个很方便的传输工具。 <code>wx.shareFileMessage</code> 必须交互后才可以触发 [代码]shareBtn() { wx.downloadFile({ url: "https://sk2.nosdn.dd996.net/1/test/1.rar", // 下载url success(res) { // 下载完成后转发 wx.shareFileMessage({ filePath: res.tempFilePath, fileName: "dayday996.rar", success() { }, fail: console.error, }) }, fail: console.error, }) }, [代码] [图片] 利用分享传输/存储文件的好处 如果你是IOS用户,可以方便把文件传给对方 利用微信自带的文件助手,还能将文件同步到电脑中 避免临时存储失效的问题 二、各种外面框架的引入方法 这里拿 weui 举例 使用 useExtendedLib的方式 官方文档:https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#useExtendedLib 在app.json中加入 [代码]{ "useExtendedLib": { "kbone": true, "weui": true } } [代码] <font color="#dd0000"> 注意这里 不需要 多此一举在wxss中添加 </font> [代码]@import 'weui-miniprogram/weui-wxss/dist/style/weui.wxss'; [代码] 使用 NPM的引入方式 这种方式适合需要其他开源功能模块的项目,也挺方便,不过坑也挺多的,有些路径需要修改,不能直接拿官方文档来用,拿weui这个项目举例 1、命令行: [代码]npm init npm install --save weui-miniprogram [代码] 2、配置package.config.json [代码]{ ... "setting": { ... "packNpmManually": true, "packNpmRelationList": [ { "packageJsonPath": "./package.json", "miniprogramNpmDistDir": "./" } ] } } [代码] 3、配置npm,并重启IDE工具(注意啊,一定要重启,不然会有很多妖怪的事情) [图片] 4、app.wxss引入 [图片] [代码]@import '/miniprogram_npm/weui-miniprogram/weui-wxss/dist/style/weui.wxss'; [代码] 5、在page对应的json中引入,注意路径,根据需要修改 [代码]{ "usingComponents": { "mp-dialog": "../miniprogram_npm/weui-miniprogram/dialog/dialog" } } [代码] 6、对应的html部分 [代码]<mp-dialog title="前端智酷" show="{{true}}" bindbuttontap="tapDialogButton" buttons="{{[{text: '取消'}, {text: '确认'}]}}"> <view>前端智酷</view> </mp-dialog> [代码] [图片] 一些错误 1、路径问题 [图片] 2、没有配置npm 初始化 [图片] npm官方文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/npm.html weui-wxss的github地址: https://github.com/Tencent/weui-wxss 如有疑问请留言~ 觉得有用,请点个赞哦,我会继续努力分享有用的实战内容~
2022-02-15 - 小程序消息推送,订阅消息的实现,借助云开发云函数实现定时推送订阅消息功能
我在云开发基础课程里给大家讲过小程序消息推送功能的实现,等下会给大家回顾下。但是有时候我们如果想实现定时推送的功能该怎么做呢 一,普通订阅消息的发送 我们先来看下订阅消息的官方简介。 [图片] 接下来我们就来借助云开发,来快速实现小程序消息推送的功能。 1-1,获取模板 ID 这一步和我们之前的模板消息推送是一样的,也是先添加模板,然后拿到模板id [图片] 首先是开通订阅消息功能,很简单,如下图 [图片] [图片] 由于长期性订阅消息,目前仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。仅就线下公共服务这一点,长期性订阅消息就和大部分开发者无缘了。 所以我们这里只能以使用一次性订阅消息为例。 [图片] 如上图,我们从公共模板库里选择一个一次性订阅的模板。然后编辑模板如下图 [图片] 下图就是我们添加好的模板,下图的模板id就是我们需要的。 [图片] 1-2,请求用户授权 我们做订阅消息授权时,只能是用户点击或者支付完成后才可以调起来授权弹窗,官方是这么要求的: [图片] 我们这里用到了wx.requestSubscribeMessage这个方法,来获取用户的授权。 1,编写index.wxml代码 [图片] 2,编写index.js代码,实现点击获取授权 [图片] 这一步tmplIds里的一串字符,就是我们自己添加的模板id [图片] 3,点击按钮运行效果如下 开发者工具模拟器上点击授权弹窗是这样的: [图片] 手机上的授权弹窗是这样的: [图片] 可以看到,这里显示的就是我们添加的 ‘上课提醒’的模板。 细心的同学可以看到, 真机上多了一个 ‘总是保持以上选择,不再询问’ 其实,你自己仔细多品一些。也能明白,我们正常订阅消息授权时,用户允许的话,你只能推送一次消息。也就是用户允许一次,我们就可以推送一条消息给用户,并且这个允许不存在过期。所以我们可以让用户尽量多的点击允许,这样我们就可以尽量多的给用户发送消息了。 这里用户允许后,我们就可以给用户推送消息了,接下来我们来借助云开发的云函数来实现消息推送功能。 1-3,获取用户的opneid 先来看官方爸爸是怎么说的。 [图片] 可以看出官方提供了两种方式,我们这里使用云调用。说白了就是在云函数里调用推送功能。 推送所需参数 [图片] 可以看到我这里用来openapi功能,并且需要用到用户的opneid,关于openid的获取,我之前有写过文章,也录过视频的。文章的话,大家去翻下我历史的文章,视频的话,点击这个即可:《借助云函数获取用户openid》 这里的openid的获取我就不再详细讲解了,把对应云函数的代码给大家贴出来。 [图片] 在使用云开发时,有几点需要注意的 1,需要在project.config.json里创建云函数目录如下图 [图片] 2,需要在app.js里初始化云开发环境 [图片] 至于云开发的环境id从哪里拿,我视频里也讲过很多遍了,直接去看我视频或者翻看我历史文章即可。 《零基础入门云开发视频》 1-4,用云函数实现消息推送 我们只需要创建一个云函数如下,然后填入用户的openid,要跳转的小程序页面链接,模板内容,模板id即可。通常这些数据都应该传进来,简单起见,我就把这里的模板内容写成固定的。 [图片] 注意:我在编写上面的代码时,推送内容的key必须和小程序模板里的key保持一致,否则就会报如下错误。 [图片] 然后看下调用这个云函数的地方 [图片] 如果用户没有授权,我们推送会报如下错误 [图片] 如果用户授权过,我们就可以成功推送了,推送后的打印日志如下 [图片] 还记得我们真机上的授权吗,如果用户只是点击了允许,没有选择一直允许,那我我们在推送成功一次后,如果再次推送,就需要用户重新授权。否则,还是会报这个错误的 [图片] 所以我们用户点击一次允许,我们就可以推送一次消息,比如,我点击了4次允许那么我就可以成功的推送4次 [图片] 效果图 [图片] 可以看到,我们成功的收到 上课提醒的模板消息,点击进去,就是我们具体的推送内容 [图片] 其实我这是连续收到了4条消息,因为我点击了4次允许推送,所以就可以成功的推送4次。 到这里我们就完整的实现模板消息推送功能了,下面我把主要代码贴给大家,大家也可以私信我获取完整源码。 index.wxml [代码]<button bindtap="shouquan" type='primary'>获取订阅消息授权</button> <button bindtap="getOpenid">获取用户的openid并推送消息</button> [代码] index.js [代码]//编程小石头wechat:2501902696 Page({ //获取授权的点击事件 shouquan() { wx.requestSubscribeMessage({ tmplIds: ['CFeSWarQLMPyPjwmiy6AV4eB-IZcipu48V8bFLkBzTU'], //这里填入我们生成的模板id success(res) { console.log('授权成功', res) }, fail(res) { console.log('授权失败', res) } }) }, //获取用户的openid getOpenid() { wx.cloud.callFunction({ name: "getopenid" }).then(res => { let openid = res.result.openid console.log("获取openid成功", openid) this.send(openid) }).catch(res => { console.log("获取openid失败", res) }) }, //发送模板消息到指定用户,推送之前要先获取用户的openid send(openid) { wx.cloud.callFunction({ name: "sendMsg", data: { openid: openid } }).then(res => { console.log("推送消息成功", res) }).catch(res => { console.log("推送消息失败", res) }) } }) [代码] 推送对应的云函数 [代码]//编程小石头wechat:2501902696 const cloud = require('wx-server-sdk') cloud.init() exports.main = async(event, context) => { try { const result = await cloud.openapi.subscribeMessage.send({ touser: event.openid, //要推送给那个用户 page: 'pages/index/index', //要跳转到那个小程序页面 data: {//推送的内容 thing1: { value: '小程序入门课程' }, thing6: { value: '杭州浙江大学' }, thing7: { value: '第一章第一节' } }, templateId: 'CFeSWarQLMPyPjwmiy6AV4eB-IZcipu48V8bFLkBzTU' //模板id }) console.log(result) return result } catch (err) { console.log(err) return err } } [代码] 后面我会分享更多小程序相关的知识出来,请持续关注。 注意:授权一次,只能发送一条消息。 二,定时发送消息 我们上面用户授权和发送消息都需要手动点击才可以实现发送。但是有时候我们需要定时提醒用户,比如做的闹钟小程序,要定时提醒用户,该怎么做呢,接下来我们就来实现定时发送消息的功能。 注意 当然了这里还是要先授权才可以发送消息的,同样也是授权一次可以发送一条消息,所以这里要尽量先多授权几次 2-1,什么是定时触发器 我们实现定时发送的功能就是要用到云函数里的定时触发器,官方介绍如下。 [图片] 大家有时间可以自己去仔细读下 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/triggers.html [图片] 官方已经教我们怎么写定时触发器了 2-2,定时触发器时间设置规则 建议大家仔细去读下官方文档。 [图片] 下面是官方给出的一些示例 [图片] 我这里就取用每隔5秒通过该定时触发器调用下我们的云函数,实现订阅消息的发送。 2-3,添加定时触发器 添加步骤如下图,我们需要新建一个云函数timer [图片] 我们要在timer云函数里调用我们的fasong云函数来实现发送功能 [图片] 然后在timer文件夹下新建一个config.json文件 [图片] [图片] 然后给config.json做如下配置 [图片] 注意json里不能有注释,配置好的触发器如下 [图片] 2-4,部署定时触发器 添加好以后,记得部署触发器 [图片] 2-5,定时发送效果 首先看定时触发器是不是每隔5秒执行了一次 [图片] 然后看手机是否接到了消息 [图片] 可以看出我们手机上每隔5秒也接到了消息。这里还是要记得多授权才可以多接消息。 当然了,我们不可能这样每隔5秒给客户发条消息,这样骚扰到客户,很容易被封的,所以可以停止触发器 2-6,停止触发器 [图片] 到这里我们的定时发送消息功能也实现了,当然了我们要发给指定用户,就要先去获取用户openid,并且得让用户多授权。
2022-02-28 - 微信小程序环境共享,多个小程序共享一个云开发数据库
我们在做小程序开发时,有时候需要多个小程序公用一个数据库,比如我们做一个外卖小程序,要配套一个骑手小程序,这个时候就要两个小程序公用一个云开发环境,公用一个数据库了。所以今天来教下大家如何多个小程序共享一个云开发环境和数据库。 其实官方给的文档很详细了,但是一个细节官方没有讲到,所以就会导致好多同学做多个小程序共享一个云开发环境时,遇到各种各样的问题。 比如下面这样的问题 [图片] 明明感觉自己按照官方要求,该配置的都配置了啊,但是为啥就是出错呢。所以我这里再带大家完整的配置一遍,把里面的一些注意事项也给大家好强调下。 一,准备条件 1-1,必须同一个主体 首先看官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/ [图片] 要共享云开发资源可以 ,但是必须是同一个主体。什么是同一个主体呢,就是两个小程序必须都是你自己的,或者是你公司的。 如果不是同一个主体,会报如下错误 [图片] 1-2,最新的基础库,最新版开发工具 这里记得调到最新的基础库,开发者工具也尽量用最新的 [图片] 开发者工具这里官方是有要求的 [图片] 二,开通环境共享 我这里以两个小程序共享一个数据库为例 小程序A [图片] 小程序B [图片] 大家这里记得我们是小程序A 共享数据库给小程序B 2-1,开通环境共享 开通,使用 1.03.2009140 或以上版本的开发者工具,进入云控制台,到 “设置 - 拓展能力 - 环境共享” 点击开通即开通环境共享能力 [图片] 2-2,开通后授权给别的小程序 [图片] 环境共享开通后将在顶部tab显示环境共享功能,进入 “环境共享” 的页面,点击“添加共享”,即可授权同主体下其他小程序/公众号使用当前小程序下的云开发资源 [图片] 这里填写你要共享小程序的appid,我们这里取小程序B的appid [图片] 授权,选择共享的云环境,默认选中所有环境操作权限,可根据实际使用场景自定义授权。这里建议保持默认即可 [图片] 比如我这里分享给小程序B(编程小石头) [图片] [图片] 2-3,使用共享的云开发环境 我们上面操作好以后,就可以在小程序B的云开发后台看到共享的云开发环境了。将我们的云开发环境切换下就可以查看和使用共享的资源了。 [图片] 可以看到小程序B(编程小石头)可以查看小程序A的数据库了 [图片] 三,请求共享的数据库 我们接下来就在小程序B里调用小程序A的数据库了。官方提示的是调用之前要在小程序A里创建一个如下的云函数,但是我在测试的时候发现不用创建也可以的。 [图片] 所以我们就先不创建cloudbase_auth 云函数,来看看能不能调取到数据。 3-1,初始化云开发环境 我们小程序B想使用小程序A的云开发环境,这里要注意,初始化的时候要如下面注释里写的一样,用小程序A的appid和云开发环境id [图片] 3-2,调用资源方数据 初始化以后不能想正常调用云开发数据库那样了,会报错 [图片] 所以我们这里要改变下使用方法。如下 [图片] 这时候还会报错,是因为我们忽略了官方的一个要求:“ 跨账号调用,必须等待 init 完成”,所以我们必须给init加一个await语法,如下,记得await要结合着async一起使用。 [图片] 可以看到我们成功的请求到了小程序A的数据。直接get的时候记得改下数据库权限奥。 [图片] 代码贴出来给大家,记得改成自己的配置 [代码]Page({ async onLoad() { // 声明新的 cloud 实例 var c1 = new wx.cloud.Cloud({ // 资源方 小程序A的 AppID resourceAppid: 'wx7c54942dfc87f4d8', // 资源方 小程序A的 的云开发环境ID resourceEnv: 'test-ec396a', }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 await c1.init() // wx.cloud.database().collection('xiaoshitou').get() c1.database().collection('xiaoshitou').get() .then(res => { console.log('共享环境请求数据成功', res) }) } }) [代码] 四,调用共享环境的云函数 4-1,调用资源方里的云函数 我们这里在小程序B(编程小石头)里调用小程序A里的云函数试试。 如小程序A里有一个xiaoshitou的云函数 [图片] 可以看到我们可以成功的调用小程序A里的xiaoshitou云函数 [图片] 是不是很简单。今天就给大家讲到这里了,欢迎关注,后面会分享更多小程序开发的知识给大家。
2022-02-24 - JavaScript对象、数组、日期操作方法封装
1.获取对象属性个数 objLength(obj) { var count = 0; for(var i in obj) { if(obj.hasOwnProperty(i)) { count++; } } return count; }, 2.数组排序 sortArr(arr,s){ // s:true 升序 false 降序 var s = '' || s; arr.sort(function (a, b) { if (s) {//从小到大排序 return a - b; }else{//从大到小排序 return b - a; } }); return arr;//返回已经排序的数组 }, 3.根据数组对象中的某个属性值进行排序的方法 sortBy(arr,attr,rev){ /*** arr 需要排序的数组 * attr 排序的属性 如number属性 * rev true表示升序排列,false降序排序 * */ //第二个参数没有传递 默认升序排列 if(rev == undefined){ rev = 1; }else{ rev = (rev) ? 1 : -1; } return arr.sort(function(a,b){ a = a[attr]; b = b[attr]; if(a < b){ return rev * -1; } if(a > b){ return rev * 1; } return 0; }) }, 4.数组去重 unique(arr) { return Array.from(new Set(arr)) }, 5.根据数组对象的某个属性进行去重 uniqueObj(arr1,from) { // arr1:要去重的数组 from:属性 const res = new Map(); return arr1.filter((a) => !res.has(a[from]) && res.set(a[from], 1)) }, 6.对象数组去重并且保留最后的对象 arrayUnique2(arr, name) { var hash = {}; return arr.reduce(function (acc, cru,index) { if (!hash[cru[name]]) { hash[cru[name]] = {index:index} acc.push(cru) }else{ acc.splice(hash[cru[name]]['index'],1,cru) } return acc; }, []); }, 7.删除数组中小于某个值的元素 handleArr(arr,smail){ // arr:数组 small:需要比这个值小 var newArr = arr.filter(item => item > smail); return newArr; }, 8.将一个数组等分成若干个数组,每个数组里有n条数据 bisectionArr(arr,n){ /**arr:数据 n每个数组里保留几条数据 * 用法 * app.bisectionArr(this.data.Arr,5); **/ var n=Number(n); var newarr = []; var len = arr.length / n; len = Math.ceil(len); for (var j = 1; j <= len; j++) { newarr[j - 1] = []; for (var i = (n * j - n); i < n * j; i++) { if (arr[i] != undefined) { newarr[j - 1].push(arr[i]); } } } return newarr; }, 9.根据身份证号获取出生年月日 getBirthdayFromIdCard(idCard) { var birthday = ""; if (idCard != null && idCard != "" && checkIdcard(idCard)) { if (idCard.length == 15) { birthday = "19" + idCard.substr(6, 6); } else if (idCard.length == 18) { birthday = idCard.substr(6, 8); } birthday = birthday.replace(/(.{4})(.{2})/, "$1-$2-"); } return birthday; }, 10.只能输入金额 onlyMoney(val){ var regStrs = [ ['^0(\\d+)$', '$1'], //禁止录入整数部分两位以上,但首位为0 ['[^\\d\\.]+$', ''], //禁止录入任何非数字和点 ['\\.(\\d?)\\.+', '.$1'], //禁止录入两个以上的点 ['^(\\d+\\.\\d{2}).+', '$1'] //禁止录入小数点后两位以上 ]; for(var i=0; i<regStrs.length; i++){ var reg = new RegExp(regStrs[i][0]); val = val.replace(reg, regStrs[i][1]); } return val; }, 11.根据身份证号返回所在省 getProvinceFromIdCard(idCard) { var aCity={11:"北京",12:"天津",13:"河北",14:"山西",15:"内蒙古",21:"辽宁",22:"吉林",23:"黑龙江 ",31:"上海",32:"江苏",33:"浙江",34:"安徽",35:"福建",36:"江西",37:"山东",41:"河南",42:"湖北 ",43:"湖南",44:"广东",45:"广西",46:"海南",50:"重庆",51:"四川",52:"贵州",53:"云南",54:"西藏 ",61:"陕西",62:"甘肃",63:"青海",64:"宁夏",65:"新疆",71:"台湾",81:"香港",82:"澳门",91:"国外"}; return aCity[idCard.substr(0,2)]; }, 12.获取当前日期 getNowDate(){ var date=new Date(); var year=date.getFullYear(); var month=date.getMonth()+1; var day=date.getDate(); var hh=date.getHours(); var mm=date.getMinutes(); var ss=date.getSeconds(); var arr=['日','一','二','三','四','五','六']; var week=arr[date.getDay()]; function add0(val){ return (val<10? '0'+val : val); } month=add0(month);day=add0(day);hh=add0(hh);mm=add0(mm);ss=add0(ss); return { data:year+'-'+month+'-'+day+' '+hh+':'+mm+':'+ss, year:year, month:month, day:day, hh:hh, mm:mm, ss:ss, week:week, }; }, 13.日期转时间戳 dateToChuo(riqi){ //riqi格式 2020-01-01 00:00:00 riqi=riqi.replace(/-/g,"/"); var date = new Date(riqi);//兼容ios var time = parseInt(date.getTime()/1000);//除以1000为10位时间戳 不除为13位 return time; }, 14.时间戳转日期 chuoToDate(timestamp){ if(!timestamp){ return false; } timestamp=timestamp.toString().length<13? (timestamp * 1000):timestamp; var date = new Date(timestamp);//时间戳为10位需*1000,时间戳为13位的话不需乘1000 var Y = date.getFullYear(); var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1); var D = (date.getDate()<10? '0'+date.getDate() : date.getDate()); var h = (date.getHours()<10? '0'+date.getHours() : date.getHours()); var m = (date.getMinutes()<10? '0'+date.getMinutes() : date.getMinutes()); var s = (date.getSeconds()<10? '0'+date.getSeconds() : date.getSeconds()); // + h + m + s return { data:Y+'-'+M+'-'+D+' '+h+':'+m+':'+s, year:Y, month:M, day:D, hh:h, mm:m, ss:s } }, 15.获取当前时间的后20分钟、后一小时(add是正数)、前一天(add是负数)等等相对时间 setDefaultTime(add,danwei){ // 调用方法:app.setDefaultTime(20,'天');20天以后的时间 var danwei=danwei || '秒';//默认单位是秒 if(danwei=='秒'){ add=add*1000;// add 单位是秒 }else if(danwei=='分'){ add=add*1000*60;// add 单位是分 }else if(danwei=='时'){ add=add*1000*60*60;// add 单位是时 }else if(danwei=='天'){ add=add*1000*60*60*24;// add 单位是天 } let t = new Date().getTime() + add; let d = new Date(t); let theMonth = d.getMonth() + 1; let theDate = d.getDate(); let theHours = d.getHours(); let theMinutes = d.getMinutes(); let getSeconds=d.getSeconds(); function add0(val){ return (val<10? '0'+val : val); } theMonth=add0(theMonth); theDate=add0(theDate); theHours=add0(theHours); theMinutes=add0(theMinutes); getSeconds=add0(getSeconds); let date = d.getFullYear() + '-' + theMonth + '-' + theDate; let time = theHours + ':' + theMinutes + ':' + getSeconds; let Spare = date + ' ' + time; return { datas:d.getFullYear()+'-'+theMonth+'-'+theDate, data:Spare, year:d.getFullYear(), month:theMonth, day:theDate, hh:theHours, mm:theMinutes, ss:getSeconds } }, 16.已知开始日期和结束日期 计算出中间的所有日期 getAllDate(start, end){ // start:2020-07-14 end:2020-07-20 let dateArr = [] let startArr = start.split('-') let endArr = end.split('-') let db = new Date() db.setUTCFullYear(startArr[0], startArr[1] - 1, startArr[2]) let de = new Date() de.setUTCFullYear(endArr[0], endArr[1] - 1, endArr[2]) let unixDb = db.getTime() let unixDe = de.getTime() let stamp const oneDay = 24 * 60 * 60 * 1000; for (stamp = unixDb; stamp <= unixDe;) { // parseInt(stamp) 13位的时间戳 dateArr.push(this.format(new Date(parseInt(stamp)))) stamp = stamp + oneDay } return dateArr }, format(time){ // time=new Date(13位的时间戳) let ymd = '' let mouth = (time.getMonth() + 1) >= 10 ? (time.getMonth() + 1) : ('0' + (time.getMonth() + 1)) let day = time.getDate() >= 10 ? time.getDate() : ('0' + time.getDate()) ymd += time.getFullYear() + '-' // 获取年份。 ymd += mouth + '-' // 获取月份。 ymd += day // 获取日。 return ymd // 返回日期。2020-07-14 }, 17.获取上个月的年月 getLastMonth(riqi){ riqi=riqi.replace(/-/g,"/"); var date = new Date(riqi); var year = date.getFullYear(); var month = date.getMonth(); if(month == 0){ year = year -1; month = 12; } return { year:year, month:month<10? '0'+month : month }; }, 18.获取指定日期的星期 getWeek(riqi){ riqi=riqi.replace(/-/g,"/"); var date = new Date(riqi);//兼容ios var arr=['日','一','二','三','四','五','六']; var week=arr[date.getDay()]; return week; }, 19.当前日期是今年的第几周 getYearWeek(year,month,date){ /* app.getYearWeek(2019,4,19) dateNow是当前日期 dateFirst是当年第一天 dataNumber是当前日期是今年第多少天 用dataNumber + 当前年的第一天的周差距的和在除以7就是本年第几周 */ let dateNow = new Date(year, parseInt(month) - 1, date); let dateFirst = new Date(year, 0, 1); let dataNumber = Math.round((dateNow.valueOf() - dateFirst.valueOf()) / 86400000); return Math.ceil((dataNumber + ((dateFirst.getDay() + 1) - 1)) / 7); }, 20.当前日期是当月的第几周 getMonthWeek(year,month,date){ /* app.getMonthWeek(2019,4,19) month = 6 - w = 当前周的还有几天过完(不算今天) year + month 的和在除以7 就是当天是当前月份的第几周 */ let dateNow = new Date(year, parseInt(month) - 1, date); let w = dateNow.getDay();//星期数 let d = dateNow.getDate(); return Math.ceil((d + 6 - w) / 7); }, 21.判断某年某月有多少天 getCountDays(ym) { var curDate = new Date(ym.replace(/-/g,"/")); /* 获取当前月份 */ var curMonth = curDate.getMonth(); /* 生成实际的月份: 由于curMonth会比实际月份小1, 故需加1 */ curDate.setMonth(curMonth + 1); /* 将日期设置为0 */ curDate.setDate(0); /* 返回当月的天数 */ return curDate.getDate(); }, 22.获取指定日期的第几个月 getHowMonth(date,num) { // date 格式为yyyy-mm-dd的日期,如:2014-01-25 // num 第几个月 下一个月 1 下两个月 2 上一个月-1 上两个月-2 以此类推 date=date.replace(/-/g,"/"); var dt=new Date(date); return this.chuoToDate(dt.setMonth(dt.getMonth() + Number(num))); }, 23.计算两个日期之间相差的年月日 monthDayDiff(startDate,endDate) { let flag = [1, 3, 5, 7, 8, 10, 12, 4, 6, 9, 11, 2]; let start = new Date(startDate); let end = new Date(endDate); let year = end.getFullYear() - start.getFullYear(); let month = end.getMonth() - start.getMonth(); let day = end.getDate() - start.getDate(); if (month < 0) { year--; month = end.getMonth() + (12 - start.getMonth()); } if (day < 0) { month--; let index = flag.findIndex((temp) => { return temp === start.getMonth() + 1 }); let monthLength; if (index <= 6) { monthLength = 31; } else if (index > 6 && index <= 10) { monthLength = 30; } else { monthLength = 28; } day = end.getDate() + (monthLength - start.getDate()); } return { year,month,day }; }, 24.计算两个时间之间的差 diffTime(startDate,endDate) { //用法 diffTime('2017-03-02 09:10:10','2017-03-17 04:10:12') startDate=startDate.replace(/-/g,'/');//ios兼容 endDate=endDate.replace(/-/g,'/');//ios兼容 startDate= new Date(startDate); endDate = new Date(endDate); var diff=endDate.getTime() - startDate.getTime();//时间差的毫秒数 //计算出相差天数 var days=Math.floor(diff/(24*3600*1000)); //计算出小时数 var leave1=diff%(24*3600*1000);//计算天数后剩余的毫秒数 var hours=Math.floor(leave1/(3600*1000)); //计算相差分钟数 var leave2=leave1%(3600*1000);//计算小时数后剩余的毫秒数 var minutes=Math.floor(leave2/(60*1000)); //计算相差秒数 var leave3=leave2%(60*1000);//计算分钟数后剩余的毫秒数 var seconds=Math.round(leave3/1000); var returnStr = seconds + "秒"; if(minutes>0){ returnStr = minutes + "分" + returnStr;} if(hours>0){returnStr = hours + "小时" + returnStr;} if(days>0){returnStr = days + "天" + returnStr;} return { data:returnStr, day:days,hh:hours,mm:minutes,ss:seconds }; }, 25.倒计时 countDown(jssj,success,times){ /*用法:app.countDown("2020-08-24 07:23:00",function(res){ console.log(res) },1000) jssj:设置结束时间 2020-08-10 12:12:12 times:设置倒计时的时间间隔 */ fun(); var timer=setInterval(function(){ fun(); },times); function fun(){ var lefttime = parseInt((new Date(jssj.replace(/-/g,"/")).getTime() - new Date().getTime())); if(lefttime <= 0) { success({day:"00",hour:"00",min:"00",sec:"00"}); clearInterval(timer); return; } var d = parseInt(lefttime /1000 /3600 /24); //天数 var h = parseInt(lefttime / 1000 / 3600 % 24); //小时 var m = parseInt(lefttime / 1000 / 60 % 60); //分钟 var s = parseInt(lefttime / 1000 % 60); //当前的秒 d < 10 ? d = "0" + d : d; h < 10 ? h = "0" + h : h; m < 10 ? m = "0" + m : m; s < 10 ? s = "0" + s : s; success({ day: d, hour: h, min: m, sec:s }) } }, 26.多长时间之前 timeago(stringTime){ var minute = 1000 * 60; var hour = minute * 60; var day = hour * 24; var week = day * 7; var month = day * 30; var time1 = new Date().getTime();//当前的时间戳 var time2 = Date.parse(new Date(stringTime));//指定时间的时间戳 var time = time1 - time2; var result = "刚刚"; if (time < 0) { console.log("设置的时间不能早于当前时间!"); } else if (time / month >= 1) { // result = "" + parseInt(time / month) + "月前"; result=stringTime.slice(0,10);//大于等于1个月的时候显示具体日期 } else if (time / week >= 1) { result=stringTime.slice(0,10);//大于等于1周的时候显示具体日期 // result = "" + parseInt(time / week) + "周前"; } else if (time / day >= 1) { result = "" + parseInt(time / day) + "天前"; } else if (time / hour >= 1) { result = "" + parseInt(time / hour) + "小时前"; } else if (time / minute >= 1) { result = "" + parseInt(time / minute) + "分钟前"; } else { result = "刚刚"; } return result; }, 27.获取n到m之间的所有数 getNDMnumber(n,m){ // n,m是整数 且n<m; var n=Number(n); var m=Number(m); var arr=[]; var i=n; while(i<=m){ arr.push(i); i++; } return arr; },
2022-02-23 - 手把手教你搭建消防安全答题小程序-用云开发实现查询题库功能
手把手教你搭建答题活动小程序系列文章,最前面的三章是界面设计篇,分别描写了如何搭建答题小程序前端界面。 现在已经进入功能交互篇了,此为功能交互篇的第三章,如何用云开发实现查询题库功能。 其实,说白了就是相当于,前后端分离架构中的发送异步请求。先看看官方文档怎么说,再看看我是怎么理解和怎么做的,希望大家能从中得到启发,然后找到适合自己的学习方法。 软件架构:微信原生小程序+云开发戳源码地址,获取源码,版本持续迭代中... 前期准备工作按照惯例,我们先看看官方文档怎么说。不一定需要通读文档,可以遵循“用到什么查什么”的原则去针对性的查阅文档。此处应该有表情包,鱿鱼兮“看文档”.jpg 关于云开发是什么,云开发能力有哪些,是这样说的,巴拉巴拉~ [图片] 关于数据库是什么,小程序怎么调用,是这样说的,巴拉巴拉~ [图片] [图片] 好了,有兴趣的话,其他的你也可以仔细阅读阅读。 不吹不黑,毕竟,微信小程序开发的官方文档,是我看过的官方技术文档中描写最详细的文档。 当然,也有的地方一笔带过,我希望它可以更加详细一些。 不成文的分析云开发能力,包含云数据库、云存储、云函数、云调用等等。可谓五花八门,这么多概念,眼花缭乱,晕乎所以了吧。 其实,大可不必,有的可用可不用。可以组合使用,也可以只用其一,这就“仁者见仁,智者见智”了。而这里,我们使用云数据库的小程序端SDK就行了。 如果你想免费、快速的开发出一个完整的答题小程序项目,用小程序的云开发可能是最好的选择。小程序的云开发所用到的主要是前端开发的知识,是的,你没听错没看错,划重点吧。 从此,摆脱“前端小哥哥小姐姐”、“后端小哥哥小姐姐”笼罩下的阴影,可以硬气一把了,整个项目自己一把梭,solo~ 云开发快速查询题库所谓“兵马未动,粮草先行”。若要调用数据库,则需要先有数据库。这句看似废话,其实是隐喻一系列的操作。 不禁发出灵魂三问: 你开通云开发服务了吗? 你创建数据库集合了吗? 你添加题目数据了吗? 没有?!没有?!没有?! 还有谁 1、手把手教你操作数据库1)点击微信开发者工具的云开发图标,打开云开发控制台。 [图片] 2)点击数据库图标进入到数据库管理页,点击集合名称右侧的+号图标,就可以创建一个数据集合了。 [图片] 3)这里我们只需要添加一个activityQuestion的集合即可,这个集合就是存放题库用的。 [图片] 4)添加题目数据,或者,导入题库,两种方式均可。 ①添加记录,一题一题地手动添加,一题一题地一题一题地...... ②导入题库,嗖的一声直接导入事先准备好的题库json文件。 [图片] 5)大佬喝茶~哦,不对。大佬,记得设置数据权限吖。不然它默认是“仅创建者可读写”,到时查不到数据就GG了。别跑,你还有bug没改完~ [图片] 2、题库的数据库设计[图片] 可以清晰地看见,一道题目其实就是对应一条记录。你可以粗暴地理解为,集合里面的记录,类似数组里面的对象。 你创建的每一道题,都会自动生成一个id字段,这个你可以不用管。一道题里面,所包含的字段不外乎就question、option、true、checked这几个。 字段解读: 1)question 题干 2)option 选项 3)true 正确答案 4)checked 该题是否已做 3、小程序端调用数据库在小程序端调用数据库的方式很简单,我们可以把下面的代码写到一个事件处理函数里,然后直接在页面的生命周期函数里面执行。 其实概括起来,就三步走: 1)先使用 wx.cloud.database()获取数据库的引用(相当于连接数据库); 2)再使用 db.collection()获取集合的引用; 3)再通过 Collection.get 来获取集合里的记录。 项目代码之逐行解读: // 连接云数据库 const db = wx.cloud.database(); // 获取集合的引用 const activityQuestion = db.collection('activityQuestion'); // 数据库操作符 const _ = db.command; Page({ /** * 页面的初始数据 */ data: { questionList: [], // 题目列表 index: 0 // 当前题目索引 }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { // 获取题库-函数执行 this.getQuestionList() }, // 获取题库-函数定义 getQuestionList() { // 显示 loading 提示框 wx.showLoading({ title: '拼命加载中' }); // 构建查询条件 activityQuestion.where({ // 指定查询条件,返回带新查询条件的新的集合引用 true: _.exists(true) }) .get() .then(res => { // 获取集合数据,或获取根据查询条件筛选后的集合数据。 console.log('[云数据库] [activityQuestion] 查询成功') console.log(res.data) let data = res.data || []; // 将数据从逻辑层发送到视图层,通俗的说,也就是更新数据到页面展示 this.setData({ questionList:data, index: 0 }); // 隐藏 loading 提示框 wx.hideLoading(); }) } }) 4、题库查询结果保存然后待代码编译之后,点击“开始答题”按钮跳转到答题页面,就能在控制台看到调用的 20 条数据库记录了。 稍微要说明一下的是,如果没有指定 limit,则默认最多取 20 条记录。 [图片]
2021-11-13 - 用云开发搭建的微信答题小程序v1.0
近来百无聊赖,遂抽空做了一个答题小程序的系列教程,以及分享源码,是用云开发搭建的微信答题小程序v1.0。 界面截图该答题小程序大致如下图: [图片] 结构层级主要程序由3个界面组成,分别是index,test以及result,结构层级如下图所示: [图片] index:包含开始答题界面的页面布局与样式,以及js逻辑;test:包含答题界面的页面布局与样式,以及js逻辑;results:包含答题成绩界面的页面布局与样式,以及js逻辑;app:全局配置文件,全局变量等;style:微信小程序的基本UI样式;云开发数据库:存储相关题目数据。[图片] 1、index开始界面主要功能是首页大图和信息的展示、按钮跳转以及分享。 (1)按钮跳转关键代码,就是catchtap点击事件与goToTest事件处理函数。.wxml <view catchtap="goToTest"> <button class='cu-btn bg-red round block lg'>开始答题</button> </view> .js //事件处理函数 goToTest: function() { wx.navigateTo({ url: '../test/test' }) }, (2)分享实现,在button上使用open-type="share"属性,并且在页面js里面配置onShareAppMessage。.wxml <button class="cu-btn line-red round block lg margin-top" open-type="share"> 推荐给好友 </button> .js onShareAppMessage(res) { return { title: '@你,快来参与消防安全知识答题活动吧~' } }, 2、test答题界面答题界面需要做的事情有: 与数据库连接,获取题目数据; 选中选项的前端交互;点击切换到下一题;系统自动判定答题结果,计算得分 ;提交得分到数据库进行保存;答题结束,跳转至答题成绩界面。(1)与数据库连接,获取题目数据; // 获取题库-函数定义 getQuestionList() { // 显示 loading 提示框 wx.showLoading({ title: '拼命加载中' }); // 构建查询条件 activityQuestion.where({ // 指定查询条件,返回带新查询条件的新的集合引用 true: _.exists(true) }) .get() .then(res => { // 获取集合数据,或获取根据查询条件筛选后的集合数据。 console.log('[云数据库] [activityQuestion] 查询成功') console.log(res.data) let data = res.data || []; // 将数据从逻辑层发送到视图层,通俗的说,也就是更新数据到页面展示 this.setData({ questionList:data, index: 0 }); // 隐藏 loading 提示框 wx.hideLoading(); }) }, (2)选中选项的前端交互;.wxml <radio-group class="radio-group" bindchange="radioChange"> <label class="radio my-choosebox" wx:for="{{questionList[index].option}}" wx:for-index="key" wx:for-item="value" wx:key="index"> <radio value="{{key}}" checked="{{questionList[index].checked}}" /> <text class="margin-left-xs">{{value}}</text> </label> </radio-group> .js // 选中选项事件 radioChange(e){ this.data.chooseValue[this.data.index] = e.detail.value; }, (3)点击切换到下一题;.wxml <button bindtap='nextSubmit' class="cu-btn bg-red round lg" wx:else>下一题</button> .js // 下一题/提交 按钮 nextSubmit(){ // 如果没有选择 if (this.data.chooseValue[this.data.index] == undefined || this.data.chooseValue[this.data.index].length == 0) { return wx.showToast({ title: '请选择答案!', icon: 'none', duration: 2000 }) } // 判断所选择的选项是否为正确答案 this.chooseJudge(); // 判断是不是最后一题 this.lastJudge(); }, // 判断是不是最后一题 lastJudge(){ if (this.data.index < this.data.questionList.length - 1) { // 如果不是最后一题,则切换下一题 let index = this.data.index + 1; this.setData({ index }) } else { // 如果是最后一题,则提交答卷 this.addExamRecord() } }, (4)系统自动判定答题结果,计算得分 ;// 判断所选择的选项是否为正确答案 chooseJudge(){ var trueValue = this.data.questionList[this.data.index]['true']; var chooseVal = this.data.chooseValue[this.data.index]; if (chooseVal.toString() != trueValue.toString()) { // 答错则记录错题 this.data.wrong++; this.data.wrongListSort.push(this.data.index); this.data.wrongList.push(this.data.questionList[this.data.index]._id); }else{ // 答对则累计总分 this.setData({ totalScore: this.data.totalScore + 5 }) } }, (5)提交得分到数据库进行保存;// 提交答卷 addExamRecord(){ wx.showLoading({ title: '提交答卷中' }); let examResult = { wrong: this.data.wrong, totalScore: this.data.totalScore }; activityRecord.add({ data: { ...examResult, createDate: db.serverDate() } }).then(res => { // 跳转到答题结果页,查看成绩 wx.redirectTo({ url: '../results/results?id=' + res._id }); wx.hideLoading(); }) } (6)答题结束,跳转至答题成绩界面。// 跳转到答题结果页,查看成绩 wx.redirectTo({ url: '../results/results?id=' + res._id }); 3、results答题成绩界面主要是查询答题情况和显示得分。在答题页面的时候,实现了提交得分到数据库进行保存。那么这里就可以从数据库中获取了。 .js Page({ data: { totalScore: null, wrong: 0, zql: null }, onLoad(options) { // 查看答题成绩 this.getExamResult(options.id); }, // 系统自动判分 getExamResult(id){ wx.showLoading({ title: '系统判分中' }); activityRecord .doc(id) .get() .then(res => { let examResult = res.data; let { wrong, totalScore } = examResult; this.setData({ totalScore, wrong, zql: (20-wrong)/20*100 }) wx.hideLoading(); }) }, }) 好了,用云开发搭建的微信答题小程序,v1.0版本至此完结,源码已经提交到gitee,撒花~ 下一个版本v2.0正在在在开发中......
2021-11-29 - 微信小程序tree-select下拉复选组件
[图片] 做的不好的还请指正,自己摸索好久,雀食是比较菜的 微信代码片段:https://developers.weixin.qq.com/s/vWL2CcmX7Zwv gittee仓库https://gitee.com/hwt_code/wx-tree-select.git csdn文章https://blog.csdn.net/usernotdefined/article/details/121392970
2022-01-10 - 表格table示例
目的 实现允许编辑的【课程表】。 效果图[图片][图片] wxml代码<view class="mainPage"> <view class="hearderView"> <van-icon class="arrow" bindtap="goHome" name="notes-o" size="20" /> <text class="tipText" style="font-size: 12px;width: 85%;">滑动查看完整课表,长按可以编辑保存。</text> </view> <scroll-view scroll-x="true" scroll-y="true" style="height: {{validHeight-50}}px;"> <view class="table"> <view class="tr h"> <view class="th h1"></view> <view class="th" wx:for="{{[0,1,2,3,4,5,6]}}" wx:key="weekIndex" wx:for-item="weekItem">{{m1.getWeekday(weekItem)}}</view> </view> <block wx:for="{{kebiao.detail}}" wx:key="index" wx:for-item="item"> <view class="tr"> <view class="td c1"> <text>第</text><text>{{item.no+1}}</text><text>节</text> </view> <view class="td c" wx:for="{{item.ke}}" wx:key="index2" wx:for-item="item2" bindlongpress="longpress" data-no="{{item.no}}" data-day="{{index}}"> <text>{{item2.name}}</text> <text>{{item2.time}}</text> <text>{{item2.classroom}}</text> <text>{{item2.teacher}}</text> </view> </view> </block> </view> </scroll-view> <view class="botView" style="width: 100%;height: {{botHeight}}px;"></view> </view> <van-popup show="{{ show }}" position="bottom" custom-style="height: 230px;" bind:close="onClose"> <view class="popView"> <view class="popHeader"> <van-icon class="arrow" bindtap="onClose" name="close" size="20" color="Red" /> <text class="tipText">{{m1.getWeekday(currentKe.day)}} 第{{currentKe.no+1}}节</text> <van-icon class="arrow" bindtap="confirmEdit" name="passed" size="20" color="#1989fa" /> </view> <view class="kebiaoLine"> <text class="label"><text class="kebiaoNo" space="ensp">{{item.no}} </text>课程名</text> <input bindinput="inputText" id="name" value="{{currentKe.ke.name}}"></input> </view> <view class="kebiaoLine"> <text class="label">时间</text> <input bindinput="inputText" id="time" value="{{currentKe.ke.time}}"></input> </view> <view class="kebiaoLine"> <text class="label">教室</text> <input bindinput="inputText" id="classroom" value="{{currentKe.ke.classroom}}"></input> </view> <view class="kebiaoLine"> <text class="label">老师</text> <input bindinput="inputText" id="teacher" value="{{currentKe.ke.teacher}}"></input> </view> </view> </van-popup> <wxs module="m1"> function getWeekday(index) { switch (index) { case 0: return "周一" case 1: return "周二" case 2: return "周三" case 3: return "周四" case 4: return "周五" case 5: return "周六" case 6: return "周日" default: return "" } } module.exports.getWeekday = getWeekday </wxs> js代码data: { kebiao: { totalWeek: 16, currentWeek: 11, currentKe:{day:-1,no:-1,ke:{name:'',time:'',classroom:'',teacher:''}}, detail: [ { no:0, ke:[{name:"计算机组成与结构",time:"1-16周",classroom:"信工楼E536",teacher:"徐苏"}, {name:"",time:"",classroom:"",teacher:""}, {name:"计算机组成与结构",time:"1-16周",classroom:"信工楼E536",teacher:"徐苏"}, {name:"",time:"",classroom:"",teacher:""}, {name:"嵌入式系统",time:"1-16单周",classroom:"信工楼E536",teacher:"周聪"}, {name:"",time:"",classroom:"",teacher:""}, {name:"党课",time:"1-13单周",classroom:"党校B302",teacher:"张三"}] }, }, }, 使用Vant组件 van-popup、van-icon 体验小程序 主页-->小玩艺-->课表日历 [图片]
2021-11-25 - 如何使用微信小程序·云开发的Node.js云函数生成Word文档(2021-10-15更新)
编者按 近期一个云开发项目有生成Word文档的需求,经过搜索,发现并没有小程序·云开发有关生成word文档的案例,因为本人还是本科生且非科班出身,一路摸着石头过河,遇到了不少困难,期间还试图向社区的大佬们求助;花了两天时间才搞定这一百行代码,现在分享给大家。 代码有些糙,希望大佬们不要嫌弃。 一、安装云函数依赖officegen、fs 工欲善其事必先利其器,我们知道云函数代码运行在云端Node.js环境中,因此,理论上来说,Node.js能做的事情,小程序·云开发的云函数基本上也能做到。officegen是Github上一款生成微软Office文档的工具,包括.docx、.xlsx、.pptx三种文件,由于我只用了.docx,本文将以Word文件为例。 https://github.com/Ziv-Barber/officegen [图片] 1. 首先我们在微信开发者工具中 新建一个云函数 => 右键云函数名 => 在终端中打开 [图片] 2. npm安装依赖officegen和fs,为了方便本地调试云函数,我们这里也安装wx-server-sdk。 [图片] 代码如下,请逐个安装,如果安装有问题,可以自行搜索“npm”或“npm taobao 镜像” ;这里不再赘述。 npm i officegen npm i fs npm i wx-server-sdk 3. 在云函数index.js开头写下以下代码,引用我们刚刚安装的包。 const cloud = require('wx-server-sdk') const officegen = require('officegen'); const fs = require('fs'); const docx = officegen('docx'); 二、创建Word文档的内容 文档地址: https://github.com/Ziv-Barber/officegen/blob/master/manual/docx/README.md 1. 首先我们根据文档定义(Ctrl CV)两个函数 //文档生成完成后调用,后来其实发现没啥用 // Officegen calling this function after finishing to generate the docx document: docx.on('finalize', async function (written) { console.log('Finish to create a Microsoft Word document.') }) //生成文档出现问题时调用 // Officegen calling this function to report errors: docx.on('error', function (err) { console.log(err) }) 2. 创建段落API: docx.createP(options) //声明一个创建段落的变量p0bj let pObj = docx.createP(options) //创建一个段落并插入文本 pObj = docx.createP({ align: 'center' //文字对齐方式,center、justify、right;默认为left indentLeft = 1440; // 段落缩进 Indent left 1 inch indentFirstLine = 440; // 首行缩进 }) pObj.addText('你要插入的文字,这里可以时变量', { bold: true, //是否加粗,默认false font_face: 'KaiTi', //字体,这里以“楷体为例”,如果填写了打开文档的电脑没有安装的字体名称,将使用默认字体。能不能用中文,我没试过。 font_size: 19, //字号 color: '595959' //文字颜色 }); 上述例子外,还可以添加下划线、设置斜体、超链接、分页等;还可以编辑页眉和页脚、插入图片等。详见后续代码示例或officegen文档。 3. 插入图片 这里以插入小程序码为例,直接上代码。 要注意的是officegen似乎不支持以buffer形式插入图片,因此要先将图片保存。 //首先定义一个用于保存小程序码图片的函数 //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } //要获取小程序码,首先要修改云函数config.json文件中的云调用权限 { "permissions": { "openapi": [ "wxacode.getUnlimited" ] } } //在云函数main中获取小程序码 //https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.get.html const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', //小程序页面地址,必须是线上版本中存在的页面的完整地址 scene: '', //小程序码参数 width: 240, //小程序码的宽度(是个正方形) }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // 这里的fileData是Buffer类型,关于路径会在第三部分生成Word文件中解释。 //将图片插入到文档中 pObj = docx.createP() //创建段落 pObj.options.indentFirstLine = 440; //首行缩进 pObj.addImage('/tmp/qr.jpg', { //图片文件路径 cx: 140, //长度 cy: 140 //宽度 }); 三、生成Word文件 文档内容完成后,就可以生成文档了。officegen似乎只能生成文件,没有文件buffer的接口,而要上传到小程序·云开发的云存储中,只能使用Buffer或fs.ReadStream,怎么办呢?先把文件保存下来再读取呗。 首先提一下云函数运行环境 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/mechanism.html 云函数运行在云端 Linux 环境中,一个云函数在处理并发请求的时候会创建多个云函数实例,每个云函数实例之间相互隔离,没有公用的内存或硬盘空间。云函数实例的创建、管理、销毁等操作由平台自动完成。每个云函数实例都在 [代码]/tmp[代码] 目录下提供了一块 [代码]512MB[代码] 的临时磁盘空间用于处理单次云函数执行过程中的临时文件读写需求,需特别注意的是,这块临时磁盘空间在函数执行完毕后可能被销毁,不应依赖和假设在磁盘空间存储的临时文件会一直存在。如果需要持久化的存储,请使用云存储功能。因此,我们将文件保存在/tmp路径下,文件名随便起,这里我取为exampl.docx。生成文档的代码如下: // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/example.docx') // Async call to generate the output file: docx.generate(out) 理论上来说,我们文档生成完毕后,通过fs.ReadFileStream读取文件调用cloud.uploadFile()即可上传到云存储 const fileStream = fs.createReadStream('/tmp/example.docx') return await cloud.uploadFile({ cloudPath: '/tmp/example.docx', fileContent: fileStream, }) 而在测试过程中我发现,云端测试时,云函数调用超时。而后使用本地调试查看问题出在何处。 云函数本地调试的方法不再赘述,看这里即可。https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/local-debug.html 通过本地调试,发现cloud.uplodaFile()的网络请求始终时挂起(pending)状态,没有传输数据。 [图片] 经过一天的调试,通过监听文件,发现officegen生成文件完成,执行了我们开头复制粘贴的生成文档后执行的docx.on("finalize",)函数,打印文档生成成功的日志后,仍有文件变动,也就是说,文件并没有生成完毕。这就导致了后续步骤的失败。 当时调试的界面我没有保存,就贴一下fs监听文件的代码吧。 let watcherObj = '/tmp/example.docx' //eventType 可以是 'rename' 或 'change'; 当改名或出现或消失的时候触发rename; recursive:是否监听到内层子目录,默认false; try { let myWatcher = fs.watch(watcherObj,{encoding:'utf8',recursive:true},(event,filename) => { if(event == 'change'){ console.log("触发change事件") } console.log(event) //encoding:文件名编码格式,buffer、默认:utf8等;filename有可能为空 if(filename){ console.log('filename: ' + filename) } }) //change 事件会触发多次 myWatcher.on('change',function(err,filename){ console.log(filename + '发生变化'); }); //50秒后 关闭监视 setTimeout(function(){ myWatcher.close() },5000); } catch (error) { console.log('文件不存在!!') } 为解决这一问题,我最先想到了await,结果发现await对officegen生成文档的接口并不起作用;最终我用了最原始的笨办法:用setTimeout等一会儿再读取文件,大佬们有更好的解决方案还请赐教。 return new Promise((resolve, reject) => { setTimeout(async function () { let data = fs.readFileSync('/tmp/example.docx'); let bufferData = new Buffer.from(data, 'base64'); console.log(bufferData); setTimeout(async function () { resolve(await cloud.uploadFile({ cloudPath: varpath, fileContent: bufferData, })); }, 1000); //等文件再读1秒 }, 6300); //等文件再写一会儿。根据自己的需求调试后确定等待时长,要预留出一定时间确保文档完全生成完毕。 }) //最终返回内容为文件云存储中的CloudID。 四、完整核心代码 const cloud = require('wx-server-sdk') const officegen = require('officegen'); const fs = require('fs'); const docx = officegen('docx'); cloud.init({ env: '这里填入你的云环境' }) // Officegen calling this function after finishing to generate the docx document: docx.on('finalize', async function (written) { console.log('Finish to create a Microsoft Word document.') }) // Officegen calling this function to report errors: docx.on('error', function (err) { console.log(err) }) //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } // 云函数入口函数 exports.main = async (event, context) => { var time = new Date() var filePath = 'exportVoluntaryData' var fileName = "zyzm" + Date.parse(new Date()) + '.docx' var varpath = filePath + '/' + fileName //get QRcode const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', scene: item._id, width: 240, }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // Add a Footer: var footer = docx.getFooter().createP(); footer.addText('XXXX证明_' + item._id, { font_size: 10 }); footer = docx.getFooter().createP(); footer.addText(time.toString(), { font_size: 10 }); //下方开始文档每一页的循环 for (var i in item.volunteerInfo) { //标题 let pObj = docx.createP({ align: 'center' }) pObj.addText('XXX证明', { bold: true,XXX font_face: 'KaiTi', font_size: 19, color: '595959' }); //此处省略了一些正文内容 pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('微信扫描下方小程序码,可核验此证明。', { font_face: 'FangSong', font_size: 12, color: '595959', italic: true, }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addImage('/tmp/qr.jpg', { cx: 140, cy: 140 }); pObj = docx.createP() pObj = docx.createP({ align: 'right' }) pObj.addText('落款', { font_face: 'FangSong', font_size: 15, color: '595959' }); if (i != ((item.volunteerInfo).length - 1)){ docx.putPageBreak() //分页 } } // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/example.docx') // Async call to generate the output file: docx.generate(out) return new Promise((resolve, reject) => { setTimeout(async function () { let data = fs.readFileSync('/tmp/example.docx'); let bufferData = new Buffer.from(data, 'base64'); console.log(bufferData); setTimeout(async function () { resolve(await cloud.uploadFile({ cloudPath: varpath, fileContent: bufferData, })); }, 1000); }, 6300); }) } 本人非计算机相关专业本科生,且本文大部分内容为手打,难免会有差错和疏漏,还请各位指教。 希望本文对你有所帮助。 Soochow University. HaoChen. 2020年2月 ======= 2021-10-15更新 ======= 经过一段时间的使用,上述内容主要存在两点问题:(1)难以判断文件何时生成完毕;(2)连续调用生成文档时,若上一个云函数实例未被销毁,会出现文件内容重复和错乱的问题。 前一段时间进行了更新,因为工作学习忙碌,此次暂不做详解,代码如下。 入口文件index.js// 云函数入口文件 delete require.cache[require.resolve('officegen')]; const cloud = require('wx-server-sdk') var office = require('office.js'); //https://github.com/Ziv-Barber/officegen/blob/master/manual/docx/README.md cloud.init({ env: 'sudaxmt1900' }) const db = cloud.database() const _ = db.command // 云函数入口函数 exports.main = async (event, context) => { return await office.genWord(event); } office.jsconst cloud = require('wx-server-sdk') const fs = require('fs'); function delDir(path) { console.log("delete Dir") let files = []; if (fs.existsSync(path)) { files = fs.readdirSync(path); files.forEach((file, index) => { let curPath = path + "/" + file; if (fs.statSync(curPath).isDirectory()) { delDir(curPath); //递归删除文件夹 } else { fs.unlinkSync(curPath); //删除文件 } }); // fs.rmdirSync(path); // 删除文件夹自身 } } readDocx_fs = function (path) { return new Promise((resolve, reject) => { fs.readFile(path,(err,data)=>{ resolve(data); reject(err); }) }) } //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } exports.genWord = async (event) => { let officegen = require('officegen'); let fs = require('fs'); let docx = officegen('docx'); //ini delDir('/tmp') var item = event.item var filePath = 'exportVoluntaryData' var fileName = "21zyzm" + Date.parse(new Date()) + '.docx' var varpath = filePath + '/' + fileName //=========以下建构文档内容========== //get QRcode const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', scene: item.id, width: 140, }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // 这里的fileData是Buffer类型 timeBottom = time.getFullYear() + '年' + (time.getMonth() + 1) + '月' + time.getDate() + '日' for (var i in item.volunteerInfo) { let pObj = docx.createP({ align: 'center' }) pObj = docx.createP({ align: 'center' }) pObj.addText('志愿服务时间证明', { bold: true, font_face: 'KaiTi', font_size: 19, color: '595959' }); pObj = docx.createP() pObj = docx.createP({ align: 'justify' }) pObj.options.indentFirstLine = 440; if (item.volunteerInfo[i].academy && item.volunteerInfo[i].major && item.volunteerInfo[i].grade) { var txt = item.volunteerInfo[i].academy + ' ' + item.volunteerInfo[i].major + '专业 ' + item.volunteerInfo[i].grade + ' ' + item.volunteerInfo[i].name + ' 同学(学号 ' + item.volunteerInfo[i].idnum + '),于 ' + date + '参加 ' + item.title + ' 工作,志愿服务时间达到 ' + item.hours + ' 小时。' } else { var txt = item.volunteerInfo[i].name + ' 同学(学号 ' + item.volunteerInfo[i].idnum + '),于 ' + date + '参加 ' + item.title + ' 工作,志愿服务时间达到 ' + item.hours + ' 小时。' } pObj.addText(txt, { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('特此证明。', { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('证明人:' + event.tea_info.name + ' ' + event.tea_info.phone, { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('微信扫描下方小程序码,可核验此证明。核验信息与此证明一致时,此证明不加盖公章仍然有效;若不一致,则以加盖公章的证明为准。', { font_face: 'FangSong', font_size: 12, color: '595959', italic: true, }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addImage('/tmp/qr.jpg', { cx: 140, cy: 140 }); pObj = docx.createP() pObj = docx.createP() pObj = docx.createP({ align: 'right' }) pObj.addText('XXXXX', { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP({ align: 'right' }) pObj.addText(timeBottom, { font_face: 'FangSong', font_size: 15, color: '595959' }); // Add a Footer: pObj = docx.createP() pObj = docx.createP() pObj = docx.createP() pObj.addText('XXXXX证明_' + item._id, { font_face: 'FangSong', font_size: 10, color: '808080' }); pObj = docx.createP() pObj.addText(time.toString(), { font_face: 'FangSong', font_size: 10, color: '808080' }); if (i != ((item.volunteerInfo).length - 1)) { docx.putPageBreak() } } //=======================建构文档内容结束========================= // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/' + fileName) return new Promise((resolve, reject) => { docx.generate(out); out.on('close', async function(){ console.log("文件已被关闭,总共写入字节", out.bytesWritten) // console.log('写入的文件路径是'+ out.path); var fileBuf = await readDocx_fs(out.path); var upd = await cloud.uploadFile({ cloudPath: varpath, fileContent: fileBuf, }); console.log(docx) resolve({ event, upd, size: Math.floor(100*out.bytesWritten/1024)/100 + "KB" }) }); out.on('error', (err) => { console.error(err); reject({ errMsg: err }) }); }) }
2021-10-15 - 实现文件查看分享
第一步: 通过 wx.downloadFile 先下载网络url文件到本地 注意:在部分安卓手机上不指定本机本地路径(filePath)预览的时候是无后缀名的weixinfile,分享出去是无法打开的 设置filepath好处:在分享出去时文件也是正规的文件命名,看着就很舒服,不会出现临时文件一串随机数的情况 第二步: 通过 wx.openDocument 打开文件 是否显示右上角菜单,showMenu默认值是false,此时要设置true 代码示例如下: const { fileUrl, fileName } = this.data; // fileUrl : 'https://temp.com/tempfile/tempxxxxx.pdf' // fileName : 'tempxxxxx' wx.showLoading({ title: "正在下载中...", mask: true }); wx.downloadFile({ url: fileUrl, filePath: `${wx.env.USER_DATA_PATH}/${fileName}.pdf`, success: (res) => { if (res.statusCode === 200) { const { filePath, tempFilePath } = res console.log(filePath); wx.openDocument({ filePath: filePath, showMenu: true, success: (openres) => { console.log('打开文档成功') }, complete: () => { wx.hideLoading(); } }) } }, fail: (err) => { wx.hideLoading(); } })
2021-06-21 - 微信小程序开发之富文本编辑器
微信小程序开发之富文本编辑器 一年多去了,还有这么多人关注这个编辑器,那就索性把这个组件放上去,各位直接引用吧!如果您感觉很好用,很实用,也请大家给点一个赞!前言:富文本在Web开发上的地位大家可想而知,很多地方都需要用到富文本编辑器,比如开发类似新闻管理小程序、商品简介等。微信小程序在基础库2.7.0之后上线了一个editor富文本编辑器组件,这个组件是本次要讲的内容。组件相关的内容大家可以去看官方文档的内容,这里我们就不进行讲解。而我们要做的就是将官方的富文本组件进行二次开发达到一个好用而又实用的地步:https://developers.weixin.qq.com/miniprogram/dev/component/editor.html 先看效果图(以下只是一个基础的实用): [图片] 代码方案: 1.引入组件(组件的下载地址链接:https://pan.baidu.com/s/15D3ejvs30BZPwn94RgyNmw 提取码:hg66) 2、在你需要的使用的页面的JSON文件中引入该组件,引入方法如下: "usingComponents": { "hg-editor":"../../../components/hg-editor/hg-editor(根据自己的放置位置修改,其中/hg-editor/hg-editor是固定的)" } 3、在wxml文件中使用,使用案例如下,可选参数有四个 参数详解: [图片] showTabBar :是否显示工具栏(默认为true,显示,如果改为false则为不显示)placeholder:文本框提示文字,默认为“请输入相关内容”name:是编辑器的name属性,默认为空uploadImageURL:图片的上传地址,默认为空使用属性案例测试: bind:input可以获得用户输入的内容: onInputtingDesc: function (e) { let html = e.detail.html; //相关的html代码 let originText = e.detail.text; //text,不含有任何的html标签 this.setData({ ['topic.text']: html, ['topic.originText']: originText }); } 使用案例: [图片][图片][图片] 您的想法有多大,组件拓展的无限可能就有多大,欢迎各位留言,欢迎各位使用! 好用,就来收藏一下,更新不易,点个赞!
2022-04-22 - table表格组件,分享给各位
前言 移动端的页面本应该很少有table表格这样的展示、操作,但总归有这样的需求,然而平时用的vant和iview的小程序组件库都没有table组件,这里将自己编写的table组件展示一下供大家查看。 小程序实现table的问题在于,自定义td的实现,而小程序没办法像react一样使用[代码]jsx[代码],也没办法像vue一样用[代码]作用域插槽[代码]传row行的信息给slot,但是小程序还是留有一样东西可以完成自定义td的功能。 抽象节点 这个特性自小程序基础库版本 1.9.6 开始支持。 有时,自定义组件模板中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。 微信官方api地址 通过抽象节点我们可以做到使用自定义组件通过key值分发组件内容到不同的td里。 具体的源码地址可点击下方查看,如果对你有帮助请点个star~~ 源码地址 具体的实现效果可以扫描下方小程序码。 [图片] API prop 参数 说明 类型 默认值 是否必填 columns 表格的配置 Columns[] [] true dataList 数据 any[] [] true getListLoading 请求列表的loading boolean false true showTipImage 无数据时的提示文本图片 boolean false true rowKey 用于指明行的唯一标识符,在勾选中有使用 string id false scrollViewHeight 控制可滚动区域高度。 string 600rpx false tipTitle 无数据时的提示文本主标题 string 提示 false tipSubtitle 无数据时的提示文本副标题 string 暂无数据 false scrollX 是否需要X轴滚动。 boolean false false select 控制是否出现勾选。 boolean false false selectKeys 勾选的初始值 any[] [] false generic:action-td 当列表项内具有操作列,需要在[代码]columns[代码]内添加[代码]type:action[代码]的一项,操作列的内容往往需要自定义,小程序不提供react,vue的[代码]rander函数[代码],所以使用到了抽象节点,该属性指明抽象节点的组件。操作列位置可以不固定,点击事件由[代码]bindclickaction[代码]触发 component undefined false isExpand 控制是否点击展开。 boolean false false expandValueKey 展开信息的key值 string false initExpandValue 当展开信息为空时的默认提示语 string ‘暂无信息’ false expandStyle 展开信息的最外层的样式 string ‘’ false generic:expand-component 如果展开区域的内容需要自定义,[代码]expandValueKey[代码]设置为空字符串,则切换到组件模式,传一个组件进来,展开区域的点击事件由[代码]bindclickexpand[代码]触发 component undefined false dynamicValue 给自定义内容的动态值,用于改变状态 ,建议{value:放的数据} object {} false Events 事件 解释 类型 bindclicklistitem 点击列表行事件 Function(e); e.detail.value = {index:number(当前行序号),item: any(当前行的内容)} bindclickexpand 点击展开内容事件 Function(e); e.detail.value = {type:(这个按钮的含义字段,如‘close’),index:(当前的行),item:(当前行的数据)};(这是我这里定义的结构,具体可以自己定义在expand-component里)} bindclickaction 点击抽象节点事件 Function(e); e.detail.value = {type:(这个按钮的含义字段,如‘close’),index:(当前的行),item:(当前行的数据)};(这是我这里定义的结构,具体可以自己定义在action-td里)} bindcheckkey 勾选事件 返回被勾选项的rowKey数组 Function(e); e.detail.value = any[]//(数组内每一项是rowKey字段定义的数据的toString()结果) bindscrolltolower 滚动触底 Function() bindscrolltoupper 滚动触顶 Function() column 列描述数据对象,是 columns 中的一项,Column 使用相同的 API。 事件 解释 类型 必填 title 字段名中文含义 string true key 字段名 string true width 单元格宽度 string false type 判断字段是否是自定义组件 ‘action’/undefined false render td内内容由函数返回 (value: any, item: any, index: number, data?: 当前页面的this.data) => any,// 设置内容 function false
2022-11-24 - 云开发实战-如何维护用户表?(优化版)
前言 之前写过一篇《云开发-如何维护用户表?》,这种方式是最简单的,经过阅读了一些开源项目的代码,我优化了部分写法。 对比 优化前实现思路: 通过 login 云函数获取 openid 存放到本地 在授权信息的时候去添加 userInfo 根据 openid 去查询是否已经存储 没有查到就是新用户进行添加并存放 id 到本地 查看后老用户就根据 id 进行更新信息 优化后实现思路: 在 app.js 通过 queryCurrentUser 云函数查询 openid 是否在用户表 得到状态后存放在 app.js 的全局变量 authorized 属性里面 当需要用户授权的时候判断状态,没有就跳转到授权页面 进行授权调用 authorize 云函数添加用户 代码 在 app.js 通过 queryCurrentUser 云函数查询 openid 是否在用户表 得到状态后存放在 app.js 的全局变量 authorized 属性里面。 [代码]wx.cloud.callFunction({ // 云函数名称 name: 'user', // 传给云函数的参数 data: { action: 'queryCurrentUser' } }).then(res => { if (res.result.errMsg === 'user.query.ok') { this.onAuthorized(res.result.data.userInfo); this.authorized = true; } wx.hideLoading(); }) onAuthorized(userInfo) { this.authorized = true; this.globalData.userInfo = userInfo; }, [代码] queryCurrentUser 云函数 [代码]async queryCurrentUser(context, params) { const { OPENID } = context; let res = await db.collection('users').where({ openid: OPENID }).get(); if (res.data.length === 0) { return { errMsg: 'user.query.none' }; } return { errMsg: 'user.query.ok', data: { userInfo: res.data[0].userInfo } }; }, [代码] 当需要用户授权的时候判断状态,没有就跳转到授权页面 index.js [代码]toInfo(res) { if (app.authorized !== true) { wx.navigateTo({ url: '/pages/authorize/authorize' }); return; } // 省略业务代码.... } [代码] 进行授权调用 authorize 云函数添加用户 authorize.js [代码]wx.cloud.callFunction({ // 云函数名称 name: 'user', // 传给云函数的参数 data: { action: 'authorize', userInfo: userInfo } }).then(res => { if (res.result.errMsg === 'user.authorize.ok' || res.result.errMsg === 'user.authorize:authorized') { app.onAuthorized(res.result.data.userInfo); wx.showLoading({ title: '授权成功' }); setTimeout(() => { wx.hideLoading(); app.navigateBack(); }, 1000); return; } wx.nextTick(() => { wx.showToast({ title: '授权失败', icon: 'none', duration: 1000 }); }); }); [代码] authorize 云函数 [代码]const authorizedRes = { env: cloud.DYNAMIC_CURRENT_ENV, errMsg: 'user.authorize:authorized' }; async authorize(context, params) { const { OPENID } = context; let getRes = await db.collection('users').where({ openid: OPENID }).get(); if (getRes.errMsg !== 'collection.get:ok') { return errorAuthorizeRes; } if (getRes.data.length > 0) { return authorizedRes; } let addRes = await db.collection('users').add({ data: { openid: OPENID, userInfo: params.userInfo, authorizedTime: new Date(), } }); return { errMsg: 'user.authorize.ok', data: { userInfo: params.userInfo } }; } [代码] 总结 这种方式优点如下: 用云函数来验证,云函数可以直接获取 openid 通用统一的授权页面进行授权,这样就不需要在不同的地方写同样的授权代码 添加逻辑在云函数中实现,改小程序前端代码需要重新发版,云函数部署就行 代码需要不断优化才能更好。
2020-09-16 - 【笔记】在线答题小程序开发中,使用node-xlsx读取云存储的Excel文件,批量导入题库
在微信答题小程序开发过程中,Excel是存储数据(题库)比较常见的格式,也是很多非技术人士常用于录题出题自定义题库的一个方式,使用非常频繁。这里使用开发者工具新建一个云函数比如node-excel,在package.json里添加latest最新版的node-xlsx,并右键云函数目录选择在终端中打开输入命令npm install安装依赖: "dependencies": { "wx-server-sdk": "latest", "node-xlsx": "latest" } 然后再在index.js里输入以下代码,这里有几点需要注意: 使用云函数处理的Excel文件的来源是你的云存储,所以你需要事先将题库数据csv文件上传到云存储,在下面的代码里换成你的云存储csv地址;当然这个fileID也可以是你在答题小程序端上传Excel文件返回的云文件地址;云函数会先从云存储里下载csv文件,然后使用node-xlsx解析Exce文件,然后再将每行每行的写入数据库,这个Excel文件用的是前面介绍过的党建知识答题小程序题库数据,这里只是写入了部分字段;由于下面是读取数据的每一行,并将读取的数据循环写入数据库,也就是把数据库的add请求放在循环里面,一般情况下我们非常不推荐大家这么做,如果要这么做,主要要把云函数的超时时间设置为更长,比如20s~60s之间,保证云函数执行成功,不然会出现只成功了一部分的情况;const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const xlsx = require('node-xlsx'); const db = cloud.database() exports.main = async (event, context) => { const fileID = 'cloud://avn-dcvsz.241v-nvc-vcxsa-1323215654/question.csv' //你需要将该csv的地址替换成你的云存储的csv地址 const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = await res.fileContent const sheets = await xlsx.parse(buffer); //解析下载后的Excel Buffer文件,sheets是一个对象,而sheets['data']是数组,Excel有多少行数据,这个数组里就有多少个数组; const sheet = sheets[0].data //取出第一张表里的数组,注意这里的sheet为数组 const tasks = [] for (let rowIndex in sheet) { //如果你的Excel第一行为字段名的话,从第2行开始 let row = sheet[rowIndex]; const task = await db.collection('question') .add({ data: { type: row[0], question: row[1], options: row[2], correct: row[3] } }) tasks.push(task) //task是数据库add请求返回的值,包含数据添加之后的_id,以及是否添加成功 } return tasks } 使用xlsx.parse解析Excel文件得到的数据是一个数组,也就是上面所说的sheets,数组里的值都是Excel的每张表,而[代码]sheets[0].data[代码] 则是第一张表里面的数据,[代码]sheets[0].data[代码]仍然是一个数组,数组里的值是Excel表的每一行数据。 在解析返回的对象里,每个数组都是Excel的一行数据。 总结 我们除了可以在云开发控制台里导入导出csv文件外,还可以在云函数使用Nodejs的一些模块来处理Excel文档,进行业务知识或党建知识等题库数据格式的处理与批量导入,极大地提升了工作效率与质量。
2020-09-08 - 微信小程序云开发教程-一个js文件如何包含多个云函数
对cloudfunction文件夹右键,选择“新建node.js云函数”,得到index.js文件,一般来说,我们通常在该文件中只包含一个功能函数,那么我们如何包含多个函数呢? 比如我们现在新建一个云函数math,其中包含两个功能add加法运算,multiply乘法运算,代码如下: // 云函数模板 // 部署:在 cloud-functions/login 文件夹右击选择 “上传并部署” const cloud = require('wx-server-sdk') // 初始化 cloud cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) /** * 这个示例将经自动鉴权过的小程序用户 openid 返回给小程序端 * * event 参数包含小程序端调用传入的 data * */ exports.main = async (event, context) => { console.log(event) console.log(context) switch (event.action) { case 'add': { return add(event) } case 'multiply': { return multiply(event) } default: { return } } } async function add(event) { return { result: event.a + event.b } } async function multiply(event) { return { result: event.a * event.b } } 前端小程序调用的代码如下: wx.cloud.callFunction({ name: "math", data: { action: 'add', a:1, b:2 }, success: res => { console.log(res) } }) 结果如下: [图片] 核心思想就是:通过前端传一个变量action控制要调用云函数中的哪个子函数。
2020-08-19 - 小程序特效、看他就够(欢迎大家收藏、点赞)
1、文字跑马灯效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1038 2、触摸水波涟漪效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1350 3、下拉菜单效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1875 4、五星评分效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1876 5、数字累加,动态效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1694 6、星战字幕效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1689 7、动画卡片效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=2193 8、列表项左滑删除效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=2189 9、图片的滤镜效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=3949 10、黑客帝国metrix效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4670 11、CSS3动画效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4628 12、仿直播点赞气泡效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=2833 13、文字弹幕效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4713 14、仿UC宣传页面的简单动画效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4266 15、发短信验证码倒计时:http://www.wxapp-union.com/portal.php?mod=view&aid=1671 16、弹出菜单特效:http://www.wxapp-union.com/portal.php?mod=view&aid=1659 17、滚动动画:http://www.wxapp-union.com/portal.php?mod=view&aid=1538 18、实时圆形进度条:http://www.wxapp-union.com/portal.php?mod=view&aid=1456 19、遮罩层:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=3617 20、仿Table效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1038 21、操作按钮悬浮固定在底部:http://www.wxapp-union.com/portal.php?mod=view&aid=1029 22、支付倒计时效果:http://www.wxapp-union.com/portal.php?mod=view&aid=890 23、文字单行背景自适应带角标:http://www.wxapp-union.com/portal.php?mod=view&aid=636 24、侧边栏滑动特效;http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1202 25、顶部导航效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1665 26、弹出和隐藏动画:http://www.wxapp-union.com/portal.php?mod=view&aid=1449 27、切换动画:http://www.wxapp-union.com/portal.php?mod=view&aid=1113
2020-07-14 - WXS功能文件共享
,在页面布局上难免会遇到要js处理数据的情况,我提供一份我目前使用的common.wxs给大家参考 module.exports = { jsonParse: function (str) { return JSON.parse(str); }, fixedFloatNumber: function (number, n) { return parseFloat(number.toFixed(n)) // 保留两位小数,末位为0时去掉 }, isEqualStrings: function (firstStr, nextStr) { var reg = getRegExp('[ ()-]', 'g') var str1 = firstStr.replace(reg, '') var str2 = nextStr.replace(reg, '') // console.log('bbb', str1, str2, str1 === str2) return str1 === str2 }, // params:倒计时的毫秒数 timeDifference: function(dateDiff){ // 剩余时间 var dayDiff = Math.floor(dateDiff / (24 * 3600 * 1000)); var leave1=dateDiff%(24*3600*1000); var hours=Math.floor(leave1/(3600*1000)); var leave2=leave1%(3600*1000); var minutes=Math.floor(leave2/(60*1000)); if(dayDiff) { return dayDiff + '天'; }else if(hours) { return hours + '小时'; }else if(minutes) { return minutes + '分钟'; }else { return '0分钟'; } // params:2019-07-18 格式化时间 getFormatDate: function(params){ var currentYear = getDate().getFullYear(); var resultDate = ''; if(params && params.indexOf('-') !== -1) { var dateSplit = params.split('-') || []; if(dateSplit.length === 3) { var year = dateSplit[0]; if(currentYear == year) { resultDate = dateSplit[1] + '月' + dateSplit[2] + '日' }else { resultDate = dateSplit[0] + '年' + dateSplit[1] + '月' + dateSplit[2] + '日' } }else { resultDate = params; } }else { resultDate = params; } return resultDate; }, getTagsList: function(tags) { // 返回\分隔数组 var tagsList = tags.split('|') || []; return tagsList; }, getCityString: function(cities) { // 返回字符串,逗号隔开 var citysList = cities || []; return citysList.join(','); }, getCustomerDate: function(timestamp, language = 'CN') { // 传入时间戳,返回格式 x月x日 x时:x分(7月1日 09:01) var formatDate = ''; if(timestamp && timestamp.toString().length > 0) { var myDate = getDate(timestamp); var myYear = myDate.getFullYear(); var myMonth = myDate.getMonth() + 1; var myDay = myDate.getDate(); var myHours = myDate.getHours(); if(myHours.toString().length === 1){ myHours = "0" + myHours; } var myMinutes = myDate.getMinutes(); if(myMinutes.toString().length === 1){ myMinutes = "0" + myMinutes; } var currentDate = getDate(); var currentYear = currentDate.getFullYear(); if(language === 'CN' || language === 'cn') { if (myYear === currentYear) { formatDate = myMonth + '月' + myDay + '日' + ' ' + myHours + ':' + myMinutes; } else { formatDate = myYear + '年' + myMonth + '月' + myDay + '日' + ' ' + myHours + ':' + myMinutes; } } else { formatDate = myYear + '/' + myMonth + '/' + myDay + '/' + ' ' + myHours + ':' + myMinutes; } } else { formatDate = ''; } return formatDate; }, searchText: function (resourceStr, keyStr) { if(resourceStr && resourceStr.length > 0){ return resourceStr.indexOf(keyStr) !== -1; } return false } };
2020-07-10 - 3行代码实现小程序直播,带美颜优惠券抽奖功能
最近准备给自己的小程序做个直播功能,看下直播所需要的一些资质,瞬间被吓止步。后面发现小程序官方出了直播插件,这就为小程序接入直播提供的诸多便利。仅仅需要一些简单的配置,就可以轻松实现直播功能了。下面带大家来一步步给自己的小程序添加直播功能吧。 老规矩,先看效果图 [图片] 一,首先要给你的小程序开通直播插件功能 登录我们的小程序后台,可以看到如下图所示的直播 [图片] 点击一下,就可以进入小程序直播开通页面 [图片] 注意我们上图红色框里的一些要求。必须要满足这些条件,才可以开通直播功能。更详细些的如下: [图片] 这就注定目前只能是通过认证的企业小程序才可以开通直播功能了。个人小程序目前是没法开通的。我刚开始还不信,用我的个人小程序试了试。结果就如下图,后面没办法就注册了一个企业小程序。 [图片] 并且小程序的服务类目也要符合官方要求 [图片] 到这里,才算真正开通了小程序直播功能。 [图片] 二,创建直播间 点击创建直播间 [图片] 选择手机直播 [图片] 这里需要用一个实名认证的微信做主播端。 [图片] 认证后如下: [图片] 这里设置直播的一些封面等信息 [图片] 直播间创建成功后如下 [图片] [图片] 这里的直播码,扫码后就可以直接开播了,还有这里的房间号一定要记牢,后面会用到。 [图片] 这里可以往直播间里添加商品,优惠券等 [图片] 下面就是根据官方文档来代码实现直播功能了 三,直播功能的代码实现 我们创建号直播间以后,接下来就要在小程序代码里实现直播功能了。 1,首先是要创建一个小程序项目 至于如何创建小程序项目我这里就不再教大家了,如果你还不知道如何创建小程序项目,建议你去翻下我的历史文章,或者看看我录的《10小时零基础入门小程序开发》 创建好的小程序项目如下 [图片] 2,在app.json里添加直播插件 其实官方的接入文档写的很清晰了。下面把官方文档贴出来给大家:https://developers.weixin.qq.com/miniprogram/dev/framework/liveplayer/live-player-plugin.html [图片] 我们只需要把上面红色框里的代码复制到app.json里就可以了。记得把注释去掉 [图片] 一定要记得,除了把注释去掉之外,其他的都不要做改动。 3,然后编写可以跳转到直播间的代码 代码很简单,就写一个button按钮,然后添加点击事件即可。 [图片] 点击事件如下 [图片] 其实官方文档里也有讲 [图片] 直播房间的房间id我们在创建直播间成功后其实可以拿的到的。 [图片] 到这里我们的直播功能就完整的实现了。下面我们来看看都有哪些直播状态 四,直播状态的显示 未开播状态,这里我们可以订阅开播提醒,等开播的时候,会有订阅消息提醒。 [图片] 如果你订阅开播提醒了,还会有开播提醒 [图片] 直播结束状态 [图片] 主播暂时离开 [图片] 主播端网络异常中断 [图片] 主播端可以设置美颜等功能 [图片] 并且我们的小程序直播间里可以设置优惠券,抽奖,添加商品。 [图片] 直播结束后,还有回放功能 [图片] 好,到这里就给大家把小程序直播功能完整的讲解完了。由于代码量太少,实现起来比较简单,所以就不给大家录讲解视频了。
2020-06-30 - [开盖即食]小程序图表插件 ECharts 实战
[图片] H5时代用来做图表的插件有很多比如:[代码]ECharts[代码]、[代码]Bizcharts[代码]、[代码]JSCharts[代码]等,而这次的小程序本人选用了 ECharts 作为图表组件。 1、选择原因主要有3点: 官方某度在持续维护这个插件 官方推出了直接适配小程序的版本,且有demo,开盖即食,不用迁移 简单实用,覆盖面广且可通过配置控制包的大小,小程序毕竟大小有限制~ eCharts来自BAT中的B前端团队,对应的小程序版本为:echarts-for-weixin 官网地址 https://echarts.apache.org/ github地址 https://github.com/ecomfe/echarts-for-weixin 小程序demo地址 https://github.com/ecomfe/echarts-examples 2、用法 (1)官方教程 [代码]index.json[代码] 配置如下: [代码]{ "usingComponents": { "ec-canvas": "../../ec-canvas/ec-canvas" } } [代码] 这一配置的作用是,允许我们在 [代码]pages/bar/index.wxml[代码] 中使用 [代码]<ec-canvas>[代码] 组件。注意路径的相对位置要写对,如果目录结构和本例相同,就应该像上面这样配置。 [代码]index.wxml[代码] 中创建了一个 [代码]<ec-canvas>[代码] 组件: [代码]<view class="container"> <ec-canvas id="mychart-dom-bar" canvas-id="mychart-bar" ec="{{ ec }}"></ec-canvas> </view> [代码] 其中 [代码]ec[代码] 是一个我们在 [代码]index.js[代码] 中定义的对象,它使得图表能够在页面加载后被初始化并设置。 [代码]index.js[代码] 配置: [代码]function initChart(canvas, width, height, dpr) { const chart = echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr // 像素 }); canvas.setChart(chart); var option = { ... }; chart.setOption(option); return chart; } Page({ data: { ec: { onInit: initChart } } }); [代码] 这对于所有 ECharts 图表都是通用的,用户只需要修改上面 [代码]option[代码] 的内容,即可改变图表。[代码]option[代码] 的使用方法参见 ECharts 配置项文档。 官方demo里的一些用法指导: 如何延迟加载图表? 参见 [代码]pages/lazyLoad[代码] 的例子,可以在获取数据后再初始化数据。 如何在一个页面中加载多个图表? 参见 [代码]pages/multiCharts[代码] 的例子。 如何使用 Tooltip? 目前,本项目已支持 ECharts Tooltip,但是由于 ECharts 相关功能尚未发版,因此需要使用当前本项目中 [代码]ec-canvas/echarts.js[代码],这个文件包含了可以在微信中使用 Tooltip 的相关代码。目前在 ECharts 官网下载的 [代码]echarts.js[代码] 还不能直接替换使用,等 ECharts 正式发版后即可。 具体使用方法和 ECharts 相同,例子参见 [代码]pages/line/index.js[代码]。 如何保存为图片? 参见 [代码]pages/saveCanvas[代码] 的例子。 (2)本人实战操作 [图片] [代码]import * as echarts from '../ec-canvas/echarts'; const app = getApp(); let chart; function initChart(canvas, width, height, dpr) { chart = echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr // new }); canvas.setChart(chart); chart.setOption(option); return chart; } var option = { title: { text: '智酷君 echarts 切换效果测试', left: 'center' }, tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' }, legend: { orient: 'vertical', left: 10, data: ['AAA', 'BBB', 'CCC', 'DDD', 'EEE'] }, series: [ { name: '访问来源', type: 'pie', radius: ['50%', '70%'], avoidLabelOverlap: false, label: { show: false, position: 'center' }, emphasis: { label: { show: true, fontSize: '30', fontWeight: 'bold' } }, labelLine: { show: false }, data: [ {value: 335, name: 'AAA'}, {value: 310, name: 'BBB'}, {value: 234, name: 'CCC'}, {value: 135, name: 'DDD'}, {value: 1548, name: 'EEE'} ] } ] }; Page({ data: { ec: { onInit: initChart } }, onLoad: function () {}, //单曲线 line() { let option2 = { title: { text: '同一canvas更新成折线图', left: 'center' }, xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line' }] }; chart.setOption(option2) }, //切换柱状图 bar(){ let option3 = { title: { text: '直接更新数据,减少性能消耗', left: 'center' }, xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [120, 200, 150, 80, 70, 110, 130], type: 'bar', showBackground: true, backgroundStyle: { color: 'rgba(220, 220, 220, 0.8)' } }] }; chart.setOption(option3) } }) [代码] 建议大家尽量使用同一个canvas对象来切换不同的图表效果,而不是初始加载多个不同的,我们可以将 [代码]chart[代码] 对象设置为全局,然后通过 [代码]chart.setOption()[代码] 的方法来更新配置数据,可以减少性能消耗避免闪退等 (3)代码片段 代码段:https://developers.weixin.qq.com/s/OOTwnsms7Cin 建议将IDE工具升级到 1.02.18以上,避免一些BUG [图片] 3、Tips (1)包大小可以配置 在线定制地址: https://echarts.apache.org/zh/builder.html [图片] [图片] 通过选择和配置想要的功能,可以大大减少原本JS包的尺寸。 (2)Canvas 2d 版本要求 最新版的 ECharts 微信小程序支持微信 Canvas 2d,当用户的基础库版本 >= 2.9.0 且没有设置 [代码]force-use-old-canvas="true"[代码] 的情况下,使用新的 Canvas 2d(默认)。 使用新的 Canvas 2d 可以提升渲染性能,解决非同层渲染问题,强烈建议开启 如果仍需使用旧版 Canvas,使用方法如下: [代码]<ec-canvas id="xxx" canvas-id="xxx" ec="{{ ec }}" force-use-old-canvas="true"></ec-canvas> [代码] (3)数据点过多造成闪退和卡死 本人简单测试了下,iphone7p手机在[代码]1500个[代码]左右数据点的时候,出现了小程序闪退,iphoneX 测试下来大概在[代码]2500个[代码]左右,猜测可能由于微信本身给小程序的内存有限,所以建议大家控制数据点的个数 (4)单页面图表canvas加载过多卡死 建议单页面图表加载不要超过[代码]5个canvas[代码],尽可能共用一个图表Canvas对象,通过动态更新数据的方式来展示内容(还有帅气的特效),如果一定要加载多个canvas的话,建议控制数量,提供复用性~ 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2020-06-29 - 小程序util.js组件函数库
const formatTime = date => { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const hour = date.getHours() const minute = date.getMinutes() const second = date.getSeconds() return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') } const formatNumber = n => { n = n.toString() return n[1] ? n : '0' + n } /** * 时间戳转化为年 月 日 时 分 秒 * number: 传入时间戳 * format:返回格式,支持自定义,但参数必须与formateArr里保持一致 */ function dateFormat(number,format) { var number = number.toString().substr(0,10); var formateArr = ['Y', 'M', 'D', 'h', 'm', 's']; var returnArr = []; var date = new Date(number * 1000); returnArr.push(date.getFullYear()); returnArr.push(formatNumber(date.getMonth() + 1)); returnArr.push(formatNumber(date.getDate())); returnArr.push(formatNumber(date.getHours())); returnArr.push(formatNumber(date.getMinutes())); returnArr.push(formatNumber(date.getSeconds())); for (var i in returnArr) { format = format.replace(formateArr[i], returnArr[i]); } return format; } //自动判断类型并判断类型是否为空 function isNull(value) { if (value == null || value == undefined) return true if (this.isString(value)) { if (value.trim().length == 0) return true } else if (this.isArray(value)) { if (value.length == 0) return true } else if (this.isObject(value)) { for (let name in value) return false return true } return false; } //判断字符串是否空 function isString(value) { return value != null && value != undefined && value.constructor == String } //判断数组是否空 function isArray(value) { return value != null && value != undefined && value.constructor == Array } //判断对象是否空 function isObject(value) { return value != null && value != undefined && value.constructor == Object } //精确的乘法结果 function accMul(arg1, arg2) { var m = 0, s1 = arg1.toString(), s2 = arg2.toString(); try { m += s1.split(".")[1].length } catch (e) { } try { m += s2.split(".")[1].length } catch (e) { } return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m) } //统计长度和数量 function count(obj) { var objType = typeof obj; if (objType == "string") { return obj.length; } else if (objType == "object") { var objLen = 0; for (var i in obj) { objLen++; } return objLen; } return false; } //删除空数组 function clearArray(array) { for (var i = 0; i < array.length; i++) { if (array[i] == "" || typeof (array[i]) == "undefined") { array.splice(i, 1); i = i - 1; } } return array; } //get参数转换数组 function strToArray(str) { var arr = str.split('&'); var newArray = new Object(); for (let i in arr) { var kye = arr[i].split("=")[0] var value = arr[i].split("=")[1] newArray[kye] = value } return newArray; } /** * 判断是否有某个值 */ function inArray(arr, value) { if (arr.indexOf && typeof (arr.indexOf) == 'function') { var index = arr.indexOf(value); if (index >= 0) { return true; } } return false; } //去除字符串左右两端的空格 function trim(str) { var str = str.toString(); return str.replace(/(^\s*)|(\s*$)/g, ""); } //随机数 function getRandom(min) { var chars = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']; var nums = ""; for (var i = 0;i < min; i++) { var id = parseInt(Math.random() * 61); nums += chars[id]; } return nums.toUpperCase(); } //腾讯地图 function baidutotencent(lng, lat) { let x_pi = 3.14159265358979324 * 3000.0 / 180.0; let x = lng - 0.0065; let y = lat - 0.006; let z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * x_pi); let theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * x_pi); let lngs = z * Math.cos(theta); let lats = z * Math.sin(theta); return { longitude:lngs, latitude:lats }; } //腾讯地图多数据 function bdtotx(lng_lat) { var data = []; for (let index = 0; index < lng_lat.length; index++) { var result = baidutotencent(lng_lat[index].longitude,lng_lat[index].latitude); data[index] = lng_lat[index]; data[index]['iconPath'] = "/img/hot.png"; data[index]['width'] = 40; data[index]['height'] = 41; data[index]['longitude'] = result.longitude; data[index]['latitude'] = result.latitude; data[index]['callout'] = { content:"名称:"+lng_lat[index].title+"\r\n"+"地址:"+lng_lat[index].address, bgColor:"#fff",padding:"5px",borderRadius:"5px",borderWidth:"1px",borderColor:"#07c160", } } return data; } //随机数 module.exports = { formatTime: formatTime, isNull: isNull, isString: isString, isArray: isArray, isObject: isObject, count: count, accMul: accMul, clearArray: clearArray, strToArray: strToArray, inArray: inArray, trim: trim, getRandom: getRandom, dateFormat: dateFormat, baidutotencent:baidutotencent, bdtotx:bdtotx }
2020-05-27 - 云开发-云函数实现联表查询和分页
相关api 官方参考文档 [代码]Aggregate.lookup Aggregate.limit Aggregate.skip Aggregate.count [代码] 实现原理 基于 [代码]aggregate[代码] 操作集合 使用 [代码]count[代码] 获取总数量和页数 使用 [代码]limit[代码] 限制一次返回的数量 使用 [代码]skip[代码] 实现发送下一页的数据 使用 [代码]lookup[代码] 实现联表 示例 假设 [代码]orders[代码] 集合有以下数据 [代码][ {"_id":4,"book":"novel 1","price":30,"quantity":2}, {"_id":5,"book":"science 1","price":20,"quantity":1}, {"_id":6} ] [代码] 假设 [代码]books[代码] 集合有以下数据 [代码][ {"_id":"book1","author":"author 1","category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, {"_id":"book3","author":"author 3","category":"science","stock":30,"title":"science 1"}, {"_id":"book4","author":"author 3","category":"science","stock":40,"title":"science 2"}, {"_id":"book2","author":"author 2","category":"novel","stock":20,"title":"novel 2"}, {"_id":"book5","author":"author 4","category":"science","stock":50,"title":null}, {"_id":"book6","author":"author 5","category":"novel","stock":"60"} ] [代码] 实现 [代码]orders[代码] 和 [代码]books[代码] 联表查询和分页的关键代码 [代码]const pageSize = 10 // 每页数据量,可以作为云函数的入参传入 const currPage = 1 // 查询的当前页数,可以作为云函数的入参传入 const db = cloud.database() const $ = db.command.aggregate // 定义联表实例 const aggregateInstance = db.collection('orders').aggregate() .lookup({ from: 'books', localField: 'book', foreignField: 'title', as: 'bookList', }) const { totalCount } = await aggregateInstance.count('totalCount').end() // 计算总页数 const totalPage = totalCount === 0 ? 0 : totalCount <= pageSize ? 1 : parseInt(totalCount / pageSize) + 1 // 分页查询数据 const data = await aggregateInstance.replaceRoot({ newRoot: $.mergeObjects([ $.arrayElemAt(['$bookList', 0]), '$$ROOT' ]) }) .project({ bookList: 0 }) .limit(pageSize) .skip(currPage * pageSize) .end() return {currPage, pageSize, totalPage, totalCount, data} [代码] 预期输出结果 [代码]{ currPage: 1, pageSize: 10, totalPage: 1, totalCount: 3, data: [ { "_id": 4, "title": "novel 1", "author": "author 1", "category": "novel", "stock": 10, "book": "novel 1", "price": 30, "quantity": 2 }, { "_id": 5, "category": "science", "title": "science 1", "author": "author 3", "stock": 30, "book": "science 1", "price": 20, "quantity": 1 }, { "_id": 6, "category": "science", "author": "author 4", "stock": 50, "title": null }] } [代码] 如有错误,欢迎拍砖 (▽)
2020-03-25 - 小程序悬浮按钮,悬浮导航球
[图片] 一个开源的悬浮按钮组件,小程序原生支持。 一直很喜欢华为的导航按钮,能够完美适合大屏手机,自由停放位置,不论是左手习惯还是右手习惯,都很方便(可能我手比较小,左右上角够不着)。 支持功能 支持自由拖动,停放 支持自定义事件(单击,双击,长按) 支持自定义导航球中间的文字/图片 开发难点 使用wxs 悬浮球的开发思路比较简单,一个view,样式[代码]position:fixed[代码],支持拖动。在web开发中,我们能够比较容易实现这样的功能。要想在小程序中实现高性能的交互动画(touch类),一定要了解如何使用页面的[代码]wxs[代码]这个残疾JS来操作对象(调试很麻烦,js极度残疾) [代码]<wxs module="tool"> function tStart(e, ins){} function tMove(e, ins){ e.instance.setStyle('transform: translate3d(...)') // e.instance指向当前操作对象 // setStyle 设置该对象的style样式 } function tEnd(e, ins){} module.exports = { tStart: tStart, tMove: tMove, tEnd: tEnd } </wxs> <view catch:touchstart="{{tool.tStart}}" catch:touchmove="{{tool.tMove}}" ... /> [代码] 这里使用catch,而不是使用bind来绑定事件,事件指向[代码]wxs[代码]的方法。考虑到悬浮导航球是作为工具在其他场景中使用,为了不会污染touch事件,或者导致页面不必要的滚动。 位移距离 手机宽高不一致,即x轴的运动距离小于y轴运动距离(单位时间),假定手机宽高比为1:2,x轴运动1px,y轴则运动了2px,我们可以设置一定的系数,使得拖动效果符合预期。 监听事件 最终的事件响应一定是在page页面(或者组件内部)实现事件监听,wxs有一套事件调用机制 [代码]function tStart(e, ins){ ins.callMethod('onTouchStart', e) // 调用当前组件/页面在逻辑层(App Service)定义的函数。funcName表示函数名称,args表示函数的参数 } [代码] wxs相关文档 GITHUB开源 DEMO及文档关注小程序 [图片]
2020-03-06 - 小程序云开发模糊查询,实现数据库多字段的模糊搜索
最近做小程序云开发时,用到了一个数据库的模糊搜索功能,并且是要求多字段的模糊搜索。 网上也有一大堆资源,但是都是单个字段的搜索。如下图 [图片] 上图只可以实现time字段的模糊搜索。但是我们如果相对数据表里的多个字段做模糊查询呢?该怎么办呢。 多字段模糊搜索 一,如我们的数据表里有以下数据,我们想同时模糊查询name和address字段 [图片] [图片] 如我们搜索“周杰”可以看到我们查询到下面两条数据。 [图片] 二,如我们搜索“编程”,可以搜索到下面数据 [图片] 可以看到我们搜索到的两条数据,一个是name字段为 编程小石头, 一个是address字段里包含“编程“ 字样。 下面把代码贴给大家 [代码] let key = "编程小石头"; console.log("查询的内容", key) const db = wx.cloud.database(); const _ = db.command db.collection('qcl').where(_.or([{ name: db.RegExp({ regexp: '.*' + key, options: 'i', }) }, { address: db.RegExp({ regexp: '.*' + key, options: 'i', }) } ])).get({ success: res => { console.log(res) }, fail: err => { console.log(err) } }) [代码] key就是我们要搜索的关键字。主要是用到了数据库查询的where,or,get方法。 代码都给大家贴出来来,如果对云开发和云数据库还不是很了解的同学可以去翻看下我以前写的文章。
2019-11-06 - 只有三行代码的神奇云函数的功能之四:获取电话号码
这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦) 云函数:login index.js: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { return { ...event, ...cloud.getWXContext() } } 神奇功能之四:获取电话号码: 还是这三行代码,获取用户的电话号码。 wxml: <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber" >{{mobile||"获得电话号码"}}</button> js: getPhoneNumber: function (e) { wx.cloud.callFunction({ name: 'login', data: {weRunData: wx.cloud.CloudID(e.detail.cloudID)} }).then(res => { this.setData({ mobile: res.result.weRunData.data.phoneNumber }) }) } 其他功能: 神奇功能之一:获取openid: https://developers.weixin.qq.com/community/develop/article/doc/00080c6e3746d8a940f9b43e55fc13 神奇功能之二:不用授权获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/000a0c6b580338e947f9db0c65b813 神奇功能之三:100%成功获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/00066a967c4e384949f93fe1151413 神奇功能之五:获取群id: 将小程序分享到某群里,可获得该群的群id, https://developers.weixin.qq.com/community/develop/article/doc/000ea802c00f70894cf9fe72556013 [图片]
2020-12-16 - 【必收】精心整理!小程序开发资源汇总(附带源码)
很多小伙伴想在春节放假期间学小程序,但是小程序学习的资源和教程可能不太好找。所以小助手精心整理了一期,全是干货!认真学,开启美妙的小程序开发之旅,做一个属于自己的微信小程序。有需要的小伙伴收藏好这期文章哦~ 本文收集整理了微信小程序开发资源,包括官方文档,云开发训练营文档,视频教程以及实战源码推荐,会不间断更新。。 欢迎添加云开发小助手CloudBase微信:Tcloudedu1 ,一起加入技术交流群~ 小程序云开发官方公众号 [图片] 目录 官方文档 云开发训练营 视频教程 小程序·云开发Demo 技术交流群 官方文档 小程序开发者工具 小程序设计指南 小程序开发教程 小程序框架 小程序组件 小程序API 小程序开发者工具 小程序云开发文档 云开发训练营 小程序开发入门 小程序与JavaScript 云开发快速入门 [图片] 视频教程 腾讯云云开发B站:https://space.bilibili.com/447496276 [图片] 小程序·云开发Demo 技术博客小程序 包括文章的发布及浏览、评论、点赞、浏览历史、分类、排行榜、分享、生成海报图等。 网盘小程序 兼具文件存储与分享功能的专属网盘小程序。 教务助手小程序 用完即走,查个成绩和课表,无需下载app或去翻看公众号内的历史内容。 功能日历小程序 既能查看日历又能备注事项,看云开发如何支持功能性日历小程序的快速开发。 客户业务需求收集小程序 用云开发快速制作客户业务需求收集小程序,教你用云开发实现小程序版“朋友圈”的发布与展示。 小程序朋友圈 把朋友圈装进小程序需要几步?借助云开发实现小程序朋友圈的发布与展示。 南苑导览 一款由学生独立开发的以地图为载体,提供中山大学南方学院具体地点的位置信息、导航、校园历史及文化介绍的小程序。 互动打卡小程序 用云开发轻松构建精美互动打卡小程序,交互式双人打卡,快乐加倍。 个性头像小程序 别再@官方啦!云开发教你轻松制作个性头像小程序,趣味挂件、个性icon。 二手书商城小程序 云开发轻松制作二手书交易商城小程序,让智慧延续,让温暖传递。 后台数据批量导出 小程序开发过程中如何将云数据库中的数据批量导出至excel。 发送邮件 初学者福音,手把手教你用小程序云开发实现邮件发送功能。 高考查分小程序 实现高考分数轻松查,小程序源码。 mini论坛 仅需两天轻松搭建mini论坛小程序。 运动圈小程序 打造运动圈小程序(以乒乓球为例),实现球友间高效互动。 心情日记小程序 我能想到最浪漫的事,可能就是“你的心事我全知晓”。 最美恋爱小程序 小程序前端用的是taro框架写的,后台用的云开发。教你用云开发为心爱的人做个小程。 校园约拍小程序 校园场景下,小程序·云开发大显身手,校园约拍小程序源码。 体重记录小程序 只想记录每日体重还得下个APP,不用那么麻烦!用云开发做个专属体重记录小程序,看看你每天瘦了多少。 口袋工具 口袋工具之历史上的今天。一个基于云开发的小程序,看看历史上的今天都发生了啥。 迷你微博 独立做个精简版微博出来让你刷刷刷吗?而且,它还兼具搜索、点赞、主页的功能 多媒体小程序 使用小程序·云开发构建多媒体小程序。 技术交流群 交流技术为主,开发学习工作中遇到问题可以在群内交流,欢迎有需要的朋友加群。 添加小助手微信(Tcloudedu1),回复“技术群”,即可加入云开发技术群。 最后 如果你有关于使用腾讯云云开发相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们~ 关注腾讯云云开发,后台回复【源码】,获取更多微信小程序云开发实战源码。 [图片] [图片] [图片] 关注「腾讯云云开发」,后台回复【 源码 】,获取更多微信小程序云开发实战源码。 持续更新中… [图片]
2020-01-16 - 开源在线答题小程序
项目概述 本文介绍的是一款能在小程序上刷题的工具类小程序,目前主要面向的用户是证券从业人员、基金从业人员,本小程序题库均来自历年真题。 小程序名字:答题优等生 [图片] 小程序技术架构 小程序端未采用第三方框架,使用微信原生开发,未引入任何UI组件库 后端接口采用PHP YII2框架 目前小程序已经实现的功能有: 选择科目在线答题,答题可以选择单题模式还是列表模式 每种考试,可以选择科目,这样保持了考试、科目二级结构 答题历史纪录查询,可以查阅当时做题情况 从目前的功能实现来看,本小程序已完成了一个在线答题小程序的全闭环功能。 未来优化的几个地方在 答题结果页UI优化 答题环节的分享优化 开发小程序过程中遇到的问题 第一个问题:radio取值问题 在单选选择题的时候,用到以下两个表单组件 radio-group https://developers.weixin.qq.com/miniprogram/dev/component/radio-group.html radio https://developers.weixin.qq.com/miniprogram/dev/component/radio.html 默认的radio组件事件 wxml文件 [代码] <radio-group class="radio-group" bindchange="radioChange"> <radio class="radio" wx:for-items="{{items}}" wx:key="name" value="{{item.name}}" checked="{{item.checked}}"> <text>{{item.value}}</text> </radio> </radio-group> [代码] js文件 [代码]Page({ data: { items: [ { name: 'USA', value: '美国' }, { name: 'CHN', value: '中国', checked: 'true' }, { name: 'BRA', value: '巴西' }, { name: 'JPN', value: '日本' }, { name: 'ENG', value: '英国' }, { name: 'FRA', value: '法国' }, ] }, radioChange: function (e) { console.log('radio发生change事件,携带value值为:', e.detail.value) } }) [代码] 没错,用的就是官方示例代码,我们看到在选择的时候,默认e.detail.value,只能取一个字符串,当时遇到的第一个问题就在这里,如果把这整个选项的信息提取出来,能简单的用{{JSON.stringfy(item)}}吗,当然不可以,因为原生小程序本身不支持。 当时在社区查到解决方案具体可以参考 [单选框radio除了可以传value可以传其他的值吗?] https://developers.weixin.qq.com/community/develop/article/doc/0006ce9771c528ed7b89a6f485bc13 方案就是引入wxs,之前看官方文档,每次到这里,因为不知道这是干什么的,以及解决什么问题,现在明白了,想了解更多关于wxs的内容,也请移步下面两篇文档 [微信小程序wxs有什么用?] https://developers.weixin.qq.com/community/develop/article/doc/0008888a01c69872b689448a051013 [小程序里面精度计算问题] https://developers.weixin.qq.com/community/develop/article/doc/0000ae30ea4da802b989540175b013 第二个问题:每次10道题目是如何选择的 在答题的时候,每次会展示10个题目,这10个题目是从当前科目题库中,随机抽取10个,在题库足够大的情况下,基本可以保证每次进来答题的10个题目跟前面的答题都是不一样的。 小程序截图 [图片] [图片] [图片] [图片] [图片] [图片] 代码地址 前端小程序代码,请移步下面 https://gitee.com/xiaofeiyang3369/myexamapp 后端接口用的PHP,代码链接如下,由于我几个小程序都用这个PHP服务,项目代码要远比该小程序的PHP代码要多一些。 https://gitee.com/xiaofeiyang3369/phpapp 如果大家细心,数据库也是可以在线登录的,如果遇到问题,可以微信我。 适用人群 该开源代码适用于小程序初学者,以及大学做在线答题小程序的毕业设计时可以参考。 扫码体验 微信小程序搜索 从业资格题库或者直接扫码 [图片]
2020-06-08 - 5行代码获取小程序用户手机号
最近有很多同学有获取小程序用户手机号的需求。其实云开发出现之前我们获取小程序用户的手机号特别繁琐。自从有了云开发,我们获取用户手机号变得非常简单。只需要5行代码即可。 老规矩,我们先来看下效果图 [图片] 再来看下核心的代码,其实只有下面这一些。 [图片] 甚至可以说核心代码只有上图红色框里的两行。是的,你没听错,只靠这2行代码,就可以轻松的获取用户小程序绑定的手机号。 下面我们就来具体讲解吧。 注意:只有企业小程序才可以获取用户手机号,个人小程序没有办法获取的。 一,首先要用到button组件的开发能力 [图片] 编写wxml文件,代码很简单 [图片] 可以看到我们的button按钮,使用了open-type。 再来看下我们对应的js方法。这样我们点击按钮时,就会弹出授权弹窗。如下图 [图片] 不管用户点击拒绝还是允许,我们都能拿到对应的回调。再用户点击了允许以后,就可以获取到以下数据。 [图片] 大家看到我们获取的数据里有一个cloudID,其实这个值很有用的。 二,开发数据检验与解密 1,首先我们看下官方提供的获取手机号的文档。 [图片] 看官方文档,可以知道,我们这里涉及到一个数据的检验与解密问题 2,开发数据检验与解密 [图片] 这里我们要使用的就是方式二,使用云函数来实现解密,然后拿到用户的手机号。 三,云函数的编写 [图片] 通过上图可以看到,我们编写的云函数很简单。这里主要用的就是cloud.getOpenData这个功能。而这个功能需要的参数就是我们上面第一步获取的cloudID [图片] 这样我们调用云函数的时候,只需要把对应的cloudID传进来即可。 [图片] 看下我们的cloudID的作用,再来看下我们通过button的open-type获取的cloudID [图片] 可以看出,我们的cloudID和encryptedData一样,是一串加密数据。我们要通过云函数获取手机号,需要的就是这串加密字段。 四,上传cloudID获取手机号。 上面第三步云函数编写好以后,我们就可以来调用了。调用之前一定要记得部署下云函数,一定要记得部署下云函数。。。。 [图片] 上图就是我们的云函数的调用。如果你对云开发和云函数还不了解,建议你去看下我之前写的云开发相关的文章,获取看下我录的《微信小程序云开发云函数入门》 这时候点击按钮,我们就可以获取到了我们所需要的手机号了 [图片] 到这里我们就可以轻松的通过云开发获取用户的手机号了,比起传统的后台开发来获取,是不是简单了很多。 今天就讲到这里了,后面我还会写更多小程序相关的技术文章出来,请持续关注。
2019-12-16 - 中国大陆地址智能解析
一直想找一个地址解析的插件,网上有,单大部分都不符合我的要求,不能模糊识别和支持各大平台的数据格式,索性就自己手动写一个出来吧!~ 中国大陆收货地址智能解析,支持京东、拼多多、淘宝等后台数据导出格式。喜欢的话就点个star!欢迎提issue!~ GitHub:https://github.com/ldwonday/zh-address-parse 预览地址:https://ldwonday.github.io/zh-address-parse/
2019-12-05 - 小程序人脸核身开发流程
1、先去腾讯云平台开通人脸核身功能,需要填写正确的小程序APPID [图片] 2、开通后、进入账号信息中->访问管理->访问秘钥,可得到调用API接口的参数: [图片] 3、进入微信小程序中申请类目:政府/明生 申请成功后、可在接口设置授权中使用。 [图片] 4、在小程序设置中关联第三方平台,不要分配开发权限 [图片] 5、下载小程序SDK ,下载位置见步骤1的图。 6、根据文档、在小程序代码中做相应的配置 https://cloud.tencent.com/document/product/1007/31071 7、需要在自己的服务器中接入腾讯接口调用API 、去获取小程序所需的 BizToken [图片] 8、访问链接:https://console.cloud.tencent.com/api/explorer?Product=faceid&Version=2018-03-01&Action=DetectAuth&SignVersion= 如下图1 获取BizToken,2、获取人脸核身后的相关信息 [图片]
2019-11-22 - 纯云开发 使用一个小程序访问另一个小程序的云资源
由于工作需要,我需要使用一个小程序与另一个小程序共同享用同一套云资源。这就需要用到'tcb-admin-node'这个sdk来帮我实现这个功能。 这个sdk有详细的教程如下:https://github.com/TencentCloudBase/tcb-admin-node 作为一个新手,刚看这个文档感觉有些懵逼,不过在群友的帮助下,还是慢慢地实现了一小步的功能,就是小程序访问另一个小程序的云函数。 废话不多说,我的使用步骤如下: 1,你要有一个已经有在使用自己开通的云资源的小程序,称为小程序A;还要有一个空的小程序,称为小程序B。 2,为小程序B开通云开发。 3,小程序B创建云函数的方法我就不多说了。按照文档来说,你是需要每建一个云函数就安装一次tcb-admin-node的,但是最新版本的wx-server-sdk貌似已经集成了tcb-admin-node,所以你可以选择安装或者不安装。 4 ,不多说,代码如下图: [图片] 其中secretId和secretKey都是必须的,均为小程序A的secretId和secretKey,获取方式文档中有链接,即从腾讯云中获取你的api密匙。如下图:env为小程序A使用的环境ID [图片] 取一对就可以了,还有必须从你的小程序A进入。 name为你小程序A使用过的云函数,data为参数,与云函数所需参数一致。 5,这就封装完成了一个云函数。别忘记上传。,这时候在前台,就像普通云函数一样调用这个云函数就可以了。我的代码如下: [图片] 访问结果如下: [图片] 这时候小程序B就成功地访问了小程序A。 当然,这只是我实践的结果,成功了,于是把方法分享给大家。你们成功不成功,就看你们自己的实践了。 由于第一次发帖,可能写的有不好的地方,希望大家多多包涵,若有不妥可以纠正一下,谢谢大家。
2019-11-15 - 小程序不同页面的异步回调,callback和promise的使用讲解
发个扫盲贴,大神请绕道。最近好多同学问我如何再请求数据成功后直接使用数据。我们通常的做法就是在请求成功后,再调用我们定义的方法,进而使用数据。如下代码 [代码] onLoad() { let that=this wx.cloud.database().collection("users").get({ success(res) { that.setData(res.data) }, fail(res) { } }) }, showData(dataList) { //.........做数据处理 }, [代码] 我们这样写其实也没什么不对,但是如果数据请求和使用是在两个不同的页面呢。 比如我们在app.js里请求位置,获取用户信息。然后在首页index.js里要使用这些数据,那么我们这么写就有问题了。下面就来教大家两种方式来很好的解决这个问题。 一,通过callback回调。 先看下代码,然后我再具体给大家讲解下原理。 app.js里定义如下方法 [图片] 然后再index.js 里这么使用 [图片] 这时候,其实就可以看到了,我们在首页index.js里调用了app.js里的请求数据的方法,并且可以在index.js里直接使用数据。 原理讲解 原理其实很简单,就是我们在app.js里的获取数据的方法里定义一个参数。而这个参数和普通参数唯一不同的地方,就是这个参数是个function方法 [图片] 我们上图的callback参数,其实就是下图 function(result){} [图片] 把function方法作为一个参数传递进去的目的,就是为了下面的回调。 [图片] 我们这个callBack参数,可以在请求数据成功或者失败的时候作为一个方法调用。这样就可以把请求到的数据,回传回去了。 讲的有点绕,不知道大家有没有被绕晕。这在java开发中,其实就相当于监听者模式。说白了就是在一个页面里监听另外一个页面的动作,如获取数据成功,当监听到数据获取成功这个动作以后,就可以直接把数据回传回来了。 如果觉得这种方法有点绕,不好使用,我们就用下面的这个第二种方式。 二,promise promise的好处就是可以不用那个层层传递,不用那么绕。 还是先看代码,后面结合代码给大家讲下原理 app.js里定义如下方法 [图片] index.js里这么调用 [图片] 用句通俗的话说,就是通过promise让我们的数据请求和使用看上去是在同一个页面完成。怎么实现的呢 1,在app.js里把数据请求封装到promise里,然后把promise返回到我们的首页index.js里 2,在首页里使用这个promise 实现数据的获取和使用。 在具体些就是下面这几步 promise基础用法 [图片] 1、new 一个Promise对象 2、请求数据的异步代码写在promise的函数中 3、promise接受两个参数,一个resolve(已成功success),一个reject(已失败fail) 4、promise有三种状态pendding(进行中,当new了promise就是pendding的状态)、fulfilled(已成功)、rejected(已失败),当成功的时候调用resolve将状态改为已成功,当失败的时候调用reject将状态改为已失败,一旦状态发生改变之后,状态就凝固了,后面就无法改变状态了,成功会将成功的数据返回,失败会将失败的信息返回。 5、在需要获取数据的地方通过promise.then()的方式获取,这里面接受两个参数,都是匿名函数,第一个是接受成功的函数,第二个是失败时候的函数 [图片] 好了,到这里我们两种不同页面的异步回调就给大家将完了。代码就完整的给大家贴出来吧,方便大家日后使用 app.js [代码]//app.js App({ //第二种,通过promise promiseGetData() { let promise = new Promise(function(success, fail) { wx.cloud.database().collection("users").get({ success(res) { success(res) }, fail(res) { fail(res) } }) }) return promise; }, //第一种,通过callback的方式来实现回调 callBackGetData(callBack) { wx.cloud.database().collection("users").get({ success(res) { callBack(res) }, fail(res) { callBack(res) } }) }, }) [代码] index.js [代码]// 异步调用,callback const app = getApp() Page({ clickBtn() { //按钮点击 //callback方式 // app.callBackGetData(function(result) { // console.log("dataList", result) // }) //promise方法 let promise = app.promiseGetData() promise.then((res) => { //获取成功的结果,res中存着获取成功时的数据 console.log("成功", res) }, (error) => { // 获取数据失败时 console.log("失败", error) }) }, }) [代码]
2019-11-14 - 小程序读取excel表格数据,并存储到云数据库
最近一直比较忙,答应大家的小程序解析excel一直没有写出来,今天终于忙里偷闲,有机会把这篇文章写出来给大家了。 老规矩先看效果图 [图片] 效果其实很简单,就是把excel里的数据解析出来,然后存到云数据库里。说起来很简单。但是真的做起来的时候,发现其中要用到的东西还是很多的。不信。。。。 那来看下流程图 流程图 [图片] 通过流程图,我看看到我们这里使用了云函数,云存储,云数据库。 流程图主要实现下面几个步骤 1,使用wx.chooseMessageFile选择要解析的excel表格 2,通过wx.cloud.uploadFile上传excel文件到云存储 3,云存储返回一个fileid 给我们 4,定义一个excel云函数 5,把第3步返回的fileid传递给excel云函数 6,在excel云函数里解析excel,并把数据添加到云数据库。 可以看到最神秘,最重要的就是我们的excel云函数。 所以我们先把前5步实现了,后面重点讲解下我们的excel云函数。 一,选择并上传excel表格文件到云存储 这里我们使用到了云开发,使用云开发必须要先注册一个小程序,并给自己的小程序开通云开发功能。这个知识点我讲过很多遍了,还不知道怎么开通并使用云开发的同学,去翻下我前面的文章,或者看下我录的讲解视频《5小时入门小程序云开发》 1,先定义我们的页面 页面很简单,就是一个按钮如下图,点击按钮时调用chooseExcel方法,选择excel [图片] 对应的wxml代码如下 [图片] 2,编写文件选择和文件上传方法 [图片] 上图的chooseExcel就是我们的excel文件选择方法。 uploadExcel就是我们的文件上传方法,上传成功以后会返回一个fildID。我们把fildID传递给我们的jiexi方法,jiexi方法如下 3 把fildID传递给云函数 [图片] 二,解下来就是定义我们的云函数了。 1,首先我们要新建云函数 [图片] 如果你还不知道如何新建云函数,可以翻看下我之前写的文章,也可以看我录的视频《5小时入门小程序云开发》 如下图所示的excel就是我们创建的云函数 [图片] 2,安装node-xlsx依赖库 [图片] 如上图所示,右键excel,然后点击在终端中打开。 打开终端后, 输入 npm install node-xlsx 安装依赖。可以看到下图安装中的进度条 [图片] 这一步需要你电脑上安装过node.js并配置npm命令。 3,安装node-xlsx依赖库完成 [图片] 三,编写云函数 我把完整的代码贴出来给大家 [代码]const cloud = require('wx-server-sdk') cloud.init() var xlsx = require('node-xlsx'); const db = cloud.database() exports.main = async(event, context) => { let { fileID } = event //1,通过fileID下载云存储里的excel文件 const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent const tasks = [] //用来存储所有的添加数据操作 //2,解析excel文件里的数据 var sheets = xlsx.parse(buffer); //获取到所有sheets sheets.forEach(function(sheet) { console.log(sheet['name']); for (var rowId in sheet['data']) { console.log(rowId); var row = sheet['data'][rowId]; //第几行数据 if (rowId > 0 && row) { //第一行是表格标题,所有我们要从第2行开始读 //3,把解析到的数据存到excelList数据表里 const promise = db.collection('users') .add({ data: { name: row[0], //姓名 age: row[1], //年龄 address: row[2], //地址 wechat: row[3] //wechat } }) tasks.push(promise) } } }); // 等待所有数据添加完成 let result = await Promise.all(tasks).then(res => { return res }).catch(function(err) { return err }) return result } [代码] 上面代码里注释的很清楚了,我这里就不在啰嗦了。 有几点注意的给大家说下 1,要先创建数据表 [图片] 2,有时候如果老是解析失败,可能是有的电脑需要在云函数里也要初始化云开发环境 [图片] 四,解析并上传成功 如我的表格里有下面三条数据 [图片] 点击上传按钮,并选择我们的表格文件 [图片] 上传成功的返回如下,可以看出我们添加了3条数据到数据库 [图片] 添加成功效果图如下 [图片] 到这里我们就完整的实现了小程序上传excel数据到数据库的功能了。 再来带大家看下流程图 [图片] 如果你有遇到问题,可以在底部留言,我看到后会及时解答。后面我会写更多小程序云开发实战的文章出来。也会录制本节的视频出来,敬请关注。
2019-11-12 - 微信小程序开发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 - 云函数获取用户手机号吗?
小程序通过云函数获得用户手机号码? 思路解析, [图片] 了解了小程序的加密方式,我们就可以自己去解密我们需要的信息。如:最困住我们的用户手机号码? 官方是有案例的,想更多学习可以给与参考,但是估计要多看几遍,有node基础的就比较好理解一些。 [图片] 下面是官网地址: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html#method-cloud 下面开始说我自己的方法; 1.首先构建云函数,需要两个云函数,一个用来解密,session_key,一个用来解密加密手机号码; [代码]//云函数:getSession; // 云函数入口文件 const cloud = require('wx-server-sdk') //npm install request-promise 通过终端下载npm install wx-server-sdk , const rp = require('request-promise'); cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const _JSCODE = event.code const AccessToken_options = { method: 'GET', url: 'https://api.weixin.qq.com/sns/jscode2session', qs: { appid: '', //你的小程序appid; secret: '', //你的秘钥 grant_type: 'authorization_code', js_code: _JSCODE }, json: true }; const resultValue = await rp(AccessToken_options); return { resultValue } } [代码] 下载好需要的两个包,就可以对云函数初始化,执行npm init 有个起名字的环节,用过node的都知道,默认index.js,有个选择 [代码]package name: (gettoken) index.js version: (1.0.0) description: git repository: keywords: author: license: (ISC) About to write to D:\projects\zy_face_id_wxs\server\getToken\package.json: { "name": "index.js", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "request-promise": "^4.2.4", "wx-server-sdk": "^0.8.1" }, "devDependencies": {}, "description": "" } Is this OK? (yes) yes [代码] 这是初始化,终端的代码; 右键点击上传并部署到云端; 下面是客户端的代码; [代码] getPhoneNumber(e) { if (!e.detail.errMsg || e.detail.errMsg != "getPhoneNumber:ok") { wx.showModal({ content: '不能获取手机号码', showCancel: false }) return; } wx.showLoading({ title: '获取手机号中...', }) console.log(e) wx.login({ success(res) { if (res.code) { console.log(res.code) console.log(e.detail.iv) console.log(e.detail.encryptedData) wx.cloud.callFunction({ name: 'getSession', //调用云函数获取session_key; data: { code: res.code, }, success: res => { wx.hideLoading() // console.log(res.result.resultValue) var data = res.result.resultValue console.log(data) console.log(data.session_key) //获取到了session_key的值; }, fail: error => { console.log(error) } }) } } }) }, [代码] 2.构建第二个云函数 GetWX; [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') // const requestpromise = require('request-promise'); var WXBizDataCrypt = require('./RdWXBizDataCrypt') // 用于手机号解密 cloud.init() exports.main = async (event, context) => { const session_key = event.session_key //appid写入你自己的appid,session_key 用第一个云函数的返回值; const pc = new WXBizDataCrypt(appid, session_key ) // -解密第一步 const data = pc.decryptData(event.encryptedData, event.iv) // 解密第二步 return { data } } [代码] 这个同样执行上面的步骤,npm install wx-server-sdk 和初始化npm init; 重点来了,解密所需要的js文件。 [图片]` 这两个文件已经上传到我的网盘里面,需要的请下载; https://pan.baidu.com/s/1VrS1gX_Bw3dKaZkQnNzy2A 提取码:cxnh; 格式和图片的保持一致,并上传到云端。 3.开始前端调用了; 代码如下; [代码]getPhoneNumber(e) { if (!e.detail.errMsg || e.detail.errMsg != "getPhoneNumber:ok") { wx.showModal({ content: '不能获取手机号码', showCancel: false }) return; } wx.showLoading({ title: '获取手机号中...', }) console.log(e) wx.login({ success(res) { if (res.code) { console.log(res.code) console.log(e.detail.iv) console.log(e.detail.encryptedData) wx.cloud.callFunction({ name: 'getSession', //调用云函数获取session_key; data: { code: res.code, }, success: res => { wx.hideLoading() var data = res.result.resultValue console.log(data) console.log(data.session_key) //获取到了session_key的值; const session_key=data.session_key wx.cloud.callFunction({ name:'getWX', //解析秘文,获得手机号码; data:{ session_key: session_key, encryptedData: e.detail.encryptedData, iv: e.detail.iv, }, success:res=>{ console.log(res) }, fail:err=>{ console.log(err) } }) }, fail: error => { console.log(error) } }) } } }) }, [代码] 4.输出的结果: [图片] 遇到问题了在私信问我,一一回答。
2019-07-17 - 数组去重常用方法
function DuplicateRemovalArray_1(arr) { var resultArr; resultArr = arr.filter(function (item, index, array) { return array.indexOf(item) == index; }); return resultArr; } function DuplicateRemovalArray_2(arr) { /** * Accumulator (acc) (累计器) Current Value (cur) (当前值) Current Index (idx) (当前索引) Source Array (src) (源数组) * */ let result = arr.sort().reduce((accumulator, currentValue, currentIndex, array) => { if (accumulator.length === 0 || accumulator[accumulator.length - 1] !== currentValue) { accumulator.push(currentValue); } return accumulator; }, []); return result; } function DuplicateRemovalArray_3(arr) { let mapArray = Array.from(new Set(arr)); return mapArray; } function DuplicateRemovalArray_4(arr) { let mapArray = [...new Set(arr)] return mapArray; } let p1 = DuplicateRemovalArray_1([1,2,2,3]); let p2 = DuplicateRemovalArray_2([3,2,6,6,6,3]); let p3 = DuplicateRemovalArray_3([1,1,0,1,5]); let p4 = DuplicateRemovalArray_4([1,1,0,1,5]); console.log(p1); console.log(p2); console.log(p3); console.log(p4); let Channel = [ { "key": "其他", "Count": 1638, "Ratio": "2.939300%" }, { "key": "其他", "Count": 23057, "Ratio": "41.374900%" }, { "key": "平面", "Count": 26674, "Ratio": "47.865400%" }, { "key": "工程", "Count": 32, "Ratio": "0.057400%" }, ]; var obj = {}; let arr = Channel.reduce(function (item, next) { obj[next.key] ? '' : obj[next.key] = true && item.push(next); return item; }, []); console.log(arr); // 对象去重 输出 [ 1, 2, 3 ] [ 2, 3, 6 ] [ 1, 0, 5 ] [ 1, 0, 5 ] [ { key: '其他', Count: 1638, Ratio: '2.939300%' }, { key: '平面', Count: 26674, Ratio: '47.865400%' }, { key: '工程', Count: 32, Ratio: '0.057400%' } ] function uniqueArray(list) { list.sort() // [1,101,2,20,21,211,3,1] 注意排序 101 跟 2 const size = list.length let slowP = 0 for( let fastP = 0; fastP < size; fastP++) { if(list[fastP] != list[slowP]) { slowP++ list[slowP] = list[fastP] } } // console.log("slowP", slowP, list) return list.slice(0, slowP + 1) } let t = uniqueArray([1,101,2,20,21,211,3,1]) t=> [ 1, 101, 2, 20, 21, 211, 3 ]
2022-11-09 - 小小排序之统计,持续更新。。。
快排 [代码]var quickSort = function(arr) { if (arr.length <= 1) { return arr; } var pivotIndex = Math.floor(arr.length / 2); var pivot = arr.splice(pivotIndex, 1)[0]; var left = []; var right = []; for (var i = 0; i < arr.length; i++){ if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort(left).concat([pivot], quickSort(right)); } [代码] 冒泡 [代码]function calc(arr) { for(var j = 0; j < arr.length -1; j++) { for(var i = 0; i < arr.length-1 - j; i++) { num++ if(arr[i] > arr[i+1]) { num = arr[i]; arr[i] = arr[i+1]; arr[i+1] = num; } } } } [代码]
2019-09-03 - 如何实现一个简单的swiper效果
简单的siwper效果,又是逛社区发现老哥提的想要该效果,那么有需要,咱们就得上啊: 效果图: [图片] 上代码: wxml [代码]<view class="container"> <swiper duration="200" previous-margin="140rpx" next-margin="140rpx" bindchange="currentHandle" circular="{{true}}" class="swiper-out"> <block wx:for="{{punchList}}" wx:key="*this"> <swiper-item class="swp-item {{current === index ?'active-item': ''}}"> <view class="slide-image" style=" background: url({{item.bannerUrl}}) no-repeat center center;background-size: 100% 100%;" id="{{index}}"></view> </swiper-item> </block> </swiper> <view class="swp-dot"> <view class="square-12 m-r-8 {{current === index ?'active': ''}}" wx:for="{{punchList}}" wx:key="{{index}}"></view> </view> </view> [代码] JS [代码]const app = getApp() Page({ data: { punchList: [{ "bannerUrl": "https://qiniu-image.qtshe.com/1536067857379_122.png" }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068379879_115.png", }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068319939_230.png", }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068074140_695.png", }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068213758_796.png", }], current: 0 }, currentHandle(e) { let { current } = e.detail this.setData({ current }) } }) [代码] wxss [代码].container { display: flex; flex-direction: column; align-items: center; justify-content: space-between; height: 100vh; } .slide-image { height: 600rpx; width: 400rpx; margin-top: 20rpx; overflow: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 10rpx; } .swiper-out { width: 750rpx; height: 660rpx; margin-top: 60rpx; } .active-item .slide-image { box-shadow: 0 5rpx 20rpx 3rpx rgba(0, 0, 0, 0.15); } .swp-item { width: 400rpx; display: flex; flex-direction: column; align-items: center; padding-top: 4rpx; opacity: 0.6; } .active-item { opacity: 1; } .swp-dot { display: flex; justify-content: center; flex: 1; margin-top: 18rpx; } .m-r-8 { margin-right: 8rpx; } .m-l-8 { margin-right: 8rpx; } .square-12 { width: 12rpx; height: 12rpx; background-color: #d8d8d8; border-radius: 6rpx; transition: width 0.2s linear; } .active { background-color: #3c3c3c; width: 36rpx; transition: width 0.2s linear; } [代码] 代码放完了,看下效果吧。代码片段如下: https://developers.weixin.qq.com/s/DCK6HJmw7kaZ
2019-12-19 - 10行代码实现小程序支付功能!丨实战
前面给大家讲过一个借助小程序云开发实现微信支付的,但是那个操作稍微有点繁琐,并且还会经常出现问题,今天就给大家讲一个简单的,并且借助官方支付api实现小程序支付功能。 传送门: 借助小程序云开发实现小程序支付功能 老规矩,先看本节效果图 [图片] 我们实现这个支付功能完全是借助小程序云开发实现的,不用搭建自己的服务器,不用买域名,不用备案域名,不用支持https。只需要一个简单的云函数,就可以轻松的实现微信小程序支付功能。 核心代码就下面这些: [图片] 一、创建一个云开发小程序 关于如何创建云开发小程序,这里我就不再做具体讲解。不知道怎么创建云开发小程序的同学,可以去翻看腾讯云云开发公众号内菜单【技术交流-视频教程】中的教学视频。 创建云开发小程序有几点注意的 1.一定不要忘记在app.js里初始化云开发环境。 [图片] 2.创建完云函数后,一定要记得上传 二、创建支付的云函数 1.创建云函数pay [图片] [图片] 三、引入三方依赖tenpay 我们这里引入三方依赖的目的,是创建我们支付时需要的一些参数。我们安装依赖是使用里npm 而npm必须安装node,关于如何安装node,我这里不做讲解,百度一下,网上一大堆。 1.首先右键pay,然后选择在终端中打开 [图片] 2.我们使用npm来安装这个依赖。 在命令行里执行 npm i tenpay [图片] [图片] [图片] 安装完成后,我们的pay云函数会多出一个package.json 文件 [图片] 到这里我们的tenpay依赖就安装好了。 四、编写云函数pay [图片] 完整代码如下 [代码]//云开发实现支付 const cloud = require('wx-server-sdk') cloud.init() //1,引入支付的三方依赖 const tenpay = require('tenpay'); //2,配置支付信息 const config = { appid: '你的小程序appid', mchid: '你的微信商户号', partnerKey: '微信支付安全密钥', notify_url: '支付回调网址,这里可以先随意填一个网址', spbill_create_ip: '127.0.0.1' //这里填这个就可以 }; exports.main = async(event, context) => { const wxContext = cloud.getWXContext() let { orderid, money } = event; //3,初始化支付 const api = tenpay.init(config); let result = await api.getPayParams({ out_trade_no: orderid, body: '商品简单描述', total_fee: money, //订单金额(分), openid: wxContext.OPENID //付款用户的openid }); return result; } [代码] 一定要注意把appid,mchid,partnerKey换成你自己的。 到这里我们获取小程序支付所需参数的云函数代码就编写完成了。 不要忘记上传这个云函数。 [图片] 出现下图就代表上传成功 [图片] 五、写一个简单的页面,用来提交订单,调用pay云函数。 [图片] 这个页面很简单: 1.自己随便编写一个订单号(这个订单号要大于6位) 2.自己随便填写一个订单价(单位是分) 3.点击按钮,调用pay云函数。获取支付所需参数。 下图是官方支付api所需要的一些必须参数。 [图片] 下图是我们调用pay云函数获取的参数,和上图所需要的是不是一样。 [图片] 六、调用wx.requestPayment实现支付 下图是官方的示例代码: [图片] 这里不在做具体讲解了,把完整代码给大家贴出来 [代码]// pages/pay/pay.js Page({ //提交订单 formSubmit: function(e) { let that = this; let formData = e.detail.value console.log('form发生了submit事件,携带数据为:', formData) wx.cloud.callFunction({ name: "pay", data: { orderid: "" + formData.orderid, money: formData.money }, success(res) { console.log("提交成功", res.result) that.pay(res.result) }, fail(res) { console.log("提交失败", res) } }) }, //实现小程序支付 pay(payData) { //官方标准的支付方法 wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, //统一下单接口返回的 prepay_id 格式如:prepay_id=*** signType: 'MD5', paySign: payData.paySign, //签名 success(res) { console.log("支付成功", res) }, fail(res) { console.log("支付失败", res) }, complete(res) { console.log("支付完成", res) } }) } }) [代码] 到这里,云开发实现小程序支付的功能就完整实现了。 实现效果 1.调起支付键盘 [图片] 2.支付完成 [图片] 3.log日志,可以看出不同支付状态的回调 [图片] 上图是支付成功的回调,我们可以在支付成功回调时,改变订单支付状态。 下图是支付失败的回调: [图片] 下图是支付完成的状态: [图片] 到这里我们就轻松的实现了微信小程序的支付功能了,是不是很简单啊。 源码地址: https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心! [图片]
2019-08-15 - 截图组件(修正版,上篇删了,有bug)
刚才发的截图时发生错位,现在重新发 https://developers.weixin.qq.com/s/hzVM3BmU7Paz 以后有时间会出一整套原生的框架,开源 开源 开源,希望大神指点一二
2019-08-14 - 10行代码实现微信小程序支付功能,使用小程序云开发实现小程序支付功能(含源码)
前面给大家讲过一个借助小程序云开发实现微信支付的,但是那个操作稍微有点繁琐,并且还会经常出现问题,今天就给大家讲一个简单的,并且借助官方支付api实现小程序支付功能。 传送门 借助小程序云开发实现小程序支付功能 老规矩,先看本节效果图 [图片] 我们实现这个支付功能完全是借助小程序云开发实现的,不用搭建自己的服务器,不用买域名,不用备案域名,不用支持https。只需要一个简单的云函数,就可以轻松的实现微信小程序支付功能。 核心代码就下面这些 [图片] 一,创建一个云开发小程序 关于如何创建云开发小程序,这里我就不再做具体讲解。不知道怎么创建云开发小程序的同学,可以去翻看我之前的文章,或者看下我录制的视频:https://edu.csdn.net/course/play/9604/204528 创建云开发小程序有几点注意的 1,一定不要忘记在app.js里初始化云开发环境。 [图片] 2,创建完云函数后,一定要记得上传 二, 创建支付的云函数 1,创建云函数pay [图片] [图片] 三,引入三方依赖tenpay 我们这里引入三方依赖的目的,是创建我们支付时需要的一些参数。我们安装依赖是使用里npm 而npm必须安装node,关于如何安装node,我这里不做讲解,百度一下,网上一大堆。 1,首先右键pay,然后选择在终端中打开 [图片] 2,我们使用npm来安装这个依赖。 在命令行里执行 npm i tenpay [图片] 安装完成后,我们的pay云函数会多出一个package.json 文件 [图片] 到这里我们的tenpay依赖就安装好了。 四,编写云函数pay [图片] 完整代码如下 [代码]//云开发实现支付 const cloud = require('wx-server-sdk') cloud.init() //1,引入支付的三方依赖 const tenpay = require('tenpay'); //2,配置支付信息 const config = { appid: '你的小程序appid', mchid: '你的微信商户号', partnerKey: '微信支付安全密钥', notify_url: '支付回调网址,这里可以先随意填一个网址', spbill_create_ip: '127.0.0.1' //这里填这个就可以 }; exports.main = async(event, context) => { const wxContext = cloud.getWXContext() let { orderid, money } = event; //3,初始化支付 const api = tenpay.init(config); let result = await api.getPayParams({ out_trade_no: orderid, body: '商品简单描述', total_fee: money, //订单金额(分), openid: wxContext.OPENID //付款用户的openid }); return result; } [代码] 一定要注意把appid,mchid,partnerKey换成你自己的。 到这里我们获取小程序支付所需参数的云函数代码就编写完成了。 不要忘记上传这个云函数。 [图片] 出现下图就代表上传成功 [图片] 五,写一个简单的页面,用来提交订单,调用pay云函数。 [图片] 这个页面很简单, 1,自己随便编写一个订单号(这个订单号要大于6位) 2,自己随便填写一个订单价(单位是分) 3,点击按钮,调用pay云函数。获取支付所需参数。 下图是官方支付api所需要的一些必须参数。 [图片] 下图是我们调用pay云函数获取的参数,和上图所需要的是不是一样。 [图片] 六,调用wx.requestPayment实现支付 下图是官方的示例代码 [图片] 这里不在做具体讲解了,把完整代码给大家贴出来 [代码]// pages/pay/pay.js Page({ //提交订单 formSubmit: function(e) { let that = this; let formData = e.detail.value console.log('form发生了submit事件,携带数据为:', formData) wx.cloud.callFunction({ name: "pay", data: { orderid: "" + formData.orderid, money: formData.money }, success(res) { console.log("提交成功", res.result) that.pay(res.result) }, fail(res) { console.log("提交失败", res) } }) }, //实现小程序支付 pay(payData) { //官方标准的支付方法 wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, //统一下单接口返回的 prepay_id 格式如:prepay_id=*** signType: 'MD5', paySign: payData.paySign, //签名 success(res) { console.log("支付成功", res) }, fail(res) { console.log("支付失败", res) }, complete(res) { console.log("支付完成", res) } }) } }) [代码] 到这里,云开发实现小程序支付的功能就完整实现了。 实现效果 1,调起支付键盘 [图片] 2,支付完成 [图片] 3,log日志,可以看出不同支付状态的回调 [图片] 上图是支付成功的回调,我们可以在支付成功回调时,改变订单支付状态。 下图是支付失败的回调, [图片] 下图是支付完成的状态。 [图片] 到这里我们就轻松的实现了微信小程序的支付功能了。是不是很简单啊。 如果感觉图文不是很好理解,我后面会录制视频讲解。 视频讲解 https://edu.csdn.net/course/detail/25701 源码地址: https://github.com/qiushi123/xiaochengxu_demos [图片] 014云开发实现小程序支付,就是我们的源码,如果你导入源码或者学习过程中有任何问题,都可以加我微信2501902696(备注小程序)
2019-08-14 - 微信小程序发送邮件,小程序云开发使用云函数发送邮件
上一节给大家讲了借助小程序云开发的云函数管理mysql数据库,这一节,就来给大家讲一讲使用云开发云函数实现邮件发送的功能。 老规矩,先看效果图 [图片] 通过上面的日志,可以看出我们是158的邮箱给250的邮箱发送邮件,下面是成功接收到的邮件。 [图片] 准备工作 1,qq邮箱一个 2,开通你的qq邮箱的授权码(会具体讲解) 3,注册自己的小程序(因为只有注册的小程序才能使用云开发) 4,电脑要安装node(会用到npm命令行) 5,跟着老师编写小程序代码 一,准备一个qq邮箱,并启动SMTP服务 这个我不做具体讲解了。你进入你的qq邮箱以后, 1,点击设置,然后点击账户 [图片] 2,开启POP3/SMTP服务,获取授权码。 [图片] 具体操作可以看官方文档,官方文档有具体的讲解,这里我就不多说了。 官方文档:https://service.mail.qq.com/cgi-bin/help?subtype=1&&no=1001256&&id=28 我们获取的授权码如下图。这个授权码,我们后面发送邮件时会用到。 [图片] 二,注册小程序获取appid,创建一个小程序。 关于小程序的注册,和创建小程序我就不在做具体讲解,感兴趣的同学或者还不会的同学可以翻看我前面的文章学习,也可以看我的零基础入门小程序的视频:https://edu.csdn.net/course/detail/9531 下图是我们创建好的小程序。 [图片] 代码很简单,就只有一个页面,页面上就一个按钮,我们点击这个按钮的时候实现邮件的发送。 三,初始化云开发,创建发送邮件的云函数。 关于云开发初始化我这里也不在做具体讲解了,感兴趣或者不会的同学,可以去看我录制的云开发入门视频:https://edu.csdn.net/course/detail/9604 初始化云开发环境时,有下面几点注意事项给大家说下。 1,一定要是注册的小程序有appid才可以使用云开发 2,一定要在app.js里初始化云开发环境id [图片] 3,在project.config.json里配置云函数目录,如下图箭头所示 [图片] 四,创建云函数 sendEmail 1,右键cloud文件,新建云函数 [图片] 这个函数名你可以随便起,只要是英文,并且调用的时候记得不要写错就行。我这里就用sendEmail 2,创建完以后,右键sendEmail选择在终端里打开 [图片] 这里我们需要用npm安装一个依赖包 nodemailer 使用npm安装依赖包需要用到node,至于node的安装大家自行百度,一大堆的讲解文章。 3,在打开的命令行窗口里输入 npm install nodemailer [图片] 4,等待 nodemailer类库的安装。 [图片] 5,安装成功时,您能看到nodemailer的版本号。 [图片] 五,编写发送邮件的核心代码。 这里一定要注意填写你自己的qq邮箱的授权码 [图片] 代码里都有注释,直接把代码给大家贴出来吧。 [代码]const cloud = require('wx-server-sdk') cloud.init() //引入发送邮件的类库 var nodemailer = require('nodemailer') // 创建一个SMTP客户端配置 var config = { host: 'smtp.qq.com', //网易163邮箱 smtp.163.com port: 465, //网易邮箱端口 25 auth: { user: '1587072557@qq.com', //邮箱账号 pass: '这里要填你自己的授权码' //邮箱的授权码 } }; // 创建一个SMTP客户端对象 var transporter = nodemailer.createTransport(config); // 云函数入口函数 exports.main = async(event, context) => { // 创建一个邮件对象 var mail = { // 发件人 from: '来自小石头 <1587072557@qq.com>', // 主题 subject: '来自小石头的问候', // 收件人 to: '2501902696@qq.com', // 邮件内容,text或者html格式 text: '你好啊,编程小石头' //可以是链接,也可以是验证码 }; let res = await transporter.sendMail(mail); return res; } [代码] 六,上传云函数 编写完代码后,一定要记得上传云函数 [图片] 七,调用云函数发送邮件 我们在index.wxml文件里写一个按钮,当点击这个按钮时就发送邮件。 [图片] 然后在index.js里调用我们的sendEmail云函数。 [图片] 八,点击发送邮件,查看效果。 可以看到我们的控制台,打印里发送成功的日志信息 [图片] 然后到我们的邮箱里,可以看到新收到的邮件。 [图片] 到这里我们就完整的实现了微信小程序云开发使用云函数发送邮件的功能了。是不是很简单呢。 源码我也已经给大家准备好了。大家先试着自己敲下,看能不能实现,如果实现不了再来找我要源码。 有任何关于小程序相关的问题,也可以加我微信 2501902696(备注小程序)
2019-08-06 - 微信小程序云开发常见问题及解决方案
我们在做微信小程序云开发的过程中,总会遇到各种奇葩的问题。今天就把我在小程序云开发过程中遇到的各种问题,及对应的解决方案总结在这里,方便以后自己回顾,也方便大家查看。 一,云函数调用失败问题(404011) [云函数] [login] 调用失败 Error: errCode: -404011 cloud function execution error | errMsg: cloud.callFunction:fail requestID , cloud function service error code -504002, error message Function not found: [login]; at cloud.callFunction api; [图片] 通常出现这种问题无非是下面2个原因 1,云函数没有部署,或者没有部署成功 2,你创建了多个云开发环境,没有配置对应的环境id 下面就针对这两个问题,具体说下解决方案 1,云函数没有部署,或者没有部署成功 [图片] 选中我们要部署的云函数,右键,如上图红色框里所示。如果点一次不能上传,就多点几次,一直到出现下面提示框为止 [图片] 2,你创建了多个云开发环境,没有配置对应的环境id 如果你创建了多个云开发环境,有时候开发者工具会脑残的不知道该选择使用那个云开发环境,这个时候,我们就要指定云开发环境了。 [图片] [图片] 如果你是多个开发环境,一定要注意环境名,和环境id必须一一对应。 二,云数据库set或者update数据时报如下错误(502001) Error: errCode: -502001 database request fail | errMsg: [FailedOperation.Insert] multiple write errors: [{write errors: [{E11000 duplicate key error collection: tnt-12p3936xo.x-j-l index: id dup key: { : “xjl” }}]}, {<nil>}] 详细错误如下图: [图片] 错误原因 造成这种错误的主要原因是因为,你修改的这条数据不是你创建的。我们直接操作云数据库时,在数据库里设置里如下权限。 [图片] 这个权限只能让你读所有人的数据,但是修改的话,你还是只能修改自己创建的数据。什么样的数据才是自己创建的呢。如下图。 [图片] 所以到这里我们就大概明白如何解决这个问题了。 解决方案 1,把_openid改为自己的openid 2,借助云函数。 这里说下借助云函数,因为你是没有办法直接修改别人的数据的,但是你借助云函数的话,就可以修改任何人的数据。 三,云函数老是不能上传成功,或者上传成功后是错误的。 [图片] 如果你上传云函数老是报上面的错误,就先关闭开发者工具。然后再打开,开发者工具,进入云开发管理界面,把错误的云函数删除了。 [图片] 然后再到你的代码目录里做下同步。 [图片] 这样我们就可以重新上传我们的pay函数了。 [图片] 上传云函数时,一定要记得选择如上图箭头所指的。 上传的时候,会有下面这个提示,可以忽略不管。 [图片] 出现下图就代码你云函数上传成功了。 [图片] 四, -404011 error message wx is not defined [图片] 问题是出在云函数里,就是你在云函数里使用数据库请求的时候代码写成下面这样了。 [图片] 解决方法 我们在云函数里使用cloud.database时,不用再写wx.cloud了,直接写成cloud.database就可以了。只有在小程序的页面的js里使用时,才需要写成 wx.cloud.database****** 五,-502005 Db or Table not exist. Please check your request, but if the problem cannot be solved, contact us 错误信息如下图所示 [图片] 其实这个问题算是官方的一个bug,因为我在app.js里已经初始化过了,但是这里找不到db或者数据表的原因还是因为我没有初始化过。奇怪的是这个问题我电脑上是没问题的,但是我的一些学员电脑上就会有这个问题。 [图片] 解决方法 这个解决方法就是在你云函数里再做下初始化。 [图片] 六,-404011 请先调用init 完成初始化 其实这个问题和上面第五个是一样的,解决方式同第五个 [图片] 云开发视频讲解 https://edu.csdn.net/course/detail/9604 持续更新中。。。。。。 有关于小程序的问题可以加我微信 2501902696(备注小程序)
2019-11-14 - wxutil开源工具封装官方API接口,提高小程序开发效率
wxutil 这是一款在公司上班无聊时写的小工具,希望能帮到那些小程序圈内的朋友们,使用该工具能在很大程度上提高开发效率 wxutil工具使用promise语法封装了微信小程序官方的高频API,以及常用的开发方法。项目地址: https://github.com/YYJeffrey/wxutil/ 快速上手 方法一:在需要使用的位置引入wxutil(下方示例调用代码均以该方法引入) [代码]const wxutil = require("../../utils/wxutil.js") [代码] 方法二:通过导入自己所需模块来引入wxutil [代码]import { request } from "../../utils/wxutil.js" [代码] 工具模块 网络请求 文件请求 socket通信 图片操作 提示 showToast showModal showLoading showActionSheet 缓存 setStorage getStorage 授权 getLocation getUserInfo requestPayment 其他工具 网络请求 封装微信小程序wx.request()方法实现五大http请求方法 get 使用promise语法异步获取请求数据或捕获请求异常信息 [代码]const handler = { url: url, data: data, header: header } wxutil.request.get(handler).then((data) => { console.log(data) // 业务处理 }).catch((error) => { console.log(error) // 异常处理 }) [代码] 亦可用更快速的方法请求 [代码]wxutil.request.get(url).then((data) => { console.log(data) }) [代码] post [代码]const handler = { url: url, data: {}, header: {} } wxutil.request.post(handler).then((data) => { console.log(data) }) [代码] 亦可用直接传递参数的方式请求 [代码]wxutil.request.post({url: url, data: data}).then((data) => { console.log(data) }) [代码] put [代码]wxutil.request.put({url: url, data: data}).then((data) => { console.log(data) }) [代码] patch [代码]wxutil.request.patch({url: url, data: data}).then((data) => { console.log(data) }) [代码] delete [代码]wxutil.request.delete({url: url, data: data}).then((data) => { console.log(data) }) [代码] 文件请求 封装微信小程序wx.downloadFile()和wx.uploadFile()方法 download [代码]wxutil.file.download({url}).then((data) => { console.log(data) }) [代码] upload [代码]wxutil.file.upload({ url: url, fileKey: fileKey, filePath: filePath, data: {}, header: {} }).then((data) => { console.log(data) }) [代码] socket通信 封装微信小程序的websocket部分方法,实现整个socket流程如下 [代码]let socketOpen = false // socket连接标识 wxutil.socket.connect(url) // 监听socket通信 wx.onSocketMessage((res) => { console.log(res) } wx.onSocketOpen((res) => { socketOpen = true if (socketOpen) { // 发送socket消息 wxutil.socket.send("hello wxutil").then((data) => { console.log(data) }) } // 关闭socket连接 wxutil.socket.close(url) }) [代码] 图片操作 封装微信小程序的wx.saveImageToPhotosAlbum()、wx.previewImage()、wx.chooseImage()方法,用于保存图片到本机相册、预览图片以及从相机或相册选择图片 save [代码]wxutil.image.save(path).then((data) => { console.log(data) }) [代码] preview [代码]wxutil.image.preview(["img/1.png"]) [代码] choose 参数:count, sourceType [代码]wxutil.image.choose(1).then((data) => { console.log(data) }) [代码] 提示 封装微信小程序的wx.showToast()、wx.showModal()、wx.showLoading()和wx.showActionSheet()方法,用于给用户友好提示 showToast [代码]wxutil.showToast("hello") [代码] showModal [代码]wxutil.showModal("提示", "这是一个模态弹窗") [代码] 亦可传入参数并在回调函数中处理自己的业务 [代码]wxutil.showModal(title: title, content: content, handler = { showCancel: showCancel, cancelText: cancelText, confirmText: confirmText, cancelColor: cancelColor, confirmColor: confirmColor }).then((data) => { console.log(data) }) [代码] showLoading [代码]wxutil.showLoading("加载中") [代码] showActionSheet [代码]wxutil.showActionSheet(['A', 'B', 'C']).then((data) => { console.log(data) }) [代码] 缓存 封装微信小程序的wx.setStorageSync()和wx.getStorageSync()方法,异步设置缓存和获取缓存内容,并可以设置缓存过期时间 setStorage [代码]wxutil.setStorage("userInfo", userInfo) [代码] 亦可为缓存设置过期时间,单位:秒 [代码]wxutil.setStorage("userInfo", userInfo, 86400) [代码] getStorage [代码]wxutil.getStorage("userInfo") [代码] 授权 封装了需要用户授权微信小程序的方法 getLocation 获取用户的地理位置 [代码]wxutil.getLocation().then((data) => { console.log(data) }) [代码] 亦可通过传入可选参数打开微信小程序的地图 [代码]wxutil.getLocation("gcj02", true).then((data) => { console.log(data) }) [代码] getUserInfo 获取用户信息,可传递两个参数:login和lang,login为true可返回wx.login获取到的code,lang默认为中文,该方法需要使用button触发 [代码]wxutil.getUserInfo().then((data) => { console.log(data) }) [代码] requestPayment 封装了微信小程序的requestPayment方法,需要传递后端的timeStamp、nonceStr、packageValue、paySign这几个参数,加密方式默认为“MD5” [代码]wxutil.requestPayment({ timeStamp: timeStamp, nonceStr: nonceStr, packageValue: packageValue, paySign: paySign }).then((data) => { console.log(data) }) [代码] 其他工具 封装了常用的微信小程序方法,便于高效开发,也可以增加自己的工具方法在下方 autoUpdate 在app.js中引用该方法,可以在微信小程序发布新版本后自动更新 [代码]wxutil.autoUpdate() [代码] isNotNull 判断字符串是否为空、空格回车等,不为空返回true [代码]wxutil.isNotNull("text") [代码] getDateTime 获取当前日期时间,格式:yy-mm-dd hh:MM:ss [代码]const datetime = wxutil.getDateTime() console.log(datetime) [代码] getTimestamp 获取当前时间戳 [代码]const timestamp = wxutil.getTimestamp() console.log(timestamp) [代码]
2019-07-25 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] wxml [代码]<view class='listbox'> <view class='list kelong' hidden='{{!showkelong}}' style='top:{{kelong.top}}px'> <view class='index'>?</view> <image src='{{kelong.xt}}' class='xt'></image> <view class='info'> <view class="name">{{kelong.name}}</view> <view class='sub-name'>{{kelong.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> </view> <view class='list' wx:for="{{optionList}}" wx:key=""> <view class='index'>{{index+1}}</view> <image src='{{item.xt}}' class='xt'></image> <view class='info'> <view class="name">{{item.name}}</view> <view class='sub-name'>{{item.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> <view class='moreiconpl' data-index='{{index}}' catchtouchstart='dragStart' catchtouchmove='dragMove' catchtouchend='dragEnd'></view> </view> </view> [代码] wxss [代码].map-list .list { position: relative; height: 120rpx; } .map-list .list::after { content: ''; width: 660rpx; height: 2rpx; background-color: #eee; position: absolute; right: 0; bottom: 0; } .map-list .list .xt { display: block; width: 95rpx; height: 77rpx; position: absolute; left: 93rpx; top: 20rpx; } .map-list .list .more { display: block; width: 48rpx; height: 38rpx; position: absolute; right: 30rpx; top: 40rpx; } .map-list .list .info { display: block; width: 380rpx; height: 80rpx; position: absolute; left: 220rpx; top: 20rpx; font-size: 30rpx; } .map-list .list .info .sub-name { font-size: 28rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #646567; } .map-list .list .index { color: #e4463b; font-size: 32rpx; font-weight: bold; position: absolute; left: 35rpx; top: 40rpx; } [代码] js [代码]data:{ kelong: { top: 0, xt: '', name: '', subname: '' }, replace: { xt: '', name: '', subname: '' }, }, dragStart: function(e) { var that = this var kelong = that.data.kelong var i = e.currentTarget.dataset.index kelong.xt = this.data.optionList[i].xt kelong.name = this.data.optionList[i].name kelong.subname = this.data.optionList[i].subname var query = wx.createSelectorQuery(); //选择id query.select('.listbox').boundingClientRect(function(rect) { // console.log(rect.top) kelong.top = e.changedTouches[0].clientY - rect.top - 30 that.setData({ kelong: kelong, showkelong: true }) }).exec(); }, dragMove: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function(rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top < -60) { kelong.top = -60 } else if (kelong.top > rect.height) { kelong.top = rect.height - 60 } that.setData({ kelong: kelong, }) }).exec(); }, dragEnd: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function (rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top<-20){ wx.showModal({ title: '删除提示', content: '确定要删除此条记录?', confirmColor:'#e4463b' }) } var target = parseInt(kelong.top / 60) var replace = that.data.replace if (target >= 0) { replace.xt = optionList[target].xt replace.name = optionList[target].name replace.subname = optionList[target].subname optionList[target].xt = optionList[i].xt optionList[target].name = optionList[i].name optionList[target].subname = optionList[i].subname optionList[i].xt = replace.xt optionList[i].name = replace.name optionList[i].subname = replace.subname } that.setData({ optionList: optionList, showkelong:false }) }).exec(); }, [代码]
2019-07-28 - 小程序图文编辑器页面制作
[图片] wxml [代码]<view class='ceng' hidden='{{!showceng}}' catchtouchmove='true'></view> <!-- editTxt-titt --> <view class='edit-txt' hidden='{{!showedittitt}}' catchtouchmove='true'> <textarea placeholder='请编辑文字' value='{{edittitt}}' bindconfirm="editedtitt" maxlength="300" /> </view> <!-- editTxt-content --> <view class='edit-txt' hidden='{{!showeditcontent}}' catchtouchmove='true'> <textarea placeholder='请编辑文字' value='{{editcontent}}' bindconfirm="editedcontent" maxlength="300" /> </view> <!-- reditTxt-content 标题输入,首编辑输入,重新编辑输入是分别使用三个不同的编辑框实现--> <view class='edit-txt' hidden='{{!showreditcontent}}' catchtouchmove='true'> <textarea placeholder='请编辑文字' value='{{reditcontent}}' bindconfirm="reditedcontent" maxlength="300" /> </view> <view class='k-bai'> <view class='txt titt' data-name='titt' bindtap='towrite'> {{titt}} <image src='/images/delet.png' class='delet-icon' data-name='titt' catchtap='ondelettitt' wx:if='{{titt!="请输入标题文字"}}'></image> </view> </view> <view class='content k-bai' wx:for="{{content}}" wx:key="" data-leixing='{{item.leixing}}' data-index='{{index}}' bindtap='redit'> <view class='txt' wx:if="{{item.leixing=='txt'}}">{{item.neirong}}</view> <image src='{{item.url}}' class='image' mode="widthFix" wx:if="{{item.leixing=='img'}}"></image> <image src='/images/delet.png' class='delet-icon' data-index='{{index}}' catchtap='ondelet' wx:if="{{content[index]!=''}}"></image> </view> <view class='add'> <image src='/images/add_06.png' bindtap='addtxt'></image> <image src='/images/add_03.png' bindtap='addimg'></image> </view> <view class='save-btn' bindtap='onsave'>上传</view> [代码] js [代码]// pages/edit/edit.js Page({ /** * 页面的初始数据 */ data: { titt: '请输入标题文字', showceng: false, showedittitt: false, showeditcontent: false, showreditcontent: false, edittitt: '', editcontent: '', reditcontent: '', target: '', content: [{ leixing: 'txt', neirong: '我爱你' }, { leixing: 'img', url: 'http://wechatpx.oss-cn-beijing.aliyuncs.com/card1_03.png' } ], }, /** * 生命周期函数--监听页面加载 */ towrite: function(e) { var that = this var target = e.currentTarget.dataset.name that.setData({ showceng: true, showedittitt: true, target: target, edittitt: '' }) }, editedtitt: function(e) { var that = this var target = that.target that.setData({ titt: e.detail.value, showceng: false, showedittitt: false, [target]: e.detail.value }) }, ondelettitt: function(e) { var that = this wx.showModal({ title: '重置标题', content: '您确定要重置标题吗?', success(res) { if (res.confirm) { that.setData({ titt: '请输入标题文字' }) } else if (res.cancel) { } }, confirmColor: '#5677fc' }) }, ondelet: function(e) { var that = this var index = e.currentTarget.dataset.index var content = that.data.content wx.showModal({ title: '删除提示', content: '您确定要删除这段编辑吗?', success(res) { if (res.confirm) { content.splice(index, 1) that.setData({ content: content }) } else if (res.cancel) {} }, confirmColor: '#5677fc' }) }, addtxt: function() { var that = this var content = that.data.content that.setData({ editcontent:'', showceng: true, showeditcontent: true }) }, editedcontent: function(e) { var that = this var input = new Object input.leixing = 'txt' input.neirong = e.detail.value var content = that.data.content content.push(input) that.setData({ content:content, showceng: false, showeditcontent: false }) }, redit:function(e){ var that = this var index = e.currentTarget.dataset.index var leixing = e.currentTarget.dataset.leixing var target = that.data.target if(leixing=='txt'){ target = "content["+index+"].neirong" that.setData({ reditcontent:'', showceng: true, showreditcontent: true, target: target }) }else if(leixing=='img'){ wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { const tempFilePaths = res.tempFilePaths target = "content[" + index + "].url" that.setData({ [target]: tempFilePaths }) } }) } }, reditedcontent: function (e) { var that = this var target = that.data.target that.setData({ [target]: e.detail.value, showceng: false, showreditcontent: false, }) }, addimg:function(){ var that = this wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { const tempFilePaths = res.tempFilePaths var input = new Object input.leixing = 'img' input.url = tempFilePaths var content = that.data.content content.push(input) that.setData({ content: content, }) } }) }, onsave:function(){ wx.showToast({ title: '上传成功!', }) }, onLoad: function(options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function() { }, /** * 生命周期函数--监听页面显示 */ onShow: function() { }, /** * 生命周期函数--监听页面隐藏 */ onHide: function() { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function() { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function() { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function() { }, /** * 用户点击右上角分享 */ onShareAppMessage: function() { } }) [代码] wxss [代码]/* pages/edit/edit.wxss */ page { background-color: #f1f1f1; font-size: 28rpx; padding-bottom: 150rpx; } .k-bai { background-color: #fff; padding: 30rpx; color: #666; } .txt { padding: 30rpx; border: 1rpx solid #eee; margin-top: 20rpx; position: relative; } .delet-icon { display: block; width: 50rpx; height: 50rpx; border-radius: 50rpx; position: absolute; right: -20rpx; top: -20rpx; z-index: 10; } .ceng { width: 750rpx; height: 1334rpx; position: fixed; left: 0; top: 0; z-index: 50; background-color: rgba(0, 0, 0, 0.3); } .edit-txt { width: 660rpx; background-color: #fff; padding: 30rpx; margin: 0 auto; position: fixed; left: 50%; margin-left: -360rpx; top: 220rpx; z-index: 60; } .edit-txt .save-btn { display: block; width: 450rpx; height: 80rpx; border-radius: 80rpx; background-color: #5677fc; color: #fff; text-align: center; line-height: 80rpx; position: absolute; left: 50%; margin-left: -225rpx; bottom: -40rpx; box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.2); } .edit-txt textarea { width: 660rpx; height: 500rpx; } .add { position: relative; padding: 20rpx 0; width: 200rpx; margin: 0 auto; height: 70rpx; background-color: #fff; border-radius: 0 0 20rpx 20rpx; box-shadow: 0 7rpx 5rpx rgba(0, 0, 0, 0.1); } .add image { display: inline-block; width: 70rpx; height: 70rpx; } .add image:first-child { margin-right: 20rpx; margin-left: 20rpx; } .content { margin-top: 30rpx; position: relative; } .content .delet-icon { right: 12rpx; top: 25rpx; } .content .image { display: block; padding: 30rpx; border: 1rpx solid #eee; margin-top: 20rpx; position: relative; width: 630rpx; } .save-btn { width: 690rpx; height: 90rpx; text-align: center; line-height: 90rpx; background-color: #5677fc; color: #fff; font-size: 34rpx; border-radius: 90rpx; position: fixed; left: 50%; margin-left: -345rpx; bottom: 30rpx; z-index: 100; } [代码]
2019-07-29 - 小程序如何生成海报分享朋友圈
摘要: 小程序开发必备技能啊… 原文:小程序如何生成海报分享朋友圈 作者:小白 Fundebug经授权转载,版权归原作者所有。 项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。 需求 利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。 生成的海报如下: [图片] 需求分析 1、利用小程序官方提供的api可以直接分享转发到微信群打开小程序 2、利用小程序生成海报保存图片到相册分享到朋友圈,用户长按识别二维码关注公众号或者打开小程序来达到裂变的目的 实现方案 一、分析如何实现 相信大家应该都会有类似的迷惑,就是如何按照产品设计的那样绘制成海报,其实当时我也是不知道如何下手,认真想了下得通过canvas绘制成图片,这样用户保存这个图片到相册,就可以分享到朋友圈了。但是要绘制的图片上面不仅有文字还有数字、图片、二维码等且都是活的,这个要怎么动态生成呢。认真想了下,需要一点一点的将文字和数字,背景图绘制到画布上去,这样通过api最终合成一个图片导出到手机相册中。 二、需要解决的问题 二维码的动态获取和绘制(包括如何生成小程序二维码、公众号二维码、打开网页二维码) 背景图如何绘制,获取图片信息 将绘制完成的图片保存到本地相册 处理用户是否取消授权保存到相册 三、实现步骤 这里我具体写下围绕上面所提出的问题,描述大概实现的过程 ①首先创建canvas画布,我把画布定位设成负的,是为了不让它显示在页面上,是因为我尝试把canvas通过判断条件动态的显示和隐藏,在绘制的时候会出现问题,所以采用了这种方法,这里还有一定要设置画布的大小。 [代码]<canvas canvas-id="myCanvas" style="width: 690px;height:1085px;position: fixed;top: -10000px;"></canvas> [代码] ②创建好画布之后,先绘制背景图,因为背景图我是放在本地,所以获取 <canvas> 组件 canvas-id 属性,通过createCanvasContext创建canvas的绘图上下文 CanvasContext 对象。使用drawImage绘制图像到画布,第一个参数是图片的本地地址,后面两个参数是图像相对画布左上角位置的x轴和y轴,最后两个参数是设置图像的宽高。 [代码]const ctx = wx.createCanvasContext('myCanvas') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085) [代码] ③创建好背景图后,在背景图上绘制头像,文字和数字。通过getImageInfo获取头像的信息,这里需要注意下在获取的网络图片要先配置download域名才能生效,具体在小程序后台设置里配置。 获取头像地址,首先量取头像在画布中的大小,和x轴Y轴的坐标,这里的result[0]是我用promise封装返回的一个图片地址 [代码]let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) let avatarurl_width = 60, //绘制的头像宽度 avatarurl_heigth = 60, //绘制的头像高度 avatarurl_x = 28, //绘制的头像在画布上的位置 avatarurl_y = 36; //绘制的头像在画布上的位置 ctx.save(); // 先保存状态 已便于画完圆再用 ctx.beginPath(); //开始绘制 //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片 [代码] 这里举个例子说下如何绘制文字,比如我要绘制如下这个“字”,需要动态获取前面字数的总宽度,这样才能设置“字”的x轴坐标,这里我本来是想通过measureText来测量字体的宽度,但是在iOS端第一次获取的宽度值不对,关于这个问题,我还在微信开发者社区提了bug,所以我想用另一个方法来实现,就是先获取正常情况下一个字的宽度值,然后乘以总字数就获得了总宽度,亲试是可以的。 [图片] [代码]let allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150); [代码] ④绘制公众号二维码,和获取头像是一样的,也是先通过接口返回图片网络地址,然后再通过getImageInfo获取公众号二维码图片信息 ⑤如何绘制小程序码,具体官网文档也给出生成无限小程序码接口,通过生成的小程序可以打开任意一个小程序页面,并且二维码永久有效,具体调用哪个小程序二维码接口有不同的应用场景,具体可以看下官方文档怎么说的,也就是说前端通过传递参数调取后端接口返回的小程序码,然后绘制在画布上(和上面写的绘制头像和公众号二维码一样的) [代码]ctx.drawImage('小程序码的本地地址', x轴, Y轴, 宽, 高) [代码] ⑥最终绘制完把canvas画布转成图片并返回图片地址 [代码] wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath // 返回的图片地址保存到一个全局变量里 that.setData({ showShareImg: true }) wx.showToast({ title: '绘制成功', }) }, fail: function () { wx.showToast({ title: '绘制失败', }) }, complete: function () { wx.hideLoading() wx.hideToast() } }) [代码] ⑦保存到系统相册;先判断用户是否开启用户授权相册,处理不同情况下的结果。比如用户如果按照正常逻辑授权是没问题的,但是有的用户如果点击了取消授权该如何处理,如果不处理会出现一定的问题。所以当用户点击取消授权之后,来个弹框提示,当它再次点击的时候,主动跳到设置引导用户去开启授权,从而达到保存到相册分享朋友圈的目的。 [代码]// 获取用户是否开启用户授权相册 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) }, fail() { // 如果用户拒绝过或没有授权,则再次打开授权窗口 openStatus = false console.log('请设置允许访问相册') wx.showToast({ title: '请设置允许访问相册', icon: 'none' }) } }) } else { // 有则直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) } [代码] 总结 至此所有的步骤都已实现,在绘制的时候会遇到一些异步请求后台返回的数据,所以我用promise和async和await进行了封装,确保导出的图片信息是完整的。在绘制的过程确实遇到一些坑的地方。比如初开始导出的图片比例大小不对,还有用measureText测量文字宽度不对,多次绘制(可能受网络原因)有时导出的图片上的文字颜色会有误差等。如果你也遇到一些比较坑的地方可以一起探讨下做个记录,下面附下完整的代码 [代码]import regeneratorRuntime from '../../utils/runtime.js' // 引入模块 const app = getApp(), api = require('../../service/http.js'); var ctx = null, // 创建canvas对象 canvasToTempFilePath = null, // 保存最终生成的导出的图片地址 openStatus = true; // 声明一个全局变量判断是否授权保存到相册 // 获取微信公众号二维码 getCode: function () { return new Promise(function (resolve, reject) { api.fetch('/wechat/open/getQRCodeNormal', 'GET').then(res => { console.log(res, '获取微信公众号二维码') if (res.code == 200) { console.log(res.content, 'codeUrl') resolve(res.content) } }).catch(err => { console.log(err) }) }) }, // 生成海报 async createCanvasImage() { let that = this; // 点击生成海报数据埋点 that.setData({ generateId: '点击生成海报' }) if (!ctx) { let codeUrl = await that.getCode() wx.showLoading({ title: '绘制中...' }) let code = new Promise(function (resolve) { wx.getImageInfo({ src: codeUrl, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) Promise.all([headImg, code]).then(function (result) { const ctx = wx.createCanvasContext('myCanvas') console.log(ctx, app.globalData.ratio, 'ctx') let canvasWidthPx = 690 * app.globalData.ratio, canvasHeightPx = 1085 * app.globalData.ratio, avatarurl_width = 60, //绘制的头像宽度 avatarurl_heigth = 60, //绘制的头像高度 avatarurl_x = 28, //绘制的头像在画布上的位置 avatarurl_y = 36, //绘制的头像在画布上的位置 codeurl_width = 80, //绘制的二维码宽度 codeurl_heigth = 80, //绘制的二维码高度 codeurl_x = 588, //绘制的二维码在画布上的位置 codeurl_y = 984, //绘制的二维码在画布上的位置 wordNumber = that.data.wordNumber, // 获取总阅读字数 // nameWidth = ctx.measureText(that.data.wordNumber).width, // 获取总阅读字数的宽度 // allReading = ((nameWidth + 375) - 325) * 2 + 380; // allReading = nameWidth / app.globalData.ratio + 325; allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; console.log(wordNumber, wordNumber.toString().length, allReading, '获取总阅读字数的宽度') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085) ctx.save(); // 先保存状态 已便于画完圆再用 ctx.beginPath(); //开始绘制 //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片 ctx.restore(); //恢复之前保存的绘图上下文状态 可以继续绘制 ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.setFontSize(28); // 文字字号 ctx.fillText(that.data.currentChildren.name, 103, 78); // 绘制文字 ctx.font = 'normal bold 44px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(wordNumber, 325, 153); // 绘制文字 ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150); ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText('打败了全国', 26, 190); // 绘制文字 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#faed15'); // 文字颜色 ctx.fillText(that.data.percent, 154, 190); // 绘制孩子百分比 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText('的小朋友', 205, 190); // 绘制孩子百分比 ctx.font = 'normal bold 32px sans-serif'; ctx.setFillStyle('#333333'); // 文字颜色 ctx.fillText(that.data.singIn, 50, 290); // 签到天数 ctx.fillText(that.data.reading, 280, 290); // 阅读时长 ctx.fillText(that.data.reading, 508, 290); // 听书时长 // 书籍阅读结构 ctx.font = 'normal normal 28px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(that.data.bookInfo[0].count, 260, 510); ctx.fillText(that.data.bookInfo[1].count, 420, 532); ctx.fillText(that.data.bookInfo[2].count, 520, 594); ctx.fillText(that.data.bookInfo[3].count, 515, 710); ctx.fillText(that.data.bookInfo[4].count, 492, 828); ctx.fillText(that.data.bookInfo[5].count, 348, 858); ctx.fillText(that.data.bookInfo[6].count, 212, 828); ctx.fillText(that.data.bookInfo[7].count, 148, 726); ctx.fillText(that.data.bookInfo[8].count, 158, 600); ctx.font = 'normal normal 18px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(that.data.bookInfo[0].name, 232, 530); ctx.fillText(that.data.bookInfo[1].name, 394, 552); ctx.fillText(that.data.bookInfo[2].name, 496, 614); ctx.fillText(that.data.bookInfo[3].name, 490, 730); ctx.fillText(that.data.bookInfo[4].name, 466, 850); ctx.fillText(that.data.bookInfo[5].name, 323, 878); ctx.fillText(that.data.bookInfo[6].name, 184, 850); ctx.fillText(that.data.bookInfo[7].name, 117, 746); ctx.fillText(that.data.bookInfo[8].name, 130, 621); ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 绘制头像 ctx.draw(false, function () { // canvas画布转成图片并返回图片地址 wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath that.setData({ showShareImg: true }) console.log(res.tempFilePath, 'canvasToTempFilePath') wx.showToast({ title: '绘制成功', }) }, fail: function () { wx.showToast({ title: '绘制失败', }) }, complete: function () { wx.hideLoading() wx.hideToast() } }) }) }) } }, // 保存到系统相册 saveShareImg: function () { let that = this; // 数据埋点点击保存学情海报 that.setData({ saveId: '保存学情海报' }) // 获取用户是否开启用户授权相册 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) }, fail() { // 如果用户拒绝过或没有授权,则再次打开授权窗口 openStatus = false console.log('请设置允许访问相册') wx.showToast({ title: '请设置允许访问相册', icon: 'none' }) } }) } else { // 有则直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) } }, [代码]
2019-06-15 - 数组再分的实现
实现数组再分 开发的时候遇到了一个比较少见的关于数组的操作,如何根据数组中的某个键的属性值是否相同,重新整合数组中的值,如同代码示例中的根据数组的name属性,将name相同的数组元素重新组合在一起。最初的设想是for循环找出每个元素之后,内嵌一个for循环进行比对,找出相应数据存放,结果是数组中的数据重复打印。于是从网上搜索并解决问题,以下便是代码示例。 代码示例 [代码]//实验代码 未分组变量 arrayDemo: [{ "name": "z", "age": 15, "high": 10, "phone": 12345678911 }, { "name": "q", "age": 15, "high": 10, "phone": 12345678910 }, { "name": "w", "age": 15, "high": 10, "phone": 12345678912 }, { "name": "e", "age": 15, "high": 10, "phone": 12345678913 }, { "name": "r", "age": 15, "high": 10, "phone": 12345678914 }, { "name": "z", "age": 15, "high": 10, "phone": 12345678915 }, { "name": "z", "age": 15, "high": 10, "phone": 12345678916 }, { "name": "f", "age": 15, "high": 10, "phone": 12345678917 } ] //调用数组排列方法 this.arrayGroup(arrayDemo); /** * 数组分组 */ arrayGroup: function(array) { var groups = []; //存放新数组 for (var i = 0; i < array.length; i++) { //遍历数组每一项 // 读取每条数据的名称 取出 分类的条件 var groupName = array[i].name; var groupValue = { // 符合分类条件的属性值组合成对象放入新数组中value属性中 'age': array[i].age, 'high': array[i].high, 'phone': array[i].phone } var groupItem = { //新数组中存放的对象 'name': '', value: [] } groupItem.name = groupName; groupItem.value.push(groupValue); if (i == 0) { //设置为基准值进行对比 groups.push(groupItem); } else { //遍历剩余数组项 var index = -1; //第二层循环找到属性值相同的数组成员并且放入新数组中 for (var k = 0; k < groups.length; k++) { if (groupName == groups[k].name) { index = k; break; } } if (index == -1) { //没有找到 groups.push(groupItem); } else { groups[k].value.push(groupValue); //将属性值存放到新数组中 } } } console.log('groups', groups); } [代码] 结果示例 [图片] 解决这个问题之后,鉴于花费在这个问题上的时间,重新搜集了有关数组的操作方法记录下来,以减少下次花费时间。 数组相关方法扩展 filter() filter()方法创建一个新数组, filter为数组中的每个元素调用一次 callback 函数,并利用调用callback函数返回true或等价于true的值的元素创建一个新数组。 [代码]filter语法: var newArray = arr.filter(callback(element[, index[, array]])[, thisArg]) /** * filterdemo 过滤掉所有小于10的数组项 */ filterDemo: function() { var filterArray = [12, 14, 8, 9, 100]; var filterResult = filterArray.filter(function(item) { if (item > 10) { return true; } }); console.log('filterResult', filterResult) } [代码] [图片] map() map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。 [代码]//map语法: var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg]) /** * map方法示例 所有数组成员乘以5 */ mapDemo: function() { var mapArray = [1, 2, 3, 4, 5]; var mapResult = mapArray.map(function(item) { return item * 5 }) console.log('mapResult', mapResult); } [代码] [图片] foreach() foreach()方法对数组中的每个元素执行一次提供的函数。 [代码]语法: array.forEach(function(currentValue, index, arr), thisValue) /** * forEach 代码示例 */ forEachDemo: function() { var arr = [1, 5, 8, 9] arr.forEach(function(item) { console.log('item', item) }) } /** * forEachDemo 无法使用break跳出循环 */ forEachDemo: function() { var arr = [1, 5, 8, 9] arr.forEach(function(item) { if (item == 1) { break } console.log('item', item) }) } /** * forEach 被调用的时候,不会直接改变调用它的对象,但是对象可能会被callback改变原数组为 var words = ['one', 'two', 'three', 'four'];调用foreach后 原数组被改变 */ forEachDemo: function() { var words = ['one', 'two', 'three', 'four']; words.forEach(function(word) { console.log(word); if (word === 'two') { words.shift(); } }); } [代码] [图片] foreach无法使用break跳出循环 [图片] forEach 被调用的时候,不会直接改变调用它的对象,但是对象可能会被callback改变 [图片] for-in for-in常用来遍历对象,以任意顺序去遍历一个对象可枚举的属性 [代码] /** * for-in 实例 输出对象的属性 */ forInDemo: function() { var obj = { name: 'ar', color: 'yellow', day: 'sunday', number: 6, age:15 } for (var key in obj) { console.log(obj[key]) } } [代码] [图片] 在合适的场景下选用合适的数组操作方法,可以使得复杂的代码变得更加的易读和简练,更容易让人理解。
2019-06-30 - CSS3 Animation动画的十二原则
作为前端的设计师和工程师,我们用 CSS 去做样式、定位并创建出好看的网站。我们经常用 CSS 去添加页面的运动过渡效果甚至动画,但我们经常做的不过如此。 [代码] 动效是一个有助于访客和用户理解我们设计的强有力工具。这里有些原则能最大限度地应用在我们的工作中。 迪士尼经过基础工作练习的长时间累积,在 1981 年出版的 The Illusion of Life: Disney Animation 一书中发表了动画的十二个原则 ([] (https://en.wikipedia.org/wiki/12_basic_principles_of_animation)) 。这些原则描述了动画能怎样用于让观众相信自己沉浸在现实世界中。 [代码] 在本文中,我会逐个介绍这十二个原则,并讨论它们怎样运用在网页中。你能在 Codepen 找到它们[] (https://codepen.io/collection/AxKOdY/)。 挤压和拉伸 (Squash and stretch) [图片] 这是物体存在质量且运动时质量保持不变的概念。当一个球在弹跳时,碰击到地面会变扁,恢复的时间会越来越短。 [代码] 创建对象的时候最有用的方法是参照实物,比如人、时钟和弹性球。 当它和网页元件一起工作时可能会忽略这个原则。DOM 对象不一定和实物相关,它会按需要在屏幕上缩放。例如,一个按钮会变大并变成一个信息框,或者错误信息会出现和消失。 尽管如此,挤压和伸缩效果可以为一个对象增加实物的感觉。甚至一些形状上的小变化就可以创造出细微但抢眼的效果。 HTML [代码] [代码] <h1>Principle 1: Squash and stretch</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle one"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].one .shape { animation: one 4s infinite ease-out; } .one .surface { background: #000; height: 10em; width: 1em; position: absolute; top: calc(50% - 4em); left: calc(50% + 10em); } @keyframes one { 0%, 15% { opacity: 0; } 15%, 25% { transform: none; animation-timing-function: cubic-bezier(1,-1.92,.95,.89); width: 4em; height: 4em; top: calc(50% - 2em); left: calc(50% - 2em); opacity: 1; } 35%, 45% { transform: translateX(8em); height: 6em; width: 2em; top: calc(50% - 3em); animation-timing-function: linear; opacity: 1; } 70%, 100% { transform: translateX(8em) translateY(5em); height: 6em; width: 2em; top: calc(50% - 3em); opacity: 0; } } body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 预备动作 (Anticipation) [图片] 运动不倾向于突然发生。在现实生活中,无论是一个球在掉到桌子前就开始滚动,或是一个人屈膝准备起跳,运动通常有着某种事先的累积。 [代码] 我们能用它去让我们的过渡动画显得更逼真。预备动作可以是一个细微的反弹,帮人们理解什么对象将在屏幕中发生变化并留下痕迹。 例如,悬停在一个元件上时可以在它变大前稍微缩小,在初始列表中添加额外的条目来介绍其它条目的移除方法。 [代码] HTML [代码]<h1>Principle 2: Anticipation</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle two"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].two .shape { animation: two 5s infinite ease-out; transform-origin: 50% 7em; } .two .surface { background: #000; width: 8em; height: 1em; position: absolute; top: calc(50% + 4em); left: calc(50% - 3em); } @keyframes two { 0%, 15% { opacity: 0; transform: none; } 15%, 25% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5,.05,.91,.47); } 28%, 38% { transform: translateX(-2em); } 40%, 45% { transform: translateX(-4em); } 50%, 52% { transform: translateX(-4em) rotateZ(-20deg); } 70%, 75% { transform: translateX(-4em) rotateZ(-10deg); } 78% { transform: translateX(-4em) rotateZ(-24deg); opacity: 1; } 86%, 100% { transform: translateX(-6em) translateY(4em) rotateZ(-90deg); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 演出布局 (Staging) [图片] 演出布局是确保对象在场景中得以聚焦,让场景中的其它对象和视觉在主动画发生的地方让位。这意味着要么把主动画放到突出的位置,要么模糊其它元件来让用户专注于看他们需要看的东西。 [代码] 在网页方面,一种方法是用 model 覆盖在某些内容上。在现有页面添加一个遮罩并把那些主要关注的内容前置展示。 另一种方法是用动作。当很多对象在运动,你很难知道哪些值得关注。如果其它所有的动作停止,只留一个在运动,即使动得很微弱,这都可以让对象更容易被察觉。 [代码] 还有一种方法是做一个晃动和闪烁的按钮来简单地建议用户比如他们可能要保存文档。屏幕保持静态,所以再细微的动作也会突显出来。 HTML [代码]<h1>Principle 3: Staging</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle three"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].three .shape.a { transform: translateX(-12em); } .three .shape.c { transform: translateX(12em); } .three .shape.b { animation: three 5s infinite ease-out; transform-origin: 0 6em; } .three .shape.a, .three .shape.c { animation: threeb 5s infinite linear; } @keyframes three { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 26%, 30% { transform: rotateZ(-40deg); } 32.5% { transform: rotateZ(-38deg); } 35% { transform: rotateZ(-42deg); } 37.5% { transform: rotateZ(-38deg); } 40% { transform: rotateZ(-40deg); } 42.5% { transform: rotateZ(-38deg); } 45% { transform: rotateZ(-42deg); } 47.5% { transform: rotateZ(-38deg); animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 58%, 100% { transform: none; } } @keyframes threeb { 0%, 20% { filter: none; } 40%, 50% { filter: blur(5px); } 65%, 100% { filter: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 连续运动和姿态对应 (Straight-Ahead Action and Pose-to-Pose) [图片] 连续运动是绘制动画的每一帧,姿态对应是通常由一个 assistant 在定义一系列关键帧后填充间隔。 [代码] 大多数网页动画用的是姿态对应:关键帧之间的过渡可以通过浏览器在每个关键帧之间的插入尽可能多的帧使动画流畅。 [代码] 有一个例外是定时功能step。通过这个功能,浏览器 “steps” 可以把尽可能多的无序帧串清晰。你可以用这种方式绘制一系列图片并让浏览器按顺序显示出来,这开创了一种逐帧动画的风格。 HTML [代码]<h1>Principle 4: Straight Ahead Action and Pose to Pose</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle four"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].four .shape.a { left: calc(50% - 8em); animation: four 6s infinite cubic-bezier(.57,-0.5,.43,1.53); } .four .shape.b { left: calc(50% + 8em); animation: four 6s infinite steps(1); } @keyframes four { 0%, 10% { transform: none; } 26%, 30% { transform: rotateZ(-45deg) scale(1.25); } 40% { transform: rotateZ(-45deg) translate(2em, -2em) scale(1.8); } 50%, 75% { transform: rotateZ(-45deg) scale(1.1); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 跟随和重叠动作 (Follow Through and Overlapping Action) [图片] 事情并不总在同一时间发生。当一辆车从急刹到停下,车子会向前倾、有烟从轮胎冒出来、车里的司机继续向前冲。 [代码] 这些细节是跟随和重叠动作的例子。它们在网页中能被用作帮助强调什么东西被停止,并不会被遗忘。例如一个条目可能在滑动时稍滑微远了些,但它自己会纠正到正确位置。 要创造一个重叠动作的感觉,我们可以让元件以稍微不同的速度移动到每处。这是一种在 iOS 系统的视窗 (View) 过渡中被运用得很好的方法。一些按钮和元件以不同速率运动,整体效果会比全部东西以相同速率运动要更逼真,并留出时间让访客去适当理解变化。 [代码] 在网页方面,这可能意味着让过渡或动画的效果以不同速度来运行。 HTML [代码]<h1>Principle 5: Follow Through and Overlapping Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle five"> <div class="shape-container"> <div class="shape"></div> </div> </article> [代码] CSS [代码].five .shape { animation: five 4s infinite cubic-bezier(.64,-0.36,.1,1); position: relative; left: auto; top: auto; } .five .shape-container { animation: five-container 4s infinite cubic-bezier(.64,-0.36,.1,2); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } @keyframes five { 0%, 15% { opacity: 0; transform: translateX(-12em); } 15%, 25% { transform: translateX(-12em); opacity: 1; } 85%, 90% { transform: translateX(12em); opacity: 1; } 100% { transform: translateX(12em); opacity: 0; } } @keyframes five-container { 0%, 35% { transform: none; } 50%, 60% { transform: skewX(20deg); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 缓入缓出 (Slow In and Slow Out) [图片] 对象很少从静止状态一下子加速到最大速度,它们往往是逐步加速并在停止前变慢。没有加速和减速,动画感觉就像机器人。 [代码] 在 CSS 方面,缓入缓出很容易被理解,在一个动画过程中计时功能是一种描述变化速率的方式。 [代码] 使用计时功能,动画可以由慢加速 (ease-in)、由快减速 (ease-out),或者用贝塞尔曲线做出更复杂的效果。 HTML [代码]<h1>Principle 6: Slow in and Slow out</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle six"> <div class="shape a"></div> </article> [代码] CSS [代码].six .shape { animation: six 3s infinite cubic-bezier(0.5,0,0.5,1); } @keyframes six { 0%, 5% { transform: translate(-12em); } 45%, 55% { transform: translate(12em); } 95%, 100% { transform: translate(-12em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 弧线运动 (Arc) [图片] 虽然对象是更逼真了,当它们遵循「缓入缓出」的时候它们很少沿直线运动——它们倾向于沿弧线运动。 我们有几种 CSS 的方式来实现弧线运动。一种是结合多个动画,比如在弹力球动画里,可以让球上下移动的同时让它右移,这时候球的显示效果就是沿弧线运动。 HTML [代码]<h1>Principle 7: Arc (1)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevena"> <div class="shape-container"> <div class="shape a"></div> </div> </article> [代码] CSS [代码].sevena .shape-container { animation: move-right 6s infinite cubic-bezier(.37,.55,.49,.67); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } .sevena .shape { animation: bounce 6s infinite linear; border-radius: 50%; position: relative; left: auto; top: auto; } @keyframes move-right { 0% { transform: translateX(-20em); opacity: 1; } 80% { opacity: 1; } 90%, 100% { transform: translateX(20em); opacity: 0; } } @keyframes bounce { 0% { transform: translateY(-8em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 15% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 25% { transform: translateY(-4em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 32.5% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 40% { transform: translateY(0em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 45% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 50% { transform: translateY(3em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 56% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 60% { transform: translateY(6em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 64% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 66% { transform: translateY(7.5em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 70%, 100% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] [图片] 另外一种是旋转元件,我们可以设置一个在对象之外的原点来作为它的旋转中心。当我们旋转这个对象,它看上去就是沿着弧线运动。 HTML [代码]<h1>Principle 7: Arc (2)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevenb"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].sevenb .shape.a { animation: sevenb 3s infinite linear; top: calc(50% - 2em); left: calc(50% - 9em); transform-origin: 10em 50%; } .sevenb .shape.b { animation: sevenb 6s infinite linear reverse; background-color: yellow; width: 2em; height: 2em; left: calc(50% - 1em); top: calc(50% - 1em); } @keyframes sevenb { 100% { transform: rotateZ(360deg); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 次要动作 (Secondary Action) [图片] 虽然主动画正在发生,次要动作可以增强它的效果。这就好比某人在走路的时候摆动手臂和倾斜脑袋,或者弹性球弹起的时候扬起一些灰尘。 在网页方面,当主要焦点出现的时候就可以开始执行次要动作,比如拖拽一个条目到列表中间。 HTML [代码]<h1>Principle 8: Secondary Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eight"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].eight .shape.a { transform: translateX(-6em); animation: eight-shape-a 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } .eight .shape.b { top: calc(50% + 6em); opacity: 0; animation: eight-shape-b 4s linear infinite; } .eight .shape.c { transform: translateX(6em); animation: eight-shape-c 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } @keyframes eight-shape-a { 0%, 50% { transform: translateX(-5.5em); } 70%, 100% { transform: translateX(-10em); } } @keyframes eight-shape-b { 0% { transform: none; } 20%, 30% { transform: translateY(-1.5em); opacity: 1; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 32% { transform: translateY(-1.25em); opacity: 1; } 34% { transform: translateY(-1.75em); opacity: 1; } 36%, 38% { transform: translateY(-1.25em); opacity: 1; } 42%, 60% { transform: translateY(-1.5em); opacity: 1; } 75%, 100% { transform: translateY(-8em); opacity: 1; } } @keyframes eight-shape-c { 0%, 50% { transform: translateX(5.5em); } 70%, 100% { transform: translateX(10em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 时间节奏 (Timing) [图片] 动画的时间节奏是需要多久去完成,它可以被用来让看起来很重的对象做很重的动画,或者用在添加字符的动画中。 [代码] 这在网页上可能只要简单调整 animation-duration 或 transition-duration 值。 [代码] 这很容易让动画消耗更多时间,但调整时间节奏可以帮动画的内容和交互方式变得更出众。 HTML [代码]<h1>Principle 9: Timing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle nine"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].nine .shape.a { animation: nine 4s infinite cubic-bezier(.93,0,.67,1.21); left: calc(50% - 12em); transform-origin: 100% 6em; } .nine .shape.b { animation: nine 2s infinite cubic-bezier(1,-0.97,.23,1.84); left: calc(50% + 2em); transform-origin: 100% 100%; } @keyframes nine { 0%, 10% { transform: translateX(0); } 40%, 60% { transform: rotateZ(90deg); } 90%, 100% { transform: translateX(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 夸张手法 (Exaggeration) [图片] 夸张手法在漫画中是最常用来为某些动作刻画吸引力和增加戏剧性的,比如一只狼试图把自己的喉咙张得更开地去咬东西可能会表现出更恐怖或者幽默的效果。 在网页中,对象可以通过上下滑动去强调和刻画吸引力,比如在填充表单的时候生动部分会比收缩和变淡的部分更突出。 HTML [代码]<h1>Principle 10: Exaggeration</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle ten"> <div class="shape"></div> </article> [代码] CSS [代码].ten .shape { animation: ten 4s infinite linear; transform-origin: 50% 8em; top: calc(50% - 6em); } @keyframes ten { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.87,-1.05,.66,1.31); } 40% { transform: rotateZ(-45deg) scale(2); animation-timing-function: cubic-bezier(.16,.54,0,1.38); } 70%, 100% { transform: rotateZ(360deg) scale(1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 扎实的描绘 (Solid drawing) [图片] 当动画对象在三维中应该加倍注意确保它们遵循透视原则。因为人们习惯了生活在三维世界里,如果对象表现得与实际不符,会让它看起来很糟糕。 如今浏览器对三维变换的支持已经不错,这意味着我们可以在场景里旋转和放置三维对象,浏览器能自动控制它们的转换。 HTML [代码]<h1>Principle 11: Solid drawing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eleven"> <div class="shape"> <div class="container"> <span class="front"></span> <span class="back"></span> <span class="left"></span> <span class="right"></span> <span class="top"></span> <span class="bottom"></span> </div> </div> </article> [代码] CSS [代码].eleven .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .eleven .shape .container { animation: eleven 4s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; } .eleven .shape span { display: block; position: absolute; opacity: 1; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; } .eleven .shape span.front { transform: translateZ(3em); } .eleven .shape span.back { transform: translateZ(-3em); } .eleven .shape span.left { transform: rotateY(-90deg) translateZ(-3em); } .eleven .shape span.right { transform: rotateY(-90deg) translateZ(3em); } .eleven .shape span.top { transform: rotateX(-90deg) translateZ(-3em); } .eleven .shape span.bottom { transform: rotateX(-90deg) translateZ(3em); } @keyframes eleven { 0% { opacity: 0; } 10%, 40% { transform: none; opacity: 1; } 60%, 75% { transform: rotateX(-20deg) rotateY(-45deg) translateY(4em); animation-timing-function: cubic-bezier(1,-0.05,.43,-0.16); opacity: 1; } 100% { transform: translateZ(-180em) translateX(20em); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 吸引力 (Appeal) [图片] 吸引力是艺术作品的特质,让我们与艺术家的想法连接起来。就像一个演员身上的魅力,是注重细节和动作相结合而打造吸引性的结果。 [代码] 精心制作网页上的动画可以打造出吸引力,例如 Stripe 这样的公司用了大量的动画去增加它们结账流程的可靠性。 [代码] HTML [代码]<h1>Principle 12: Appeal</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle twelve"> <div class="shape"> <div class="container"> <span class="item one"></span> <span class="item two"></span> <span class="item three"></span> <span class="item four"></span> </div> </div> </article> [代码] CSS [代码].twelve .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .twelve .shape .container { animation: show-container 8s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; position: relative; } .twelve .item { background-color: #1f7bb6; position: absolute; } .twelve .item.one { animation: show-text 8s 0.1s infinite ease-out; height: 6%; width: 30%; top: 15%; left: 25%; } .twelve .item.two { animation: show-text 8s 0.2s infinite ease-out; height: 6%; width: 20%; top: 30%; left: 25%; } .twelve .item.three { animation: show-text 8s 0.3s infinite ease-out; height: 6%; width: 50%; top: 45%; left: 25%; } .twelve .item.four { animation: show-button 8s infinite cubic-bezier(.64,-0.36,.1,1.43); height: 20%; width: 40%; top: 65%; left: 30%; } @keyframes show-container { 0% { opacity: 0; transform: rotateX(-90deg); } 10% { opacity: 1; transform: none; width: 4em; height: 4em; } 15%, 90% { width: 12em; height: 12em; transform: translate(-4em, -4em); opacity: 1; } 100% { opacity: 0; transform: rotateX(-90deg); width: 4em; height: 4em; } } @keyframes show-text { 0%, 15% { transform: translateY(1em); opacity: 0; } 20%, 85% { opacity: 1; transform: none; } 88%, 100% { opacity: 0; transform: translateY(-1em); animation-timing-function: cubic-bezier(.64,-0.36,.1,1.43); } } @keyframes show-button { 0%, 25% { transform: scale(0); opacity: 0; } 35%, 80% { transform: none; opacity: 1; } 90%, 100% { opacity: 0; transform: scale(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码]
2019-03-21 - Licia 支持小程序的 JS 工具库
导语 Licia 是一套在开发中实践积累起来的实用 JavaScript 工具库。该库目前拥有超过 300 个模块,同时支持浏览器、node 及小程序运行环境,提供了包括日期格式化、md5、颜色转换等实用模块,可以极大地提高开发效率。 前言 因为小程序运行的是 JavaScript 代码,传统前端所使用的 JS 库理应也能够被用在小程序中才对。然而,经过实际测试,你会发现有相当一部分 npm 包是无法直接在小程序中跑起来的。比如前端工程师十分常用的 lodash,在小程序中引入会报错。 为什么会这样? 主要原因就是绝大部分库的开发者在设计时只会考虑两种运行环境,浏览器和 node,而小程序并不会在其考虑范围内。因此,只要开发者的 JS 代码使用了只有浏览器与 node 中才有的接口,如 DOM 操作、文件读写等,该库就不能正常地运行在小程序环境中。除此之外,假如他们使用了小程序禁用的功能,例如全局变量与动态代码执行,这时候代码跑在小程序环境也会出错。 使用 使用 npm 安装 1、 安装 npm 包 [代码]npm i miniprogram-licia --save [代码] 2、点击开发者工具中的菜单栏:工具 --> 构建 npm 3、直接在代码中引入使用 [代码]const licia = require('miniprogram-licia'); licia.md5('licia'); // -> 'e59f337d85e9a467f1783fab282a41d0' licia.safeGet({a: {b: 1}}, 'a.b'); // -> 1 [代码] 生成定制化 util.js 使用 npm 包的方式会将所有功能引入到代码包中,大概会增加 100 kb 的大小。如果你只想引入所需脚本,可以使用在线工具生成定制化 util 库。 1、访问 https://licia.liriliri.io/builder.html 2、输入需要的模块名,点击生成下载 util.js。 3、将生成的工具库拷贝到小程序项目任意目录下然后直接引入使用。 [代码]const util = require('../lib/util'); util.wx.getStorage({ key: 'test' }).then(res => console.log(res.data)); [代码] 优点 1、目前拥有 270 多个模块可在小程序中正常运行,而 underscore 只有 120 个函数左右。 2、与 lodash 相比增加了不少更加实用的函数,比如 md5、atob、btoa、Emitter、dateFormat 等。 3、可以直接在小程序中引入运行,不像 lodash 需要进行一定的修改才能正常跑在小程序中。 4、定制化生成可以使用更小体积的工具库,这在限制了代码包大小的小程序中十分有用。 附录 这里只简单列出函数及其功能介绍,详细的用法请访问官网查看。 注:模块名右边有小程序图标即表明可以在小程序中使用。 Class: 创建 JavaScript 类。 Color: 颜色转换。 Dispatcher: Flux 调度器。 Emitter: 提供观察者模式的 Event emitter 类。 Enum: Enum 类实现。 JsonTransformer: JSON 转换器。 LinkedList: 双向链表实现。 Logger: 带日志级别的简单日志库。 Lru: 简单 LRU 缓存。 Promise: 轻量 Promise 实现。 PseudoMap: 类似 es6 的 Map,不支持遍历器。 Queue: 队列数据结构。 QuickLru: 不使用链表的 LRU 实现。 ReduceStore: 简单类 redux 状态管理。 Stack: 栈数据结构。 State: 简单状态机。 Store: 内存存储。 Tween: JavaScript 补间动画库。 Url: 简单 url 操作库。 Validator: 对象属性值校验。 abbrev: 计算字符串集的缩写集合。 after: 创建一个函数,只有在调用 n 次后才会调用一次。 allKeys: 获取对象的所有键名,包括自身的及继承的。 arrToMap: 将字符串列表转换为映射。 atob: window.atob,运行在 node 环境时使用 Buffer 进行模拟。 average: 获取数字的平均值。 base64: base64 编解码。 before: 创建一个函数,只能调用少于 n 次。 binarySearch: 二分查找实现。 bind: 创建一个绑定到指定对象的函数。 btoa: window.btoa,运行在 node 环境时使用 Buffer 进行模拟。 bubbleSort: 冒泡排序实现。 bytesToStr: 将字节数组转换为字符串。 callbackify: 将返回 Promise 的函数转换为使用回调的函数。 camelCase: 将字符串转换为驼峰式。 capitalize: 将字符串的第一个字符转换为大写,其余字符转换为小写。 castPath: 将值转换为属性路径数组。 centerAlign: 字符串居中。 char: 根据指定的整数返回 unicode 编码为该整数的字符。 chunk: 将数组拆分为指定长度的子数组。 clamp: 将数字限定于指定区间。 className: 合并 class。 clone: 对指定对象进行浅复制。 cloneDeep: 深复制。 cmpVersion: 比较版本号。 combine: 创建一个数组,用一个数组的值作为其键名,另一个数组的值作为其值。 compact: 返回数组的拷贝并移除其中的虚值。 compose: 将多个函数组合成一个函数。 concat: 将多个数组合并成一个数组。 contain: 检查数组中是否有指定值。 convertBase: 对数字进行进制转换。 createAssigner: 用于创建 extend,extendOwn 和 defaults 等模块。 curry: 函数柯里化。 dateFormat: 简单日期格式化。 debounce: 返回函数的防反跳版本。 decodeUriComponent: 类似 decodeURIComponent 函数,只是输入不合法时不抛出错误并尽可能地对其进行解码。 defaults: 填充对象的默认值。 define: 定义一个模块,需要跟 use 模块配合使用。 defineProp: Object.defineProperty(defineProperties) 的快捷方式。 delay: 在指定时长后执行函数。 detectBrowser: 使用 ua 检测浏览器信息。 detectMocha: 检测是否有 mocha 测试框架在运行。 detectOs: 使用 ua 检测操作系统。 difference: 创建一个数组,该数组的元素不存在于给定的其它数组中。 dotCase: 将字符串转换为点式。 each: 遍历集合中的所有元素,用每个元素当做参数调用迭代器。 easing: 缓动函数,参考 http://jqueryui.com/ 。 endWith: 检查字符串是否以指定字符串结尾。 escape: 转义 HTML 字符串,替换 &,<,>,",`,和 ’ 字符。 escapeJsStr: 转义字符串为合法的 JavaScript 字符串字面量。 escapeRegExp: 转义特殊字符用于 RegExp 构造函数。 every: 检查是否集合中的所有元素都能通过真值检测。 extend: 复制多个对象中的所有属性到目标对象上。 extendDeep: 类似 extend,但会递归进行扩展。 extendOwn: 类似 extend,但只复制自己的属性,不包括原型链上的属性。 extractBlockCmts: 从源码中提取块注释。 extractUrls: 从文本中提取 url。 fibonacci: 计算斐波那契数列中某位数字。 fileSize: 将字节数转换为易于阅读的形式。 fill: 在数组指定位置填充指定值。 filter: 遍历集合中的每个元素,返回所有通过真值检测的元素组成的数组。 find: 找到集合中第一个通过真值检测的元素。 findIdx: 返回第一个通过真值检测元素在数组中的位置。 findKey: 返回对象中第一个通过真值检测的属性键名。 findLastIdx: 同 findIdx,只是查找顺序改为从后往前。 flatten: 递归拍平数组。 fnParams: 获取函数的参数名列表。 format: 使用类似于 printf 的方式来格式化字符串。 fraction: 转换数字为分数形式。 freeze: Object.freeze 的快捷方式。 freezeDeep: 递归进行 Object.freeze。 gcd: 使用欧几里德算法求最大公约数。 getUrlParam: 获取 url 参数值。 has: 检查属性是否是对象自身的属性(原型链上的不算)。 hslToRgb: 将 hsl 格式的颜色值转换为 rgb 格式。 identity: 返回传入的第一个参数。 idxOf: 返回指定值第一次在数组中出现的位置。 indent: 对文本的每一行进行缩进处理。 inherits: 使构造函数继承另一个构造函数原型链上的方法。 insertionSort: 插入排序实现。 intersect: 计算所有数组的交集。 intersectRange: 计算两个区间的交集。 invert: 生成一个新对象,该对象的键名和键值进行调换。 isAbsoluteUrl: 检查 url 是否是绝对地址。 isArgs: 检查值是否是参数类型。 isArr: 检查值是否是数组类型。 isArrBuffer: 检查值是否是 ArrayBuffer 类型。 isArrLike: 检查值是否是类数组对象。 isBool: 检查值是否是布尔类型。 isBrowser: 检测是否运行于浏览器环境。 isClose: 检查两个数字是否近似相等。 isDataUrl: 检查字符串是否是有效的 Data Url。 isDate: 检查值是否是 Date 类型。 isEmail: 简单检查值是否是合法的邮件地址。 isEmpty: 检查值是否是空对象或空数组。 isEqual: 对两个对象进行深度比较,如果相等,返回真。 isErr: 检查值是否是 Error 类型。 isEven: 检查数字是否是偶数。 isFinite: 检查值是否是有限数字。 isFn: 检查值是否是函数。 isGeneratorFn: 检查值是否是 Generator 函数。 isInt: 检查值是否是整数。 isJson: 检查值是否是有效的 JSON。 isLeapYear: 检查年份是否是闰年。 isMap: 检查值是否是 Map 对象。 isMatch: 检查对象所有键名和键值是否在指定的对象中。 isMiniProgram: 检测是否运行于微信小程序环境中。 isMobile: 使用 ua 检测是否运行于移动端浏览器。 isNaN: 检测值是否是 NaN。 isNative: 检查值是否是原生函数。 isNil: 检查值是否是 null 或 undefined,等价于 value == null。 isNode: 检测是否运行于 node 环境中。 isNull: 检查值是否是 Null 类型。 isNum: 检测值是否是数字类型。 isNumeric: 检查值是否是数字,包括数字字符串。 isObj: 检查值是否是对象。 isOdd: 检查数字是否是奇数。 isPlainObj: 检查值是否是用 Object 构造函数创建的对象。 isPrime: 检查整数是否是质数。 isPrimitive: 检测值是否是字符串,数字,布尔值或 null。 isPromise: 检查值是否是类 promise 对象。 isRegExp: 检查值是否是正则类型。 isRelative: 检查路径是否是相对路径。 isSet: 检查值是否是 Set 类型。 isSorted: 检查数组是否有序。 isStr: 检查值是否是字符串。 isTypedArr: 检查值是否 TypedArray 类型。 isUndef: 检查值是否是 undefined。 isUrl: 简单检查值是否是有效的 url 地址。 isWeakMap: 检查值是否是 WeakMap 类型。 isWeakSet: 检查值是否是 WeakSet 类型。 kebabCase: 将字符串转换为短横线式。 keyCode: 键码键名转换。 keys: 返回包含对象自身可遍历所有键名的数组。 last: 获取数组的最后一个元素。 linkify: 将文本中的 url 地址转换为超链接。 longest: 获取数组中最长的一项。 lowerCase: 转换字符串为小写。 lpad: 对字符串进行左填充。 ltrim: 删除字符串头部指定字符或空格。 map: 对集合的每个元素调用转换函数生成与之对应的数组。 mapObj: 类似 map,但针对对象,生成一个新对象。 matcher: 传入对象返回函数,如果传入参数中包含该对象则返回真。 max: 获取数字中的最大值。 md5: MD5 算法实现。 memStorage: Web Storage 接口的纯内存实现。 memoize: 缓存函数计算结果。 mergeSort: 归并排序实现。 methods: 获取对象中所有方法名。 min: 获取数字中的最小值。 moment: 简单的类 moment.js 实现。 ms: 时长字符串与毫秒转换库。 negate: 创建一个将原函数结果取反的函数。 nextTick: 能够同时运行在 node 和浏览器端的 next tick 实现。 noop: 一个什么也不做的空函数。 normalizeHeader: 标准化 HTTP 头部名。 normalizePath: 标准化文件路径中的斜杠。 now: 获取当前时间戳。 objToStr: Object.prototype.toString 的别名。 omit: 类似 pick,但结果相反。 once: 创建只能调用一次的函数。 optimizeCb: 用于高效的函数上下文绑定。 pad: 对字符串进行左右填充。 pairs: 将对象转换为包含【键名,键值】对的数组。 parallel: 同时执行多个函数。 parseArgs: 命令行参数简单解析。 partial: 返回局部填充参数的函数,与 bind 模块相似。 pascalCase: 将字符串转换为帕斯卡式。 perfNow: 高精度时间戳。 pick: 过滤对象。 pluck: 提取数组对象中指定属性值,返回一个数组。 precision: 获取数字的精度。 promisify: 转换使用回调的异步函数,使其返回 Promise。 property: 返回一个函数,该函数返回任何传入对象的指定属性。 query: 解析序列化 url 的 query 部分。 quickSort: 快排实现。 raf: requestAnimationFrame 快捷方式。 random: 在给定区间内生成随机数。 randomItem: 随机获取数组中的某项。 range: 创建整数数组。 rc4: RC4 对称加密算法实现。 reduce: 合并多个值成一个值。 reduceRight: 类似于 reduce,只是从后往前合并。 reject: 类似 filter,但结果相反。 remove: 移除集合中所有通过真值检测的元素,返回包含所有删除元素的数组。 repeat: 重复字符串指定次数。 restArgs: 将给定序号后的参数合并成一个数组。 rgbToHsl: 将 rgb 格式的颜色值转换为 hsl 格式。 root: 根对象引用,对于 nodeJs,取 [代码]global[代码] 对象,对于浏览器,取 [代码]window[代码] 对象。 rpad: 对字符串进行右填充。 rtrim: 删除字符串尾部指定字符或空格。 safeCb: 创建回调函数,内部模块使用。 safeDel: 删除对象属性。 safeGet: 获取对象属性值,路径不存在时不报错。 safeSet: 设置对象属性值。 sample: 从集合中随机抽取部分样本。 selectionSort: 选择排序实现。 shuffle: 将数组中元素的顺序打乱。 size: 获取对象的大小或类数组元素的长度。 sleep: 使用 Promise 模拟暂停方法。 slice: 截取数组的一部分生成新数组。 snakeCase: 转换字符串为下划线式。 some: 检查集合中是否有元素通过真值检测。 sortBy: 遍历集合中的元素,将其作为参数调用函数,并以得到的结果为依据对数组进行排序。 spaceCase: 将字符串转换为空格式。 splitCase: 将不同命名式的字符串拆分成数组。 splitPath: 将路径拆分为文件夹路径,文件名和扩展名。 startWith: 检查字符串是否以指定字符串开头。 strHash: 使用 djb2 算法进行字符串哈希。 strToBytes: 将字符串转换为字节数组。 stringify: JSON 序列化,支持循环引用和函数。 stripAnsi: 清除字符串中的 ansi 控制码。 stripCmt: 清除源码中的注释。 stripColor: 清除字符串中的 ansi 颜色控制码。 stripHtmlTag: 清除字符串中的 html 标签。 sum: 计算数字和。 swap: 交换数组中的两项。 template: 将模板字符串编译成函数用于渲染。 throttle: 返回函数的节流阀版本。 timeAgo: 将时间格式化成多久之前的形式。 timeTaken: 获取函数的执行时间。 times: 调用目标函数 n 次。 toArr: 将任意值转换为数组。 toBool: 将任意值转换为布尔值。 toDate: 将任意值转换为日期类型。 toInt: 将任意值转换为整数。 toNum: 将任意值转换为数字。 toSrc: 将函数转换为源码。 toStr: 将任意值转换为字符串。 topoSort: 拓扑排序实现。 trim: 删除字符串两边指定字符或空格。 tryIt: 在 try catch 块中运行函数。 type: 获取 JavaScript 对象的内部类型。 types: 仅用于生成 ts 定义文件。 ucs2: UCS-2 编解码。 unescape: 和 escape 相反,转义 HTML 实体回去。 union: 返回传入所有数组的并集。 uniqId: 生成全局唯一 id。 unique: 返回数组去重后的副本。 unzip: 与 zip 相反。 upperCase: 转换字符串为大写。 upperFirst: 将字符串的第一个字符转换为大写。 use: 使用 define 创建的模块。 utf8: UTF-8 编解码。 values: 返回对象所有的属性值。 vlq: vlq 编解码。 waitUntil: 等待直到条件函数返回真值。 waterfall: 按顺序执行函数序列。 wrap: 将函数封装到包裹函数里面, 并把它作为第一个参数传给包裹函数。 wx: 小程序 wx 对象的 promise 版本。 zip: 将每个数组中相应位置的值合并在一起。
2019-05-07 - json2canvas:使用JSON生成小程序海报
作者:诗人的咸鱼 原文:小程序生成分享海报,一个json就够了。同时支持web Fundebug经授权转载,版权归原作者所有。 需求 在项目里写过几个canvas生成分享海报页面后,觉得这是个重复且冗余的工作.于是就想有没有能通过类似json直接生成海报的库. 然后就在github找到到两个项目: wxa-plugin-canvas,不太喜欢配置文件的写法.就没多去了解 mp_canvas_drawer,使用方式就比较符合直觉,不过可惜功能有点少. 然后就想着能不能自己再造个轮子.于是就有了这个项目 json2canvas,你可以简单的理解为是mp_canvas_drawer的增强版吧. json2canvas canvas绘制海报,写个json就够了. 项目的canvas绘制是基于cax实现的.所以天然的带来一个好处,json2canvas同时支持小程序和web 功能 支持缩放. 如果设计稿是750,而画布只有375时.你不需要任何换算,只需要将scale设置为0.5即可. 支持文本(长文本自动换行,感谢 coolzjy@v2ex 提供的正则 https://regexr.com/4f12l ,优化了换行的计算方式(不会粗暴的折断单词)) 支持图片(圆角) 支持圆型,矩形,矩形圆角 支持分组(cax里很好用的一个功能) 同时支持小程序和web 示例 web-demo 界面左边的json,可以进行编辑,直接看效果哟~ 小程序-demo [代码]git clone https://github.com/willnewii/json2canvas.git 微信开发者工具导入项目 example/weapp/ [代码] 小程序安装 [代码]npm i json2canvas 微信开发者工具->工具->构建npm [代码] 在需要使用的界面引入Component [代码]{ "usingComponents": { "json2canvas":"/miniprogram_npm/json2canvas/index" } } [代码] 效果图 想要生成一个这样的海报,需要怎么做?(红框是图片元素,蓝框是文字元素,其余的是一张背景图。) [图片] 一个json就搞定.具体支持的元素和参数,请查看项目readme [代码]{ "width": 750, "height": 1334, "scale": 0.5, "children": [ { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/bg_concerts_1.jpg", "width": 750, "height": 1334 }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/wxapp_code.jpg", "width": 100, "x": 48, "y": 44, "isCircular": true, }, { "type": "circle", "r": 50, "lineWidth": 5, "strokeStyle": "#CCCCCC", "x": 48, "y": 44, }, { "type": "text", "text": "歌词本", "font": "30px Arial", "color": "#FFFFFF", "x": 168, "y": 75, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/medal_concerts_1.png", "width": 300, "x": "center", "y": 361 }, { "type": "text", "text": "一生活一场 五月天", "font": "38px Arial", "color": "#FFFFFF", "x": "center", "y": 838, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "text", "text": "北京6场,郑州2场,登船,上班,听到你想听的歌了吗?", "font": "24px Arial", "color": "#FFFFFF", "x": "center", "y": 888, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "rect", "width": 750, "height": 193, "fillStyle": "#FFFFFF", "x": 0, "y": "bottom" }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/wxapp_code.jpg", "width": 117, "height": 117, "x": 47, "y": 1180 }, { "type": "text", "text": "长按识别小程序二维码", "font": "26px Arial", "color": "#858687", "x": 192, "y": 1202 }, { "type": "text", "text": "加入五月天 永远不会太迟", "font": "18px Arial", "color": "#A4A5A6", "x": 192, "y": 1249 }] } [代码] 问题反馈 有什么问题可以直接提issue
2019-06-29 - 如何用小程序实现类原生APP下一条无限刷体验
1.背景 如今信息流业务是各大互联网公司争先抢占的一个大面包,为了提高用户的后续消费,产品想出了各种各样的方法,例如在微视中,用户可以无限上拉出下一条视频;在知乎中,也可以无限上拉出下一条回答。这样的操作方式用户体验更好,后续消费也更多。最近几年的时间,微信小程序已经从一颗小小的萌芽成长为参天大树,形成了较大规模的生态,小程序也拥有了一个很大的流量入口。 2.demo体验 那如何才能在小程序中实现类原生APP效果的下一条无限刷体验? 这篇文章详细记录了下一条无限刷效果的实现原理,以及细节和体验优化,并将相关代码抽象成一个微信小程序代码片段,有需要的同学可查看demo源码。 线上效果请用微信扫码体验: [图片] 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a 3.实现原理 出于性能和兼容性考虑,我们尽量采用小程序官方提供的原生组件来实现下一条无限刷效果。我们发现,可以将无限上拉下一篇的文章看作一个竖向滚动的轮播图,又由于每一篇文章的内容长度高于一屏幕高度,所以需要实现文章内部可滚动,以及文章之间可以上拉和下拉切换的功能。 在多次尝试后,我们最终采用了在[代码]<swiper>[代码]组件内部嵌套一个[代码]<scroll-view>[代码]组件的方式实现,利用[代码]<swiper>[代码]组件来实现文章之间上拉和下拉切换的功能,利用[代码]<scroll-view>[代码]来实现一篇文章内部可上下滚动的功能。 所以页面的dom结构如下所示: [代码]<swiper class='scroll-swiper' circular="{{false}}" vertical="{{true}}" bindchange="bindChange" skip-hidden-item-layout="{{true}}" duration="{{500}}" easing-function="easeInCubic" > <block wx:for="{{articleData}}"> <swiper-item> <scroll-view scroll-top="0" scroll-with-animation="{{false}}" scroll-y > content </scroll-view> </swiper-item> </block> </swiper> [代码] 4.性能优化 我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。例如减少代码包体积,使用分包,渲染性能优化等。下面主要讲一下渲染性能优化。 4.1 dom优化 由于页面需要无限上拉刷新,所以要在[代码]<swiper>[代码]组件中不断的增加[代码]<swiper-item>[代码],这样必然会导致页面的dom节点成倍数的增加,最后非常卡顿。 为了优化页面的dom节点,我们利用[代码]<swiper>[代码]的[代码]current[代码]和[代码]<swiper-item>[代码]的[代码]index[代码]来做优化,控制是否渲染dom节点。首先,仅当[代码]index <= current + 1[代码]时渲染[代码]<swiper-item>[代码],也就是页面中最多预先加载出下一条,而不是将接口返回的所有后续数据都渲染出来;其次,对于用户已经消费过的之前的[代码]<swiper-item>[代码],不能直接销毁dom节点,否则会导致[代码]<swiper>[代码]的[代码]current[代码]值出现错乱,但是我们可以控制是否渲染[代码]<swiper-item>[代码]内部的子节点,我们设置了仅当[代码]current <= index + 1 && index -1 <= current[代码]时才会渲染[代码]<swiper-item>[代码]中的内容,也就是仅渲染当先文章,及上一篇和下一篇的文章内容,其他文章的dom节点都被销毁了。 这样,无论用户上拉刷新了多少次,页面中最多只会渲染3篇文章的内容,避免了因为上拉次数太多导致的页面卡顿。 4.2 分页时setData的优化 setData工作原理 [图片] 小程序的视图层目前使用[代码]WebView[代码]作为渲染载体,而逻辑层是由独立的 [代码]JavascriptCore[代码] 作为运行环境。在架构上,[代码]WebView[代码] 和 [代码]JavascriptCore[代码] 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 [代码]evaluateJavascript[代码] 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 [代码]JS[代码] 脚本,再通过执行 [代码]JS[代码] 脚本的形式传递到两边独立环境。 而 [代码]evaluateJavascript[代码] 的执行会受很多方面的影响,数据到达视图层并不是实时的。 每次 [代码]setData[代码] 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。 [代码]setData[代码] 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。 [代码]setData[代码] 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 避免不当使用setData [代码]data[代码] 应仅包括与页面渲染相关的数据,其他数据可绑定在this上。使用 [代码]data[代码] 在方法间共享数据,会增加 setData 传输的数据量,。 使用 [代码]setData[代码] 传输大量数据,通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。仅传输页面中发生变化的数据,使用 [代码]setData[代码] 的特殊 [代码]key[代码] 实现局部更新。 避免不必要的 [代码]setData[代码],避免短时间内频繁调用 [代码]setData[代码],对连续的setData调用进行合并。不然会导致操作卡顿,交互延迟,阻塞通信,页面渲染延迟。 避免在后台页面进行 [代码]setData[代码],这样会抢占前台页面的渲染资源。可将页面切入后台后的[代码]setData[代码]调用延迟到页面重新展示时执行。 优化示例 无限上拉刷新的数据会采用分页接口的形式,分多次请求回来。在使用分页接口拉取到下一刷的数据后,我们需要调用[代码]setData[代码]将数据写进[代码]data[代码]的[代码]articleData[代码]中,这个[代码]articleData[代码]是一个数组,里面存放着所有的文章数据,数据量十分庞大,如果直接[代码]setData[代码]会增加通讯耗时和页面更新开销,导致操作卡顿,交互延迟。 为了避免这个问题,我们将[代码]articleData[代码]改进为一个二维数组,每一次[代码]setData[代码]通过分页的 [代码]cachedCount[代码]标识来实现局部更新,具体代码如下: [代码]this.setData({ [`articleData[${cachedCount}]`]: [...data], cachedCount: cachedCount + 1, }) [代码] [代码]articleData[代码]的结构如下: [图片] 4.3 体验优化 解决了操作卡顿,交互延迟等问题,我们还需要对动画和交互的体验进行优化,以达到类原生APP效果的体验。 在文章间上拉切换时,我们使用了[代码]<swiper>[代码]组件自带的动画效果,并通过设置[代码]duration[代码]和[代码]easing-function[代码]来优化滚动细节和动画。 当用户阅读文章到底部时,会提示下一篇文章的标题等信息,而在页面上拉时,由于下一篇文章的内容已经加载出来了,这样在滑动过程中会出现两个重复的标题。为了避免这种情况出现,我们通过一个占满屏幕宽高的空白[代码]<view>[代码]来将下一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]hidden="{{index !== current && index !== current + 1}}"[代码]来隐藏这个空白[代码]<view>[代码],并对这个空白[代码]<view>[代码]的高度变化增加动画,来实现下一篇文章从屏幕底部滚动到屏幕顶部的效果: [代码].fake-scroll { height: 100%; width: 100%; transition: height 0.3s cubic-bezier(0.167,0.167,0.4,1); } [代码] [图片] 而当用户想要上拉查看之前阅读过的文章时,我们需要给用户一个“下滑查看上一条”提示,所以也可以采用同上的方式,通过一个占满屏幕宽高的提示语[代码]<view>[代码]来将上一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]wx:if="{{index + 1 === current}}"[代码]来隐藏这个提示语[代码]<view>[代码],并对这个提示语[代码]<view>[代码]的透明度变化增加动画,来实现下拉时提示“下滑查看上一条”的效果: [代码].fake-previous { height: 100%; width: 100%; opacity: 0; transition: opacity 1s ease-in; } .fake-previous.show-fake-previous { opacity: 1; } [代码] 至此,这个类原生APP效果的下一条无限刷体验的需求的所有要点和细节都已实现。 记录在此,欢迎交流和讨论。 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a
2019-06-25 - 极简代码之云开发的触底无限加载
js: [代码]const db = wx.cloud.database() const _ = db.command const col = "test" const sql = { _id: _.neq(1) } //获取所有记录 Page({ data: { isEndOfList: false, list: [], limit: 20 //每次拉取数量 }, onLoad: function(options) { this.getData() }, getData: function() { db.collection(col) .where(sql) .skip(this.data.list.length) .limit(this.data.limit) .get() .then(res => { this.setData({ list: [...this.data.list, ...res.data], //合并数据 isEndOfList: res.data.length < this.data.limit ? true : false //判断是否结束 }) }) }, onReachBottom: function() { this.data.isEndOfList || this.getData() } }) [代码] wxml [代码]<view style="height:100px" wx:for='{{list}}' wx:key='none'>{{index}}</view> <view style="padding:15px;text-align:center;color:grey" wx:if='{{list.length>limit}}'> <view wx:if='{{(!isEndOfList)}}'>正在加载数据...</view> <view wx:else>----END----</view> </view> [代码]
2020-06-16 - 云函数中生成excel并且上传到云存储中
云环境1.0.51 小程序的云开发功能为我们带来了很大的方便,于是就打算研究一下如何在云函数中拉取数据,之后生成excel到云存储中,过程中踩了些坑,这里分享给大家,希望能有所帮助。 首先了解一个node端生成excel的库excel-export 虽然已经许久未更新了,但是目前还没有什么太大的问题,所以在他的基础上进行开发,并且上手也比较容易 主要用法 引入 [代码]let nodeExcel = require('excel-export'); [代码] 创建配置对象 [代码]let conf = { stylesXmlFile, // 约束文件(不然生成的excel打开会报一些问题) cols, // 可理解为表头 [{ caption: 'columnName', type: 'string' }], 这里出于方便,type为string,具体可移步其文档查看 rows, // 可以理解为填充的数据 ['wechat', 'mp'] } [代码] 创建流对象 [代码]let result = nodeExcel.execute(conf) // 普通node后端可以直接使用 res.end(result, 'binary'); 进行下载,要记得添加相应的头,其文档里也有说明 // 最终可以使用 Buffer.from(result.toString(), 'binary') 转换为一个Buffer对象 [代码] 嵌入云开发 大致的思路就是 [拉取数据] -> [生成excel流对象] -> [上传到云存储中] -> [返回该fileID] 几个踩坑点 读入文件要使用 [代码]path.resolve(__dirname, 'xxx')[代码] 得到excel流对象 使用 [代码]Buffer.from(result.toString(), 'binary')[代码] 再配合[代码]cloud.uploadFile[代码] 生成时conf要配置[代码]stylesXmlFile[代码],不然打开文件总有个提示,很不爽![代码]styles.xml[代码]这个文件可以在[代码]node_modules/excel-export/example/styles.xml[代码]找到。 数据和表头最好是对应的,数据也可以存在空值 云函数目录结构 [代码]- testDownload - |- index.js - |- styles.xml - |- package.json - |- package-lock.json - |- node_modules (在开发工具中应该是不显示的) [代码] package.json中的依赖 [代码] "dependencies": { "excel-export": "^0.5.1", "wx-server-sdk": "latest" } [代码] index.js文件代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const nodeExcel = require('excel-export') const fs = require('fs') const path = require('path') cloud.init({ env: "xxxx" // 你的环境 }) const db = cloud.database() // 生成分数项并且下载对应的excel exports.main = async (event, context) => { let collectionId = '123666' // 模拟的集合名 let openId = 'sda6248daa888764' // 模拟openid let confParams = ['姓名', '学号', '签到时间'] // 模拟表头 let jsonData = [] // 获取数据 await db.collection(collectionId).get().then(res => { jsonData = res.data }) // 转换成excel流数据 let conf = { stylesXmlFile: path.resolve(__dirname, 'styles.xml'), name: 'sheet', cols: confParams.map(param => { return { caption: param, type: 'string' } }), rows: jsonToArray(jsonData) } let result = nodeExcel.execute(conf) // result为excel二进制数据流 // 上传到云存储 return await cloud.uploadFile({ cloudPath: `download/sheet${openId}.xlsx`, // excel文件名称及路径,即云存储中的路径 fileContent: Buffer.from(result.toString(), 'binary'), }) // json对象转换成数组填充 function jsonToArray (arrData) { let arr = new Array() arrData.forEach(item => { let itemArray = new Array() for (let key in item) { if (key === '_id' || key === '_openid') { continue } itemArray.push(item[key]) } arr.push(itemArray) }) return arr } } [代码] 触发云函数,可以看到云存储中有了刚刚生成的文件 [图片]数据库中的数据, 由于表头都是一样的,所以这边的key可以适当的简化,但是要注意数据库中拉取数据顺序的问题 [图片]最终生成的excel
2019-06-05 - lufylegend基本实现原理及其简单应用
前言 近年来,随着前端技术的迅速发展,H5游戏也开始得到普及,那么什么是H5游戏呢?它的本质其实就是一组组可以控制的动画,那么动画又要怎样去实现呢?是通过css3?还是说直接用js来操作dom节点?我们知道,频繁地操作dom节点会触发浏览器的重绘和回流,过多的重绘和回流会降低页面的性能,从而就会影响到用户的体验,那么应该要使用什么来实现动画呢?答案就是canvas。 内容 canvas动画的实现 我们对于canvas的认识就只是知道它是用来绘制图形、图片等等,这些都是静态的并不会动,那么要如何使用canvas来做动画呢?这里首先要说一下动画是怎样形成的,动画的形成是基于“视觉暂留”的原理,所谓的“视觉暂留”指的就是人的眼睛在看到一个画面后,在0.34秒内这个画面是不会消失的,如果在前一个画面还没有消失前就出现下一个画面,这样就会给人造成一种流畅的视觉变化效果,也就形成了动画。这样我们就可以先用canvas绘制出一个画面,然后在0.34秒内将canvas清掉再绘制下一个画面,如此循环,就实现了canvas的动画效果。 lufylegend的基本实现原理 canvas动画的实现可以总结如下图: [图片] 1、首先是用ctx.clearRect()清除掉上一次绘制的画面。 2、计算两次绘制之间的时间间隔,为什么要计算这个时间间隔呢?因为一个合理的动画,其每一次循环所要移动的距离都是要基于时间的,也就是位移量=速度*时间。 3、遍历绘图对象数组,计算出每一个绘图对象在这一次循环中的最后位置,然后调用其绘制的方法将其绘制到canvas上,这里的绘图对象是通过事先创建好的绘图对象构造函数来创建的,之所以要创建一个绘图对象的构造函数,是因为这些绘图对象都有一些相同的属性和方法,用构造函数的方式会更便于管理这些属性和方法。 由此可见,要做一个canvas动画,既要关注绘制的逻辑,又要关注运动的逻辑,而且随着动画复杂程度的增加,这些逻辑就会越来越复杂,一不小心就很容易搞混出错,出错后的debug也会异常的困难。因此,lufylegend就是为了解决这种问题而产生的。 Lufylegend其实就是将上面讲到的动画的循环过程、绘图对象的构造以及绘制的逻辑都封装起来,因此通过使用lufylegend,我们只需要关注每一个绘图对象是如何运动以及多个绘图对象之间的交互逻辑就可以了,这样就大大降低了出错率以及缩小了出错后的debug范围,让我们把更多的精力放在动画效果的实现逻辑上。 lufylegend的简单应用 那么要如何去使用lufylegend来实现动画呢?这里首先要介绍几个核心的类。 LInit 严格来说LInit并不是lufylegend里面的一个类,它其实是一个方法,主要是用来创建canvas画布以及开始动画的轮询。 用法: [代码] LInit(requestAnimationFrame,'canvasBox',500,500,function(){}); [代码] 第一个参数可以传轮询函数或轮询的时间间隔,如果传的是轮询的时间间隔则使用的是setInterval,这里我建议用requestAnimationFrame,它是一个专门制作动画的函数,是由浏览器来控制帧速率的,不过在使用之前要进行封装一下。第二个参数传的是canvas的容器节点的id,第三第四个参数分别是canvas的宽和高,第五个参数是生成canvas节点后的回调函数。至此就生成了一个已经在不断绘制的canvas节点,这时有人可能会问,不是已经在绘制了吗?为什么页面上什么都没有?是不是没有初始化成功?前文已经提到过,在动画一次循环的最后是要遍历绘图对象数组把每一个绘图对象绘制出来的,而初始化canvas的时候这个绘图对象数组里面根本就没有东西,所以哪来的图像呢? LBitmapData LBitmapData是创建LBitmap这个类的实例所需要的参数对象。 用法: [代码] var bitmapData = new LBitmapData('#4381fd',0,0,100,100); [代码] 第一个参数可以传image对象或十六进制颜色,如果传的是image对象则绘制出来的是一张图片,如果传的是十六进制颜色则绘制出来的是一个该颜色的填充矩形,第二第三个参数传的是图片或矩形在画布中左上角的x坐标和y坐标,第四第五个参数传的是显示的宽和高。 LBitmap LBitmap是lufylegend用于显示位图图像的一个类,它只能接收LBitmapData所创建的实例作为参数。 用法: [代码] var bitmapData = new LBitmapData('#4381fd',0,0,100,100); var bitmap = new LBitmap(bitmapData); [代码] LSprite LSprite是lufylegend的一个基本显示列表构造类,它的实例是一个可以显示图形并且也可以包含子项的显示列表节点,也就是说,只有放到LSprite实例上的LBitmap实例才能显示在canvas上。 用法: [代码] var bitmapData = new LBitmapData('#4381fd',0,0,100,100); var bitmap = new LBitmap(bitmapData); var sprite = new LSprite(); sprite.addChild(bitmap); addChild(sprite); [代码] LTweenLite LTweenLite是lufylegend一个比较常用的动画类,其包含了各种缓动效果。 用法: [代码] var bitmapData = new LBitmapData('#4381fd',0,0,100,100); var bitmap = new LBitmap(bitmapData); LTweenLite.to(bitmap,0.3,{ x: 100, y: 100, delay: 2, loop: true, onComplete: function(e){} }); [代码] 第一个参数传的是要进行缓动的对象,可以是LSprite或LBitmap的实例,第二个参数传的是缓动持续的时间,第三个参数传的是要缓动的对象的所有属性,可以是xy坐标、宽高、透明度等等,只要是该对象有的属性都能进行缓动,除了对象属性以外还可以配置一些特殊值,比如配置了delay: 2缓动就会延迟2秒后才开始,配置了loop: true缓动就会持续循环,配置了onComplete: function(e){}缓动结束时就会触发这个方法,其中e是当前进行缓动的对象,这里只列举几个比较常用的特殊值。 小结 市面上除了lufylegend以外还有其他的H5游戏引擎,比如egret、laya、cocos-js等等,相比于这些H5游戏引擎,lufylegend的优势在于它比较轻量级,而且源码看起来也不是特别的复杂,比较容易能看得懂,对于实现一些2d类的游戏动画来说,用lufylegend就已经足够了。 最后附上api文档链接http://lufylegend.com/api/zh_CN/out/index.html
2019-06-11 - 如何实现一个6位数的密码输入框
背景: 因为公司业务调整需要做用户支付这一块 开发者需要在小程序上实现一个简单的6位数密码输入框 [图片] 首先想下如何实现该效果: 1.使用input覆盖在框上面,设置letter-spacing达到数字之间间距的效果,实现时发现在input组件上使用letter-spacing无效果 2.循环六个view模拟的框,光标使用动画模拟,一个隐藏的input,点击view框时触发input的Focus属性弹起键盘,同时模拟的光标展示出来,输入值后,input的value长度发生变化,设置光标位置以及模拟的密码小黑圆点 好了,废话不多数,咱们直接上手。 wxml [代码]<view class='container'> <!-- 模拟输入框 --> <view class='pay-box {{focusType ? "focus-border" : ""}}' bindtap="handleFocus" style='width: 604rpx;height: 98rpx'> <block wx:for="{{boxList}}" wx:key="{{index}}"> <view class='password-box {{index === 0 ? "b-l-n":""}}'> <view wx:if="{{(dataLength === item - 1)&& focusType}}" class="cursor"></view> <view wx:if="{{dataLength >= item}}" class="input-black-dot"></view> </view> </block> </view> <!-- 隐藏input框 --> <input value="{{input_value}}" focus="{{isFocus}}" maxlength="6" type="number" class='hidden-input' bindinput="handleSetData" bindfocus="handleUseFocus" bindblur="handleUseFocus" /> </view> [代码] wxss [代码]/* 第一个格子输入框 */ .container .b-l-n { border-left: none; } .pay-box { margin: 0 auto; display: flex; flex-direction: row; border-left: 1px solid #cfd4d3; } /* 支付密码框聚焦的时候 */ .focus-border { border-color: #0c8; } /* 单个格式样式(聚焦的时候) */ .password-box { flex: 1; border: 1px solid #0c8; margin-right: 10rpx; display: flex; align-items: center; justify-content: center; } /* 模拟光标 */ .cursor { width: 2rpx; height: 36rpx; background-color: #0c8; animation: focus 1.2s infinite; } /* 光标动画 */ @keyframes focus { from { opacity: 1; } to { opacity: 0; } } /* 模拟输入的password的黑点 */ .input-black-dot { width: 20rpx; height: 20rpx; background-color: #000; border-radius: 50%; } /* 输入框 */ .hidden-input { margin-top: 200rpx; position: relative; } [代码] JS [代码]Component({ data: { //输入框聚焦状态 isFocus: false, //输入框聚焦样式 是否自动获取焦点 focusType: true, valueData: '', //输入的值 dataLength: '', boxList: [1, 2, 3, 4, 5, 6] }, // 组件属性 properties: { }, // 组件方法 methods: { // 获得焦点时 handleUseFocus() { this.setData({ focusType: true }) }, // 失去焦点时 handleUseBlur() { this.setData({ focusType: false }) }, // 点击6个框聚焦 handleFocus() { this.setData({ isFocus: true }) }, // 获取输入框的值 handleSetData(e) { // 更新数据 this.setData({ dataLength: e.detail.value.length, valueData: e.detail.value }) // 当输入框的值等于6时(发起支付等...) if (e.detail.value.length === 6) { // 通知用户输入数字达到6位数可以发送接口校验密码是否正确 this.triggerEvent('initData', e.detail.value) } } } }) [代码] 实现方式很简单,有点小问题,还有一些后续准备做的优化点,等完善后上线后再来修改一波。 最后附上代码片段: https://developers.weixin.qq.com/s/8CtRqJmT7W8k
2020-07-06 - 计算文本段落的高度和行数
段落的高度和行数,在canvas绘制海报图的场景下需要用到,其他使用场景暂时还没遇到。 使用微信接口获取段落高度: wx.createSelectorQuery().selectAll('.paragraph').fields({ size: true, }, function (res) { //res里面有高度值 }).exec() 获取行数: 自己模拟写一个只有一行的段落,比如<view class="demo">模拟一行</view>,样式跟原段落设置成一样。 使用上面的高度接口,获取段落只有一行时的高度,然后 原段落高度/一行段落高度 = 原段落行数
2019-06-03 - 如何实现小程序的强制更新
大家都知道小程序提交审核发布以后是不会马上更新版本的,用户需要下次使用才会更新到新的版本,这就是冷更新。 那么如果要做到及时生效怎么办呢?这时候就要做处理了,将下面的代码添加到app.js,提交审核,发布就会生效了 [代码]onLaunch: [代码][代码]function[代码] [代码](options) {[代码] [代码] [代码][代码]this[代码][代码].autoUpdate()[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]autoUpdate: [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码] [代码] [代码][代码]// 获取小程序更新机制兼容[代码] [代码] [代码][代码]if[代码] [代码](wx.canIUse([代码][代码]'getUpdateManager'[代码][代码])) {[代码] [代码] [代码][代码]const updateManager = wx.getUpdateManager()[代码] [代码] [代码][代码]//1. 检查小程序是否有新版本发布[代码] [代码] [代码][代码]updateManager.onCheckForUpdate([代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]// 请求完新版本信息的回调[代码] [代码] [代码][代码]if[代码] [代码](res.hasUpdate) {[代码] [代码] [代码][代码]//检测到新版本,需要更新,给出提示[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'更新提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'检测到新版本,是否下载新版本并重启小程序?'[代码][代码],[代码] [代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]if[代码] [代码](res.confirm) {[代码] [代码] [代码][代码]//2. 用户确定下载更新小程序,小程序下载及更新静默进行[代码] [代码] [代码][代码]self.downLoadAndUpdate(updateManager)[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](res.cancel) {[代码] [代码] [代码][代码]//用户点击取消按钮的处理,如果需要强制更新,则给出二次弹窗,如果不需要,则这里的代码都可以删掉了[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'温馨提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'本次版本更新涉及到新的功能添加,旧版本可能无法正常访问哦'[代码][代码],[代码] [代码] [代码][代码]showCancel: [代码][代码]false[代码][代码],[代码][代码]//隐藏取消按钮[代码] [代码] [代码][代码]confirmText: [代码][代码]"确定更新"[代码][代码],[代码][代码]//只保留确定更新按钮[代码] [代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]if[代码] [代码](res.confirm) {[代码] [代码] [代码][代码]//下载新版本,并重新应用[代码] [代码] [代码][代码]self.downLoadAndUpdate(updateManager)[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 下载小程序新版本并重启应用[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]downLoadAndUpdate: [代码][代码]function[代码] [代码](updateManager) {[代码] [代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码] [代码] [代码][代码]wx.showLoading();[代码] [代码] [代码][代码]//静默下载更新小程序新版本[代码] [代码] [代码][代码]updateManager.onUpdateReady([代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]wx.hideLoading()[代码] [代码] [代码][代码]//新的版本已经下载好,调用 applyUpdate 应用新版本并重启[代码] [代码] [代码][代码]updateManager.applyUpdate()[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]updateManager.onUpdateFailed([代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]// 新的版本下载失败[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'已经有新版本了哟'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'新版本已经上线啦,请您删除当前小程序,重新搜索打开哟'[代码][代码],[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码]
2019-06-07