个人案例
- 个人小程序是不是不能使用地图组件?
小程序提交审核时,一直提示“个人主体小程序暂不支持出行与交通 ,路况插件,请修改后再提交。” 我的小程序并没有使用出行与交通 ,路况插件,而是使用腾讯位置服务地图选点功能和地图显示功能,而地图显示并没有涉及出行与交通 ,路况等信息,仅将个人需要的点标记在地图并显示出来,便于查询,仅为一个办公查询工具而已 请告知具体原因和需要整改地方
2022-05-09 - 用户支付后交易记录详情商家头像展示规则(推荐关注已下线可以忽略)
用户支付后订单详情显示的商家头像由开发者自行设置的,头像拉取来源于:推荐关注appid对应头像、发起支付的subappid(例如子商户的公众号APPID)对应头像、发起支付的appid(例如由服务商发起的支付,则拉取服务商的公众号appid)对应头像,拉取优先级规则如下: 直连模式拉取优先级 推荐关注appid>发起支付的appid 服务商模式拉取优先级 推荐关注appid>发起支付的subappid>发起支付的appid。 ~~交易记录订单详情展示商家头像与appid头像不一致时排查指引: 首先确认当前支付模式是直连模式支付还是服务商模式支付,确认后根据下述方案取排查:~~ 直连模式排查指引 1、登录「微信支付商户后台」->「营销中心」->「支付后配置」,检查发起支付的appid是否配置了支付后推荐关注服务号,如配置了支付后支付后推荐关注,根据规则,订单详情展示为「推荐关注APPID」对应头像,如需修改,将支付后支付后推荐关注服务号留空提交就取消了推荐关注,此时支付展示头像即为「发起支付时APPID」。 [图片] ~~注:由服务商代为进件特约商户,在微信支付后台是无该入口,需要联系服务商进行修改。 服务商模式排查指引 1、登录「微信支付服务商后台」->「服务商功能」->「特约商户管理」->「特约商户开发配置」->「推荐关注公众号」是否配置了推荐关注,如配置了推荐关注,优先展示推荐关注APPID头像。~~ [图片] 2、如未配置服务商推荐关注,请确认发起支付时,是否有传参subappid,根据规则,如未传,此时支付展示头像未发起支付的APPID。
2023-10-19 - 小程序端调用云函数,callfunction怎么能带上where参数进行查询呢?
小程序端访问代码如下, [图片] 结果返回了users表的全部数据(两条),如下 [图片] 后来我把where条件改为查询id,name等等,也是返回全部数据 这是云函数,功能为返回某张表某个查询条件下的全部数据,如下 [图片] where条件不生效,求教访问云函数时要怎么带where参数呢?
2021-08-09 - 小程序右上角胶囊按钮的具体样式,边框大小、颜色,背景色等?
如题,现在遇到一个需求是自定义一个风格和胶囊按钮类似的按钮,所以需要知道按钮的边框、背景色等。
2020-02-08 - 【笔记】解决用户头像过期无法显示问题
小根据官方规则,用户如果修改了头像,那么一段时间之后,用户原始的头像链接会失效。而因为我们一般用户资料储存的时候只储存了链接,就会造成失效,因此需要把用户头像转换成base64直接存数据库中,这样就不怕失效了。 云开发代码 /** * 插入用户数据 */ function addUserData(openid, userInfo) { if (!userInfo) { console.log('无用户信息,更新失败') } // 将头像图片转换为base64 http.get(userInfo.avatarUrl.replace("https", "http"), function (res) { let chunks = []; //用于保存不断加载的缓冲数据 let size = 0; //保存缓冲数据的总长度 res.on('data', function (chunk) { chunks.push(chunk); //把接受到的数据逐段保存在缓冲区(Buffer size += chunk.length;//累加缓冲数据的长度 }); res.on('end', function () { var data = Buffer.concat(chunks, size);//Buffer.concat将chunks数组中的缓冲数据拼接起来 if (Buffer.isBuffer(data)) { //如果为Buffer转换为base64并赋值给avatarImg var base64Img = 'data:image/png;base64,' + data.toString('base64'); userInfo.avatarImg = base64Img } db.collection('user').doc(openid).set({ data: userInfo }).then(e => { console.log('用户数据更新成功', e) }) }); }); } 小程序端直接渲染 <!-- 直接渲染到页面 page.wxml --> <view style="background-image:url({{detail.avatarImg||detail.avatarUrl}});"></view> 小程序端将图片保存到本地 //如果需要将头像转成图片保存,如cavans绘图场景 page.js const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(src) || []; if (format) { const filePath = `${wx.env.USER_DATA_PATH}/tmp_base64src.${format}`; // console.log(filePath) // const buffer = wx.base64ToArrayBuffer(bodyData); FileSystemManager.writeFile({ filePath, data: bodyData, encoding: 'base64', success() { console.log(filePath) }, fail() { console.log (new Error('ERROR_BASE64SRC_WRITE')); }, }); } 小程序端 已授权用户进入时自动更新 //进入小程序时,自动更新授权用户的信息到云端 app.js onLaunch: function () { this.getUserAuth(); } getUserAuth: function () { wx.getSetting({ success: res => { res.authSetting['scope.userInfo'] && wx.getUserInfo({ success: res => { wx.cloud.callFunction({ name: 'user', data: { userData: res.userInfo, } }) } }) } }) },
2020-07-07 - socket小程序开发工具内运行正常,发布线上连接成功后马上监听错误1004?
[图片] initSocket: function () { var that = this; SocketTask.onOpen(res => { socketOpen = true; console.log('监听 WebSocket 连接打开事件。', res) }) SocketTask.onClose(onClose => { console.log('监听 WebSocket 连接关闭事件。', onClose) socketOpen = false; }) SocketTask.onError(onError => { console.log('监听 WebSocket 错误。错误信息', onError) socketOpen = false }) SocketTask.onMessage(onMessage => { console.log('监听WebSocket接受到服务器的消息事件。服务器返回的消息', JSON.parse(onMessage.data)) var onMessage_data = JSON.parse(onMessage.data) that.data.chatList.push(onMessage_data); that.setData({ chatList: that.data.chatList }) that.bottom() }) }, webSocket: function () { let that = this var url = `wss://ws.dusun.com.cn/websocket/${this.data.shopId}|${this.data.mid}/${this.data.mid}`; // 创建Socket SocketTask = wx.connectSocket({ url: url, header: { 'content-type': 'application/json' }, method: 'post', success: function (res) { console.log('WebSocket连接创建', res,url) }, fail: function (err) { wx.showToast({ title: '网络异常!', }) console.log(err) }, }) //监听 that.initSocket() },
2021-04-07 - 云函数触发定时器的时间可以动态设置吗?
动态传递一个时间给定时器应该怎么做?
2020-07-18 - 基于echarts实现3D地图的定时高亮和点击事件
技术选型 文章所选技术栈:vue、echarts、echarts-gl 安装Vue和echarts 1、安装echarts和echarts-al [代码]npm i echarts --save npm i echarts-gl --save [代码] 2、引用echarts和echarts-gl [代码]import echarts from 'echarts'; import 'echarts-gl' Vue.prototype.$echarts = echarts [代码] 3、页面引入 [代码]require('../../node_modules/echarts/map/js/china') [代码] 此时地图消息就在你的node_modules/echarts/map/china中 初始化echarts-gl 3D地图 1、新建一个option.js 这个文件是用来放配置项的,不建立也可以,但是页面代码多会不不美观 2、配置页代码如下 (主要是地点标识和3D地图的颜色样式) [代码]//标识数据,用来标识地图上的点,给用户提供点击事件 var geoCoordMap = { '黑龙江': [127.9688, 45.368], '内蒙古': [110.3467, 41.4899], "吉林": [125.8154, 44.2584], '北京市': [116.4551, 40.2539], "辽宁": [123.1238, 42.1216], "河北": [114.4995, 38.1006], "天津": [117.4219, 39.4189], "山西": [112.3352, 37.9413], "陕西": [109.1162, 34.2004], "甘肃": [103.5901, 36.3043], "宁夏": [106.3586, 38.1775], "青海": [101.4038, 36.8207], "新疆": [87.9236, 43.5883], "西藏": [91.11, 29.97], "四川": [103.9526, 30.7617], "重庆": [108.384366, 30.439702], "山东": [117.1582, 36.8701], "河南": [113.4668, 34.6234], "江苏": [118.8062, 31.9208], "安徽": [117.29, 32.0581], "湖北": [114.3896, 30.6628], "浙江": [119.5313, 29.8773], "福建": [119.4543, 25.9222], "江西": [116.0046, 28.6633], "湖南": [113.0823, 28.2568], "贵州": [106.6992, 26.7682], "云南": [102.9199, 25.4663], "广东": [113.12244, 23.009505], "广西": [108.479, 23.1152], "海南": [110.3893, 19.8516], '上海': [121.4648, 31.2891] }; var chinaDatas = [ [{ name: '黑龙江', value: 100 }], [{ name: '内蒙古', value: 300 }], [{ name: '吉林', value: 300 }], [{ name: '辽宁', value: 300 }], [{ name: '河北', value: 300 }], [{ name: '天津', value: 300 }], [{ name: '山西', value: 300 }], [{ name: '陕西', value: 300 }], [{ name: '甘肃', value: 300 }], [{ name: '宁夏', value: 300 }], [{ name: '青海', value: 300 }], [{ name: '新疆', value: 300 }], [{ name: '西藏', value: 300 }], [{ name: '四川', value: 300 }], [{ name: '重庆', value: 300 }], [{ name: '山东', value: 300 }], [{ name: '河南', value: 300 }], [{ name: '江苏', value: 300 }], [{ name: '安徽', value: 300 }], [{ name: '湖北', value: 300 }], [{ name: '浙江', value: 300 }], [{ name: '福建', value: 300 }], [{ name: '江西', value: 300 }], [{ name: '湖南', value: 300 }], [{ name: '贵州', value: 300 }], [{ name: '广西', value: 300 }], [{ name: '海南', value: 300 }], [{ name: '上海', value: 1300 }] ]; //处理数据,是的数据格式符合echarts var convertData = function(data) { var res = []; for (var i = 0; i < data.length; i++) { var geoCoord = geoCoordMap[data[i][0].name]; if (geoCoord) { res.push({ name: data[i][0].name, value: geoCoord.concat(data[i][0].value) }); } } return res; }; //具体配置,并输出 export default { backgroundColor: '#fff', geo3D: { data: convertData(chinaDatas), map: 'china', color: '#fff', roam: true, //是否开启鼠标缩放和平移漫游。默认不开启。 itemStyle: { areaColor: 'rgba(255,255,255,1)', opacity: 1, borderWidth: 1, borderColor: '#000' }, //地图上每个省的颜色配置 label: { show: false, },// 标特是否显示,显示配置 emphasis: { //当鼠标放上去的状态 label: { show: true }, itemStyle: { color: '#000' } }, tooltip: 'axis', //提示框设置 formatter: val => { return val }, /** 标签内容格式器,支持字符串模板和 回调函数两种形式,字符串模板与回调函数 返回的字符串均支持用 \n 换行。**/ // legendHoverLink: true, regions: [{ name: '山东', itemStyle: { color: '#000', opacity: 1, }, label: { show: true }, }],//默认高亮区域 }, series: [{ name: 'light', type: 'scatter3D', //标识点 symbol: 'pin', //散点的形状。默认为圆形。 coordinateSystem: 'geo3D', data: convertData(chinaDatas), symbolSize: function() { return 36 }, label: { show: false }, itemStyle: { normal: { color: '#f00' } }, zlevel: 6, emphasis: { //当鼠标放上去 地区区域是否显示名称 label: { show: false }, itemStyle: { color: '#000' } }, }, ] }; [代码] 3、引入option [代码]import option from '你的option地址' [代码] 4、创建 div [代码]<div id="myChart" style="height: 1000px; width: 1000px;"></div> [代码] 5、初始化 [代码]var myChart = this.$echarts.init(document.getElementById('myChart')); [代码] 6、加载配置项 [代码]myChart.setOption(option); [代码] 7、效果图展示,颜色可以自己配置 [图片] 增加定时高亮事件和点击事件 1、定时器代码(如何高亮关键就是改变geo的regions的name属性) [代码]let regions = setInterval(function() { option.geo3D.regions[0].name = option.geo3D.data[count].name myChart.setOption(option); count ++ if (count === option.geo3D.data.length) { count = 0 } }, 1000); [代码] 2、点击事件 [代码]myChart.on('click',function(params){ clearInterval(regions) console.log(params) count = params.dataIndex option.geo3D.regions[0].name = params.name myChart.setOption(option); }); [代码] 3、双击重新开始定时器事件 [代码]myChart.getZr().on('dblclick', function(params) { regions = setInterval(function() { option.geo3D.regions[0].name = option.geo3D.data[count].name console.log(count) myChart.setOption(option); count ++ if (count === option.geo3D.data.length) { count = 0 } }, 1000); }); [代码] 4、其实要是在2D上可以用 [代码]myChart.dispatchAction({ type: 'highlight', // 可选,系列 index,可以是一个数组指定多个系列 seriesIndex?: number|Array, // 可选,系列名称,可以是一个数组指定多个系列 seriesName?: string|Array, // 可选,数据的 index dataIndex?: number, // 可选,数据的 名称 name?: string }) [代码] 这个事件,很遗憾的是3D并不支持这些api 特别注意 点击事件(click) 它只能使用getZr()来搞点击,而且返回的信息只有鼠标在屏幕的x,y轴左边,你也可以使用[代码]echartsInstance.convertFromPixel[代码]来转换,但是其中转换公式和代码的时间也许比你写出来的时间更长 当然你也可以使用ecahrts-gl的 1.0.0 beta-6 版本来做这个版本就可以直接绑定事件,但是Radeon高亮设置不了,最好直接引入他的源代码把[代码]import 'echarts-gl'[代码]替换成[代码]import '../node_modules/echarts-gl/dist/echarts-gl.js';[代码] 作者:Xia12137817 链接:https://juejin.cn/post/6844903865347735559 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2020-11-20 - 使用wx.getBackgroundAudioManager()来实现背景音乐的播放,暂停,切换页面时,背景音乐停止播放
对用到这个api的伙伴,有点帮助,这是我参考网上的教程,总结出来的 /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { // 获取BackgroundAudioManager 实例 this.back = wx.getBackgroundAudioManager() // 对实例进行设置 // 设置了 src 之后会自动播放(src为云开发中云存储空间文件的链接) this.back.src = "音乐地址" this.back.title = '夜的钢琴曲' // 标题为必选项 this.back.play() // 开始播放 // 背景音乐循环的方法 var that = this.back // 1、onEnded监听播放自然结束 this.back.onEnded (function(){ // 2、必须重新设置src才能循环之后会重新自动播放 that.src = "音乐地址" }) }, handleProxy() { this.back.pause(); // 点击音乐图标后出发的操作 this.setData({ on: !this.data.on }) if (this.data.on) { this.back.play(); // toast 提示信息 var that = this; this.setData({ showModal: true // 显示 toast }) // 定时器 500ms 关闭toast setTimeout(function() { that.setData({ showModal: false }) },500) console.log("背景音乐已开启"); }else{ this.back.pause(); // toast 提示信息 var that = this; this.setData({ showModalh:true // 显示 toast }) // 定时器 500ms 关闭toast setTimeout(function() { that.setData({ showModalh: false }) },500) console.log("背景音乐已暂停"); } }, /** * 生命周期函数--监听页面显示 */ onShow: function () { // 切换页面时,再次回到原页面, // 1.onHide() 中使用 pause() 会继续播放背景音乐 // 1.onHide() 中使用 stop() 会重新播放背景音乐 this.onReady() }, /** * 生命周期函数--监听页面隐藏 */ onHide: function () { // 页面隐藏时,暂停背景音乐 let that = this.back; // that.pause(); //当重新回到原页面时,继续播放 that.stop(); //当重新回到原页面时,重新播放 },
2021-01-19 - wx.compressImage压缩后图片变大2
上一次提的bug,但是因为代码片段乱码2次搁置了。查看上一次的问题: https://developers.weixin.qq.com/community/develop/doc/0002ec484c0ed0d834b7da3265b400?highLine=wx.compressImage 这一次是用一个哥们分享的代码片段,修改后重新上传的,查看这个哥们提的Bug: https://developers.weixin.qq.com/community/develop/doc/0006c6b1ed01486372a73983251000?highLine=wx.compressImage 此次修改,主要增加了再选择图片的时候,选择[原图]的选项,并对压缩后弹出压缩前压缩后的图片大小对比。 我这边测试截图如下: [图片] 测试机型: iphone 6s ios 12 压缩系数: quality : 80
2018-11-26 - rich-text 解析富文本 图片过大 如何自定义大小?
从后台查出来的富文本数据,使用 rich-text 进行展示时,其中的图片过大,超出屏幕。 [图片] [图片] [图片] 真的是一点效果都没有,我都要急疯了,大佬们救救我把,
2019-10-09 - canvas动态设置高度
- 当前 Bug 的表现(可附上截图) 我在js中的data里设置 height: "650px" 这个是最大的高度,而在onshow中读取图片,设置文字,这些高度都是变化的,所以我用dynamic_height来指示实际也就是最后的canvas高度,而在有些情况下,canvas的高度是650px而不是在onshow中动态设置的高度 - 预期表现 [图片] 这个是正常的图片。bug后的图片会比这个高,但是下面有一部分是空白。 我在onload中设置了这个canvas要显示什么文字,在onshow中用wx.getImageInfo获取了图片的高度,在success方法中,通过res来获取图片的高度,并得到整个图片的高度,最后用this.setdata设置高度 - 提供一个最简复现 Demo <!--这里是wxml--> [代码]<[代码][代码]canvas [代码][代码]canvas-id[代码][代码]=[代码][代码]"shareCanvas" [代码][代码]style[代码][代码]=[代码][代码]"width:400px;height:{{height}};" [代码][代码]></[代码][代码]canvas[代码][代码]>[代码] //这里是onshow函数,可以看到有dynamic_height = dynamic_height + const这种的语句,用来增加图片高度 //在onshow中我还用了fillrect来绘制一个大的矩形作为背景,其高度也是固定的 [代码]onShow: [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]let self = [代码][代码]this[代码][代码] [代码][代码]console.log([代码][代码]this[代码][代码].data.stuffname.length)[代码][代码] [代码][代码]wx.getImageInfo({[代码][代码] [代码][代码]src: self.data.src,[代码][代码] [代码][代码]success(res) {[代码][代码] [代码][代码]const ctx = wx.createCanvasContext([代码][代码]'shareCanvas'[代码][代码])[代码][代码] [代码][代码]const stuffnamewidth = ctx.measureText(self.data.stuffname)[代码] [代码][代码] ctx.setFillStyle('#FFFFFF') ctx.fillRect(0, 0, 400, 700) [代码] [代码][代码]ctx.setTextBaseline([代码][代码]'top'[代码][代码])[代码][代码] [代码][代码]var[代码] [代码]dynamic_height = pic_name + 17[代码][代码] [代码][代码]var[代码] [代码]stuffname = self.data.stuffname[代码][代码] [代码][代码]var[代码] [代码]chr = stuffname.split([代码][代码]""[代码][代码]);[代码][代码] [代码][代码]var[代码] [代码]temp = [代码][代码]""[代码][代码];[代码][代码] [代码][代码]var[代码] [代码]row = [];[代码][代码] [代码][代码]ctx.setFontSize(33)[代码][代码] [代码][代码]ctx.setFillStyle([代码][代码]'black'[代码][代码])[代码][代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]b = 0; b < row.length; b++) {[代码][代码] [代码][代码]ctx.setFontSize(33)[代码][代码] [代码][代码]ctx.setFillStyle([代码][代码]'black'[代码][代码])[代码][代码] [代码][代码]ctx.fillText(row[b], 20, pic_name + 10 + b * 40);[代码][代码] [代码][代码]dynamic_height = dynamic_height + 40[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]ctx.setFontSize(27)[代码][代码] [代码][代码]ctx.setFillStyle([代码][代码]'black'[代码][代码])[代码][代码] [代码][代码]if[代码] [代码](self.data.qq != [代码][代码]null[代码] [代码]&& self.data.qq != [代码][代码]"null"[代码] [代码]&& self.data.qq != [代码][代码]""[代码][代码]) {[代码][代码] [代码][代码]ctx.setFontSize(27)[代码][代码] [代码][代码]ctx.setFillStyle([代码][代码]'black'[代码][代码])[代码][代码] [代码][代码]ctx.fillText([代码][代码]"请联系QQ: "[代码] [代码]+ self.data.qq, 20, dynamic_height)[代码][代码] [代码][代码]dynamic_height = dynamic_height + 40[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](self.data.phone != [代码][代码]null[代码] [代码]&& self.data.phone != [代码][代码]"null"[代码] [代码]&& self.data.phone != [代码][代码]""[代码][代码]) {[代码][代码] [代码][代码]ctx.setFontSize(27)[代码][代码] [代码][代码]ctx.setFillStyle([代码][代码]'black'[代码][代码])[代码][代码] [代码][代码]ctx.fillText([代码][代码]"请联系手机:"[代码] [代码]+ self.data.phone, 20, dynamic_height)[代码][代码] [代码][代码]dynamic_height = dynamic_height + 40[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]ctx.setFontSize(17)[代码][代码] [代码][代码]ctx.setFillStyle([代码][代码]'gray'[代码][代码])[代码][代码] [代码][代码]ctx.fillText(self.data.time + [代码][代码]"发布"[代码][代码], 20, dynamic_height - 3)[代码][代码] [代码][代码]dynamic_height = dynamic_height + 25[代码] [代码] [代码][代码]ctx.moveTo(20, dynamic_height)[代码][代码] [代码][代码]ctx.lineTo(380, dynamic_height)[代码][代码] [代码][代码]ctx.setStrokeStyle([代码][代码]'gray'[代码][代码])[代码][代码] [代码][代码]ctx.stroke()[代码][代码] [代码][代码]dynamic_height = dynamic_height + 15[代码] [代码] [代码][代码]ctx.drawImage([代码][代码]'/icon/code.jpg'[代码][代码], 20, dynamic_height, 100, 100)[代码][代码] [代码][代码]dynamic_height += 115[代码] [代码] [代码][代码]self.setData({[代码][代码] [代码][代码]height: dynamic_height + [代码][代码]"px"[代码][代码] [代码][代码]})[代码] [代码] [代码][代码]ctx.draw([代码][代码]false[代码][代码], [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]// 3. canvas画布转成图片[代码][代码] [代码][代码]wx.canvasToTempFilePath({[代码][代码] [代码][代码]canvasId: [代码][代码]'shareCanvas'[代码][代码],[代码][代码] [代码][代码]quality: 1,[代码][代码] [代码][代码]success(res) {[代码][代码] [代码][代码]self.setData({[代码][代码] [代码][代码]filePath: res.tempFilePath,[代码][代码] [代码][代码]})[代码][代码] [代码][代码]// self.save()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]})[代码][代码] [代码][代码]console.log(self.data.filePath)[代码] [代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码]
2019-03-05 - 关于组件open-data的建议
[代码]<open-data type="userAvatarUrl"></open-data>[代码]现在能展示当前用户头像,能不能实现多一个属性openId,则可以展示对应openId的头像,这样就能实现用户可以不授权公开信息也能做排行榜。如果觉得openId在网络传输不安全,可以提示开发者做一层openId加密或多个类似openId的标识字段。
2018-09-28 - 能否提几个关于onCameraFrame接口(及其相关)的若干个改进建议?
onCameraFrame接口是个很有意思也很强大的接口,在微信小程序还没有正式的宣告AR相关能力时我已经让团队基于此功能做了些尝试了。但是在使用过程中还是有诸多不便。所以谈谈我的一点想法和小建议。 这个接口的推出,我相信产品团队是出于目前诸多的AR/视频需求而开发出来的,但是从这两个场景来看这个接口直接用起来不太方便: 采样频率不可控,目前貌似是30FPS。但是这30FPS每帧都把数据扔出来的话,直接在这个事件中接到数据然后去做些相关的业务逻辑。。。。。。根本处理不过来啊。实际的应用场景我相信大家都需要降低频率来处理数据,像我就是先写个环形缓冲区,然后再按照我实际的业务逻辑测试好速度跳帧把数据插入到环形缓冲区。最后随时需要取数据的时候,去环形缓冲区里面拿。所以,从这个角度来说,我希望这个接口提供以下两个功能以便使用起来更加灵活: 能够提供相关的参数配置使得能够按照预先设定的频率输出数据 能够提供一个接口在调用请求的时候再返回最新的当前帧数据 onCameraFrame给出的是RGBA的数据,这玩意儿数据量太大用起来也不方便,尤其是要把数据再传回服务器端处理的或者需要做些裁剪缩放的动作就呵呵了。。。别说用Canvas,用那玩意儿的性能会活活拖死你。所以建议提供以下相关接口功能以便使用起来更加灵活: 对于onCameraFrame的数据,能够有参数配置使得得到jpg或者png的数据 提供一些原生功能对于得到的图像数据进行缩放、裁剪 当然,并不是说onCameraFrame现在的功能不可用。。。。用倒是可以用。。。我们做的一个用摄像头获取数据然后回传后台服务器进行分析判断的测试小程序,如果简单自行小心翼翼的跳帧然后再放到canvas里进行裁剪缩放最后传输到服务器。。。这时间简直是无法忍受的(若干秒) 后来我们加入环形缓冲区,自己用JS来进行图像的压缩裁剪什么的再传输,才成功的把整体处理时间压缩到300~400ms。 所以,我相信如果官方能够提供原生的采样速率控制或者按需请求数据,以及原生的图像缩放裁剪功能。那么对于AR/即时视频处理类的小程序将会得到速度上质的飞跃。 -------------------------------9月5日 追加------------------------- 随便搜一下onCameraFrame关键字就能找到我这里提到的几个同类问题贴: https://developers.weixin.qq.com/community/develop/doc/00004c1441c30840dce8b4d7956000?highLine=onCameraFrame (频率相关) https://developers.weixin.qq.com/community/develop/doc/0000ce49770450ded3e88522e56000?highLine=onCameraFrame (格式相关) https://developers.weixin.qq.com/community/develop/doc/000eeab7238bc8cba6f8840045bc00?highLine=onCameraFrame (真机无法调试onCameraFrame,我猜测也是频率过高造成,当然,仅仅只是猜测) https://developers.weixin.qq.com/community/develop/doc/0008229fccc948215bc8a8bba56800?highLine=onCameraFrame (8月9日 Keep的回复其实也是格式的问题) https://developers.weixin.qq.com/community/develop/doc/00062e5cc14b08e3750982bd457800?highLine=onCameraFrame (频率相关) https://developers.weixin.qq.com/community/develop/doc/00026e57b08e70d06519a7af656c00?highLine=onCameraFrame (图像数据的裁剪) https://developers.weixin.qq.com/community/develop/doc/000a6646b4cce8acd2981b4855f400?highLine=onCameraFrame (格式相关) https://developers.weixin.qq.com/community/develop/doc/00084ed934c730d6b8191124151400?highLine=onCameraFrame (频率相关)
2019-09-05 - 微信小程序室内地图导航开发-微信小程序怎么加载室内三维地图?
给大家分享下自由配置个性化室内三维地图如何在微信里面进行配置显示,然后进行地图导航。 一、在微信小程序里显示室内三维地图 需要满足的两个条件: 调用ESMap室内地图需要用到小程序web-view组件,想要通过 web-view 调用ESMap室内地图需要满足以下 2 个条件: 小程序是企业主体,微信 web-view 组件不对个人类型的小程序开放。 您需要有一个自己的域名,在嵌入网页的时候需要在微信后台验证域名(只有自己域名下的网页才能被正确地显示哦,不能随便找一个公开链接)。 [图片] 二、具体实现步骤 1、域名验证: 由于微信平台的规定,web-view 指向的地址,必须是在微信小程序后台登记的域名,否则会出现“不支持打开非业务域名,请重新配置”的提示。 首先我们在微信的后台找到开发 > 开发设置 > 业务域名模块,并填上你需要绑定的域名。 [图片] 需要注意的是,这里的域名强制 https,需要配置好 https 证书,购买服务器的时候也要注意购买支持 https 的服务器。 接下来,我们需要下载一个微信的验证文件,放在你域名的根目录下,并且支持访问。 具体来说,如果您的域名www.esmap.cn,微信的验证文件是WATLNxupm4.txt,您需要确保https://www.esmap.cn/WATLNxupm4.txt 可以公开访问。确认无误之后,点击保存即可成功保存。 注:小程序所有用到的https请求都需要配置合法域名 2、嵌入带有室内地图的web-view 这个过程其实很简单,找到你微信小程序的.wxml文件,添加以下代码 <web-view src=“https://www.esmap.cn /esmapabc.html”/> 其中 https://www.esmap.cn /esmapabc.html 是带有地图的 H5 页面 室内地图制作流程,您可以使用下面两种方式构建这个页面: 从https://www.esmap.cn 官网中复制测试地图源码DEMO,在您自己的服务器进行免费部署。 参考https://www.esmap.cn 室内三维地图SDK开发说明,在您已有的 H5 页面上添加自己制作的室内地图。 3、小程序 web-view 的一些提示 微信小程序的 web-view 只能是全屏的,并且会覆盖页面中的所有其他组件。 如果想在网页中判断是否处于微信小程序中 var ua = window.navigator.userAgent.toLowerCase(); if (ua.indexOf(‘micromessenger’) == -1) {//说明不在微信中 // 走不在小程序的逻辑 } else { wx.miniProgram.getEnv(function(res) { if (res.miniprogram) { // 走在小程序的逻辑 } else { // 走不在小程序的逻辑 } }) } 如果网页想给小程序传递信息,可以通过 wx.miniProgram.postMessage 方法。 小程序中可以通过 postMessage 方法监听网页传递回来的数据,但是该方法仅在特定时机(小程序后退、组件销毁、分享)触发,没法实时传递消息。 网页跳转到小程序页面 wx.miniProgram.navigateTo({ url: ‘/pages/esmap/esmap?location=’ + obj.location }); 三、更多效果! [图片] [图片]
2019-09-04 - 小程序中如何实现表情组件
先上效果图(无图无真相) [图片] 1. 第一步准备表情包素材 我这里用的微博的表情包可以点击下面的链接查看具体JSON格式这里不展示 表情包文件weibo-emotions.js 2. 第二步编写表情组件(基于wepy2.0) 如果不会 wepy 可以先去了解下如果你会vue那非常容易上手 首先我们需要把表情包文件weibo-emotions.js中的JSON文件转换成我们需要的格式 [代码]emojis = [ { id: 编号, value: 表情对应的汉字含义 例如:[偷笑], icon: 表情相对图片路径, url: 表情具体图片路径 } ] [代码] 具体转换方法 [代码]function () { const _emojis = {} for (const key in emotions) { if (emotions.hasOwnProperty(key)) { const ele = emotions[key]; for (const item of ele) { _emojis[item.value] = { id: item.id, value: item.value, icon: item.icon.replace('/', '_'), url: weibo_icon_url + item.icon } } } } return _emojis } [代码] 编写组件的html代码 [代码]<template> <div class="emoji" style="height:{{height}}px;" :hidden="hide"> <scroll-view :scroll-y="true" style="height:{{height}}px;"> <div class="icons"> <div class="img" v-for="img in emojis" :key="img.id" @tap.stop="onTap(img.value)"> <img class="icon-image" :src="img.url" :lazy-load="true" /> </div> </div> <div style="height:148rpx;"></div> </scroll-view> <div class="btn-box"> <div class="btn-del" @tap.stop="onDel"> <div class="icon icon-input-del" /> </div> </div> </div> </template> [代码] html代码中的height变量为键盘的高度,通过props传入 编写组件的css代码 [代码].emoji { position: fixed; bottom: 0px; left: 0px; width: 100%; transition: all 0.3s; z-index: 10005; &::after { content: ' '; position: absolute; left: 0; top: 0; right: 0; height: 1px; border-top: 0.4px solid rgba(235, 237, 245, 0.8); color: rgba(235, 237, 245, 0.8); } .icons { display: flex; flex-wrap: wrap; .img { flex-grow: 1; padding: 20rpx; text-align: left; justify-items: flex-start; .icon-image { width: 48rpx; height: 48rpx; } } } scroll-view { background: #f8f8f8; } .btn-box { right: 0rpx; bottom: 0rpx; position: fixed; background: #f8f8f8; padding: 30rpx; .btn-del { background: #ffffff; padding: 20rpx 30rpx; border-radius: 10rpx; .icon { font-size: 48rpx; } } } .icon-loading { height: 100%; display: flex; justify-content: center; align-items: center; } } [代码] 这里是使用less来编写css样式的,flex布局如果你对flex不是很了解可以看看 这篇文章 组件JS代码比较少 [代码]import { weibo_emojis } from '../common/api'; import wepy from '@wepy/core'; wepy.component({ options: { addGlobalClass: true }, props: { height: Number, hide: Boolean }, data: { emojis: weibo_emojis, }, methods: { onTap(val) { this.$emit('emoji', val); }, onDel() { this.$emit('del'); } } }); [代码] 表情组件基本已经编写完成是不是很简单 那么编写好的组件怎么用呢? 其实也很简单 第一步把组件引入到页面 [代码]<config> { "usingComponents": { "emoji-input": "../components/input-emoji", } } </config> [代码] 第二步把组件加入到页面html代码中 [代码]<emoji-input :height="boardheight" @emoji="onInputEmoji" @del="onDelEmoji" :hide="bottom === 0" /> [代码] 第三步编写onInputEmoji,onDelEmoji方法 [代码] /** * 选择表情 */ onInputEmoji(val) { let str = this.content.split(''); str.splice(this.cursor, 0, val); this.content = str.join(''); if (this.cursor === -1) { this.cursor += val.length + 1; } else { this.cursor += val.length; } this.canSend(); }, /** * 删除表情 */ onDelEmoji() { let str = this.content.split(''); const leftStr = this.content.substring(0, this.cursor); const leftLen = leftStr.length; const rightStr = this.content.substring(this.cursor); const left_left_Index = leftStr.lastIndexOf('['); const left_right_Index = leftStr.lastIndexOf(']'); const right_right_Index = rightStr.indexOf(']'); const right_left_Index = rightStr.indexOf('['); if ( left_right_Index === leftLen - 1 && leftLen - left_left_Index <= 8 && left_left_Index > -1 ) { // "111[不简单]|23[33]"left_left_Index=3,left_right_Index=7,leftLen=8 const len = left_right_Index - left_left_Index + 1; str.splice(this.cursor - len, len); this.cursor -= len; } else if ( left_left_Index > -1 && right_right_Index > -1 && left_right_Index < left_left_Index && right_right_Index <= 6 ) { // left_left_Index:4,left_right_Index:3,right_right_Index:1,right_left_Index:2 // "111[666][不简|单]"right_right_Index=1,left_left_Index=3,leftLen=6 let len = right_right_Index + 1 + (leftLen - left_left_Index); if (len <= 10) { str.splice(this.cursor - (leftLen - left_left_Index), len); this.cursor -= leftLen - left_left_Index; } else { str.splice(this.cursor, 1); this.cursor -= 1; } } else { str.splice(this.cursor, 1); this.cursor -= 1; } this.content = str.join(''); }, [代码] 好了基本就完成了一个表情组件的编写和调用 如果你想看完整的代码请点击这里 如果你想体验可以扫下面的二维码自己去体验下 [图片] 下篇 我们写写怎么实现一个简单的富文本编辑器
2020-03-09 - getFileSystemManager().readFile()的bug
读取普通图片的临时路径是ok的: wx.getFileSystemManager().readFile({ filePath: xxx, //选择图片返回的相对路径 encoding: 'base64'}); 但是取无法转换通过wx.canvasToTempFilePath()处理过的路径;
2020-02-28 - 云服务器调用security.imgSecCheck完成代码分享
云服务器代码: // 云函数入口文件 const cloud = require(‘wx-server-sdk’) cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const {value} = event; try { const res = await cloud.openapi.security.imgSecCheck({ media: { header: {‘Content-Type’: ‘application/octet-stream’}, contentType: ‘image/png’, value:Buffer.from(value) } }) return res; } catch (err) { return err; } } 本地函数: wx.chooseImage({count: 1}).then((res) => { if(!res.tempFilePaths[0]){ return; } console.log(JSON.stringify(res)) if (res.tempFiles[0] && res.tempFiles[0].size > 1024 * 1024) { wx.showToast({ title: ‘图片不能大于1M’, icon: ‘none’ }) return; } wx.getFileSystemManager().readFile({ filePath: res.tempFilePaths[0], success: buffer => { console.log(buffer.data) wx.cloud.callFunction({ name: ‘checkImg’, data: { value: buffer.data } }).then( imgRes => { console.log(JSON.stringify(imgRes)) if(imgRes.result.errorCode == ‘87014’){ wx.showToast({ title:‘图片含有违法违规内容’, icon:‘none’ }) return }else{ //图片正常 } [代码] } ) }, fail: err => { console.log(err) } } ) 我相信做出来的人很多,但是没有分享出来,我今天分享出来就是为了避免更多程序员不要在这种简单的问题上,浪费太多的时间,我就浪费了很多时间,兼职太坑爹了[代码]
2019-07-26 - 插件使用wx.navigateTo的方式跳转不了
- 当前 Bug 的表现(可附上截图) [图片] - 预期表现 两种方式都可以跳转 - 复现路径 - 提供一个最简复现 Demo https://developers.weixin.qq.com/s/Wcok9jm37S5h 备注:插件未发布过
2019-01-10 - 插件中的组件无法使用 wx.navigateTo 跳转插件的页面
BUG 表现插件中的组件无法使用 [代码]wx.navigateTo[代码] 跳转插件的页面,但能使用 [代码]navigator[代码] 标签跳转 期望表现[代码]navigator[代码] 标签与[代码]wx.navigateTo[代码]表现一致,都能正常跳转至插件内的页面 代码片段页面不能跳转,也无任何报错信息 <button type="primary" bind:tap="goto">navigateTo 跳转页面 </button> Page({ goto() { wx.navigateTo({ url: 'plugin-private://appid/pages/index/index' }) } })能正常跳转 <navigator url="plugin-private://appid/pages/index/index"> <button type="primary">navigator 插件页面 </button> </navigator>
2019-03-29 - 如何再微信云函数中使用腾讯云的接口?(身份证卡证识别)
场景:用户上传身份证照片后,把照片保存再云存储,然后通过cloud.getTempFileURL换出这个照片的临时url,把这个url通过云函数传递给腾讯云接口并且得到返回的结果。 已知:通过腾讯云的 API Explorer 得到node环境中请求的代码(附上API Explorer链接:https://console.cloud.tencent.com/api/explorer?Product=ocr&Version=2018-11-19&Action=IDCardOCR&SignVersion= 代码如下:(腾讯云ID和密钥,以及图片url没有写入,替换进去即可)(我已经再node环境中使用过以下代码可以得到满意的结果) 问题:如何把这个代码迁移到微信云函数里去?? const tencentcloud = require("../../../../tencentcloud-sdk-nodejs"); const OcrClient = tencentcloud.ocr.v20181119.Client; const models = tencentcloud.ocr.v20181119.Models; const Credential = tencentcloud.common.Credential; const ClientProfile = tencentcloud.common.ClientProfile; const HttpProfile = tencentcloud.common.HttpProfile; let cred = new Credential("腾讯云ID", "腾讯云密钥"); let httpProfile = new HttpProfile(); httpProfile.endpoint = "ocr.tencentcloudapi.com"; let clientProfile = new ClientProfile(); clientProfile.httpProfile = httpProfile; let client = new OcrClient(cred, "ap-guangzhou", clientProfile); let req = new models.IDCardOCRRequest(); let params = '{"ImageUrl":"这里写入图片URL连接","CardSide":"FRONT"}' req.from_json_string(params); client.IDCardOCR(req, function(errMsg, response) { if (errMsg) { console.log(errMsg); return; } console.log(response.to_json_string()); }); 云函数 // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // exports.main = async (event, context) => { }
2020-01-20 - 「服务平台·疫情服务专区」新增开源代码模板
小程序,一直在行动! 新冠肺炎疫情牵动人心,为便于小程序开发者或服务商能够便捷、高效开发更多便民小程序,「服务平台·疫情服务专区」新增疫情小程序【开源代码模板】,开发者可根据需求接入使用。 同时,欢迎开发者报名入驻专区,开源分享自己开发的疫情小程序模板,为更多的开发者提供参考和支持!我们一同抗击疫情! 「扫码报名入驻」 [图片] 平台将对投稿的作品进行审核和筛选,优秀作品将会直接展示在「服务平台·疫情服务专区」中。平台还将联系并返回修改意见,开发者配合修改优化后,也有机会入驻专区。 请申请入驻专区的开发者详细阅读以下项目规范: 【小程序源码模板规范】小程序源码模板须符合“开源项目规范”、“项目部署手册规范”、“代码规范”,才能报名。 |开源项目规范 以下内容为本次开源项目征集规范 关于开源代码托管你可以根据自己的喜好,选择任一代码托管平台,官方对于代码托管平台不做限制。但,你的项目必须为公开可访问,不得设置任何限制。 关于开源项目许可证本次征集不限制项目的具体许可证。如果您没有明确选择,我们推荐您选择 Apache LICENSE 、MIT LICENSE 等常用许可证。 关于项目必备文件一个开源项目必须包含以下支持文件,确保你的项目完整提供服务: • README.md: 项目的介绍 • LICENSE: 项目的开源许可证 • code-of-conduct.md: 项目的行为准则 • contributing.md: 项目的贡献指南 • deployment.md: 部署说明 • changelog.md: 项目的更新日志 README.md 模版以下是 README.md 的模板,供你参考。 # 项目名称 项目简介 ## 特性 1. 产品特性1 2. 产品特性2 ## 依赖 - 产品运行依赖服务1 - 产品运行依赖服务2 ## 部署说明 这个项目具体的部署参考 deployment.md ## 开发说明 这个项目应该如何开发、如何贡献。 ## Bug 反馈 如果有 Bug ,请通过 XXX 反馈 ## 联系方式(非必须,但建议提供) 有任何问题,可以通过下方的联系方式联系我。 ## LICENSE 你选择的开源协议 |项目部署手册规范作为项目的开发者,你需要为你的用户撰写相应的部署手册,确保用户可以快速的将你的小程序模板应用在其自己的项目中。 必备要素在你的部署手册中,应当包含下述内容,以确保用户可以正常使用你的模板 • 如何下载代码 • 如何将代码导入到开发者工具 • 哪些参数需要修改 • 哪些云函数需要部署 • 涉及到的外部服务 • 云数据库中需要创建哪些数据 • 云存储中需要上传哪些文件 • 小程序后台需要配置哪些 HTTP 安全域名请求 • 小程序后台需要配置哪些服务 非必备要素以下内容非必须,但推荐你提供,以更好的帮助用户使用你的模板 • 数据维护指南 • 联系方式 交付方式推荐使用 PDF 交付部署手册 |代码规范[图片] 微信团队
2020-04-23 - 云开发时执行用户数据update操作和insert(add)操作时的测试用例
在我们使用云开发时,我们经常遇到需要保存用户信息,而如何把记录保存或者使用更新用户的最后一次访问时间则是统计用户活跃性的重要依据。 [代码] 一般用户打开初次打开小程序时,需要用户授权访问用户的信息,后续则不再需要。用户授权后小程序可直接获取用户的信息。 处理用户数据时一般在文件的生命周期函数中处理,个人业务需要。 首先需要获取用户信息。 第二步进行数据查询 第三步根据查询的数据判定当前用户是否为初次访问用户,若是初次访问则执行插入,若不是则执行更新。 下面直接贴出代码。注:person为用户集合名称 [代码] onReady: function() { app.userInfoReadyCallback = res => { app.globalData.userInfo = res.userInfo; console.log(‘初始化页面获取用户信息’, app.globalData.userInfo); var userInfo = res.userInfo; var openid = app.globalData.OPENID;//设置全局参数 //进行用户数据信息处理 wx.cloud.database().collection(“person”).where({ _openid: openid, //当前用户 openid }).get({ success: function(res) { //返回数据进行数据更新或插入 if (res.data.length == 0) {//若无返回数据,则用户是初次访问用户 wx.cloud.database().collection(“person”).add({ data: { avatarUrl: userInfo.avatarUrl, city: userInfo.city, country: userInfo.country, gender: userInfo.gender, language: userInfo.language, nickName: userInfo.nickName, province: userInfo.province, createDate: new Date()//初次访问小程序时间 }, success: function(res) { console.log(“保存数据成功!”,res); }, fail: function(res) { console.log(“保存数据失败!”,res); } }); } else {//若返回数据,则用户是老用户 var _id = res.data[0]._id;//获取需要更新用户数据的集合主键 console.log(“更新用户数据”, _id); wx.cloud.database().collection(“person”).doc(_id).update({ data: { avatarUrl: userInfo.avatarUrl, city: userInfo.city, country: userInfo.country, gender: userInfo.gender, language: userInfo.language, nickName: userInfo.nickName, province: userInfo.province, lastVisitDate: new Date() //最后一次访问时间 }, success: function(res) { console.log(“更新用户数据成功!”,res); }, fail: function(res) { console.log(“更新用户数据失败!”, res); } }); } } }); }; }
2019-11-12 - 为什么我的button按钮修改不了宽高,求各位大神帮忙?
[图片] [图片] [图片]
2019-11-16 - [打怪升级]小程序评论回复和发贴组件实战(一)
[图片] 在学习成长的过程中,常常会遇到一些自己从未接触的事物,这就好比是打怪升级,每次打倒一只怪,都会获得经验,让自己进步强大。特别是我们这些做技术的,逆水行舟不进则退。下面分享下小程序开发中的打怪升级经历~ [图片] 先来看下实际效果图,小程序开发中有时会要做一些的功能复杂的组件,比如评论回复和发帖功能等,这次主要讲的是关于评论模块的一些思路和实战中的经验,希望能抛砖引玉,给大家一些启发,一同成长~ [代码片段]评论回复组件实战demo demo的微信路径: https://developers.weixin.qq.com/s/oHs5cMma7N9W demo的ID:oHs5cMma7N9W 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] [图片] 根据这个demo.gif,本人做了一个简单的流程图,帮助大家理解。下面罗列一些开发中需要“打的怪”: 1、组件目录结构 [代码]├─components ---小程序自定义组件 │ ├─plugins --- (重点)可独立运行的大型模块,可以打包成plugins │ │ ├─comment ---评论模块 │ │ │ │ index.js │ │ │ │ index.json │ │ │ │ index.wxml │ │ │ │ index.wxss │ │ │ │ services.js ---(重点)用来处理和清洗数据的service.js,配套模板和插件 │ └─submit ---评论模块子模块:提交评论 index.js index.json index.wxml index.wxss [代码] 为什么要单独做个评论页面页面(submit)? 因为如果是当前页面最下面input输入的形式,会出现一些兼容问题,比如: 不同手机的虚拟键盘高度不同,不好绝对定位和完全适配 弹窗输入框过小输入不方便,如果是大的textare时,容易误触下面评论的交。 注:目录结构,仅供参考。 2、NODE端API接口返回结构和页面结构 [代码]//node:API接口返回 { "data": { "commentTotal": 40, "comments": [ { "contentText": "喜欢就关注我", //评论内容 "createTime": 1560158823647, //评论时间 "displayName": "智酷方程式", //用户名 "headPortrait": "https://blz.nosdn.127.net/1/weixin/zxts.jpg", //用户头像 "id": "46e0fb0066666666", //评论ID 用于回复和举报 "likeTotal": 2, //点赞数 "replyContents": [ //回复评论 { "contentText": "@智酷方程式 喜欢就回复我", //回复评论内容 "createTime": 1560158986524, //回复时间 "displayName": "神秘的前端开发", //回复的用户名 "headPortrait": "https://blz.nosdn.127.net/1/2018cosplay/fourth/tesss.jpg", //回复的用户头像 "id": "46e0fb00111111111", //回复评论的ID "likeTotal": 2, //回复评论的点赞数 "replyContents": [], //回复的回复 盖楼 "replyId": "46e0fb001ec222222222", //回复评论的独立ID,用于统计 }, { "contentText": "@智酷方程式: 威武,学习学习", "createTime": 1560407232814, "displayName": "神秘的前端开发", "headPortrait": "https://blz.nosdn.127.net/1/2018cosplay/fourth/tesss.jpg", "id": "46e0fb00111111111", "likeTotal": 0, "replyContents": [], "replyId": "46e0fb001ec222222222", } ], "replyId": "", "topicId": "46e0fb001ec3333333", } ], "curPage": 1, //当前页面 //通过ID 判断 当前用户点赞了 哪些评论 "likes": [ "46e0fb00111111111", "46e0fb001ec222222222", "46e0fb0066666666", ], "nextPage": null, //下一页 "pageSize": 20, //一页总共多少评论 "total": 7, //总共多少页面 }, "msg": "success", "status": "success" } [代码] [代码]<!-- HTML 部分 --> <block wx:if="{{commentList.length>0}}"> <!-- 评论模块 --> <block wx:for="{{commentList}}" wx:for-item="item" wx:for-index="index" wx:key="idx"> <view class="commentItem" catchtap="_goToReply" data-contentid="{{item.id}}" data-replyid="{{item.id}}" data-battle-tag="{{item.displayName}}"> <view class="titleWrap"> <image class="logo" src="{{item.headPortrait||'默认图'}}"></image> <view class="authorWrap"> <view class="author">{{item.displayName}}</view> <view class="time">{{item.createTime}}</view> </view> <view class="starWrap" catchtap="_clickLike" data-index="{{index}}" data-like="{{item.like}}" data-contentid="{{item.id}}" data-topicid="{{item.topicId}}"> <text class="count">{{item.likeTotal||""}}</text> <view class="workSprite icon {{item.like?'starIconHasClick':'starIcon'}}"></view> </view> </view> <view class="text"> {{item.contentText}} </view> </view> <!-- 评论的评论 --> <block wx:for="{{item.replyContents}}" wx:for-item="itemReply" wx:for-index="indexReply" wx:key="idxReply"> <view class="commentItem commentItemReply" catchtap="_goToReply" data-contentid="{{itemReply.id}}" data-replyid="{{item.id}}" data-battle-tag="{{itemReply.displayName}}"> ... 和上面类似 </view> </block> </block> <!-- 加载更多loading --> <block wx:if="{{isOver}}"> <view class="more">评论加载完成</view> </block> </block> [代码] 通过node提供一个API接口,通过用户的openId来判断是否点赞,这里提供一个参考的JSON结构。 JSON尽量做成array循环的结构方便渲染,根据ID来BAN人和管理。底部加上加载更多的效果,同时,记得做一些兼容,比如默认头像等。 3、评论中的一些微信原生交互 这里建议很多交互如果不是必须要特别定制,可以才用微信原生的组件,效果和兼容性都有保障,而且方便简单。 对评论进行回复/举报 [代码]<!-- HTML部分 通过绑定事件:_goToReply 进行交互--> <view class="commentItem" catchtap="_goToReply" data-contentid="{{item.id}}" data-replyid="{{item.id}}" data-battle-tag="{{item.displayName}}"> ... 内部省略 </view> [代码] [代码]//JS部分 微信原生wx.showActionSheet 显示操作菜单交互 _goToReply(e) { // 上面的各种授权判断省略... let self = this; wx.showActionSheet({ itemList: ['回复', '举报'], success: function (res) { if (!res.cancel) { console.log(res.tapIndex); //前往评论 if (res.tapIndex == 0) { //判断是否是 评论的评论 self._goToComment(replyid); } //举报按钮 if (res.tapIndex == 1) { //弹出框 self.setComplain(contentid); } } else { //取消选择 } }, fail(res) { console.log(res.errMsg) } }); } //当选择“举报”的时候,二次调用 wx.showActionSheet 方法 setComplain(contentid){ let complainJson = ["敏感信息", "色情淫秽", "垃圾广告", "语言辱骂", "其它"]; wx.showActionSheet({ itemList: complainJson, success: async res => { if (!res.cancel) { //选择好后,提交举报 try { let complainResult = await request.postComplainReport(complainJson[index], openid, contentid); if (complainResult.msg == "success") { //提交成功后反馈 } else { } } catch (e) { console.log(e) } } } }); } [代码] 显示操作菜单 wx.showActionSheet 方法说明 属性 类型 说明 itemList Array.<string> 按钮的文字数组,数组长度最大为 6 itemColor string 按钮的文字颜色 success function 接口调用成功的回调函数 fail function 接口调用失败的回调函数 complete function 接口调用结束的回调函数(调用成功、失败都会执行) 使用这个方法,即是主流的做法,也能很好的兼容不同机型,同时给予用户“习惯性体验”。 原生评论排序切换 [图片] [代码]<!-- picker组件 html部分--> <picker bindchange="bindPickerChange" value="{{index}}" range="{{array}}"> <view class="picker"> 当前选择:{{array[index]}} </view> </picker> [代码] [代码]// js部分 Page({ data:{ //查看评论类型切换 array: ["最佳", "最新", "只看自己"], //选择数组中的第几个显示 index:0 }, bindPickerChange(e) { console.log('picker发送选择改变,携带值为', e.detail.value) this.setData({ index: e.detail.value }) } }) [代码] picker组件是一个从底部弹起的滚动选择器,这里我们用它来切换不同评论的排序。每次切换都可以通过 bindchange获得对应的变化,通过 e.detail.value获取用户选择的索引值。 官方文档: https://developers.weixin.qq.com/miniprogram/dev/component/picker.html 4、传参跳转写评论页 [代码]let uriData = { logo: "xxx.jpg", type: "commentReply", title: "文章:小程序评论,动态发帖开发指北\n 作者:智酷方程式", openId:"xxxxxxxxxxx", replyId:"aaaaaa" //用户回复的是哪个评论的ID }; wx.navigateTo({ url: `/components/plugins/comment/submit/index?uriData=${encodeURIComponent(JSON.stringify(uriData))}` }); [代码] 这个可以用encodeURIComponent的方式处理下参数中的中文,避免跳转发布评论页接收数据时出现乱码。 5、发表评论页 [图片] 显示和控制评论的字数 [代码]<!-- html部分 关于textarea 的配置 --> <view class='feedback-cont'> <textarea auto-focus="true" value="{{replyName}}" maxlength="200" bindinput="textareaCtrl" placeholder-style="color:#999;" placeholder="留下评论,共同学习,一起进步" /> <view class='fontNum'>{{content.length}}/200</view> </view> <view class='feedback-btn' bindtap='commentSubmit'>提交</view> [代码] [代码]// js部分 Page({ data: { //初始化评论内容,如果是回复则通过传参变成 @xxxx的形式 content: "@xxxx", }, textareaCtrl: function (e) { if (e.detail.value) { this.setData({ content: e.detail.value }) } else { this.setData({ content: "" }) } } }) [代码] textarea 在小程序中改动不大,这个标签原有的一些属性都可以继续使用,通过配置maxlength来控制字数,同时,设置auto-focus="true"可以让用户进到这个发表评论页面时自动弹出虚拟键盘和光标定位在输入的区域。 当然,也可以将发表评论和评论展示区域做在一起,这个就要考虑到要么通过“小程序API”获取键盘高度,要么将“发布评论”置顶区域显示,也是可以做的,只是相对考虑的点会多些。当时开发评论组件的时候,考虑开发时间短和用户体验,权衡后,最终决定以上方案,希望能给到大家一些参考和借鉴,在其他组件开发中触类旁通。 总结,“组件化思想”对于无论做小程序、react/VUE还是其他项目来说,减少重复开发,提高复用性都是一个非常重要的点。评论功能其实只要理清楚整体思路,做起来难度并不大,通过一些原生组件,可以大大提高开发效率,同时保证良好的兼容性。 后面一期还将分享下功能点较多的发帖组件开发。 往期回顾: [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二) [填坑手册]小程序目录结构和component组件使用心得
2021-09-13 - 关于 wx.createInnerAudioContext安卓MP3文件不能播放的解决问题
好多人都遇到了 wx.createInnerAudioContext 这个api在开发者工具上或者是iphone上可以播放的MP3文件但是在安卓上报错不能播放的问题 主要是MP3还分各种格式 具体的可以看这个文档 https://blog.csdn.net/datamining2005/article/details/78954367 大家可以按照文中所述 去对比一下 一个安卓能播放的MP3和不能播放的有什么区别 我这边遇到的是文字转语音的需求碰到这个问题 eg: 我们之前是用科大讯飞的接口转化的 MP3 安卓上就不能播放 IOS没问题 之后用百度的就可以了 希望可以帮到你们 第一次写文章 格式什么的不重要 看内容
2019-08-15 - textToSpeech返回的res.filename,在安卓端播放异常
待合成文本: 您好,我是知识助手。\n请按住麦克风说出问题或直接输入问题吧~\n请按住麦克风说出问题或直接输入问题吧?\n请按住麦克风说出问题或直接输入问题吧?\n请按住麦克风说出问题或直接输入问题吧~?\n 成功返回: innerAudioContext.autoplay = true; innerAudioContext.src = res.filename; 经测试,在ios端,返回的网址,可以正常播放,但是,在安卓端有时候可以正常播放,有时候不可以正常播放(播放了一点,播放就报10001错误了); 经排查,发现是返回的网址文件,在安卓端异常,因为,我把网址替换成其他在线音频文件,没有问题。 大致代码如下: plugin.textToSpeech({ lang: "zh_CN", tts: true, content: text, success: function (res) { innerAudioContext.autoplay = true; // innerAudioContext.obeyMuteSwitch = false; innerAudioContext.src = res.filename;
2018-11-18 - 小程序顶部自定义导航组件实现原理及坑分享
为什么使用自定义导航 对比默认导航栏,我们会更需要: 统一Android、IOS手机对于页面title的展示样式及位置 更丰富的导航栏定制效果,如添加home图标等 左上角返回事件的监听处理 统一实现双击返回顶部功能 自定义导航组件实现思路 自定义导航组件实现的核心是需要计算导航栏的真实高度 这里以官方文档->扩展能力中的Navigation组件为例分析实现思路。当使用"navigationStyle": "custom"时,默认导航被移除,页面的开始位置变成了屏幕顶部,这时我们需要实现的导航栏是在状态栏下面。 导航栏的真实高度=状态栏高度+导航栏内容。 [图片] 使用wx.getSystemInfo获取到statusBarHeight便是导航栏的高度,但是导航栏内容高度呢? 有人可能觉得导航栏内容高度顾名思义就是导航栏内容高度啊,内容撑起还用管嘛!要,必须要! 因为右上角胶囊按钮是原生加载的,我们的导航栏内容需要正好贴在胶囊的下方且垂直居中。 导航栏内容高度=(胶囊按钮的顶部距离 - 状态高度)*2 + 胶囊高度 [图片] 如何计算胶囊的数据呢?幸运的是我们有 wx.getMenuButtonBoundingClientRect() 获取胶囊按钮的布局位置信息,那么动态计算导航栏的内容高度就很方便啦。 好了,以上就是动态计算的核心思路,我们再来看官方Navigation组件高度是怎么实现的 [代码]page{--height:44px;--right:190rpx;} .weui-navigation-bar .android{--height:48px;--right:222rpx} .weui-navigation-bar__inner{ position:fixed;top:0;left:0;z-index:5001;display:flex;align-items:center; height:var(--height);padding-right:var(--right);width:calc(100% - var(--right)) } [代码] 导航栏内容的高度是通过- -height这个css变量提前声明好的,安卓机型会重新覆盖为新的css变量值,目前没发现有适配问题。 官方就是官方啊,具体尺寸都知道,那就不用一番计算周折啦,直接拿来主义即可。 导航的布局位置已经搞定啦,剩下就是写具体的内容,不同业务实现需求不同这里就不一一赘述了。 完善官方顶部导航组件 本着拿来主义,直接使用官方Navigation组件,但在实际业务开发中还是遇到不少需要自定义的需求,就比如: loadding样式没实现 标题内容超出没有出现省略号 和原生顶部的样式不兼容,导致单个页面引入时跳转有明显差异出现 没有双击返回顶部功能开关功能 引入页面需要获取导航栏的高度,来控制其他元素距离顶部的位置, 不能根据页面栈数据动态显示隐藏back按钮, 针对以上需求,我们对官方的组件进行二次完善开发,满足常规的自定义需求绰绰有余,直接引入开箱即用。 源码使用示例 https://github.com/YuniorZen/minicode-debug/tree/master/minicode02 [图片] 使用说明 [代码]/*自定义头部导航组件,基于官方组件Navigation开发。*/ <navigation-bar title="会员中心" bindgetBarInfo="getBarInfo"></navigation-bar> [代码] 组件属性列表 属性 类型 描述 bindgetBarInfo function 组件实例载入页面时触发此事件,首参为event对象,event.detail携带当前导航栏信息,如导航栏高度 event.detail.topBarHeight bindback function 点击back按钮触发此事件响应函数 backImage string back按钮的图标地址 homeImage string home按钮的图标地址 ext-class string 添加在组件内部结构的class,可用于修改组件内部的样式 title string 导航标题,如果不提供为空 background string 导航背景色,默认#ffffff color string 导航字体颜色 dbclickBackTop boolean 是否开启双击返回顶部功能,默认true border boolean 是否显示顶部导航下边框分割线,默认false loading boolean 是否显示标题左侧的loading,默认false show boolean 显示隐藏导航,隐藏的时候navigation的高度占位还在,默认true left boolean 左侧区域是否使用slot内容,默认false center boolean 中间区域是否使用slot内容,默认false Slot name 描述 left 左侧slot,在back按钮位置显示,当left属性为true的时候有效 center 标题slot,在标题位置显示,当center属性为true的时候有效 自定义顶部导航目前存在的坑 弹窗的背景蒙层无法覆盖原生胶囊按钮 页面下拉刷新的圆点会被自定义导航遮盖 如果要自定义顶部导航,以上问题避免不了,只能忍着接受。 目前还没遇到完美的解决方案,针对下拉刷新圆点被遮挡的问题微信官方还在需求开发中,如果你有好的想法欢迎留言反馈,一起学习交流。
2019-10-31 - chooseImage上传图片压缩情况分析
[图片] [代码]wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success (res) { // tempFilePath可以作为img标签的src属性显示图片 const tempFilePaths = res.tempFilePaths } }) [代码] 总结下就是在苹果手机下,即使选择了原图,在上传的时候还是会被压缩的。 以上分析建立在网络环境为4G的情况,wifi情况下未验证。 本文是公众号api,小程序api未做实验验证。
2020-02-15 - 高考大数据
[图片] 数据更新至2019-09-23
2019-09-24 - 小程序绘图篇:如何正确地给一个渐变背景按钮绘制阴影
最近在开发一个小程序项目中遇到了一个小的问题。那就是需要给一个背景是渐变色的按钮添加一层阴影,下图中红色线标记的地方。 [图片] 目的是为了让这个按钮更有立体感。刚开始做的时候觉得没有什么难度,在开发工具上也能够很好的展示;但是到了真机测试的时候,才发现原来给按钮绘制阴影的方法在真机上显示不出来。于是就有了下面我寻找解决方案的过程,最终我顺利的解决了这个问题。我把我解决这个问题的思路记录下来,方便我日后回顾,也希望能够跟大家一起交流,我们一起进步。 当我在手机上发现绘制的按钮没有阴影的时候,我就知道接下来我将可能要付出几个小时的时间去解决这个问题。我也做好了心理准备,于是就踏上了寻找解决方案的道路。 第一步:搜索问题 当我遇到这个问题的时候,我的第一反应就是先上网搜索一下看看有没有同学也遇到过这种问题。于是我就搜索小程序canvas绘图与按钮阴影相关的一些问题和资料。我的确找到了一些相关的文章和问题,但是我发现这些资料要么跟自己遇到的问题不太符合,要么搜到的问题还没有回复。 我提取了这些文章和问题中有用的点,得出了下面的结论: 小程序canvas可以绘制圆角阴影 小程序绘制阴影的API有新旧两个版本 没有说明渐变色背景的按钮是否可以直接添加阴影 第二步:分析对比 先来看一下我现在的代码是这样的: [代码]// ... 此处省去部分代码 // 绘制渐变色 _ctxc.beginPath() _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2, btn_h + r, r, 0.5 * Math.PI, 1.5 * Math.PI) // 绘制右边圆弧 _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2, btn_h + r, r, -0.5 * Math.PI, 0.5 * Math.PI) // 闭合路径 _ctxc.closePath() // 添加渐变色 const grd = _ctxc.createLinearGradient((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2 - r, btn_h, (SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2 + r, btn_h) grd.addColorStop(0, first_color) grd.addColorStop(1, second_color) _ctxc.setFillStyle(grd) // 设置阴影 _ctxc.shadowOffsetX = 2 _ctxc.shadowOffsetY = 2 _ctxc.shadowColor = '#43454c' _ctxc.shadowBlur = 5 // 填充渐变色 _ctxc.fill() // 取消阴影 _ctxc.shadowOffsetX = 0 _ctxc.shadowOffsetY = 0 _ctxc.shadowColor = '#ffffff' _ctxc.shadowBlur = 0 // 绘制中间的文案 _ctxc.font = `normal normal bold 36px sans-serif` _ctxc.fillStyle = '#ffffff' _ctxc.setTextBaseline('middle') _ctxc.setTextAlign('center') _ctxc.fillText(`立即打卡`, SHARE_TO_CHAT_CANVAS_WIDTH / 2, btn_h + r) // 绘制 _ctxc.draw() [代码] 现在的代码在模拟器上可以完美的绘制出我想要的结果,如下图 [图片] 但是在真机上不行。我们可以看到在开发工具上是有阴影效果的,但是在真机上的效果却是没有阴影的。如下图 [图片] 所以我要做的就是逐步排查,要依次确定下面几个问题的答案: 问题一:看一下官方文档给出的代码在真机上能不能够显示出来阴影。 官方文档给出的代码如下: [代码]const ctx = wx.createCanvasContext('myCanvas') ctx.setFillStyle('red') ctx.setShadow(10, 50, 50, 'blue') ctx.fillRect(10, 10, 150, 75) ctx.draw() [代码] 我们直接测试一下在编辑器和手机上的效果,编辑器上的效果如下: [图片] 模拟器上是可以显示效果的,真机上的效果如下: [图片] 也是可以显示的,这说明在真机上的确是可以绘制出来阴影的。 但是我同时也注意到,这个实例的代码使用的是老的API。 [图片] 所以我再次尝试把老的API替换成新的API进行测试 代码修改如下: [代码] // ...省略部分代码 const ctx = wx.createCanvasContext('share-to-chat') ctx.setFillStyle('red') // ctx.setShadow(10, 50, 50, 'blue') // 使用新的API ctx.shadowOffsetX = 10 ctx.shadowOffsetY = 50 ctx.shadowColor = 'blue' ctx.shadowBlur = 50 ctx.fillRect(10, 10, 150, 75) ctx.draw() [代码] 这次修改之后,在编辑器上绘图没有发生什么变化。但是,在真机上却显示不出来了,只有一个红色的矩形,如下图所示: [图片] 所以到这里我们可以知道,使用新的绘制阴影的API在我这台真机上是绘制不出来阴影的。那我们就先使用老的API进行阴影的绘制,到这里这个问题已经有了一点进展了,我们继续进行排查。 问题二:是否能够绘制出来圆角的阴影 我们上面的测试绘制的阴影是矩形的阴影,如果我们绘制的是一个圆弧呢?还能绘制出来阴影吗?我们接着向下走。这次我们先绘制一个圆形,然后再给这个圆添加一个阴影。那么代码如下: [代码]// ...省略部分代码 const ctx = wx.createCanvasContext('share-to-chat') ctx.arc(100, 75, 50, 0, 2 * Math.PI) ctx.setFillStyle('red') ctx.setShadow(10, 50, 50, 'blue') // ctx.shadowOffsetX = 10 // ctx.shadowOffsetY = 50 // ctx.shadowColor = 'blue' // ctx.shadowBlur = 50 ctx.fill() ctx.draw() [代码] 这次在编辑器上的效果如下: [图片] 在手机上的效果如下: [图片] 也是可以的,那么按道理来说,如果我们使用老版本绘制阴影的API的话,我们绘制的这个圆形按钮也应该能够显示出来阴影。来我们来试一试吧。 修改最初的代码如下: [代码] // ...省略部分代码 _ctxc.setFillStyle(grd) // 设置阴影 // _ctxc.shadowOffsetX = 2 // _ctxc.shadowOffsetY = 2 // _ctxc.shadowColor = '#43454c' // _ctxc.shadowBlur = 5 // 使用老的API _ctxc.setShadow(2, 2, 5, '#43454c') // 填充渐变色 _ctxc.fill() // ...省略部分代码 [代码] 在编辑器上还是可以正常绘制阴影的,但是在手机上还是不能够显示阴影。 那么问题出现在哪里呢?这时候就要找能够绘制出来阴影和不能够绘制出来阴影这两种情况我们改变了什么。 问题3:在绘制渐变色背景按钮的同时是否可以绘制阴影 想到这里,我就猜想会不会是因为我绘制的圆形按钮使用的是渐变色背景的原因,导致我们没有办法绘制出来阴影呢?还是要实践一下,继续修改代码。这次我不使用渐变色背景,而改为使用纯色背景来测试一下。修改代码如下: [代码]// ...省略部分代码 // 添加渐变色 // const grd = _ctxc.createLinearGradient((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2 - r, btn_h, (SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2 + r, btn_h) // grd.addColorStop(0, first_color) // grd.addColorStop(1, second_color) // _ctxc.setFillStyle(grd) // 使用纯色背景 _ctxc.setFillStyle('#4DAEFE') // 设置阴影 // _ctxc.shadowOffsetX = 2 // _ctxc.shadowOffsetY = 2 // _ctxc.shadowColor = '#43454c' // _ctxc.shadowBlur = 5 // 使用老的API _ctxc.setShadow(2, 2, 5, '#43454c') // 填充渐变色 _ctxc.fill() // ...省略部分代码 [代码] 这次在编辑器的显示如下图: [图片] 是有阴影的,那么在手机上效果如何呢?我们看一下: [图片] 很棒,这次终于在真机上也绘制出来的阴影了;距离我们成功也不远啦。 那么我们接下来要解决的问题就是,如何能够把阴影和渐变色背景都添加上去,这一次我们鱼和熊掌都想得到。 那怎么办呢,办法总比问题多,我们只需要在绘制好阴影的基础上再绘制一次渐变的背景就可以了。 就是先绘制一个纯色的带有阴影的按钮,然后在同样的位置我们再次绘制一个有渐变背景色的按钮,最后绘制文字就好啦。真是机智如我呀。最终的代码如下: [代码]// ...省略部分代码 // 开始绘制 _ctxc.beginPath() // 绘制左边圆弧 _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2, btn_h + r, r, 0.5 * Math.PI, 1.5 * Math.PI) // 绘制右边圆弧 _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2, btn_h + r, r, -0.5 * Math.PI, 0.5 * Math.PI) // 绘制中间的渐变矩形 _ctxc.closePath() _ctxc.setFillStyle('#000') // 设置阴影 // 新版本添加阴影 _ctxc.shadowOffsetX = 5 _ctxc.shadowOffsetY = 5 _ctxc.shadowColor = '#a5a6a7' _ctxc.shadowBlur = 5 // 旧版本添加阴影 _ctxc.setShadow(5, 5, 5, '#a5a6a7') _ctxc.fill() // 重置取消阴影 // 新版本重置阴影 _ctxc.shadowOffsetX = 0 _ctxc.shadowOffsetY = 0 _ctxc.shadowColor = '#ffffff' _ctxc.shadowBlur = 0 // 旧版本重置阴影 _ctxc.setShadow(0, 0, 0, '#ffffff') // 绘制渐变色 _ctxc.beginPath() _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2, btn_h + r, r, 0.5 * Math.PI, 1.5 * Math.PI) // 绘制右边圆弧 _ctxc.arc((SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2, btn_h + r, r, -0.5 * Math.PI, 0.5 * Math.PI) // 关闭路径 _ctxc.closePath() const grd = _ctxc.createLinearGradient((SHARE_TO_CHAT_CANVAS_WIDTH - w) / 2 - r, btn_h, (SHARE_TO_CHAT_CANVAS_WIDTH + w) / 2 + r, btn_h) grd.addColorStop(0, first_color) grd.addColorStop(1, second_color) _ctxc.setFillStyle(grd) _ctxc.fill() // 立即打卡的文案字体大小 _ctxc.font = `normal normal bold 40px sans-serif` _ctxc.fillStyle = '#ffffff' _ctxc.setTextBaseline('middle') _ctxc.setTextAlign('center') _ctxc.fillText(`立即打卡`, SHARE_TO_CHAT_CANVAS_WIDTH / 2, btn_h + r) // 绘制 _ctxc.draw() // ...省略部分代码 [代码] 让我们来看一下在手机上的效果吧: [图片] 这一次终于完美地解决了这个问题,开心。 还有,一些同学可能注意到,我上面的代码在添加阴影的时候新版本和老版本的API我都使用了,主要是因为我能够测试的手机比较少,不知道在别的品牌的手机上表现如何。所以为了保险起见,我两种方法都使用了。 还有一点需要注意的是,我本次实践的环境是:基础调试库 2.3.0,手机型号 iPhone 8 最终这个带阴影的有渐变背景色的按钮就完成啦。 第三步:总结与思考 关于小程序绘图这次遇到的问题我们可以总结一下:遇到在编辑器和真机表现不一样的情况,我们首先应该先上网查一下看看小伙伴们有没有遇到类似的问题,如果人家已经有比较好的解决方案,我们就可以参考别人的解决方案去解决我们自己遇到的问题,这样会比较省时省力。如果没有的话,我们就需要自己去逐步排查问题,不断地缩小出现问题的范围,最终定位出出现问题的地方;然后再去想办法解决。 其实,如果想达到这样的效果,我们不一定非得通过代码绘制去实现这么一个功能;也可以直接让设计师把按钮这一部分导出一张小的图片来,我们直接把这个包含阴影的按钮图片绘制在图上就好了。如果我们这个按钮是不经常发生变化的话,那么直接把包含阴影按钮的图片绘制在画布上也许是挺好的一个解决方法。所以,我们首先要考虑的是我们的代码想要达到什么样的效果,那么达到这样的效果有哪些途径,各有什么优缺点。然后我们选择一个最佳的方案去开发就可以啦。 这篇文章到这里就结束啦,希望我的分享能够帮助到一些遇到同样绘图问题的小伙伴。最后一点私心,推荐一下我们开发的这一款小程序主线程。 [图片] 一个可以帮助大家规划时间和任务,并且可以组队学习找伙伴一起打卡的小程序,如果你觉得的不错的话,记得帮我们分享一下哟。
2019-10-28 - 微信人脸核身接口能力
一、能力背景 近年来,国家在医疗挂号、APP注册、快递收寄、客运、运营商等多领域规定,需要用户实名才可办理业务,预计后续也会有越来越多的此类法规。因此,微信参照公安部“互联网+”可信身份认证服务平台标准,依托腾讯公司及微信的生物识别技术,建立微信“实名实人信息校验能力” ,即通过人脸识别+权威源比对,校验用户实名信息和本人操作(简称微信人脸核身)。 目前接口限定主体及行业类目开放公测,提供给资质符合要求的业务方,在合适的业务场景内使用。目前仅支持持二代身份证的大陆居民。 由于人脸核身功能涉及到用户的敏感、隐私信息,因此调用此接口的小程序,需要满足一定的条件。即:小程序的主体以及类目,需要在限定的类目范围内,且与小程序的业务场景一致。开展的业务也需要是国家相关法规、政策规定的需要“实名办理”的相关业务(其他未在范围内的业务,则暂不支持)。 以下为接口接入及开发的详细内容。如开发中遇到任何疑问,可以点击此处通过社区反馈,将有工作人员跟进回复。 文档第四部分【再次获取核验结果api】,有助于提高业务方安全性,请务必接入! 现阶段微信人脸核验能力,针对小程序,开放的主体类目范围包含: 小程序一级类目 小程序二级类目 小程序三级类目 使用人脸核验接口所需资质 物流服务 收件/派件 / 《快递业务经营许可证》 物流服务 货物运输 / 《道路运输经营许可证》(经营范围需含网络货运) 教育 学历教育(学校) / (2选1):1、公立学校:由教育行政部门出具的审批设立证明 或 《事业单位法人证书》;2、私立学校:《民办学校办学许可证》与《民办非企业单位登记证书》 医疗 公立医疗机构 / 《医疗机构执业许可证》与《事业单位法人证书》 医疗 互联网医院 / 仅支持公立医疗机构互联网医院(2选1):1、卫生健康部门的《设置医疗机构批准书》;2、 《医疗机构执业许可证》(范围均需含“互联网诊疗”或名称含“互联网医院”等相关内容 医疗服务 三级私立医疗机构 / 仅支持三级以上私立医疗机构,提供《医疗机构执业许可证》、《营业执照》及《医院等级证书》 政务民生 所有二级类目 / 仅支持政府/事业单位,提供《组织机构代码证》或《统一社会信用代码证》。 金融业 银行 / (2选1):1、《金融许可证》; 2、《金融机构许可证》。 金融业 信托 / (2选1):1、《金融许可证》; 2、《金融机构许可证》。 金融业 公募基金 / (4选1):1、《经营证券期货业务许可证》且业务范围必须包含“基金”;2、《基金托管业务许可证》; 3、《基金销售业务资格证书》;4、《基金管理资格证书》。 金融业 证券/期货 / 《经营证券期货业务许可证》 金融业 保险 / (8选1):1、《保险公司法人许可证》;2、《经营保险业务许可证》;3、《保险营销服务许可证》;4、《保险中介许可证》;5、《经营保险经纪业务许可证》;6、《经营保险公估业务许可证》或《经营保险公估业务备案》;7、《经营保险资产管理业务许可证》 ;8、《保险兼业代理业务许可证》。 金融业 消费金融 / 银监会核准开业的审批文件与《金融许可证》与《营业执照》 金融业 汽车金融/金融租赁 / 仅支持汽车金融/金融租赁主体,同时提供:1、《营业执照》(公司名称包含“汽车金融” /“金融租赁”;营业范围包含“汽车金融”/“金融租赁”业务);2、《金融许可证》或银保监会及其派出机构颁发的开业核准批复文件。 交通服务 网约车 快车/专车/其他网约车 (自营性网约车)提供《网络预约出租汽车经营许可证》。(网约车平台)提供与网约车公司的合作协议以及合作网约车公司的《网络预约出租汽车经营许可证》。 交通服务 航空 / (航司)提供《公共航空运输企业经营许可证》。(机场)提供《民用机场使用许可证》或《运输机场使用许可证》。 交通服务 公交/地铁 / 提供公交/地铁/交通卡公司《营业执照》 交通服务 水运 / (船企)提供《水路运输许可证》。(港口)提供《港口经营许可证》 交通服务 骑车 / 仅支持共享单车,提供共享单车公司《营业执照》 交通服务 火车/高铁/动车 / 仅支持铁路局/公司官方,提供铁路局/公司《营业执照》 交通服务 长途汽车 / (2选1):1、《道路运输经营许可证》(经营范围需含客运);2、官方指定联网售票平台(授权或协议或公开可查询文件)。 交通服务 租车 / 运营公司提供《备案证明》与对应公司《营业执照》,且营业执照中包含汽车租赁业务 交通服务 高速服务 / 仅支持ETC发行业务,(2选1):1、事业单位主体,需提供《事业单位法人证书》;2、官方指定的发行单位(一发单位),需提供“官方授权或协议,或公开可查询的文件”; 生活服务 生活缴费 / (供电类)提供《电力业务许可证》与《营业执照》,且《营业执照》且经营范围含供电。(燃气类)提供《燃气经营许可证》与《营业执照》,且《营业执照》且经营范围含供气。(供水类)提供《卫生许可证》与《营业执照》。(供热类)提供《供热经营许可证》与《营业执照》,且《营业执照》且经营范围含供热。 IT科技 基础电信运营商 / (2选1):1、基础电信运营商:提供《基础电信业务经营许可证》;2、运营商分/子公司:提供营业执照(含相关业务范围)。 IT科技 转售移动通信 / 仅支持虚拟运营商,提供《增值电信业务许可证》(业务种类需含通过转售方式提供移动通信业务) 旅游服务 住宿服务 / 仅支持酒店,提供《酒店业特种行业经营许可证》 商业服务 公证 / 仅支持公证处,提供《公证处执业许可证》或《事业单位法人证书》 社交 直播 / (2选1):1、《信息网络传播视听节目许可证》;2、《网络文化经营许可证》(经营范围含网络表演)。 如对以上类目或资质有疑问,可点击参考小程序“非个人主体开放的服务类目”,详细了解小程序开放的服务类目及对应资质。 二、准备接入 (请在小程序发布后,再提交人脸核身接口申请) 满足第一节中描述的类目和主体的小程序,可申请微信人脸核验接口。目前微信人脸核身接口已改为线上自助申请方式,需按照如下图例指引,进行接口申请: 第一步:请通过mp.weixin.qq.com登录小程序账号在后台“功能-人脸核身”的路径,点击开通按钮—— [图片] 第二步:仔细查阅《人脸识别身份信息验证服务条款》后,点击“同意并下一步”—— [图片] 第三步:请正确填写服务信息,并上传该小程序类目下所要求的资质—— [图片] 第四步:请按照业务实际需求填写使用人脸接口的场景和用途—— [图片] 第五步:请完善测试信息和联系人—— [图片] 第六步:提交后请耐心等待1-3个工作日的审核期,审核结果将以站内信通知—— 如申请期间遇到问题,可联系腾讯工作邮箱 wx_city@tencent.com,将会有相关工作人员进一步指引。 三、接口文档: (一)接口描述 名称: wx.startFacialRecognitionVerify(OBJECT) 功能:请求进行基于生物识别的人脸核身 验证方式:在线验证 兼容版本: 一闪:android 微信7.0.22以上版本, iOS 微信7.0.18以上版本 建议在微信官网升级至最新版本 (二)参数说明 1、OBJECT参数说明: 参数 类型 必填 说明 name String 是 姓名 idCardNumber String 是 身份证号码 success Function 否 调用成功回调 fail Function 否 调用失败回调 complete Function 是 调用完成回调(成功或失败都会回调) 2、CALLBACK返回参数 参数 类型 说明 errMsg String 错误信息 errCode Number 错误码 verifyResult String 本次认证结果凭据,第三方可以选择根据这个凭据获取相关信息 注 1:传递用户姓名和身份证有两种方式 业务方没有用户实名信息,用户需要在前端填写身份证和姓名,那么前端直接通过jsapi 调用传递 name 和 idCardNumber。 业务方已经有用户实名信息,后台通过微信提供的 api(详情见文档后面“上传姓名身份证后台 api”)上传用户身份证姓名和身份证,api 返回 user_id_key 作为凭证传给前端,前端再调用 jsapi,用户姓名、身份证信息不需要经过前端,参数只需要传递 userIdKey。Tips:使用该功能需要小程序基础库版本号>=1.9.3。 3、回调结果说明 回调结果请参考以下释义: [图片] [图片] [图片] 4、示例代码 [图片] [图片] (三)上传用户姓名身份证的后台api 1、API说明 1.1说明 业务方上传用户姓名和身份证,获取用户凭证,把凭证给到前端通过 jsapi 调用。 Tips :使用该功能需要小程序基础库版本号>=1.9.3。 1.2请求URL https://api.weixin.qq.com/cityservice/face/identify/getuseridkey?access_token={ac cess_token} 1.3请求方式 POST 2、请求数据格式 [代码]Json { "name" : “张三”, "id_card_number" : "452122xxxxxxx43215" } [代码] 请求示例 [代码]#!/bin/bash TOKEN='xxxxxxxxxxxx' URL='https://api.weixin.qq.com/cityservice/face/identify/getuseridkey' JSON='{ "name": "张三", "id_card_number": "452344xxxxxxxxxxxxx234"}' curl "${URL}?access_token=${TOKEN}" -d "${JSON}" [代码] 参数说明 json 字段 中文显示 是否必传 name 姓名 是 id_card_number 身份证号码 是 out_seq_no 业务方唯一流水号 否 3、返回数据 参数 类 型 说明 errcode int 错误码 errmsg string 错误信息 user_id_key string 用于后台交互表示用户姓名、身份证的凭证 expires_in uint32 user_id_key 有效期,过期需重新获取 [代码]{ "errcode" : 0, "errmsg" : "ok", "user_id_key" : "id_key_xxxx", "expires_in": 3600 } [代码] 4、后台消息推送 如果业务方传入out_seq_no,核身完成后会通过消息推送回调给业务方的服务器,如果回调业务方失败,会在5s尽力推送,超过5s不再推送。 参数说明 参数 类 型 说明 ToUserName string 小程序原始ID FromUserName string 事件消息openid CreateTime uint32 消息推送时间 MsgType string 消息类型 Event string 事件类型 openid string 核身用户的openid out_seq_no string 业务方唯一流水号 verify_result string 核身返回的加密key(凭据) 返回示例 [代码]{ "ToUserName": "gh_81fxxxxxxxx", "FromUserName": "oRRn15NUibBxxxxxxxxx", "CreateTime": 1703657835, "MsgType": "event", "Event": "face_identify", "openid": "oRRn15NUibBxxxxxxxxx", "out_seq_no": "test1234", "verify_result": "XXIzTtMqCxwOaawoE91-VNGAC3v1j9MP-5fZJxv0fYT4aGezzvYlUb-n6RWQa7XeJpQo0teKj8mGE4ZcRe1JI3GqzADBYORBu613rKjKAFfEXTXw_bu1bs7MnmPOpguS" } [代码] 四、再次获取核验结果api 此接口是前端完成人脸核身后,基于前端返回的凭据,通过后台api再次进行核验结果和身份信息的校验,有助于提高安全性,请务必接入! 前端获取结果不可信,存在被篡改的风险,为了保障请求结果安全性,请务必对identify_ret、id_card_number_md5、name_utf8_md5字段进行校验! (一)API说明 1、说明 人脸核身之后,开发者可以根据jsapi返回的verify_result向后台拉取当次认证的结果信息。 2、请求URL https://api.weixin.qq.com/cityservice/face/identify/getinfo?access_token={access_token} 3、请求方式 POST 4、请求格式 json (二)请求数据说明 1、请求 参数 类型 是否必填 描述 verify_result String 是 jsapi返回的加密key(凭据) 2、数据返回 HTTP 头如下 Date: Mon, 06 Feb 2017 08:12:58 GMT Content-Type: application/json; encoding=utf-8 Content-Length: 85 Connection: close json示例 [代码]{ "errcode" : 0, [代码] [代码]"errmsg" : "ok", "identify_ret" : 0, "identify_time" : 1486350357 "validate_data": "8593" [代码] [图片] (三)返回参数说明 1、返回参数 注:errcode和identify_ret同时为0,代表本次认证成功。 参数 类型 描述 errcode int 错误码, 0表示本次api调用成功 errmsg string 本次api调用的错误信息 identify_ret int 人脸核身最终认证结果 identify_time uint32 认证时间 validate_data string 用户读的数字(如是读数字) openid string 用户openid user_id_key string 用于后台交互表示用户姓名、身份证的凭证 finish_time uint32 认证结束时间 id_card_number_md5 string 身份证号的md5(最后一位X为大写) name_utf8_md5 string 姓名MD5 2、错误码对应信息 errcode 备注 84001 非法identity_id 84002 用户信息过期 84003 用户信息不存在 五、小程序辅助接口:检查设备是否支持人脸检测 1、接口名称 接 口 :wx.checkIsSupportFacialRecognition(OBJECT) 功能:检查设备是否支持人脸检测 2、接口说明和使用 小程序调用该接口,可以检测当前手机设备是否具备支持人脸检测的能力,可与以上接口分开使用,为了用户体验,建议调用后对手机设备不支持的用户做对应功能处理。 3、接口说明和使用 01 OBJECT 参数说明: 参数 类型 是否必填 描述 success Function 否 调用成功回调 fail Function 否 调用失败回调 complete Function 是 调用完成回调(成功或失败都会回调) checkAliveType Number 否 人脸核验的交互方式,默认读数字(见表 2) 表 2:checkAliveType 的值和对应的解释: 参数 解释 2 先检查是否可以屏幕闪烁,不可以则自动为读数字 02 CALLBACK 返回参数 参数 类型 说明 errMsg Boolean 错误信息 errCode Number 错误码 03 回调结果说明 回调类型 ErrCode 说明 sucess 0 支持人脸采集 fail 10001 不支持人脸采集:设备没有前置摄像头 fail 10002 不支持人脸采集:没有下载到必要模型 fail 10003 不支持人脸采集:后台控制不支持 回调结果说明仅对Android生效,iOS不返回errcode。 04 示例代码 [图片] 六、安全性说明 为保障业务可用性以及安全性,请详细研读微信人脸核身接口相关基础说明及安全说明文档:https://docs.qq.com/doc/DTFB0YWFIdGV6amly 备注:如开发中遇到任何疑问,可以点击此处通过社区反馈,将有工作人员跟进回复。 七、案例展示及补充说明 安徽医科大学第二附属医院,微信人脸核验登录: 安徽医科大学第二附属医院,是三级甲等综合医院。其小程序为用户提供挂号、门诊费用、住院费用、检查报告、体检等医疗服务,同时也提供停车、餐饮等便民服务,是医疗小程序中完整的案例。 小程序使用了微信人脸核验能力作为登录的核验。满足医院管理要求,也满足国家对于实名就医的管理规则。 案例实现的截图效果如下: [图片] [图片] 针对近期少数小程序方面反馈的两类问题,也在本课程进行补充说明。 1、本接口的开放范围,即:可支持的主体类目,是否可以扩大? 说明:基于本接口整体使用范围的评估、相关法规的参考、监管策略的理解执行等,暂时未立刻进行扩大开放范围的工作。 但我们会持续基于不同行业的法规、政策及监管要求等,逐一进行研究考量,以便确认如何扩大开放范围。 2、小程序如果涉及用户本人的生物特征采集,(如本人人脸照片、人脸视频),或涉及采集用户本人生物特征信息并开展人脸核验功能,则存在被驳回的情况? 说明:近两年“人脸识别”技术在社会上掀起了热潮。人脸识别虽然作为摆脱“中间媒介”或“承载载体”的一种直接技术手段,解决了部分政务、交通、医疗、零售等证明“操作者是本人”的问题,但也因此,引入了新的更大的安全风险。 一是,虚假安全风险。 身份认证领域的安全三因素包括“我知道什么”、“我拥有什么”、“我的特征是什么”,通用的安全做法,是要双因素认证(2FA),人脸识别技术如仅凭“我的特征是什么”这一个因素,则容易被攻破或利用。表象给用户以安全的感觉,但实际并不能达到安全效果。 二是,信息泄漏的风险。 越来越多的组织或个人,在并非必需用户敏感信息、生物特征的情况下,采集并存储此类信息。在信息加密、传输、存储过程中,容易暴漏更多的网络节点,使得此类信息有更大的风险被网络黑客拦截、窃听、窃取,或直接被脱库。 三是,消除风险的难度大。 以往基于“中间媒介”或“承载载体”的方式,如出现丢失、被冒用、恶意盗用等风险,可以通过挂失、更换、使用新载体或新媒介等方式,快速排除一定的风险。C端主动,B端主动,都能解决一部分问题。但人脸识别做为更直接的方式,一旦出现冒用、盗用,受害者将面临更大的财产及人生安全风险,且C端用户更多时候无法主动消除风险。 基于以上问题风险,加之国家出台《网络安全法》、《用户隐私保护条例》等法律法规标准,网信办、公安部、工信部及市场监管总局等四部委发起的app获取隐私整治,结合平台安全、用户敏感隐私信息保护要求及监管,针对部分暂无相关法规或要求,需要采集或生物认证方式进行身份核验的,或以“追热点”或“尝鲜”为目的,采集用户生物特征或进行身份核验的,进行严格审核,必要时不予以支持。
星期一 15:19 - 【分享】小程序全景图片展示的几个方案
概述 以下方案均需要有全景照片后方可实现(自己拍的 or 网上下载)。 一、方案一:自建网页 自建网页,自己有服务器,可以用全景图转换器(如pano2vr)直接生成html代码,然后通过 webview 嵌入到小程序访问。 建议:图片可以放在七牛云或其他地方,CDN 能有效优化网页中全景图的打开速度(一般全景图片体积都是较大的)。 体验效果: [图片] 二、方案二:720yun 使用 720云,这也是大部分全景摄影社或爱好者最习惯用的平台了。他们也提供了小程序打开全景图的方案。但核心还是使用 webview,并且需要开通会员,具体参考: 建议:经费足的可以考虑一下这个方案,毕竟720云的操作和体验是真的是十分优秀的! 参考:小程序校验指南 | 720yun https://bbs2.720yun.com/article?id=687 [图片] 三、方案三:小程序插件 以上两种方案都是借助webview来实现,也就是说必须要企业或其他单位的主体才能使用。个人的小程序如果要实现全景,建议使用这位大佬写的小程序插件——wxPano。项目一直在不断更新中,而且还免费,很值得期待! 建议:①该插件限制全景图片分辨率需在2048*1024及以下,因此无法打开画质很高清的全景图片。②插件代码包超过1MB,对小程序打开速度有微小的影响。 链接:https://mp.weixin.qq.com/wxopen/pluginbasicprofile?action=intro&appid=wx386c038238531f87 [图片] 结语 以上来自我自己开发时的一些经验,欢迎前辈老师们补充。 也欢迎社区三连——点赞收藏关注!!
2019-10-24 - 要求小程序添加社交-陌生人交友/熟人社交,审核问题?
公司开发的小程序,只是为客户提供注册和发表自己的动态,相当于论坛的功能,并没有涉及到交友这个类别!里边的,打招呼和喜欢只是关注的动作,没有时时沟通!我们没有提供给客户跟陌生人时时交流的入口。 [图片] [图片]
2019-10-22 - 定时后的订阅消息,能够为企业产生多大价值?
10 月 12 日,微信官方发布消息,将使用一次性订阅消息和长期订阅消息,对于广大开发者而言,半喜半忧。喜的是长期订阅消息终于发布了,以后可以以更加简单的方式去通知用户了,而无需再使用大量的表单去收集 Form ID,担心 FormID 失效无法使用了。 而昨天,云开发发布了新的能力,支持在定时器触发的云函数中调用订阅消息,这个能力的开放,对于广大企业和开发者来说,可以以更低的成本,来接入订阅消息。 长期订阅消息能够为企业带来什么? 一直以来,小程序的模板消息都饱受批评,原因是其限制过于死板,例如一个 FormID 只能使用一次,而微信支付的 prepayId 也只能推送两次消息。而时间上,FormID 仅能在 7 天内使用,超过 7 天就必须重新获取,使得借助模板消息来促活的工作变得十分困难。 此外,一些合情合理的场景,也因为模板消息本身的限制,而不得不冒着被封号的风险,使用大量采集 FormID 的方式,来实现通知用户的目的。 新的长期订阅消息,给了企业和开发者一个机会,可以名正言顺的触达到用户,当然,长期订阅消息的开放也并非完全开放给所有开发者,目前仅为政务民生、医疗、交通、金融、教育等线下公共服务开放,后续会逐渐支持到其他类目的服务。 长期订阅消息 x 政务民生 ##对于政务民生领域的小程序来说,如何将自己原有的业务与长期订阅消息相结合,以提升原本流程中低效的部分,进而更好的服务大众,是十分重要的。 这里,我们以活动比较多的工会为例,对于各基层工会来说,工作的一大重心是带领工会成员组织各项党建活动。那么对于工会的场景来说,原本需要口口相传的活动通知方式,可以转为由小程序统一发布长期订阅消息,进一步来说,工会的小程序可以由统一发布订阅消息,加入一些自定义关注的能力,工会成员可以自行关注自己感兴趣的活动类型,并根据活动情况进行选择性的推送,让工会成员可以快速获取关键信息。 ##此外,一些流程较长的业务,例如房产证过户,需要涉及多个部门、多个单位办理时,可以借助于小程序的长期订阅消息,来实现流转状态的通知,这样可以在一个单位办结的时候,主动提醒群众,前往柜台领取资料等。 长期订阅消息 x 医疗 ##我们去医院诊疗时,多数情况下都不是一蹴而就的,可能需要定期去复查、购药等,在此场景下,往往会有病患在完成了第一次诊疗以后即忘记或记错第二次诊疗的时间而错过复查的情况发生,而小程序的长期订阅消息的诞生有望有效解决这一痛点,开发者可以借助长期订阅消息,实现面向医疗场景下的定期提醒,例如说,定期提醒患者吃药、定期提醒患者前往医院复查、定期提醒患者购买新药等等。引入了长期订阅消息以后,医生和医院可以以更低的成本,来完成诊断结果的落地,借助种种提醒,帮助患者早日康复。 ##此外,小程序长期订阅消息还能在医疗活动的其他流程中发挥效用,例如病患可以在小程序中关注某位专家,以及时获取专家坐诊时间等。 长期订阅消息 in 云开发 为了帮助开发者更加平滑的完成从模板消息到一次性订阅消息,以及新需求长期订阅消息的接入,云开发团队在订阅消息发布的第一时间,就实现了对订阅消息的支持。更是在随后的几天里实现了定时触发器情况下的长期订阅消息的触发。 对于开发者来说,实现长期订阅消息的成本,从过去需要数百行代码,自行维护 AccessToken,转变为无需维护 AccessToken 和只需一行代码就可以完成调用。 如何在自己的小程序中实现长期订阅消息 想要在自己的小程序中实现长期订阅消息,则需要符合以下两个条件: 企业帐号开通的小程序:目前长期订阅消息仅支持企业小程序帐号,个人小程序仅能使用一次性订阅消息。 政务民生、医疗、交通、金融、教育等类目:目前长期订阅消息仅针对一些公共服务场景提供,如果你不是对应场景,也无法申请相关的模板。 如果你满足上面的要求,则可以继续进行后续的操作, 在微信小程序后台添加长期订阅消息模板,并复制其模板 ID ,将其放在你的云函数中。 在你的云函数中配置模板所需要的关键词等信息 配置云函数的定时触发器,由定时触发器自行完成触发,并发送订阅消息给用户。 ##在使用的时候,你需要注意,一般而言,为了确保我们的消息可以被准时发出,我们的定时触发器一般定为每分钟触发一次。 长期订阅消息 x More 对于存在的订阅消息的场景来说,他们都会需要用到长期订阅消息,通过和现有行业的结合,长期订阅消息将获得更多不一样的能力。
2019-10-22 - 微信小程序echarts中地图加载文件过大问题?
微信小程序echarts中地图行政区划数据加载文件过大问题: 解决方案 1、分包处理 2、压缩处理 [代码]var[代码] [代码]UglifyJsPlugin = require([代码][代码]'uglifyjs-webpack-plugin'[代码][代码])[代码][代码]new[代码] [代码]UglifyJsPlugin({sourceMap:[代码][代码]true[代码][代码]})[代码] 3、使用动态请求数据加载,把数据放在服务器上请求加载。
2019-10-21 - 【开箱即用】分享一个3D环物展示的解决方案
概述 有时候我们需要立体展示一个物体时,可能需要用到以下效果。当然实现的效果可能有很多,这里就为大家介绍一个大神写的方案,希望能帮到大家! 利用小程序开放的接口模拟简单的3D环物功能。只需传入物品序列照片数组即可。 [图片] 截图来自小程序“白海豚保护区” 一、小程序插件 AppID:wx0f253bdf656bfa08 基础库要求:>= 2.4.3 文档链接:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx0f253bdf656bfa08 图片要求:网络URL链接 [图片] 二、兼容云开发 由于插件已经有段时间没有更新了,笔者在开发时又用到了小程序的云开发储存图片资源。拜原作者开源所赐,为了兼容云开发,我在开源的小程序插件代码中进行了部分修改。 开源代码:https://github.com/hiteochew/DimensionalShow-wxapp-plugin 修改方法: 将原代码中 downloadFile 方法替换为以下代码即可。 [代码]// 文件位置:plugin/api/util.js // 代码位置:第 124 行 function downloadFile(src) { return new Promise((resolve, reject) => { //云储存 wx.cloud.downloadFile({ fileID: src }).then(res => { resolve(res.tempFilePath); }).catch(error => { reject(err); }) }) } [代码] 结语 欢迎社区三连——关注点赞收藏!
2019-10-20 - 【实战】云开发在白海豚保护区小程序中的应用
介绍 中华白海豚是国家一级保护动物、香港回归吉祥物,仅剩2千余头,与现存大熊猫数量相当;珠江口海域是世界最大的白海豚栖息地,但受人类活动影响生存状况不容乐观;保护区承担着科普教育、管理执法、保护救助、科研监测等诸多职能。 小程序“白海豚保护区”是我个人以志愿者身份,无偿帮助保护区开发的,该项目在2019高校小程序大赛中获全国总决赛一等奖。 [图片] 选择云开发 这个项目是以小程序云开发(TCB)为核心的。在笔者看来,每个云开发的运行环境是独立而隔离的,因此能够有效避免在一些突发情况时,不同项目之间产生的负面影响,这对于一个对稳定性要求较高的项目来说是十分合适的选择。同时,也不用考虑服务器的运维这些杂七杂八的问题。因此,可以说云开发是让开发者十分省心了! [图片] 不同场景下的应用 小程序聚焦保护区工作中的三个场景,包括两个高频场景:科普基地参观、志愿综合服务;以及一个低频场景,但却是十分重要的应用场景,那就是在遇到鲸豚搁浅等情况时,小程序能告诉我们该如何正确处理。详细的功能大家可进入小程序体验。 保护区拥有首个白海豚主题科普基地,迎接着来自珠三角乃至全国的参观者。以下,笔者将以这个场景下的参观预约功能的实现流程为例,向大家介绍一下云开发在本项目中应用! [图片] 一、用户界面 关键词:数据库 云存储 选择图片时将图片压缩,并使用 wx.cloud.uploadFile 方法上传到云存储中。 [代码]chooseImage: function (e) { wx.chooseImage({ count: 1, sizeType: ['compressed'], //压缩图片 sourceType: [e], success: chooseResult => { wx.cloud.uploadFile({ cloudPath: 'orderPic/' + new Date().getTime() + '.jpg', //定义上传至云端的路径、名称 filePath: chooseResult.tempFilePaths[0] }) }, }) } [代码] 通过<form>组件获取到用户填写的信息后,调用 add 方法往数据库的集合中插入记录。 [代码] formSubmit(form) { db.collection('order_list').add({ data: form, success: res => { console.log('[数据库] [新增记录] 成功,记录 _id: ', res._id) } }) } [代码] [图片] 二、审核界面 关键词:数据库 云函数 利用企业微信支持微信小程序运行的天然优势,本项目与保护区单位的企业微信相结合,工作人员在企业微信中接收通知、处理申请。 利用 get 方法查询数据库该条预约的信息,并渲染到页面中。 [代码] onLoad: function (e) { var that = this db.collection('order_list').where({ _id: e.id }) .get({ success(res) { that.setData({ content: res.data[0] }) } }) } [代码] 审核后更新该条预约的状态(通过或不通过),这个时候因为权限问题,就需要在云函数中调用 update 的方法。 [代码]const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { var docid = event.docid var state = event.state var reply = event.reply try { return await db.collection('order_list').doc(docid).update({ data: { state: state, reply: reply } }) } catch (e) { console.log(e) } } [代码] [图片] 三、统计界面 关键词:聚合 聚合是云开发一种对数据批处理的操作。聚合操作可以将数据分组,然后对每组数据执行多种批处理操作,最后返回结果,以实现统计的功能。 [代码]GetData: function () { var that = this db.collection('order_all') .aggregate() .match({ _id: _.gte(that.data.time1) //统计开始时间 }) .match({ _id: _.lte(that.data.time2) //统计截止时间 }) .group({ _id: null, people: $.sum('$people'), group: $.sum('$group') }) .end() .then(function (res) { that.setData({ people: res.list[0].people, group: res.list[0].group }) }) } [代码] [图片] 四、订阅消息 关键词:云函数 预约成功或失败后,结果会通过模板消息通知到用户。10月12日,微信团队发布了《小程序模板消息能力调整通知》,订阅消息正式上线。笔者也第一时间更新了用户的通知形式。 [图片] 引导用户开启订阅消息,这里也注意到了基础库的要求,对低版本的用户会作相应提醒。 [代码] <view bindtap="RequestSubscribeMessage"> <view >审核进度通知</view> <switch disabled checked="{{subscribeMessageCheck}}"></switch> </view> //index.js RequestSubscribeMessage: function () { var that = this wx.requestSubscribeMessage({ tmplIds: [template_ID], //订阅消息 success(res) { if (res[app.globalData.template_ID] == 'accept') { that.setData({ subscribeMessageCheck: true }) } }, fail: function (err) { console.log(err) } }) } [代码] 管理员审核后触发云函数,调用 subscribeMessage.send 发送订阅消息。 [代码]const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { var openId = event.openId var value1 = event.value1 var value2 = event.value2 var templateId = event.templateId try { const result = await cloud.openapi.subscribeMessage.send({ touser: openId, page: 'pages/order/user/history', data: { name1: { value: value1 }, date3: { value: value2 } }, templateId: templateId }) console.log(result) return result } catch (err) { console.log(err) return err } } [代码] 总结 作为云开发的第一批用户,也是业余的编程爱好者(本专业汉语言文学),云开发帮助我解决了很多小程序开发上的问题,有效提高了编程的效率,也让我有机会将许许多多头脑中有趣的想法、通过小程序编译成现实!
2019-10-20 - 爸妈搜通讯录
使用方法:1、 在微信小程序管理后台——设置——第三方服务,按 AppID(wxea14f39af1d4d74a)搜索到该插件并申请授权(ps:一般不会出现拒绝的情况。如果申请被拒绝了,请重新申请,有时候管理员手抽点错了,请见谅! 有任何好的建议可以通过微信、邮箱、手机号联系!)。 2、在要使用该插件的小程序 app.json 文件中引入插件声明。 [代码]"plugins": { "BmsDirectory": { "version": "1.0.0", "provider": "wxea14f39af1d4d74a" } }[代码]3、在需要使用到该插件的小程序页面的 JSON 配置文件中,做以下配置: [代码]{ "usingComponents": { "BmsDirectory": "plugin://BmsDirectory/BmsDirectory" } }[代码]4、在相应的 HTML 页面中添加以下语句即可完成插件的嵌入。 [代码]<BmsDirectory />[代码]属性属性名类型默认值说明[代码]userList[代码][代码]Array[代码][代码]"[]"[代码][代码]通讯录数据[代码]userList的属性属性名类型默认值说明[代码]name[代码][代码]String[代码][代码]''[代码][代码]名字信息[代码][代码]tel[代码][代码]String[代码][代码]''[代码][代码]电话信息[代码][代码]avatarurl[代码][代码]String[代码][代码]有默认头像[代码][代码]头像信息[代码][代码]<BmsDirectory userList="{{userList}}" headportrait='headportrait' />[代码][代码]data:{ userList: [ { name: '咖啡', tel: '12345678900', avatarurl:'' }, { name: '小咖啡', tel: '12345678900' }, { name: '小小咖啡', tel: '12345678900' }, { name: 'c小小咖啡', tel: '12345678900' }, { name: '-小小咖啡', tel: '12345678900' }, { name: '+小小咖啡', tel: '12345678900' }, ] }[代码]样式属性名类型说明[代码]headportrait[代码][代码]String[代码][代码]头像的样式[代码][代码]phonestyle[代码][代码]String[代码][代码]电话号码信息的演示[代码][代码]namestyle[代码][代码]String[代码][代码]名字信息的样式[代码][代码]titlestyle[代码][代码]String[代码][代码]名字上方分类小标题的样式[代码][代码]msgstyle[代码][代码]String[代码][代码]每一条信息的整体样式[代码][代码]/* 头像 */ .headportrait{ /* width: 100rpx !important; height: 100rpx !important; */ } /* 手机号 */ .phonestyle{ font-size: 26rpx !important; } /* 名字 */ .namestyle{ font-size: 28rpx !important; } /* 首字母标题 */ .titlestyle{ font-size: 28rpx !important; /* color: red !important; */ } /* 每个信息的样式 */ .msgstyle{ /* padding: 50rpx 20rpx !important; */ }[代码]事件属性名类型说明[代码]bindgetcall[代码][代码]String[代码][代码]点击每条信息时触发的事件[代码][代码]<BmsDirectory userList='{{userList}}' bindgetcall='getcall'/>[代码]效果[图片] 联系方式如果使用过程中遇到问题,或者更好的建议,欢迎大家添加微信号,进行讨论交流! [图片]
2018-11-29 - 爸妈搜日历
提供简约不简单的日历基本功能,自定义样式,考勤状态等功能。
2018-10-08 - 爸妈搜海报Maker
使用方法1、 在微信小程序管理后台——设置——第三方服务,按 AppID(wxbf07f0f22c6c200d)搜索到该插件并申请授权(ps:一般不会出现拒绝的情况。如果申请被拒绝了,请重新申请,有时候管理员手抽点错了,请见谅)。 2、在要使用该插件的小程序 app.json 文件中引入插件声明。 [代码]"plugins": { "calendar": { "version": "1.0.0", "provider": "wxbf07f0f22c6c200d" } }[代码]3、在需要使用到该插件的小程序页面的 JSON 配置文件中,做以下配置: [代码]{ "usingComponents": { "calendar": "plugin://poster/poster" } }[代码]4、在相应的 HTML 页面中添加以下语句即可完成插件的嵌入。 [代码]<poster />[代码]属性[代码]属性名[代码][代码]类型[代码][代码]默认值[代码][代码]说明[代码][代码]drawing[代码][代码]Array[代码][代码][][代码][代码]画图的数据[代码][代码]savebtnText[代码][代码]String[代码][代码]"点击按钮进行图片保存"[代码][代码]按钮文字信息[代码][代码]drawing参数说明[代码][代码]drawing[代码]数据目前有两种数据信息,一种是图片信息,另一种是文字信息。 [代码]图片信息[代码][代码]属性名[代码][代码]类型[代码][代码]值[代码][代码]说明[代码][代码]type[代码][代码]String[代码][代码]image[代码][代码]图片类型[代码][代码]url[代码][代码]String[代码] [代码]图片路径,为线上图片[代码][代码]left[代码][代码]Number[代码] [代码]距离画布的左边距[代码][代码]top[代码][代码]Number[代码] [代码]距离画布的顶部距离[代码][代码]width[代码][代码]Number[代码] [代码]绘画图片的宽度[代码][代码]height[代码][代码]Number[代码] [代码]绘画图片的高度[代码][代码]circle[代码][代码]Boolean[代码][代码]true、false[代码][代码]是否是绘制圆形,默认为false[代码]文字信息[代码]属性名[代码][代码]类型[代码][代码]值[代码][代码]说明[代码][代码]type[代码][代码]String[代码][代码]text[代码][代码]文字类型[代码][代码]content[代码][代码]String[代码] [代码]绘图的文字内容[代码][代码]left[代码][代码]Number[代码] [代码]距离画布的左边距[代码][代码]top[代码][代码]Number[代码] [代码]距离画布的顶部距离[代码][代码]width[代码][代码]Number[代码] [代码]文字绘画的宽度[代码][代码]color[代码][代码]String[代码] [代码]文字信息[代码][代码]textAlign[代码][代码]String[代码] [代码]文字水平对齐方式[代码][代码]fontSize[代码][代码]Number[代码][代码]默认为26rpx[代码][代码]文字大小[代码][代码]textAlign参数[代码][代码]属性名[代码][代码]类型[代码][代码]说明[代码][代码]left[代码][代码]String[代码][代码]左对齐[代码][代码]center[代码][代码]String[代码][代码]居中对齐[代码][代码]right[代码][代码]String[代码][代码]右对齐[代码]如图: [图片] 实例: [代码]data:{ data: { drawing: [ { type: 'image', url: '此处是线上图片', left: 0, top: 0, width: 650, height: 950 }, { type: 'text', content: '此处是文本信息', fontSize: 26, color: 'white', textAlign: 'left', left: 170, top: 50, width: 650, } ], }[代码]样式类名说明[代码]canvas-style[代码][代码]画布样式样式[代码][代码]savebtn-style[代码][代码]按钮样式[代码][代码]/* 画布样式 */ .canvas-style{ width: 650rpx !important; height: 950rpx !important; margin: 0 auto; border: 1px solid orangered; margin-top: 10rpx; }[代码][代码]<poster drawing='{{drawing}}' savebtnText='{{savebtnText}}' canvas-style='canvas-style' savebtn-style='savebtn-style' />[代码]注意: 样式的优先级! 效果[图片] 联系方式[图片]
2018-10-30 - 定时发送模板消息功能实现(云开发实现)。
在准备开发这个功能之前,请确保你已经阅读过云开发文档和以下相关官方文档。 模板消息 获取AccessToken 发送模板消息 云函数定时触发器 我们来设定一个需求场景。以小程序【抽奖助手】为例,用户参与抽奖后,需要在开奖时间发送给用户开奖结果通知。这个通知采用模板消息形式下发。 先整理一下思路,实现这个功能我们需要哪些模块? 定时任务执行器。根据任务类型调用相应任务处理程序。 开奖任务处理程序,开奖后发送模板消息,通知用户结果。 云函数中调用 sendTemplateMessage 后端接口,发送模板消息。 周期获取AccessToken。请求后端接口需要用AccessToken,周期更新AccessToken,放入数据库中,随用随取。 模块实现 1.定时任务执行器 以云数据库形式实现。添加一个定时任务就是在该集合增加一条记录,移除同理。 记录字段设计: [代码]timeingTask{ _id: taskType: //任务类型,决定如何处理这个任务 execTime: // 触发时间。到达这个时间开始执行。 data:{} // 必要数据 } [代码] 然后设置云函数周期执行。每分钟查询一次该定时任务数据库,是否有任务到达执行时间。如果有则根据类型进行处理,并在数据库中移除该任务。 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: '你的云环境ID' }) const db = cloud.database() exports.main = async(event, context) => { const execTasks = []; // 待执行任务栈 // 1.查询是否有定时任务。(timeingTask)集合是否有数据。 let taskRes = await db.collection('timeingTask').limit(100).get() let tasks = taskRes.data; // 2.定时任务是否到达触发时间。只触发一次。 let now = new Date(); try { for (let i = 0; i < tasks.length; i++) { if (tasks[i].execTime <= now) { // 时间到 execTasks.push(tasks[i]); // 存入待执行任务栈 // 定时任务数据库中删除该任务 await db.collection('timeingTask').doc(tasks[i]._id).remove() } } } catch (e) { console.error(e) } // 3.处理待执行任务 for (let i = 0; i < execTasks.length; i++) { let task = execTasks[i]; if (task.taskType == 1) { // 定时开奖任务 const kaiJinag = require('kaiJiang.js') try { await kaiJinag.kai(task.data.activity_id) } catch(e) { console.error(e) } } } } [代码] 使云函数每分钟执行的触发器代码: [代码]{ "triggers": [ { "name": "timeingTaskExecutor", "type": "timer", "config": "0 */1 * * * * *" } ] } [代码] 2.开奖任务处理程序 [代码]kaijiang.js[代码] [代码] const cloud = require('wx-server-sdk') const templateMessage = require('templateMessage.js') const COLL_FIELD_NAME = 'publicField'; const FIELD_NAME = 'ACCESS_TOKEN' const MSGID = '你的模板消息ID'; cloud.init({ env: '你的云环境ID' }) const db = cloud.database() const kai = async activity_id => { // 根据活动id,获取参与用户信息,获取到用户的 openid 和 formid. // 开奖程序省略 // 从数据库中获取AccessToken let tokenRes = await db.collection(COLL_FIELD_NAME).doc(FIELD_NAME).get(); let token = tokenRes.data.token; // access_token let page = '点击模板消息,想要打开的小程序页面'; let msgData = { "keyword1": { "value": activity.prizeName }, "keyword2": { "value": "你参与的抽奖活动正在开奖,点击查看中奖名单" }, }; let openid = '用户openid'; let formid = '用户formid'; await templateMessage.sendTemplateMsg(token, MSGID, msgData, openid, formid, page); } module.exports = { kai: kai, } [代码] 3.发送模板消息 [代码]templateMessage.js[代码] 封装在一个 js 文件里,传入必要参数调用即可。 [代码]const rp = require('request-promise'); const sendTemplateMsg = async (token, msgid, msgData, openid, formid, page) => { await rp({ json: true, method: 'POST', uri: 'https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=' + token, body: { touser: openid, template_id: msgid, page: page, form_id: formid, data: msgData } }).then(res => { }).catch(err => { console.error(err) }) } module.exports = { sendTemplateMsg: sendTemplateMsg, } [代码] 4.周期获取AccessToken 使用云函数触发器,使云函数每小时请求一次AccessToken,并将AccessToken存入云数据库中。 [代码]const cloud = require('wx-server-sdk') const rq = require('request-promise') const APPID = '你的APPID'; const APPSECRET = '你的APPSECRET'; const COLLNAME = 'publicField'; const FIELDNAME = 'ACCESS_TOKEN' cloud.init({ env: '你的云环境ID' }) const db = cloud.database() exports.main = async(event, context) => { try { let res = await rq({ method: 'GET', uri: "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + APPID + "&secret=" + APPSECRET, }); res = JSON.parse(res) let resUpdate = await db.collection(COLLNAME).doc(FIELDNAME).update({ data: { token: res.access_token } }) } catch (e) { console.error(e) } } [代码] 使云函数一小时执行一次的触发器代码: [代码]{ "triggers": [ { "name": "pollGetAccessToken", "type": "timer", "config": "0 0 */1 * * * *" } ] } [代码] 这是我做的产品册小程序中的部分代码,目前项目还没有发布,发布后会开源出来。项目中用到了挺多开源组件,给我很大的帮助,希望我的分享也可以帮助到一些人。 有问题可以在公众号后台联系我,我看到都会回复的。 [图片]
2019-03-15 - 纯云开发二手书商城的全开源demo
这是为母校写的一个纯粹的公益小程序,原生+云开发,写文章太累了,所以所有代码我都写了注释,还是很适合入门学习的,特别是云开发 [图片] [图片] [图片] 程序本身来说,我认为没啥多大的亮点,只不过把很多单个案例综合起来了,云开发方面,比如:支付、提现、获取用户手机号、发短信、发邮箱。。。。。。。界面上,清一色的flex布局。 和完整版得商城小程序,还差了一丢丢–购物车,因为思考了一下,这个小程序着实用不着,用来学习还是可以了滴 源码和使用教程发在Github: https://github.com/xuhuai66/used-book-pro
2019-09-18 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11 - 小程序·云开发实战 - 校园约拍小程序
创意来源于生活,之所以开发这个校园约拍小程序,是因为在摄影选修课上常听老师抱怨外出写生老找不到模特,许多大学生都想拥有一套专属自己记忆的摄影作品,记录下不会磨灭的美好回忆,可如何找到让自己满意的摄影师是他们的难题。悦拍屋是一个校园摄影o2o的约拍平台,提供全方位的约拍服务,同时提供一个自我展示,学习交流,互动娱乐的平台。接下来我将结合项目的讲解给大家分享一些实用技术和对于云开发的一些经验,希望对正在学习小程序的你有帮助。 前言 在开发一个项目之前首先要进行技术选型从而降低产品开发的技术风险和提高开发效率,技术选型必须得紧紧围绕着业务场景来选择。 产品原型设计:墨刀 UI组件库 1.微信原生样式库[代码]WeUI[代码],让用户使用感知更加统一 2.注重视觉交互体验的[代码]ColorUI[代码]组件库,在感知统一的基础上视觉元素多样化 前端 1.小程序原生语法以及[代码]API[代码] 2.[代码]Promise[代码]实现异步调用 3.[代码]ES6[代码]编写页面交互逻辑 后端 1.云函数:无需自建服务器,在云端运行的代码,微信私有协议天然鉴权,开发者只需编写自身业务逻辑代码 2.云数据库:无需自建数据库,一个既可在小程序前端操作,也能在云函数中读写的 [代码]JSON[代码] 数据库 3.云存储:实现小程序前端直接上传/下载云端文件,在云开发控制台可视化管理 4.云调用:由原生微信服务集成,基于云函数免鉴权使用小程序开放接口的能力,包括服务端调用、获取开放数据等能力 其他 1.使用微信提供的云测试对未上线的小程序进行缺陷测试、性能数据分析、机型覆盖测试,确保小程序上线后正常运营 2.使用基于云开发的[代码]AI视觉能力[代码]-身份证识别实现实名认证,智能鉴黄结合人工完成发布信息的审核 3.开发工具:微信开发者工具、VScode 4.部分图标使用自阿里巴巴矢量图标库 总体设计 功能结构图 大家可以通过此图了解整个项目的主要功能点 [图片] 产品原型图 此处给出一张主页原型图示例,墨刀还是挺好用的 [图片] 色彩设计图 悦拍屋的整体色调为浅蓝色,各位小伙伴在开发自己项目的时候可以根据色彩标准搭配来设计项目所采用的色彩,合适的色彩搭配可以给用户良好的视觉体验 [图片] 功能模块详解 接下来我会对部分功能模块以图文结合的形式详细描述,将其中涉及的技术、知识分享给大家 约拍邀请 用户可在首页查看约拍需求,并点击查看需求详情,用户在了解需求后,若自己符合条件即可提交约拍信息,等待发布者的回复,可将此需求收藏方便查看 [图片] 技术分享:自定义顶部导航栏 官方默认的导航栏只能对背景颜色进行更改,对于想要在导航栏添加一些比较酷炫的效果则需要通过自定义导航栏实现 实现原理:通过设置[代码]app.json[代码]中页面配置的[代码]navigationStyle[代码](导航栏样式)配置项的值为[代码]custom[代码],即可实现自定义导航 [代码]"window":{ "navigationStyle":"custom" } [代码] 本项目的部分页面自定义导航栏实现使用了[代码]ColorUI[代码]的导航栏组件,在完成上一步属性设置后再引入导航栏组件即可 [代码]"usingComponents":{ "cu-custom":"/colorui/components/cu-custom" //该路径替换为自己项目内ColorUI组件所在位置 } [代码] 主页自定义导航栏通过设置背景图片加上GIF波浪效果 [代码] <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://image.weilanwl.com/gif/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> [代码] 效果图 [图片] 使用组件定义的导航栏 [代码]<cu-custom bgImage="https://s2.ax1x.com/2019/05/02/Etiyng.jpg" isBack="{{true}}"> <view slot="backText">返回</view> <view slot="content">认证信息说明 </view> </cu-custom> [代码] 效果图 [图片] [代码]特别提醒1:使用自定义导航后,页面的返回需要在自定义导航栏中自行设置 [代码] [代码]特别提醒2:导航栏组件需要自行引入ColorUI组件库后才能使用,具体引入教程地址在附录中给出 [代码] 发布约拍 选择发布约拍功能填写约拍需求,提交审核通过后可在首页实时查看发布结果 [图片] 技术分享:入场动画 额。。录制可能略微有点卡顿,实际效果挺流畅的,各位大佬有什么好的录制工具推荐可以在评论中回复 实现原理:通过[代码]toggleDelay[代码]的布尔值为真动态添加动画类名,在生命周期函数[代码]onReady[代码]中控制[代码]toggleDelay[代码]的值从而控制整个动画过程(原理与[代码]Vue[代码]的动态类名相似) [代码]data:{ toggleDelay;false }, onReady:function(){ let that = this //toggleDelay的值为真,动画开始 that.setData({ toggleDelay: true }) //控制整个动画的时长 setTimeout(function() { that.setData({ toggleDelay: false }) }, 2000) } [代码] [代码]<view class="padding-xs {{toggleDelay?'animation-slide-bottom':''}}" style="animation-delay: {{item.time}}s;" wx:for="{{list}}" wx:key="{{index}}"> <image class="img" id='img{{index}}' src="{{item.src}}" mode="widthFix" /> </view> [代码] [代码]//所有动画的定义 [class*=animation-] { animation-duration: .5s; animation-timing-function: ease-out; animation-fill-mode: both } //animatioon-slide-bottom所定义的动画 .animation-slide-bottom { animation-name: slide-bottom } //动画效果 @keyframes slide-bottom { 0% { opacity: 0; transform: translateY(100%) } 100% { opacity: 1; transform: translateY(0) } } [代码] [代码]animation-slide-bottom[代码]是动画类名,[代码]animation-delay[代码]是每一个卡片动画执行的延迟时间,每一个动画的执行时长为0.5s,所以延迟时间是以0.5s递增的,三个卡片的动画总时长就为2s,即2s后就执行[代码]onReady[代码]中的[代码]settimeout[代码]事件结束动画 [代码]特别提醒:动画的延迟时间,执行时间可以自行设计,动画效果过渡自然即可 [代码] [代码]特别提醒:由于触发动画的钩子函数定义在页面初次渲染的生命周期函数中,故只有在页面初次渲染时才执行,避免每次显示页面时加载动画造成用户的视觉疲劳 [代码] 智能推荐约拍对象 系统会根据约拍需求自动推荐约拍对象(个人开发精力有限,推荐算法后续推出。。。) [图片] 技术分享:CSS3实现酷炫搜索动画 在模态框内放置两个[代码]view[代码]标签,以下是标签定义 [代码] <view id='preloader'> //外围的圆形框定义 <view id='loader'></view> //内部的线条定义 </view> [代码] [代码]#preloader { width: 150px; height: 150px; border-radius: 50%; border: 1px solid #97b2ff; } #loader { //中间线条定义 display: block; position: relative; left: 50%; top: 50%; width: 150px; height: 150px; margin: -75px 0 0 -75px; border-radius: 50%; border: 3px solid transparent; border-top-color: #97b2ff; -webkit-animation: spin 2s linear infinite; animation: spin 2s linear infinite; } #loader:before { //通过伪类元素定义外围线条 content: ""; position: absolute; top: 5px; left: 5px; right: 5px; bottom: 5px; border-radius: 50%; border: 3px solid transparent; border-top-color: #97b2ff; -webkit-animation: spin 3s linear infinite; animation: spin 3s linear infinite; } #loader:after { //通过伪类元素定义最内部线条 content: ""; position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; border-radius: 50%; border: 3px solid transparent; border-top-color: #97b2ff; -webkit-animation: spin 1.5s linear infinite; animation: spin 1.5s linear infinite; } [代码] 实名认证 [图片] 嘿嘿,由于懒得给个人信息打码,就暂时不给大家演示认证过程了。。 技术分享:Ai视觉能力 很多小伙伴都有过在自己项目中使用AI技术的想法,但又因为入门AI的难度比较大,并且需要的时间较长就放弃了,现在给大家安利一个可以直接使用的AI服务,让AI不再具有神秘感(AI大佬可以忽略此部分。。) 方案一 在腾讯云中搜索身份证识别,上面会有详细的API文档以及测试工具帮助你快速使用 [图片] 点击查看腾讯云-身份证识别 方案二 方案一是以提供API接口的形式提供身份证识别服务,而接下来要介绍的方案真的就比较简单了,在腾讯云中搜索智能图像,其中的增值服务AI智能图像能力,你可以通过云函数和云存储实现相应功能,基于小程序云开发的 AI DEMO中开发好了部分功能,你只需通过教程将云函数和组件引入你的项目即可使用 [图片] 点击查看腾讯云-智能图像 点击查看基于小程序云开发的 AI DEMO [代码]特别提醒:当然使用这些服务也并非是完整的解决方案,对于身份证信息的加密、存储方案、安全协议等还是需要各位小伙伴自行设计解决方案哦。 [代码] 云开发 云开发为开发者提供完整的原生云端支持和微信服务支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代,同时这一能力,同开发者已经使用的云服务相互兼容,并不互斥。 官方文档中API被分为了小程序端和服务端,一开始看过两端的API之后,感觉好像没有什么不同啊,在查阅相关资料以及实际开发中某些业务的处理总结出一些经验后才明白了两者的不同,下面给各位具体说说两者的不同之处,应该能帮助大家在使用云开发实战时少踩一点坑 初始化的不同 小程序端 全局声明一次 [代码]if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else { wx.cloud.init({ env:'xxx', traceUser: true, }) } [代码] 服务端 每个云函数中声明一次 [代码]const cloud = require('wx-server-sdk') cloud.init() [代码] 权限不同 小程序端 在小程序端可以选择直接操作数据库,但由于是前端操作数据库存在一些安全问题,有较多的权限限制,在云控制中可对每个集合进行权限设置,这也就是为什么有小伙伴在小程序端对某些数据进行更新,显示更新成功但并未更新数据,就是因为小程序端默认只能更新当前用户写入的数据 [图片] [代码]特别提醒:在小程序端使用创建者的权限对数据进行修改时一定要确保该集合中有_openid字段,否则系统在权限判断时是没有办法识别当前操作为创建者的,数据修改无法执行 [代码] 服务端 服务端拥有管理员的权限,对所有数据拥有读写权限 语法支持不同 小程序端 在微信开发者工具里,以及Android端手机(浏览器内核是QQ浏览器的X5),[代码]async[代码]/[代码]await[代码]是天然支持的,但 iOS 端手机在较低版本则不支持,因此需要引入额外的[代码]polyfill[代码]。可以在有使用[代码]async[代码]/[代码]await[代码] 的文件当中引入[代码]polyfill[代码]文件。 [代码]const runtime = require('相对路径/lib/runtime') [代码] 服务端 在云函数里,由于 Node 版本最低是 8.9,因此是天然支持 async/await 语法的 示例:获取约拍需求列表 [代码]//云函数入口文件 const cloud = require('wx-server-sdk') //初始化 cloud.init() //连接数据库 const db = cloud.database() async function getAll(){ const result = await db.collection('ypList') .orderBy('cameraInfo.launchTime','desc').where({}).get() return result } // 云函数入口函数 exports.main = async (event, context) => { //此处的action是用来判断该调用哪一个方法 if(event.action === 'getAll'){ return getAll() } } [代码] 结语 一个人手撸个全栈项目确实很辛苦,但收获也很多。至少对于小程序的实战开发更为熟练了,对MVVM的思想的理解也更加深刻了。技术发展得很快,学习一项技术如果不深入其本质,那么技术是学不完的。深入学习就是个解决问题的过程,或是帮助别人解决问题,或是借助他人的力量解决问题。目前在正在学习Vue、React、TypeScript等技术,后续会推出相关技术的项目解析文章,希望对于同样在学习的你有帮助。 [代码]特别说明:本项目已参加2019届中国高校计算机-微信应用开发赛完,开源至github,感兴趣的小伙伴可以看看 [代码] 附录 在此提供一些本项目涉及到的技术、工具等链接供大家学习使用 产品原型设计工具:墨刀 色彩搭配设计:配色网 在线作图:ProcessOn UI样式库:WeUI UI样式库:ColorUI 图标库:Iconfont阿里巴巴矢量图标库 开发工具:微信开发者工具 开发者工具:Vscode 腾讯云服务:身份证识别 腾讯云服务:智能图像 API文档:微信官方文档.小程序 技术文档:ES6 源码链接 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心! [图片]
2019-08-05 - 记录一下,微信小程序 view的文本不能自动换行问题
一行文字太多,用view标签包裹时,超出屏幕,如何让其换行? <view style=“overflow-wrap: break-word”> [代码]123213123123213213213213213213213213213213213211232131231232132132132132132132132132132132132112321312312321321321321321321321321321321321321 [代码] </view> 简单粗暴
2019-09-23 - 【大赛文章征集】在自然保护区场景下,小程序能连接什么?
一、灵感来源 我在大学期间加入了「海精灵志愿者协会」,这是一个关注和保护中华白海豚的学生社团,因此有机会接到保护区的一些志愿工作,甚至有机会直接在港珠澳大桥的水域参与观测野生的中华白海豚。这次我的比赛作品以白海豚保护区为主题,其实也是这段经历给我带来的灵感。 二、项目背景 中华白海豚是国家一级保护动物、香港回归吉祥物,仅剩2千余头,与现存大熊猫数量相当;珠江口海域是世界最大的白海豚栖息地,但受人类活动影响生存状况不容乐观;保护区承担着科普教育、管理执法、保护救助、科研监测等诸多职能。 三、使用场景 1、高频场景一:科普基地参观 保护区拥有首个白海豚主题科普基地,迎接着来自珠三角乃至全国的参观者。而参观者的类别无非两种,一种是个人、一种是团体,对个人而言,无需预约、随到随看;而团队呢,就需要走一个线上预约的流程。来访者可能不熟悉我们的位置,这就需要用到参观导航了。来到科普馆,对于团队,自然会有工作人员带领;而对于个人,就可以用上了我们的语音导览了。 [图片] 2、高频场景二:志愿综合服务 保护区管理单位的人力资源是十分宝贵的,如果要在全珠海的社区和高校里大范围宣传,就需要发挥到我们志愿者的力量了。志愿者任务系统,主要通过积分和奖励鼓励志愿者参与志愿服务。对宣传志愿者,我们提供了丰富的科普学习资料。而对于一些有机会接触到野生白海豚的专业志愿者,我们则会鼓励他们进行信息反哺。而这些信息可以为日后研究白海豚种群状况,提供第一手材料。 [图片] 3、低频场景 保护区工作中还有一个低频应用场景,但却是十分重要的应用场景,那就是在遇到鲸豚搁浅等情况时,小程序能告诉我们该如何正确处理。 [图片] 四、产品目标 在这个小程序中,我们做到了什么?都说微信连接一切,那么我们则是通过小程序连接了与保护区息息相关的人、物、事,比如预约参观连接了来访群众与工作人员、语音导览连接了参观者与展品……这些“连接”能够有效提高宣传科普工作的效率,让保护区有限的人力资源投入到更加需要的地方(如科学研究和海兽救助)。 [图片] 五、技术开发 在整个开发框架里,这个项目是以小程序云开发为核心的,配合必要的第三方服务。至于这个项目的一些技术细节和踩坑经验嘛,因为都是一些很细碎的东西,所以希望有机会再在另篇讲述,这里还是希望和大家聊聊产品。 后台持续定位接口的使用与踩坑 如何写一个好看的波浪动画效果 …… [图片] 六、我的收获 在比赛过程中,我有幸能和微信团队的同学面对面交流(这也太幸运了趴),我也把握住机会向前辈请教了一些我的困惑,现在分享出来供大家参考。 1、问:“小程序用完即走的特性对开发者和运营者是不是不太友好?” 答:“不是还有后半句吗——用完即走,走了再来”。 原本在做这个小程序时,为了保持小程序的活跃度,设计了在志愿者群的互动功能,但实际上,这个功能并不是最需要的,这样虚高的活跃度也是没有意义的。如果一个产品能有效帮到一个人一次,哪怕他不会再次使用了,我认为这也是有价值的。 2、问:“是不是应该将最重要的信息都放在首屏显示?” 答:“朋友圈的入口就不在首页,用户真正需要的功能,他们会自己找到入口的。” 必须得承认,为了模仿“粤省事”的排布方式,这个小程序呈现在首页的内容实际上是有些冗杂的。在向微信团队的同学请教后,我在设计第二个小程序时有意识地对各种功能进行分类。 七、未完待续 我认为,一个好的项目,不仅应该立足于当前,更应该着眼于未来。看到很多去年的参赛作品(甚至今年的),一旦知道了自己的比赛结果,就不再运营下去了,甚至直接把服务器撤了,我觉得其实是很惋惜的。 我在准备做这一款小程序的时候,就已经有了这样的决心:即使没有获奖,这个项目依然要运营下去。事实证明,我不仅实现了这个目标,也借这次参赛的经验,开启了一个全新的项目「儒艮保护区」,也希望能通过自己的努力,使更多人了解并参与到濒危野生动物的保护事业之中。 我,一名文科生,生态环保志愿者、编程爱好者。今年是单人组队参赛,独立完成小程序的策划、开发、展示,但能力又十分有限,所以难免有不足,也欢迎各位前辈指出。 相关链接:小程序“白海豚保护区”演示视频 [图片] 附上我接触小程序以来的几个作品,感谢微信!
2019-10-31 - 如何制作按钮的双击事件
一:在wxml文件里面制作一个按钮<button> <button bindtap='myDB'>双击事件</button> 二:在js文件里面定义一个变量last(初始化上次的时间) data: { last:0//初始化上次的时间 三:绑定事件,定义两个变量c(当前点击的时间),L(上一次点击的时间),判断是否做了点击事件,再判断上次点击的时间和当前点击的时间是否小于500,如果是,作双击事件,否则做单击事件,就可以得到上一次的点击数据是单击还是双击如下: myDB:function(e){ var c = e.timeStamp;//当前点击的时间 var L = this.data.last;//上一次点击的时间 if(L>0){ if(c-L<500){ console.log("作双击"); }else{ console.log("作点击"); } } else{ console.log("第一次点击"); } this.setData({ last:c }); },
2018-03-21 - 一种小成本的线下定位方案 ---2019腾讯数字文创节小程序开发有感
去年的TGC小程序,我们采用了小成本的智能印章来连接线上线下(点此查看知晓程序的报道),今年,我们利用了成本更低的ibeacon设备,来做室内定位。先放码: [图片] 介绍下今年的TGC小程序 今年的2019腾讯数字文创节(以下简称TGC)的举办地点是在世界最大的单体建筑----成都环球中心里面,整个场馆区域非常大,同时场馆内有很多商区,为了能更加突出打卡TGC的整体性,我们将整个TGC所有的场馆地点设计在一个全场地图上,玩家可以很清楚的看到所有打卡点的分布和场馆的具体位置: [图片] 每年的TGC小程序我们在尝试一些新的技术形式的加入。今年TGC整体升级为腾讯数字文创节,整个活动以展会形式为主,整个TGC共设为值四大展区-----IP主题(该主题展区内有每个游戏ip单独布置的展区)、传统文化、竞技文化和未来探索,相较于去年的形式,今年更加侧重在和传统文化进行集合,所以我们在玩法上还是和去年一样,采用打卡的方式,但是在形式上,则采用了更加适合玩家感受游戏文化和传播内容的拍照打卡。通过打卡得积分、分享打卡照邀请好友点赞得积分和积分抽奖的方式,来带动活动线下的参与以及线上的传播。整体的效果图如下: [图片] 既然是玩家参观TGC场馆打卡得积分,那么如何验证玩家是否在该打卡点内呢? 小程序ibeacon ibeacon简介 ibeacon是2013年苹果提出来的一套可用于室内定位系统的协议,它可以以指定频率广播自身信号,信号本身带有设备的数据帧,只要手机设备支持解析这个数据帧,处于ibeacon信号广播范围的手机设备可以接收到这些数据信息,详细介绍请点击这里。 小程序ibeacon API提供的数据信息包含以下部分: UUID 设备的唯一通用标识符,一般不同的厂商不同的批次,这个编号也不一样,具体是设备厂商自己设置 Major 设备的主ID,这个一般代表设备的型号,同一批次的ibeacon设备,这个编号一般也一样 Minor 设备的次ID,每隔设备的这个编号都不同,一般用来指代唯一性 Accuracy 设备的距离,单位为米 Rssi 接收到的设备信号强度 设备就是联系供应商提供的,价格是45到60左右,成本非常低,就是这个小白盒子: [图片] 首先,因为活动的举办地点是在商场,可以预见的是,商场本身会有些商家会部署ibeacon设备来做活动营销(微信摇一摇),所以如何保证小程序接收到的ibeacon设备信息就一定是我们部署的ibeacon设备发射出来的呢?答案就是通过上面提到的Major。 收到设备后,我们首先设置这一批ibeacon设备的Major为我们特定的数字,在接收到的ibeacon信息后过滤掉不是我们制定Major号的设备信息即可。通过在数据库绑定打卡地点和设备Minor的关系,玩家手机接收到ibeacon设备的信号的时候,就可以通过接收到设备的Minor号判断玩家当前是在哪个体验点。坑爹的是,设备的厂家在出厂这些设备的时候,每个设备的Major和Minor号都是随机,幸好网上有很多ibeacon设备信息查看更改软件,推荐使用 “摇一摇助手”,app store和安卓应用商店都可以下载。 [图片] ibeacon踩坑 1、通常市面上的ibeacon设备是可以设置ibeacon设备的广播频率的,默认一般是500ms。为了提升感应玩家当前所在的地点的精度,我们会去调高ibeacon设备的广播频率。设备的广播频率是可以通过专门的ibeacon设置软件调整(PS:会导致设备更加费电,但是现在的ibeacon设备基本都可以用个好几年,如果不是长期使用的话,可以不care这个),但是小程序ibeacon API读取设备广播信息的频率是系统控制,也就是说,其实我们调整设备的广播频率是起不到作用的,因为最终读取ibeacon的广播信息频率是由系统所决定的,请教过微信的同学,安卓的是500ms,iOS 这边跟着系统走,目前观测是 1秒1次。 [图片] 2、接收ibeacon信息的API需要在wx.startBeaconDiscovery成功的回调中调用才能拿到ibeacon的设备信息。 [代码]wx.startBeaconDiscovery({ uuids: ['B9407F30-F5F8-466E-AFF9-25556B57FE6D'] // ibeacon uuid }).then((res) => { wx.onBeaconUpdate(() => { console.log('onBeaconUpdate') wx.getBeacons().then((res) => { let beacons = res.beacons, len = beacons.length, i = 0, nearbyBeacons = [], if (len === 0) { return } for (; i < len; i++) { if (beacons[i]['major'] !== 700) { continue } else { if (beacons[i]['accuracy'] > 0 && beacons[i]['accuracy'] < 8) { // 读取周围ibeacon设备的精度,可根据现场情况动态调整 nearbyBeacons.push(beacons[i]['minor']) } } } let _nearbyBeaconsResult = that.data.nearbyBeaconsResult if (_nearbyBeaconsResult.length >= 4) { _nearbyBeaconsResult.shift() } _nearbyBeaconsResult.push(nearbyBeacons) that.setData({ nearbyBeaconsResult: _nearbyBeaconsResult }) }) }) }) [代码] 在实际的开发过程中,我们发现,少数ibeacon设备的信息读取到的距离信息(accuracy)会出现负数,这个是因为之前提到的设备的广播频率的原因,如果在小程序内接收到设备的广播信息恰好出现在设备的广播周期之外,那么这个设备的的信息其实算作没读取到。为了规避这个问题,我们设置一个数组,这个数组存储最近4次接收到的ibeacon数据,按照一般ibeacon的广播频率的话,也就是2s的时间内,接收到的ibeacon设备信息。同时,我们在线下会针对体验点的区域的大小的不同,部署不同数量的ibeacon设备,这样可以大大的降低玩家接收不到ibeacon信息的概率。 现场方案执行 最基础的技术方案也想对容易实现一些,但是往年的线下活动的经验告诉我们,问题往往不是出现在实现的技术方面,我们需要考虑的往往是一些非技术侧的问题: 1、现场地形复杂,在什么地方部署ibeacon设备才算合适呢,不同的展区部署多少个点才算合适? 2、ibeacon的信息接收需要依赖蓝牙,玩家的手机因不明原因无法开启蓝牙或蓝牙功能失效了,如何处理? 第一个问题,我们在活动开始的前一个多礼拜,就带着我们做好的小程序测试版本去到了活动举办地成都环球中心进行了测试。 [图片] 实地的测试主要是对每个体验点的区域进行确定,再根据体验点的大小来决定需要放置多少ibeacon设备。 第二点在有了去年的经验,其实处理起来也有成熟的方案可以去执行。TGC的每个线下体验店会有我们Part Time(场馆负责人),还会有全场的巡视人员,我们只需要做一套后备方案,在少数玩家因设备问题无法打卡时,让现场我们的工作人员赋予玩家特殊的权限,可以让玩家不需要因设备的限制进行打卡即可。所以,我们做了一套玩家扫码打卡的后备方案,在活动开始之前,我们给予管理员特殊权限,它们可以在小程序的管理端选择打卡点二维码,玩家扫一扫即可完成定位打卡,当然每个小程序二维码只能够用一次,剩下的工作就是和现场的Part time进行培训。 [图片] 小程序云 这次活动的开发排期十分紧张,后台的开发人力又无法及时跟进项目,所以这次的整个活动的开发我们十分大胆的尝试了小程序云。其实在小程序云内部测试的时候,我们就已经有预研过小程序云。 小程序云提供了云存储、云函数和数据库,提供了较为完整的云端支持,还搭配了一套基础运维体系,开发者无需关心服务器搭建和代码部署。关于一些基础的类似云函数提供了鉴权的内容啥的,这里受限篇幅也就不再阐述,可自行查看开发者文档,这里讲下我在开发过程中的小总结吧。 1、小程序云环境 每个小程序账号开通了小程序云能力后会默认得到一套云开发环境,每个小程序账号最多可以创建两个云开发环境,一个云环境用于开发环境,一个云环境用于线上环境。 小程序端只需要在小程序云初始化函数配置当前运行的环境ID即可: [代码]// app.js App({ ...... globalData: { wxCloudEnv: 'tgc-production-xxxx' // 当前运行环境ID } }) // index.js wx.cloud.init({ env: app.globalData.wxCloudEnv }) [代码] 但是小程序云上,即使云函数当前运行的云环境不一样,也需要在每个云函数上显式的配置当前运行的云环境ID,要不然可能会在线上环境的云函数也会调用到测试环境的数据库和云存储。 现在每个云环境默认是基础的资源配额,可以自行发邮件申请. 2、小程序云的权限控制 小程序云提供的API分为小程序端API和服务端API,顾名思义,一套是在小程序的代码里调用的,一套是在服务端云函数调用的,两套API都可以进行数据库操作、云函数调用和云文件的操作。 [图片] 小程序端的数据库API在添加一条数据库记录的时候,该条数据库记录会默认加上_openid字段,值为该条记录创建者(也就是用户)的openid,而如果是服务端数据库API进行同样的操作就不会带上该字段。小程序云数据库的集合默认的权限都是设置为"仅创建者和管理员可读写",在小程序端如果通过数据库API访问数据库某个集合的数据时,只能访问到用户自己在小程序上调用数据库API创建的数据,写的逻辑也是一样,另外几个权限也是很好理解。小程序云管理后台可以设置数据库的操作权限,不同的用户对数据的读写权限不同,通过这个操作,可以灵活的调整数据库中的数据使用场景。 3、小程序云的局限性 小程序云暂时还不支持数据外部调用,所以,如果运营人员需要有很强的数据配置和数据管理的功能的话,小程序云想对做起来会很吃力(预计是年后会支持,大赞小程序云的团队)。而且,我们暂时还只能使用其提供的几项能力,如果我们想继续扩展我们的应用且需要在服务器上部署一些其他的服务的话,在小程序云上暂时还是做不到的。小程序云的数据库查询支持还是较为基础的,一些例如在关系型数据库常见的连表查询啥的还不支持,所以在做一些复杂的查询的时候,为了效率通常需要再一个数据库集合中增加一些冗余字段。 感觉小程序云还有很大的发展潜力,后续这些功能应该都会逐步的开放出来,相信小程云也会变得越来越强大。 线上结合线下ibeacon的优势 1、统计当前玩家所在的区域,绘制全场的玩家分布热力图,及时做好访问量较低的现场场馆引导。 [图片] 2、成本极低。成都环球中心在周末高峰时段是可以达到5~6w的人流量,在那么大的人流量下,我们也只需要在现场近20个体验点布置将近100个设备,就能覆盖我们全部的打卡体验点,按照每个设备本身价格近45元算,整个总成本可想而知。而且设备本身可以回收继续再利用,技术方案也是可以继续复用。 总结 今年是参与TGC小程序开发的第二年了,这种线下的项目最大的挑战在于需要对接的需求方非常之多,涉及到线上、线下、供应商等等多个角色。在开发周期如此紧张的情况下又需要同时兼顾前后端开发情况,对于个人来说是个非常大的挑战,但也是一个非常好的锻炼机会。有了去年TGC小程序的参与经验,今年在线下的部分也较为顺利的执行下去,但是也还是出现了一些线下培训细节没有勾兑清楚地额沟通问题。接下来,我也会写一篇开发线下项目的经验分享,也非常欢迎和期待各位小伙伴与我们在小程序上有更多的交流合作和技术探讨。
2019-03-19 - 「高校开发者」小程序 + 云开发 = 个人开发者快速创作的平台
个人介绍 大家好,我是Zero,一名大三的前端开发爱好者,目前主要研究微信小程序和iOS开发。 这是第二次参加微信小程序应用开发赛,2018年我们设计了一款通过二维码寻找丢失物品的小程序《蝴蝶寻物》,获得了华北赛区三等奖。 [图片] 今年,在小程序云开发功能的大力推广下,我决定采用云开发的方式,实现一个双人互动打卡互动的小程序《Mango Daily》(中文名称《芒果日常》)。 [图片] 得力于云开发提供的API,本项目在较短的时间内就实现了比较理想的效果。 接下来,我想从项目的立项开始,讲讲我是如何依靠小程序+云开发平台将想法快速实现。 1. 立项 1.1 项目背景 熟悉我的朋友都知道,我是个懒人 😦 ,所以我决定做一个两人互相监督完成目标的小程序。 目前市场上存在的大部分习惯日常类应用是通过个人或者群体打卡,我们更希望互相绑定的两个用户之间去完成一些日常活动。例如在完成自己的每日任务时,可以查看对方的打卡进度,提醒对方及时完成任务;或者可以根据任务为对方制订一套奖励计划,鼓励对方按时完成任务。另外,你可以在广场记录当天完成的任务,发生的故事,这可以只在你与同伴之间分享,也可以设置允许其他人阅读。 1.2 设计 一开始UI是在脑海中浮现的,只是想做一个以芒果色为主,并且能够快速看到对方打卡进度的界面。于是就有了一个以芒果色和树叶绿为主的界面。 [图片] 后来进行了第二版的设计,抛去了复杂的颜色组合,采用更简洁的设计,同时自定义了部分组件。 [图片] 1.3 技术准备 在去年的项目中,我们采用ThinkPHP开发了一套API系统,其中需要实现小程序的授权登录,设置鉴权来保证数据安全等操作。整个过程只有我一名开发人员,所以大致就是“先搞定后端,其次搞定界面,最后进行联调”的一个过程。 后来在云+社区看到一篇文章:《1个开发如何撑起一个过亿用户的小程序》,觉得确实可以通过新的方式去尝试一下前后端分离的开发过程。通读小程序云开发文档之后,发现并不需要学习新的技术,就可以快速上手。 这是第一次使用Serverless实现一款作品,打开了我创作应用的大门。 这次使用了 Teambition 来做任务看板,由于是个人开发维护,所以简单的 ‘ToDo, Doing, Done’ 模式就完全够用了。 [图片] 2. 开发 Mango Daily 使用的是小程序原生开发+云开发结合的方式进行开发的。 2.1 界面开发 界面没有使用第三方UI框架,而是自己将常用的模块封装成了组件。 [图片] 图中比较核心的模块包括 TabBar、Toast、Modal、Nav等。 [图片] 这里打算介绍一下我自己设计的Toast提示框,通过自定义视图+图片,替换掉了官方的loading,success,failure,warning效果。 在实现该组件的时候,我考虑了两种方案,一是通过普通image组件和设置z-index的方式实现Toast;另一个是使用cover-view组件。之所以考虑这两种方案,是因为我在发布随笔页面会是用到textarea,如果使用方案一会被原生组件遮挡。 自定义TabBar则是使用小程序官方提供的方法去实现的。但是这个方法会在首次切换时出现跳动,页面滑动时错位等问题,后来使用过完全自定义的方式去实现TabBar。 [代码]"usingComponents": { "tabbar": "/components/tabbar/tabbar" } [代码] 在每个需要使用自定义TabBar的页面调用自定义组件。 [代码]<tabbar sIndex="1"></tabbar> [代码] 下图是通过自定义组件实现的一个TabBar: [图片] 另外,Mango Daily中使用CSS3实现了部分动画,考虑到iOS端和安卓端小程序的渲染引擎不同,需要对代码进行兼容性处理。 [代码]/* 淡入 */ .fade-in { animation-name: fade-in; -webkit-animation-name: fade-in; } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @-webkit-keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } [代码] 这是我个人实现的一个动画: [图片] 2.2 云开发 云开发包括云数据库,云函数和云存储。本项目中三个功能均使用到。 2.2.1 云数据库 云数据库是一个非关系型数据库,在实际开发中基本符合本项目的需求。部分表关联查询则是通过分步查询的方式代替。 云数据库已经实现了自动鉴权,可以保证数据的安全性。目前云数据库只支持以下几种权限: 所有用户可读,仅创建者可读写 仅创建者可读写 所有用户可读 所有用户不可读写 默认情况下是***仅创建者可读写***,所以在首次开发时,手动插入的测试数据并不一定可以在前端顺利读取,需要修改集合的权限。 云数据库的调用在前端代码中即可完成。但是从上面几种读写权限来看,并没有办法实现对另一个用户创建的数据进行修改或者删除的操作(当然这也是非常不可取的),于是云函数就派上用场了。 2.2.2 云函数 我理解的云函数,则是跑在云端的一个函数脚本文件。 在接触云开发之前,如果我们想要去调用微信公众平台提供的API(例如发起退款、发送模板消息等),则需要在后端代码去实现,然后只需要给前端返回一个JSON表示请求状态即可。或者想要去实现上述描述中,修改一条由他人创建的数据的功能时,都是有后端工程师去完成的。 在本次开发中,我深刻体会到了云函数的强大,以及微信公众平台工程师设计产品的严谨性。 Mango Daily用到了微信公众平台的模板消息功能,所以需要在合适的时机请求微信官方提供的API。 因为取消了后端的开发,所以一开始打算直接在小程序端去请求官方API。但是失败了。因为此请求涉及APPKEY等重要信息,禁止在前端直接请求。这样就可以通过云函数去代替先前的后端开发,最后将状态返回给小程序端即可。 另外,云函数对云数据库有更高的操作权限,所以想要修改、删除他人生成的数据时,云函数可以直接进行操作。 云函数还提供定时触发功能,不过在本项目中暂未涉及。 2.2.3 云存储 本次开发省去了使用其他服务商的存储服务,全部得力于云存储功能。云存储允许上传多种文件类型,像图片、音频等文件还可以直接在小程序端调用。这里我们使用云存储实现了文章插图的功能。 [图片] 2.3 优化 2.3.1 全局配置 之前在某个项目中实现过国际化功能,即按照不同使用地区显示中英文的操作。这里我沿用了之前的设计,将代码中所有的提示文字提取到一个文件中,在wxml中使用变量代替文字。这样处理之后,文案可以集中管理。 全局文案配置文件: [图片] js文件 [代码]const app = getApp(); Page({ data: { text: app.text.calendar, } // ... }) [代码] wxml文件 [代码]<!-- 保存 --> <view class="btn-bg"> <view class='finish-btn mango-bg' bindtap="editHabit">{{ text.editHabit }}</view> </view> <!-- 保存 end --> [代码] 2.3.2 数据层封装 Mango Daily 数据操作进行了两次封装,一层是对云数据库API进行封装,第二层是每一个数据集合都对应一个Manager管理层。 以用户集合 User,Article 为例,项目中的结构如下: util |- db.js manager |- Article.js |- User.js db.js 是对云数据库API的封装,实现了增删查改等操作,以更新数据为例。 [代码]/** * 更新数据 */ const update = (collection, _id, data) => { return new Promise((resolve, reject) => { if (!exist(collection)) { reject(401, resCode[401]); } db.collection(collection).doc(_id).update({ data: data }).then(res => { resolve(res); }).catch((code, msg) => { reject(code, msg); }); }); } [代码] Article.js 是文章集合的管理类,同样实现了增删查改等操作,不过其是基于 db.js 进行扩展的。以更新文章操作为例: [代码]/** * 更新 */ const update = (_id, data) => { return new Promise((resolve, reject) => { db.update(collection, _id, data).then(res => { resolve(res); }).catch((code, msg) => { reject(db.errMsg); }); }); } [代码] 之所以封装两层,是想尽量减少Page对象中对云数据库的直接调用。这样在页面js文件中只需要调用某一个Manager提供的函数即可。 2.3.3 后台上传策略 Mango Daily还实现了发送模板消息的功能,这就涉及到了FromID的收集。目前FromID的收集大部分采用埋点的方式。 如果每次采集到新的FromID都直接上传到数据库存储,可能会造成网络资源的浪费,所以需要选择合适的时机上传数据。 在本项目中,每次采集到FromID,首先存到 globalData 中,当小程序进入后台状态时,再进行数据的上传。 app.js 中的实现: [代码]/** * 后台监听 */ onHide: function() { this.uploadFormID(); }, /** * 上传token */ uploadFormID: function() { let ids = this.globalData.formIds; if (ids.length == 0) { return ; } let formId = ids.pop(); this.push.upload(formId).then(_ => { console.log("上传formID:" , formId); this.uploadFormID(); }).catch(err => { console.log(err); }); }, [代码] 3. 维护 很遗憾,这一部分可能没有太多需要写的。 在18年的项目中,需要考虑数据库的维护问题。但是使用了云开发之后,Serverless的优点就表现出来了。我无须将太多的精力放在后端的维护上。 4. 总结 在本次项目开发中,我深刻体会到了云开发的便捷性。无须自己实现鉴权,对接第三方存储。数据方面,增删查改功能非常方便。云开发提供的种种便利,让我在有新创意的时候,优先选择小程序+云开发的方式去实现。 [代码]你好,你的小程序涉及用户自行生成内容的发布/分享/交流,属社交范畴,为个人主体小程序未开放类目,建议申请企业主体小程序 [代码] 另外,Mango Daily中的随笔功能属于用户自行生成内容功能,所以在上架的时候,个人开发者账号是不被允许的,所以在考虑上架产品的时候,请按照实际情况酌情考虑选择账号主体类型。 2019.6.21补充:很遗憾,本项目没有进决赛。后续加油啦!
2019-06-28 - 【大赛文章征集】八“粤”你好——浅谈Attack 足球
大家好,我们是来自华中赛区的NCHU_文体两开花团队,意在文娱与体育,两面皆开花。 一、开篇 在路上,开着导航,副驾驶吐槽司机:“你又开错路了!路口慢点,赶紧找地方还车!” 在饭店,老师诧异着:“他们俩怎么还没过来?啥?你们租车去玩的?” 这就是我们在长沙最后一天的缩影——一群富有朝气的年轻人和两位负责的老师,喜悦地享受着长沙之行的尾巴。 我们是开心的,也是痛苦的。不过,好的是先经历了痛苦,后品尝了快乐。时光拉回到比赛前一天的晚上,为了使现场演示和答辩达到更好的效果,团队全体准备到凌晨3点,从一次次的排练,到PPT版本的兼容性,每一个细节,都尽心准备着。多少个日夜的辛苦,最终成就了我们的故事。 从南昌到长沙的一路走来,我们成长着、蜕变着。其中点点滴滴,心路历程,技术经验,无不是我们想要分享的。 二、源起,Attack!足球! 为什么是Attack 足球?最初的Attack 足球,本是为南昌航空大学软件学院足球队能更好的在比赛中发挥而定做的。“我们不仅是足球队的一员,还是软件学院的一名学生,具有得天独厚的优势。如果将足球战术板与软件结合到一起,会碰撞出怎样奇妙的火花呢?”这句话,是我们在 《【征文大学篇】Attack!足球!》 对Attack 足球的一段描述。 [图片] 软件学院足球队,在战术讲解方面遇到了多种问题,我们本希望开发一款小程序,帮助其解决这些困难。但随着对产品的深入理解,不禁反问自己,既然软件足球队有使用战术板的不便,那么其他球队有没有这种不便呢?能不能做一款普适大众的 APP,让更多有需求的校园球队,享受Attack 足球带来的便利呢? 三、市场分析,产品定位 要从哪几个方面去分析一个产品呢?在所做的工作中,我们选取了市场调研、应用场景、用户三个角度为大家进行分析,希望我们的思路能对大家有帮助。 既然有了方向,便要付出行动。经过研究调查,2019年7月23日教育部发布的《全国青少年校园足球工作报告》中指出,五年来参加小学、初中、高中、大学四级联赛学生共计1255万人次,有3万多名省(区、市)级最佳阵容的学生参加全国夏(冬)令营活动。“经过过去五年的努力,我们现在已经在全国38万所中小学中遴选认定校园足球特色学校24126所,设立校园足球改革试验区38个,遴选校园足球试点县(区)135个,在全国布局建设‘满天星’训练营47个。”教育部体育卫生与艺术教育司司长王登峰说。 校园足球目前主要集中在小、初、高、大学的校园四级联赛中,本就拥有大量的用户基数。且前有“一带一路”国际青少年足球邀请赛,后有《全国青少年校园足球工作报告》,国家政策的支持,意味着校园足球的发展前景广阔,越来越多的人会接触足球,这个群体也会愈加庞大。 有潜在用户群体,就会有市场,而Attack 足球能否在其中抓住一丝机会呢? 从应用场景分析,战术板的使用场景可以分为三个阶段: (1)赛前的战术布置; (2)赛中的战术调整; (3)赛后的复盘分析。 显然 Attack 足球战术板不适用于赛中的战术调整阶段,因为大部分赛中的战术调整更多是临场发挥,管理人员再利用手机去调整录音发布安排效果不佳。以此为导向,Attack 足球的使用场景应集中在赛前及赛后的两个阶段。 从用户角度分析,谁才是Attack 足球的真正终端用户?教练/队长还是队员?To B or To C,这是一个问题。就目前看,教练/队长是管理和发布战术安排的用户,队员是查阅战术的用户,那么产品主要功能聚焦在数量较少的管理人员身上,是否会对用户量有实际上的影响。考虑目前国内的情况,教练员也未必是能够对足球理念和知识通透的管理者,小初高联赛中有条件的球队能够配备专业或者具备足球知识基础的教练进行指导训练,但还有大部分条件不够的球队是其他科老师进行兼职,该问题现象在大学阶段尤其显著,非校队或专业队员获得专业指导的机会少之又少。Attack 足球的问题在于,工具有了,谁来更好的使用工具。 因此,为什么一定要局限于教练/队长讲授战术的情景呢?如果能解决教与练的问题,Attack 足球战术板、战术安排、球队管理将是符合目前用户需求的一个有利导向工具,在减轻教练在绘制战术时的负担的同时,也能给予缺少专业教练球队队员获取优秀战术的机会。至此,我们将Attack 足球的产品定位定为高效、便捷提高校园足球战术素养的工具,旨在提高足球比赛赛前及赛后的两个阶段,教练讲授战术和队员学习、吸收战术的效率。 四、设计理念,足球文化 学习前人的优秀设计,总结自身的独特特点。设计大师Dieter Rams:十条优秀设计准则。足球文化:独特的设计理念。 产品定位有了,现在如何设计出一款超出用户体验预期,能够给用户带来长期价值的产品是关键。设计大师Dieter Rams曾总结出十条优秀的设计准则,我们对其进行了揣摩与解读,整理出符合Attack 足球的要点,并将其作为指导我们产品设计的准则。 Good design is innovative 与 Good design makes a product useful. Attack 足球作为一款工具类小程序,实用是基本属性。当然,创新也是Attack 足球的重要特点。Attack 足球除可以利用战术板功能绘制战术外,还能对战术进行保存,反复观看,解决了传统战术板零件携带不方便、零件易掉落,无法保存讲解战术的缺点;同时,它对应用场景进行了深度扩展,拥有实时演练功能,除用手机观看外,还可以利用PC,现场投影,供球队聚集在室内讲解战术。 Good design is aesthetic 与 Good design helps a product be understood. [图片] 如上图4-1,在UI设计方面,我们在不断的改进。Attack 足球经历了从最初的功能堆叠,到目前的结构清晰,再到即将发布的追求设计美感几个阶段。席克定律指出:面对更多选择,人们需要更多时间做出选择。某些情况下,需要花费的时间太长了,以至于他们根本做不出决定,因为决定的负担太沉重。在Attack 足球Version 1.0中,我们将所有功能铺开,用户进入小程序后很容易陷入选择的困境。而Version 2.0中,我们将功能模块化展示,界面也符合用户习惯的阅读模式——F模式。在Version 3.0中,保留了V2.0中功能模块及其布局,同时新增了功能战术库。为了不将功能杂糅在一个页面,给用户一个快速的选择反应,我们重新设计了界面,应用了导航栏,导航栏精简成三个,分别是战术板、战术库、我的。由V 2.0过渡到现在的V 3.0,我们将战术板与球队管理的功能分离,使导航栏的指向性更加明确。 Attack 足球不仅为第一次进入的用户精心准备了引导页,而且按功能模块划分了界面,使用户在不同场景下明确知道使用哪个功能模块,做到了“让产品不言自明”。 Good design is thorough to the last detail Attack 足球在每一个细节方面,都用心去思考,用心去打磨,这里举3个例子供大家参考。 (1)以引导页为例,在目前的版本中,我们的引导页只能通过点击下一步进行引导,通过思考,我们认为这是对用户不友好的。在即将推出的版本中,我们取消了这种强迫式设计,在引导页中增加了跳过与单击其他部位也可进入下一页功能。 [图片] (2)以我的球队页面为例,如上图4-2所示,Attack 足球使用了去线条化的方式,搭配卡片式设计,让页面看起来更加层次分明,干净利落。同时利用统一间距或符合五分原则、黄金比例的间距留白,区分信息的内容,将相互关联、相同或同类的内容放在一起, 使其形成一个视觉单元而不再是一个孤立的元素。这些细节的设计都是更好的帮助内容信息反馈的一种视觉形式,有助于组织信息、减少混乱、为用户提供清晰的结构。 (3)在Attack 足球战术板的功能中,之前的箭头有方向不确定的Bug,为了给用户更好的体验,我们的用心打磨,通过数学分析,目前已经解决该Bug,这个问题在下文的技术板块中也会提及。 除了吸收他人优秀的经验,我们也有属于自己的独特设计理念——将足球文化融入小程序的设计当中。如图,在初始使用界面,采用2.5D风格插画,模拟红蓝双方对垒,带来足球场的立体感。配色与图标方面,Attack 足球采用足球场的绿色作为主色调,搭配红牌黄牌的颜色,使用灰绿搭配的图标,整个画面更加明快活泼,简单大方而又不落俗套。如图,在Version 3.0 的首页,我们融入了更多的足球元素,如插画中的冠军杯、记分牌、战术板、足球、角旗杆等,并准备精心绘制每个省份的地方特色,比如天河足球场,希望给广大的足球爱好者以亲切感。 五、技术支撑,细节体现 在Attack 足球从idea变为现实的过程中,我们也遇到了很多技术难点。这里选取几个讲述,希望大家遇到相似的问题时,这些思路能够有些许帮助。 当我们的设计成型,就需要开发人员来实现了。好的产品背后必然有技术支撑,Attack 足球也不例外。我们采用分布式,基于微服务,将不同的功能部署到多个服务器中。图5-1是我们的系统总体架构。 [图片] 下面我们将从分布式服务注册发现、安全性、服务部署三个方面大致阐述一下。在阐述之前,先为大家讲述一下相关知识。 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。在所有情况下,每个任务代表着一个小的业务能力。 分布式是将系统独立分布在不同的物理机上面,其各个系统之间依赖网络来进行通信。在通信过程中处理繁多的服务器的主机与端口号、版本号、通信协议就是一个繁琐的过程,并且由于网络的隔离性,服务的状态并不能实时的被其他服务所发现。 因为分布式通信的繁琐性,我们在系统中引入了注册发现服务Eureka来实现以下功能。 服务注册:其构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议告知注册中心,并为其配置相应的服务名。 服务发现:服务之间的调用不再通过IP或者Host来进行调用,而是通过其所分配的服务名来进行调用。 心跳机制:客户端将心跳发送到Eureka服务器,让服务器了解其状态,进而更新其注册中心的服务清单。 通过使用Eureka,我们将各个独立的服务注册到其上,共同组成一个分布式系统。 分布式系统不像单体系统,只需开放所需开放的端口,就可以保障系统一定的封闭性与安全性。因为资源有限,我们只能采用多个云服务器来实现。无法通过构建内网,来确保系统内部的安全性,基础服务实例的相关端口也不可避免的向公网暴露,此举带来了极大的安全隐患。 如图5-2我们开启服务端Centos的Firewall服务。Firewall 能将不同的网络连接归类到不同的信任级别。将其自身的公网IP归类到trusted zone (信任所有连接)。将其他于其有通信需求的服务器IP添加到public zone(允许指定的进入连接),然后将其所需通信的端口与其自身的IP配置成rich rules来允许请求通过。 [图片] 这样公网只能访问到我们对外开放的两个433端口,我们只需对这两个端口的请求进行权限验证,就可以确保系统的安全性。在分布式架构中,传统的单体Session认证方案已经不能满足现有系统的需求。其服务独立分布在不同的服务器中,并且HTTP是无状态的。 传统的解决方案有以下两种: 使用共享Session机制。通过使用共享的Session缓存,各个服务的可以通过这个缓存获取的Session来进行认证,用户的权限信息保存在服务端,当系统的权限机制做出改变,可以灵活的对其进行修改 使用Token验证,将权限信息交由客户端来保存,可以减轻服务端的存储压力。每一次服务通过解析Token来验证用户的权限,需要合理设计加密机制,避免用户通过恶意更改Token来非法访问。 通过对上述两种机制的学习,我们为客户端产生唯一的身份标识符。用户的权限信息保存在Redis中,通过与客户端的身份标识符映射来确认用户的权限。这个技术方案提高了用户认证鉴定权限的效率,避免了大量的数据库和缓存查询。但是,这种方案的存在太大的性能开销,随着学习的深入,我们会在不久的将来采用JWT+ Refresh Token的方案对系统进行重构。 在最后的服务部署阶段。云服务器的带宽上限为5M,因此如果采用唯一的服务器来反向代理、路由处理,业务的性能上限将被这唯一的流量入口所限制。 如图5-3我们通过取消设立专门的路由服务器,直接将业务分为两块。用户直接访问两大业务服务器。将流量入口重定向为这两大业务服务器,可以在一定程度上降低服务带宽的限制。进一步将这两个业务的基础服务分别部署在不同的物理机上,可以在充分利用服务器的硬件资源和节省网络带宽开销中寻找一个平衡点。 [图片] 以上内容阐述了我们在架构微服务分布式后台架构时所面临的问题,通过使用这个架构我们将后台的服务解耦成了两大块业务,极大的提高了服务的灵活性。通过使用Fegin组件的服务端负载均衡功能,在一定程度上提高了业务的性能上限。 因个人才疏学浅以及对于微服务架构的理解不够透彻,内容中如有错误,敬请斧正。通过此次比赛,将个人所学的相关理论知识第一次应用到产品中。其中也走了不少弯路、错路,无论如何都是一次宝贵的经验。后续我们将会投了更多的精力去学习。 在产品的前端方面,如图5-4,初期版本存在一个极大影响了用户的使用体验的地方。那就是之前提到过的箭头方向问题——如何根据用户随意画的一条曲线,在这条曲线末尾加上正确方向的箭头。 [图片] 如何优化箭头的方向,给用户更好的体验呢?在Attack 足球中绘制的线条是一条不规则的曲线,描述一条不规则的曲线是比较难的,进而要判断这条不规则曲线的走向、根据走向添加箭头的方向是更加困难的。 首先,由图5-5可知,可以在一条曲线中取样若干离散的点来对这条曲线进行拟合,因此曲线的特征可以近似通过这些离散点序列来描述。每一条曲线都可以得到一个离散点的序列: [图片] 其中 L 表示由这条曲线取样得到的离散点序列,(x_n,y_n )表示曲线最终点坐标。 将连续的曲线转化为离散点的序列,我们便可以根据这个序列来确定曲线的特征。故可以选取序列中最后两个点坐标来近似描述曲线的方向,即有方向向量: [图片] 已知曲线终点方向向量,再由曲线终点坐标便可以确定箭头的位置和方向。 [图片] 由图5-6可知,箭头可以由三个点C,D,E,两条向量(CD) ⃗,(ED) ⃗ ((CD) ⃗代表向量CD,下同)来确定,我们假定向量(CD) ⃗与(ED) ⃗的夹角,即箭头的夹角为90度。已知曲线的方向向量(AB) ⃗与x轴夹角为θ,便可以确定(CD) ⃗与x轴夹角为θ+45°,(ED) ⃗与x轴夹角θ-45°,曲线终点作为D点从而确定箭头的方向与位置。 [图片] 在设计的初期中,我们粗略地认为L的终点方向是由第n个点和第n-1个点决定的,从而确定箭头的方向和位置。 不过,这种设计真的合理吗?让我们看看图5-7出现的问题。 [图片] 出现图5-7中的情况,是因为用户当即将绘制完成曲线L时误触屏幕造成了“回钩”,导致最后一个点(B点)落到了点A之前.按照之前的解决思路,箭头的方向是(AB) ⃗,因此就会出现图中的情况,如何避免这种情况呢? [图片] 为解决上述问题,我们在确定曲线方向时由三个点A(x_(n-2),y_(n-2) ),B(x_(n-1),y_(n-1) ),C(x_n,y_n )来对曲线方向进行约束。在这里引出一个自定义概念叫做终点的有效落点:如图5-9所示,过A点做AB的垂线DF,过B点做AB垂线EG,落在矩形区域DFGE内的点均不是C的有效落点,否则点C就是C的有效落点,C的有效落点可以通过BC两点判断L的最终方向。 具体解决步骤如下: Step1:首先由点A,B可以确定直线方程: [图片] 其中a , b, c为未知参数,确定直线位置。将点A (x_(n-2),y_(n-2) ),B (x_(n-1),y_(n-1) )带入上式确定参数a ,b ,c。 由于直线DF,GE分别与直线AB垂直,故已确定了直线L,易确定直线DF方程L_1,GE方程L_2: [图片] Step2:已知C点坐标为(x_n,y_n ),可以分别求出点C到L_1的距离d_1,到L_2的距离d_2以及到L的距离d。设直线DF和EG长度已知为μ,AB直线长度d_l。 Step3:判断点C是否为有效落点。 若d_1+d_2>d_l或者d >μ,说明C点在矩形外面,即C点为C的有效落点。 若d_1+d_2≤d_l并且d<=μ,说明C点在矩形内,即C点不是C的有效落点。 [图片] [图片] 那么,DF、EG的长度μ,取何值是较为合理的呢?若想减少用户误触屏幕对箭头方向的影响,提高箭头方向的正确性,则在DF、EG构成的平面内,CC_1、CC_2不应与AB相交。 如图5-10,由直线CC_1,CC_2构成一个箭头,其中CC_1和CC_2长度均为v,并且CC_1⊥CC_2,则取μ=2*v,即两倍的CC_1。证明如下: C点从B点沿着BEDA路线移动时,直线CC_1与直线AB的夹角范围是45°~135°。 当C点在BE之间时,若CC_1与AB不相交,BC> √2/2v ,所以BE ≥ BC 当C点在ED之间时,若CC_1与AB不相交,则只要保证CC_1⊥AB时不想交,即BE > v。 当C点在DA之间时,若CC_1与AB不相交,设CC_1与AD夹角为θ,则需要保证μ>vcosθ(0°<θ<90°)。 综上,v > vcosθ >√2/2v ,所以取μ=2*v。 如图5-11所示,此时C为有效落点,将BC两点作为方最后两个点确定箭头方向。如图5-12所示,此时C不是有效落点,把AB两点作为最后两个点确定箭头方向。 [图片] [图片] 呼,终于解决了令人头疼的箭头,这可是个让我们的前端小哥哥头痛了七天七夜的问题。 六、尾声 回望这一路,时间过的很快,长沙站的赛区决赛也已经结束多日,我们小黑板上距离微信小程序全国总决赛的倒计时也从两位数变成了个位数,大胆展出我们的小黑板。 [图片] 这一路上,Attack 足球团队已经和足球结下了不解之缘。足球是一种文化,是一种艺术,更是一种精神。它蕴含着奉献、实干、进步,蕴含着团队协作、顽强拼搏、永不言败、永葆激情等精神。而这些精神,也是我们团队不断内化的地方。本次大赛提供了这么一个机会,以赛促学、以赛促教、以赛促用,使我们每个人都有了蜕变,我们也会抓住这个机会,带着足球教会我们的精神,全力以赴,将更好的Attack 足球呈现给大家。 最后放上我们的小程序二维码,为了保证鲁棒性,最新版的Attack足球还在最后的测试阶段,不过现在已上线的版本不影响基本功能的正常使用,各位可以体验一下,更欢迎有球迷朋友来和我们沟通交流!谢谢! [图片]
2019-08-09 - 【高校开发者】双生日记开发经验分享
双生日记开发经验分享 Hello,我是双生日记的 Founder & Developer Airing。该项目的小程序端获得了 2018 C4——微信小程序应用开发赛的一等奖,而 iOS 端则获得了 2018 C4——移动应用创新赛的一等奖,目前累计注册用户已达 1 万+,并仍在不断开发维护中~ 本文将简要概括我们团队在产品的整个研发流程中的所做的工作,更侧重于介绍产品研发与团队管理的方法。 我将整个产品的研发分为以下四步: 立项 设计 开发 维护 [图片] 可以看到,以上四步形成了项目流程的闭环,使得产品能够良性发展。接下来我来具体谈谈这四步工作的内容。 1. 立项 项目立项是所有环节最开始的部分,我觉得也是最重要的部分,它的工作内容类似于“产品经理”的职责。虽然我是 Founder,但产品的探讨还是与大家共同完成的。具体而言,这个环节有两个内容: 产品脑暴 文档整理 1.1 产品脑暴 首先,我会先在团队中提出我的想法,并创建一个讨论区供大家讨论。我们是一个非常大的兴趣团队,虽然参与双生研发的只有寥寥三人,但是在产品脑暴的时候,团队的成员都提出了各自的见解与建议。例如,下图是我们在团队研发中讨论的内容。 [图片] 这里我们团队用的是产品是“语雀”,当然工具是随意的,用腾讯文档我觉得也非常方便,重要的是一定要形成电子版记录材料,如果只是在微信群里讨论或者线下简单聊聊,那讨论了、忘记了,那就相当低效,约等于没有讨论。 1.2 文档整理 第二步,整理脑暴的文档并撰写相关的研发文档,具体来说,包括但不限于: 需求文档 产品文档 模型文档 接口文档 [图片] PS. 这是我们团队的文档库,仅供参考:零熊 | 语雀 [图片] 2. 设计 设计工具我们用的 Sketch,但是不会把源文件直接发给研发同学,因为正版 Sketch 挺贵的,而且只支持 mac 系统。这里我们使用的工具是蓝湖,开始用的也是语雀的画板,但是发现实在是太难用了…另外,在蓝湖中的设计稿是可以分享的,并且邀请团队里的同学进行点评。 设计稿的内容具体包括: 规范 原型 UI 切图 规范重点是色彩规范、组件规范、和字体规范。原型更多的是交互说明,这里我们只是用批注的方式在 ui 上详细说明了一下交互,但如果直接用 flinto 去做也是可以的。flinto 的好处是更加直观,但是开发人员不一定能 get 到设计同学的全部内容。 [图片] 3. 开发 本部分分享的是产品研发的核心环节:项目开发。本环节我分享的内容会稍微多一些,但也略微零散,主要包含三个内容: 规划记录 开发工具 建议事项 3.1 规划记录 在开发之前,我习惯于自己先列一个 todolist 去罗列出项目中的各个需求点或技术点,从整体上会有一个直观的感受,也方便我去安排和规划自己的开发任务。这里我使用的工具是 Notion,我先按照重要的模块把产品分割成了 8 个部分,然后再在每个部分里写各自的 todolist,以免单文件 todolist 过长。 [图片] [图片] 当然,todolist 不单单记录待办事项而已,它更多的是承担一个开发日记的作用。我个人倾向于把开发中遇到的难点问题及解决方法,或者用到的资源顺手记录下来。我认为开发是一个学习和成长的过程,而不仅仅只是完成业务需求。 随手记录是方便日后整理为博客或者再遇到类似的问题可以快速定位,若不记录则很容易忘记。因此,做开发日记对学习的成效是非常大的。 [图片] 3.2 开发工具 针对微信小程序开发,我建议对开发很熟悉的同学可以尝试去使用 VS Code + 扩展 + 真机的模式进行开发,个人觉得这套流程既高效又不会出错。“高效”是 VS Code 自身的高效,而“出错”指的则是模拟器有时候效果与真机不同。 这里顺便安利一下我自己的 VS Code 配置: [图片] 我喜欢把资源管理器放在右边,有两个原因:一是左边是人的注意区,故应该放代码编辑器;二是我随时按 Cmd + B 可以隐藏资源管理器而同时不改变编辑器的位置,如果放左边,隐藏的时候编辑器会有一个位移,眼睛会很不舒服。 对于扩展,我这里用了几个比较有意思的: Color Highlight:颜色值高亮可视化为颜色本身,方便前端样式开发 TODO Highlight:高亮 TODO 与 FIXME miniapp:小程序标签与属性自动补全 Bracket Pair Colorizer:括号着色配对,这个特别方便。 Image preview:方便在代码里预览 uri 上的图片,我是用来看看自己资源路径有没有引错。 REST Client:HTTP 测试,方便开发、分享与 mock。 主题我用的是 Winter is Coming Theme + Material Icon Theme,我个人觉得黑色默认也非常好看。 3.3 注意事项 如果是协同开发我推荐搭配 Git History + Eslint 插件,当然如果自己开发,也免不了 Eslint。Git Commit 规范我们用的是这套: Commit 提交规范 [图片] 开发的时候也别忘记埋点,做一些打点统计,需要打点的地方根据项目需要检测的内容来定。如 PV、UV 这些小程序自带帮你统计了你可以不用打,但其他项目还是要统计的,或者直接规划好 Nginx 的日志,再对日志做分析也是 ok 的~ 如果前后端分离开发,前端同学可以自己接 mockjs 做一套符合接口文档规范的 mock 接口。 4. 维护 对于用户的反馈,我们智能筛选后自动提交到 github issue,再针对 issue 进行 label 和优先级分配。这是我们项目开源的主地址:oh-bear/2life。 [图片] 可以看到 issue 是比较杂乱的,所以还需要 github 的 project 去做一个任务画板。 [图片] by the way,安利一下小工具Devhub,可以很方便的检测自己负责项目的 issue。 [图片] 针对这些 issue,可以做一个阶段性的文档,回归到“立项”步骤,进行下一个小版本的开发。 [图片] 可以发现,我始终没有去选择使用甘特图软件,虽然甘特图更加直观,但是我不太喜欢把任务排的满满的、紧紧的,这样会不自觉地产生工作压力。最重要的是,我们毕竟不是工作嘛,只是一个兴趣开发,所以还是遵循自己的喜好来便好~ 好了,这次的分享就到这里。我是 Airing,我的个人博客是:https://me.ursb.me,欢迎大家来访交流~
2019-05-14 - 一个云函数里只能调用一个api吗?
@官方 想实现根据自定义主键的upsert功能,先add,失败在update, 就在云函数里运行两个api,但是只有add的被调用,update的没有被调用 这是为啥?
2019-08-29 - 7个工作日内未审核的请不要催审了,靠自己吧!
最近社区里关于审核的帖子很多,不能愉快的在社区划水了。 很多人在社区发帖各种急,有活动要上线的,有严重BUG要修复的,有客户催的,有老板提着大刀在后面等着等等的理由,但是这些理由官方几乎是直接忽略的,不要指望审核人员能为你做什么。 社区能看到不少几个小时未审核就发帖催的,只有真正超过7个工作日未审核的小程序官方才会处理,否则普通的催审帖子发的越多不仅不会加快审核效率,可能还会降低审核的速度。与其浪费时间发无意义的帖子,还不如利用官方提供的资源和奖励去争取真正的极速审核资格,如果自己没有能力满足极速审核的条件,请耐心等待正常审核。 审核周期 普通小程序: 工作时间:1-7个工作日(无特殊情况都能在这期间审核完成)。 极速审核: 工作时间:周一到周五,9点-21点;周六周日,9点-19点。 第三方服务商预上线请参考官方公告:《第三方服务商quota调整与加急审核机制上线》 PS:审核未通过时候尽量不要撤回重新提交,否则重新排队。 极速审核 【推荐】小程序评测 小程序评测,旨在鼓励开发者将小程序做的更好,提供更优质的服务、更优秀的用户体验。通过运营、性能、用户指标综合评估小程序的数据情况,并经由人工审核评估小程序的功能体验情况,最终得出综合评定,并给予达标的小程序诸如加速审核,内测能力的奖励。 根据上个月最后一天的运营、性能、用户指标情况判断是否进行服务审核评定,每月初更新审核结果。小程序进入服务审核的前提是,运营指标为达标,性能指标为优秀,用户指标为优秀;反之,则当月不进行服务审核。 《小程序评测服务审核细则》 社区突出贡献者激励计划 小程序极速审核通道,开发者参与开发的小程序将可享受一个月快速审核的奖励。 时间:每个自然月的贡献统计,下个月将清零重新计算。 《社区突出贡献者激励计划》 催审官方回复截图(仅供参考) [图片] [图片] [图片]
2019-09-12 - 记录--根据经纬度计算直线距离
/** 经纬度计算两点之间的距离,不是很准确 */ [代码]function distance(lat1, lng1, lat2, lng2) { lat1 = lat1 || 0; lng1 = lng1 || 0; lat2 = lat2 || 0; lng2 = lng2 || 0; var rad1 = lat1 * Math.PI / 180.0; var rad2 = lat2 * Math.PI / 180.0; var a = rad1 - rad2; var b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; var r = 6378.137; var distance = r * 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(rad1) * Math.cos(rad2) * Math.pow(Math.sin(b / 2), 2))); return distance; } [代码]
2019-07-24 - “小程序征文·大学篇——分享你和小程序的故事”评选结果
[图片] 感谢大家对【小程序征文·大学篇——分享你和小程序的故事】的热情参与与支持。主办方已基于评审规则,综合评委打分,评选出了本次活动的各个奖项。奖品将在结果公示后一个月内发放到作者帐户/联系并邮寄给作者。 现将评选结果公示如下: 评选结果 奖项 作者 作品 奖品 一等奖 金煜峰 小程序富文本能力的深入研究与应用微信开放社区官方认证“优秀创作者”勋章一枚 限量版微信数码相框1个 卓生 【征文大学篇】Attack!足球! 二等奖 林绪鑫🕊 #小程序大赛# 如何以产品视角打造一款小程序? 微信开放社区官方认证“优秀创作者”勋章一枚 限量版微信跳一跳运动系列套装1份 Zero 「高校开发者」小程序+ 云开发= 个人开发者快速创作的平台唐家四少官微 【高校开发者】小程序,遇见你真好Gasink 高校微信小程序大赛经验分享杂谈Zing22.渔政🍳 征文大学篇- 云开发·后端鉴权思路分享最佳人气奖 卓生 【征文大学篇】Attack!足球!限量版微信logo-T恤1件最受欢迎奖 在所不辞 小程序开发 · 一个大二学生的成长之旅限量版微信黄脸-T恤1件 微信团队 2019/6/28
2019-06-28 - Thor UI组件库,小程序代码片段分享
尊敬的开发者,欢迎体验Thor UI! 该项目主要是一些小程序代码片段的分享,以及基础组件的封装。项目免费开源,源码可在GitHub上下载,会不定期进行更新。 项目可能存在缺陷或者bug,如果您在使用过程中发现问题或者有更好的建议,可反馈给我。您也可以将自己觉得不错的案例分享给我,我会扩展到此项目中。 ThorUI QQ交流群:928308676 扫码体验(一) [图片] 扫码体验(二) [图片] 组件文档地址: http://www.thorui.cn/doc ThorUI uni-app版本GitHub地址: https://github.com/dingyong0214/ThorUI-uniapp ThorUI uni-app版本插件市场地址: https://ext.dcloud.net.cn/plugin?id=556 ThorUI 小程序版本GitHub地址: https://github.com/dingyong0214/ThorUI ThorUI 小程序版本插件市场地址: https://ext.dcloud.net.cn/plugin?id=569 V1.6.5(2021-05-24) 1.tui-validation(表单验证)优化,新增validator自定义验证配置项,具体查看文档。 2.tui-round-progress(圆形进度条)组件优化,修复已知问题。 3.tui-cascade-selection(级联选择器)组件优化,修复已知问题。 4.tui-tabs(标签页)组件优化,选项卡可设置数字角标。 ===================== 【ThorUI示例V1.1.0】更新: 1.tui-org-tree(组织架构树)组件优化,可控制节点内容排版方式、节点选中状态、展开收起子节点,具体查看文档。 2.新增tui-form(表单)组件,主要用于表单验证。 3.新增tui-input(输入框)组件,原生input组件增强。 4.新增tui-textarea(多行输入框)组件。 5.新增tui-label(标签)组件,用来改进表单组件的可用性。 6.新增tui-radio(单项选择器)组件。 7.新增tui-checkbox(多项选择器)组件。 8.新增tui-switch(开关)组件。 9.新增tui-picker(选择器)组件,支持1~3级数据。 10.新增tui-landscape(压屏窗)组件。 11.新增tui-segmented-control(分段器)组件。 12.新增tui-notice-bar(通告栏)组件。 13.新增tui-alerts(警告框)组件。 14.新增tui-request(数据请求)封装,支持Promise,支持请求拦截和响应拦截,支持请求未结束之前阻止重复请求等。 15.tui-utils(工具类)优化,具体查看文档。 16.新增tui-row组件,配合组件tui-col组件使用(24栅格化布局)。 17.新增tui-tree-view(树型菜单)组件。 18.新增tui-charts-column(柱状图-css版)组件。 19.新增tui-charts-bar(横向柱状图-css版)组件。 20.新增tui-charts-line(折线图表-css版)组件。 21.新增tui-charts-pie(饼状图表-css版)组件。 22.tui-lazyload-img(图片懒加载)组件优化,修复已知问题。 23.新增tui-pagination(分页器)组件。 部分功能截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] V1.4.0: 1.新增日期时间选择器组件。 2.H5新增复制文本功能。 3.新增悬浮按钮组件。 4.新增Tabbar组件。 5.新增tabs标签页组件。 6.新增折叠面板组件。 7.新增图片上传组件。 8.NumberBox组件优化调整。 9.Modal组件优化调整。 10.sticky组件优化调整。 11.countdown组件优化调整。 12.商城模板新增购物车、我的、提交订单、支付成功、我的订单、地址列表、新增地址、设置、用户信息等页面。 V1.3.0 1.新增倒计时组件:时分秒倒计时,支持设置大小,颜色等。 2.新增分隔符组件:Divider分隔符,可设置占据高度,线条宽度,颜色等。 3.新增卡片轮播:包含顶部轮播,秒杀商品轮播等。 4.nvue下拉刷新优化。 5.修复已知bug。 V1.2.2 1.新增组件Modal弹框:可设置按钮数,按钮样式,提示文字样式等,还可自定义弹框内容。 2.修复部分已知bug。 ThorUI V1.2.1 1.新增组件Modal弹框:可设置按钮数,按钮样式,提示文字样式等,还可自定义弹框内容。 2.修复已知bug。 3.ThorUI已上线uni-app版本,请移步uni-app插件市场搜索ThorUI。 ThorUI V1.2.0 1.新增组件NumberBox数字框:可设置步长,支持浮点数,支持调整样式(可单独设置)。 2.新增组件Rate评分:可设置星星数,可设置大小颜色。 3.新增聊天模板,包含:消息列表,好友列表,聊天界面等。 4.新增商城模板,包含:商城首页,商城列表,商城详情等。 5.优化部分体验。 ThorUI V1.1.0 1.将基础组件移出扩展,单独出来。 2.扩展改为单独tab bar选项extend。 3.新增滚动消息(extend=>滚动消息):包括顶部通告栏,滚动新闻,以及搜索框中出现的热搜产品。 4.新增弹层下拉选择(extend=>弹层下拉选择):包含顶部下拉选择列表、输入框下拉选择以及底部分享弹层。 5.新增ActionSheet操作菜单(extend=>ActionSheet):可加入提示信息,可单独设置字体样式。 6.新增新闻模板(extend=>新闻模板):包含新闻列表,新闻详情,评论等。 7.部分功能优化,修复已知bug。 ThorUI V1.0.0 1.【地图】新增拖拽定位功能 2.【扩展】新增基础组件,包括:字体图标,按钮,Grid宫格,List列表,Card卡片… 3.【扩展】新增数字键盘 4.【扩展】新增时间轴 5.完善thor(个人中心)功能,包括:关于Thor UI,模拟登录,GitHub地址复制,赞赏,反馈,更新日志等 6.已知bug修复,以及部分功能优化 商城模板部分截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] 新闻模板部分截图 [图片] [图片] [图片] [图片] [图片] [图片] 聊天模板截图 [图片] [图片] [图片] 组件功能部分截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片]
2021-06-01 - #小程序大赛# 如何以产品视角打造一款小程序?
[图片] 5月31日,小程序大赛作品提交,终于告一段落。 这段时间,相信很多同学和我们一样,在项目过程中积累、收获了不少经验,或是设计,或是开发,或是其他,都能从不同角度体会到小程序大赛的意义。今天,我想分享一下,在项目过程中,是如何以一种产品的视角,去打造小程序“ddl冲鸭”。扫描下方小程序码即可体验哦~ [图片] 【写在前面】虽然本文是个人撰写,但小程序项目是团队共同完成,无论如何,在这里都应该先致谢我们“咕咕咕”队伍的指导老师和参赛队友,以及各位作为体验用户、帮忙测试程序并给予意见反馈的朋友。 本文篇幅较长,主要包括以下内容: 一、产品定位——目标用户与主要功能 二、产品灵感——为什么选择小程序“ddl冲鸭”? 三、关注用户——从用户角度进行需求分析与功能设计 四、竞品现状——我们如何做得比他人更好? 五、精益求精——抓住用户心理进行产品设计 六、把握细节——优化产品体验,提高用户粘性 七、创新运营——产品已从0到1,还要从1到100 八、小结 感谢阅读。 一、产品定位——目标用户与主要功能这次参赛作品是微信小程序“ddl冲鸭”,主要目标用户是高校学生,主要是为了解决大学生ddl记录、管理需求。“ddl”即“deadline”,意为“最后期限/截止时间”,相信很多同学对这个词并不陌生,甚至内心有着强烈的共鸣。大学生日常ddl:提交作业的ddl,提交社团工作方案的ddl,提交比赛作品的ddl……日常ddl任务繁多,高效记录与管理成一大需求。 然而,目前市场上并没有针对ddl推出的产品,因此,针对性地解决用户需求、并实现产品的特色化功能,可成为产品的一大亮点。 “ddl冲鸭”的主要功能有: (1)记录ddl(可设置ddl分类、使用文本或语音记录、上传相关图片、选择相关位置等); (2)通过将ddl设为星标、设置分类,以及查看不同状态,高效地管理ddl; (3)通过分享ddl,让好友了解自己的ddl,更可将好友的类似ddl(如同班同学具有共同的班级作业)直接添加为自己的ddl,简化手动创建步骤; (4)发起群组ddl,小组好友共同为ddl努力,交流感想,促进完成; (5)消息提醒功能,如ddl过期提醒等。 [图片] [图片] 由于介绍作品功能并非本文重点,更多详情可查看以下演示视频: [视频] 如果您看完了演示视频,相信接下来的大部分内容都更加易懂。 二、产品灵感——为什么选择小程序“ddl冲鸭”?同样作为大学生,我们深感ddl在大学日常生活中的影响,推出“ddl冲鸭”,在为自己解决需求的同时,也希望它能够给更多人带来方便。因此,我们从实际出发,着重于解决ddl记录、管理需求,并在此基础上力求创新、创意。 小程序的命名: 我们很熟悉的微信、淘宝、饿了么这些产品的命名,基本都是通过名称就可以比较直观地体现主要功能,同时又避免枯燥无味而缺乏新意。比如,微信为什么不叫“信息”,淘宝为什么不叫“购物”,饿了么为什么不叫“叫外卖”,所以产品的命名还是比较重要的。 “ddl”不仅与主题对应,同时引起目标用户内心强烈共鸣,也让用户对产品的主要功能有初步设想;“冲鸭”即“冲呀”,结合了当下的网络流行词语,符合大学生的喜好。同时,小程序命名也体现了希望用户在面对ddl时向前冲、加油努力。 小程序的出现,不是为了取代手机app,它是为了在适当的情景下,更好地为用户提供服务。如小程序官方所介绍的,它无需下载,实现了应用“触手可及”的梦想,也体现了“用完即走”的理念。相对app,小程序开发门槛较低,能够满足用户的简单需求,同时,结合微信本身为小程序奠定的基础,如“搜一搜”等,也有利于产品运营、降低引流成本。 相信使用iPhone的朋友都了解,iOS 12有个新功能,可以统计每天使用手机上各款app的时长。不难发现,对于多数人来说,微信使用时长稳居第一,且占比极大。既然如此,我们也应该思考基于微信环境下,用户的使用习惯。“ddl冲鸭”在UI设计方面,以白色为主色,绿色为辅色,与微信7.0版本设计风格基本对应,能让用户产生高度融入之感。同时,风格简洁直观,让用户易懂易操作。用户长期使用微信的习惯也为小程序的使用带来了方便的基础,比如使用微信小程序可以方便用户充分利用碎片时间,进行ddl管理。 三、关注用户——从用户角度进行需求分析与功能设计相信之前有关注过2019微信公开课的朋友,应该都对张小龙的这句话比较熟悉:“我们应该关注用户,而不是竞争对手。”作为一个产品策划的角色,应该有着“用户至上”的基本理念,同理心、善于换位思考成为了必备基本技能之一。所以我们从用户的角度出发,注重用户的实际需求,进行需求分析、管理、功能设计。 从宏观到微观,整体的功能设计都应该以解决用户实际需求为基本原则。 我们以问卷收集、用户访谈、竞品调研等多种形式,挖掘用户需求,同时也是为了针对市场上已有竞品的不足,针对性进行弥补。 [图片] 具体的调研分析,这里不进行详细介绍,但我们大致得到了如下结论: (1)用户现状:ddl记录与管理需求存在,产品开发必要性强——这充分论证了我们产品定位的合理性; (2)竞品现状:目前市场无针对ddl产品,用户个性化需求未被满足——在针对ddl主题进行产品设计的同时,我们也应该针对用户的个性化需求,打造具有特色的产品; (3)用户关联:一般在完成任务中有一定的好友互动,群组任务也较为常见——我们据此设计了好友分享ddl、群组ddl等功能,满足用户的常见需求; (4)用户习惯:存在ddl紧迫感、未完成愧疚感、已完成成就感等——便于我们抓住用户习惯与心理特点,完善产品设计; (5)用户期望:希望需求得到满足,产品体验良好——实现功能后,应该注意细节,追求为用户提供最好的产品体验。 进行需求分析、分类后,应对其进行分类管理。不可能第一个版本就解决所有的需求,也没有这个必要,因为这样会导致开发成本大、风险也高、且发展空间小。我们会着重于解决基本型需求与期望型需求,在此基础上完善魅力型需求,从各种细节优化出发,力求在满足功能需求的基础上提升用户对产品的体验好感度。 四、竞品现状——我们如何做得比他人更好?有的人会说,“ddl冲鸭”不就是一个记事本工具那样的产品吗?不是这样的,我们针对ddl主题,并对比竞品,实现了许多特色化的功能。以下谈谈“ddl冲鸭”的几个功能上的亮点: (1)ddl记录 区别于传统的文本记录,“ddl冲鸭”加入了语音、图片、位置等多种不同形式的记录,多样化完善ddl记录,满足用户的个性化需求。 比如,有时用户觉得输入大段文字记录比较麻烦,就可以用语音记录;上课时拍到老师的PPT,不方便转换为文字,就可以用图片记录;需要到某地办理港澳通行证,ddl又与位置有关等。 [图片] (2)ddl管理 在ddl列表页面可以快速切换不同状态的ddl(全部、进行中、已完成、已删除、已过期),并可以通过设置ddl星标、分类等,快速筛选符合条件的ddl,还可以选择按最新创建时间、ddl时间为ddl列表排序,使原本繁琐的ddl列表变得一目了然; [图片] (3)管理进行中的ddl 相信大家都很了解,许多类似产品会在待办事项右侧显示剩余时间,如3天、5天等,而“ddl冲鸭”则在此基础上,根据剩余时间的不同,给予不同颜色的文本和不同长度的进度条进行提示,时间越少时文字颜色越深(从绿色到黄色、橙色、红色)、同时进度条长度越小,从视觉上给予用户ddl紧迫之感,督促用户更快完成ddl。 [图片] (4)便捷的创建ddl 当与好友具有共同或类似ddl时(如同班同学具有相同的课程作业ddl),可直接将好友分享给自己的ddl添加为自己的ddl,简化了用户手动创建ddl的步骤,为用户带来了方便(创建时也可对原有内容进行修改)。 [图片] (5)群组ddl功能 为进一步增强用户互动与提高效率,设计了群组ddl功能,当一个小组具有共同的ddl(如班级课程作业小组)时,可发起群组ddl,大家在同一个ddl中共同加油努力,交流ddl感想,互相促进完成。但我们设计这个功能的定位是小组,而不是社区,因此群组上限为20人。 [图片] 以上是关于作品,在区别于其他竞品方面的几个特色功能。总体而言,“ddl冲鸭”具有以下几个特点: (1)用途明确,用户共鸣强 (2)使用便捷,用户粘性大 (3)形式新颖,用户兴趣大 五、精益求精——抓住用户心理进行产品设计根据调研结果,结合我们自身换位思考、深入体验,我们从用户心理的角度,精益求精,完善产品设计。 (1)未完成ddl的心理罪恶感——设计过期提醒功能 当有未完成的ddl时,进入小程序首页将会弹出提示,为鸭子落泪的图片,并播放“咕咕咕”的鸽子声,提醒用户已经放了自己的ddl的鸽子; 这样设计是为了与用户未完成ddl时的心理愧疚感对应,提醒用户以后要按时完成ddl任务,不能再放了自己的鸽子。 [图片] (2)无ddl时的悠闲感——无ddl时提示相关图片 没有ddl时,显示一只正在睡觉的鸭子,表明当前没有ddl任务的悠闲感,与用户当前的心理对应。 [图片] (3)有ddl时的紧迫感——通过不同颜色、进度条显示剩余时间 这个前面已经简单介绍过,即根据剩余时间的不同,给予不同颜色的文本和不同长度的进度条进行提示,时间越少时文字颜色越深(从绿色到黄色、橙色、红色)、同时进度条长度越小,从视觉上给予用户ddl紧迫之感,督促用户更快完成ddl。 [图片] (4)用户懒、害怕选择——合理设置默认值、提供方便用户的功能 ①创建ddl时,默认时间为当前时间三天后的22时,默认ddl类别为“学习”。这是根据我们调研结果,得出通常在ddl剩余三天时,用户会开始重视ddl,且当下大学生面临的多数ddl为学习方面的(如课程作业)。设置默认值,用户无需选择,方便用户。 [图片] ②记录ddl,可以粘贴文本、语音记录、将好友分享的ddl直接添加为自己的ddl,这些都是为了方便用户而推出的功能。 [图片] [图片] (5)用户渴望得到鼓励——弹图提示,群组交流 ①个人创建或完成ddl时,要与用户充满斗志的激情感、完成时的喜悦感与成就感对应,因此我们设计了相关的图片进行提示,而不是普通的文本模态框。 [图片] [图片] ②设计了群组功能,完成ddl时可以交流感想,鼓励他人;同时,群组排行榜也有利于促进用户完成ddl。 [图片] [图片] 六、把握细节——优化产品体验,提高用户黏性产品设计过程中,充分考虑多个细节。虽然它们不一定会对产品的主要功能产生影响,但积少成多,优化了用户使用体验,有利于提高用户黏性,如: (1)启动页展示轮播图,让未使用过的新用户初步了解小程序的功能,只有第一次进入时会显示。 [图片] (2)不是第一次使用的用户,已经了解过小程序的基本功能,那么用户进来最想要看到的内容是什么——当然ddl列表,且是“进行中”状态(即还未完成的)的ddl列表。 就像微信,我们打开微信后最先想要看到的是什么?当然是微信的未读消息,而不是通讯录之类的页面。 (3)操作体验优化,更加易用:标签页除了点击顶部导航切换,也可以左右滑动进行切换,有利于方便用户单手操作。 [图片] (4)操作时,必要时给予提示,避免用户误操作,如误删除 [图片] [图片] (5)充分考虑多种可能的不同情况,如: ①用户第一次进入小程序时,进入启动页,在启动页进行授权,授权后才可以使用小程序;但如果用户是通过他人分享ddl页面第一次进入小程序,则还需要进行授权才能使用。 [图片] [图片] ②前面提到,群组ddl功能的设计,是针对小组,而不是为了打造社区,因此群组上限为20人,必要时也应给予提示。这样的设计是遵循了产品的定位,与产品定位对应——即做效率类工具,而不是做社交平台。 [图片] (6)尊重用户隐私与体验,如: ①用户只是想与他人分享自己正在忙的ddl,但没有打算让他人添加为自己的ddl——分享时可进行选择。 [图片] ②为避免消息过多给用户带来骚扰,消息列表最多只显示最近20条消息。 [图片] (7)在线客服、意见反馈等不可缺少,这是作为用户与我们沟通的桥梁之一,也有利于不断改进产品。 [图片] [图片] 七、创新运营——产品已从0到1,还要从1到100由于本文主要是在讲产品策划,因此开发方面不多讲。但产品运营与产品策划密不可分,产品策划强调从0到1,产品运营强调从1到100,在产品被设计开发出来之后,还要结合运营推广,让它真正投入使用,才能体现它的价值。 这里简单提几点想法: (1)既然产品是微信小程序,那么最基本的就是结合微信进行推广,除了普通的微信群、朋友圈、公众号,还可以结合微信新功能“时刻视频”、“看一看”等。在微信环境下的推广,有利于用户快速定位到产品,减少引流成本; (2)针对目标用户:由于目标用户是高校学生,可以借助各高校相关微信公众号、微博、贴吧等进行线上宣传; (3)培养目标KOL(关键意见领袖),KOL在产品上线后发挥小范围同化辐射作用,提高其他用户对产品的信任度,也是作为用户自传播的方式之一; (4)组建线上ddl交流群或活动,在收集用户反馈的同时提高产品存在感,同时也可作为MVP产品,更好地了解用户的需求与实际情况; (5)结合当下深受年轻群体(包括目标用户即大学生在内)喜爱的短视频,可制作相关宣传短视频(要追求创意),借助腾讯微视、快手、抖音等短视频平台进行宣传; (6)结合小程序项目已有“冲鸭”系列图片设计,进一步拓展,制作微信表情包“冲鸭”系列,可申请在微信表情包商店上架,在微信聊天中进行使用,提高产品曝光、辅助产品推广。 [图片] 如果能在作品提交前上线运营,有运营数据支撑(如用户量、用户使用情况等),就说明产品的确有人用,说明产品开发的合理性、必要性。 八、小结其实我和其他的队友一样,我们队里的同学没有软件专业的,也没有计算机专业的,甚至有中文之类的文科专业同学。我们都是凭借兴趣自学了相关的技能,这也是驱动我们认真投入的重要原因之一——是为了兴趣,为了挑战,不是为了参赛,不是为了拿奖。论开发技术,我们也许并不如很多计算机专业的,但在我眼里,产品思维比技术重要,这也是我今天写文章,是以产品的角度来写、而不是以开发的角度来写的原因(当然,写得也比较简单,还望见谅)。以下是我的几点简单的参赛感想: 1、首先应该确定的问题,是产品定位、目标用户等,这些作为整个项目的基础以及方向,至关重要;在构思时应该多方面考虑(功能设计、目标用户、开发成本、运营方案等),在满足基本需求的前提下,力求创新,才能比别人做得更好(一味地实现必备需求而不追求创新,则很难突出产品亮点); 2、开发固然占了很重要的一部分,但一个完整的项目,不是只有开发,设计、运营等都很重要,这些是共同推进一个产品成功的必不可少的因素; 3、大赛提到了“以赛促学”,个人认为对于初学者来说,不要急于使用高大上的技术以便快速做出功能很高大上的产品。技术基础要扎实,才有利于长远的发展。所以我们用的是原生开发技术,不使用任何第三方开发框架(这里说的是开发框架,但我们有一定程度地使用了UI组件库)。在WXML、WXSS、js等原生技术基础上,力求创新实践。 【写在最后】“ddl冲鸭”产品初步成形,虽已上线运营投入使用,但依然有很多不足之处,后面会尽力再继续完善。这一次做项目,同时也是为了实现个人理想。我本人是打算往互联网产品策划方向发展的,这次项目也是相当于一个伟大的尝试。 在学习、借鉴一些去年参赛作品的过程中,我发现有一些作品现在已经停止了更新维护,甚至还有作品暂停服务、无法访问、或者至今没有正式发布上线等,这也让我再次思考了大赛和作品的意义。 单纯抱着“想试试、想参与”的态度,真的容易做出一款很好的产品吗?我想起以前面试社团,其实那个部门没有很想去,尽管一开始很顺利,面到最后一轮的时候还是挂了。当时我的老师对我说:“其实你的内心根本就没有那种特别想去的决心,那么这种情况下你又怎么可能发挥出你最大的潜力呢?” 4月底,面了腾讯暑期实习产品策划岗,面了三轮后挂在了总监面,无缘HR面。但是也并不遗憾,毕竟当时面试好像是随机分配?所以我面的部门和产品,并不是我想要做的,我不怎么接触、兴趣也很一般,确实没有“非去不可”的心态。然而塞翁失马,焉知非福?印象深刻,当时面试官问我:“你为什么想要选择做腾讯的产品策划?”现在我想说:“我和腾讯一样,有着一颗想要创造出能够改变世界的产品的心。”鹅厂是我美丽羞涩的梦,祝鹅厂越来越好,也希望我和鹅厂有缘再次相遇。 文粗词浅,感谢阅读。如有意见,恳请指教,感激不尽。
2019-06-28 - 随手图标字体微信小程序插件
让开发者使用更多的图标字体插件,即可申请,即可使用。详情见下方文档。
1970-01-01 - 【开源】精美文艺的小程序,小白也能学习使用!
如果你喜欢,欢迎点个star⭐表示支持哈!感谢!
2019-01-27 - 小程序通用函数让你高效开发
这是一个小程序的通用函数库,欢迎大家下载使用
2018-10-29 - 微信小程序常用云函数模板分享
[图片]
2019-04-12 - 美食地图小程序模板
[图片]
2019-01-31 - 一个菜单按钮
去找小程序的菜单按钮,没有找到,于是自己摆弄了一个出来,虽然是个很简单的东西,考虑到可能还有其他人觉得写一个麻烦,现在把代码发一下,大神勿喷。 先看一下效果: [图片] 代码: cc-mainbutton.js [代码] Component({ lifetimes: { attached: function attached() { // 在组件实例进入页面节点树时执行 this.animation = wx.createAnimation(); }, detached: function detached() { // 在组件实例被从页面节点树移除时执行 } }, data: { dial_btn_options_show: false }, methods: { // 菜单按钮的动画 rotate: function rotate() { if (this.data.dial_btn_options_show == false) { this.animation.rotate(-135).step(); this.setData({ dial_btn_options_show: true animation: this.animation.export() }); } else { this.animation.rotate(0).step(); this.setData({ dial_btn_options_show: false animation: this.animation.export() }); } }, //点击子按钮 click_option: function click_option(e) { switch (e.currentTarget.dataset.option) { case '1': break; case '2': break; case '3': break; default: break; } } } }); [代码] cc-mainbutton.wxml [代码]<view class="main_btn_ctn" style="width: 60px;height: 60px;"> <image animation="{{animation}}" bindtap="rotate" class="dial-btn {{dial_btn_options_show?'dial-btn-active':''}}" src="../static/images/main-btn.png" /> <view bindtap="click_option" data-option="1" class="dial-btn--option flex-def flex-zCenter flex-cCenter flex-zTopBottom"> <image style="height: 25px;width: 25px" class="" src="../static/images/add_shuoshuo.png" mode="widthFix" /> </view> <view bindtap="click_option" data-option="2" class="dial-btn--option flex-def flex-zCenter flex-cCenter flex-zTopBottom"> <image style="height: 25px;width: 25px" class="" src="../static/images/reflesh.png" mode="widthFix" /> </view> <view bindtap="click_option" data-option="3" class="dial-btn--option flex-def flex-zCenter flex-cCenter flex-zTopBottom"> <image style="height: 25px;width: 25px" class="" src="../static/images/go-top.png" mode="widthFix" /> </view> </view> [代码] cc-mainbutton.wxss。 [代码]/* index/main-button/cc-mainbutton.wxss */ .flex-def { display: flex; } /* 主轴居中 */ .flex-zCenter { justify-content: center; } /* 侧轴居中 */ .flex-cCenter { align-items: center; } /* 主轴从上到下 */ .flex-zTopBottom { flex-direction: column; } .dial-btn { border: none; z-index: 7; position: absolute; height: 60px; width: 60px; left: 50%; top: 50%; margin: -30px 0 0 -30px; } /*子按钮初始位置隐藏在主按钮后面,透明度0*/ .dial-btn--option { background: yellowgreen; position: absolute; height: 46px; width: 46px; border-radius: 100%; left: 50%; top: 50%; margin: -23px 0 0 -23px; transform: translate(0, 0); /* 过渡效果 */ transition: opacity 0.25s ease-in-out, transform 0.25s ease 0s; } .dial-btn--option:nth-of-type(1) { z-index: 2; opacity: 0; transition-delay: 0.2s; } .dial-btn--option:nth-of-type(2) { z-index: 3; opacity: 0; transition-delay: 0.3s; } .dial-btn--option:nth-of-type(3) { z-index: 4; opacity: 0; transition-delay: 0.4s; } /* 通过nth-of-type定义每个子按钮的不同定位,设置透明度1 */ .dial-btn-active ~ .dial-btn--option:nth-of-type(1) { opacity: 1; transform: translate(-65px, 5px); } .dial-btn-active ~ .dial-btn--option:nth-of-type(2) { opacity: 1; transform: translate(-40px, -40px); } .dial-btn-active ~ .dial-btn--option:nth-of-type(3) { opacity: 1; transform: translate(5px, -65px); } [代码] 预览网址:https://developers.weixin.qq.com/s/if7B8SmT7E8q
2019-06-04 - 云函数中生成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 - 初试小程序接入three.js
看着小程序下的canvas日渐完善,特别是2.7.0库下新增了WebGL,终于可以摆脱原来用wx.createCanvasContext创建的2d上下文(不知为何在使用魔改后three.js中的CanvasRenderer渲染画面就是很慢,捕获JavaScript Profiler看着就是慢在draw方法上)。 不过理想很丰满,现实很骨感,想要在小程序上用three.js依然要来个大改造。让我们开始吧 官方文档里提供了一段如何获取WebGL Context的代码: [代码]Page({[代码][代码] [代码][代码]onReady() {[代码][代码] [代码][代码]const query = wx.createSelectorQuery()[代码][代码] [代码][代码]query.select([代码][代码]'#myCanvas'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]const canvas = res[0].node[代码][代码] [代码][代码]const gl = canvas.getContext([代码][代码]'webgl'[代码][代码])[代码][代码] [代码][代码]console.log(gl)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]})[代码]我们就从这里入手 首先先写个wxml: [代码]<[代码][代码]canvas[代码] [代码]type[代码][代码]=[代码][代码]"webgl"[代码] [代码]id[代码][代码]=[代码][代码]"webgl"[代码] [代码]width[代码][代码]=[代码][代码]"{{canvasWidth||(320*2)}}"[代码] [代码]height[代码][代码]=[代码][代码]"{{canvasHeight||(504*2)}}"[代码] [代码]style[代码][代码]=[代码][代码]'width:{{canvasStyleWidth||"320px"}};height:{{canvasStyleHeight||"504px"}};'[代码] [代码]bindtouchstart[代码][代码]=[代码][代码]'onTouchStart'[代码] [代码]bindtouchmove[代码][代码]=[代码][代码]'onTouchMove'[代码] [代码]bindtouchend[代码][代码]=[代码][代码]'onTouchEnd'[代码][代码]></[代码][代码]canvas[代码][代码]>[代码]其中width和height是设置画布大小的,style中的width和height是设置画布的实际渲染大小的 然后js: [代码]onReady:[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]var[代码] [代码]query = wx.createSelectorQuery().select([代码][代码]'#webgl'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]var[代码] [代码]canvas = res[0].node;[代码][代码] [代码][代码]requestAnimationFrame = canvas.requestAnimationFrame;[代码][代码] [代码][代码]canvas.width = canvas._width;[代码][代码] [代码][代码]canvas.height = canvas._height;[代码][代码] [代码][代码]canvas.style = {};[代码][代码] [代码][代码]canvas.style.width = canvas.width;[代码][代码] [代码][代码]canvas.style.height = canvas.height;[代码][代码] [代码][代码]self.init(canvas);[代码][代码] [代码][代码]self.animate();[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码]先模拟dom构造一个canvas对象,然后传入init方法中,我们在这里创建场景、相机、渲染器等 [代码]init: [代码][代码]function[代码] [代码](canvas) {[代码][代码]...[代码][代码] [代码][代码]camera = [代码][代码]new[代码] [代码]THREE.PerspectiveCamera(20, canvas.width / canvas.height, 1, 10000);[代码][代码] [代码][代码]scene = [代码][代码]new[代码] [代码]THREE.Scene();[代码][代码]...[代码][代码] [代码][代码]renderer = [代码][代码]new[代码] [代码]THREE.WebGLRenderer({ canvas: canvas, antialias: [代码][代码]true[代码] [代码]});[代码][代码] [代码][代码]}[代码]这样一个最基础的三维场景就搭好了,然后继续执行animate方法,开始渲染场景 [代码]animate:[代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]requestAnimationFrame([代码][代码]this[代码][代码].animate);[代码][代码] [代码][代码]this[代码][代码].render();[代码][代码] [代码][代码]}[代码]接下来尝试跑一下three.js提供的例子 webgl_geometry_colors : [图片] 锯齿问题比较严重,暂时没找到解决办法,但总体来说还是可以的,至少场景渲染出来了 由于暂时没想到如何改造CanvasTexture,我把例子中的 [代码]var[代码] [代码]canvas = document.createElement( [代码][代码]'canvas'[代码] [代码]);[代码][代码]canvas.width = 128;[代码][代码]canvas.height = 128;[代码][代码]var[代码] [代码]context = canvas.getContext( [代码][代码]'2d'[代码] [代码]);[代码][代码]var[代码] [代码]gradient = context.createRadialGradient( canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2 );[代码][代码]gradient.addColorStop( 0.1, [代码][代码]'rgba(210,210,210,1)'[代码] [代码]);[代码][代码]gradient.addColorStop( 1, [代码][代码]'rgba(255,255,255,1)'[代码] [代码]);[代码][代码]context.fillStyle = gradient;[代码][代码]context.fillRect( 0, 0, canvas.width, canvas.height );[代码][代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.CanvasTexture( canvas );[代码]替换成 webgl_geometries 例子中的TextureLoader [代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.TextureLoader().load(canvas,[代码][代码]'../../textures/UV_Grid_Sm.jpg'[代码][代码]);[代码]可能有人会发现load方法中传入的参数多了一个canvas,因为小程序提供的api没法直接创建Image对象,仅有一个Canvas.createImage()方法可以创建Image对象。因此我们还需要改造一下TextureLoader中的load方法,先看一下原版中的load方法: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( url, [代码][代码]function[代码] [代码]( image ) {[代码]其中实际调用了ImageLoader来加载图片,在看看ImageLoader: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]var[代码] [代码]image = document.createElementNS( [代码][代码]'http://www.w3.org/1999/xhtml'[代码][代码], [代码][代码]'img'[代码] [代码]);[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemError( url );[代码][代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]image.addEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.addEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]document.createElementNS这种东西肯定是没法存在的,没办法,把canvas传进来用createImage方法创建Image对象,改造后: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//var image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );[代码][代码] [代码][代码]console.log([代码][代码]this[代码][代码], canvas);[代码][代码] [代码][代码]var[代码] [代码]image = canvas.createImage();[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码][代码] [代码][代码]scope.manager.itemError( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//image.addEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.addEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = onImageLoad;[代码][代码] [代码][代码]image.onerror = onImageError;[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]然后TextureLoader的load方法也改一下传参顺序: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( canvas,url, [代码][代码]function[代码] [代码]( image ) {[代码]OK! 这个例子代码我放在https://github.com/leo9960/xcx_threejs,大家可以接着研究一下。潜力还是比较大的,比如我拿它搞了个全景展示 [图片] ---------------------------------------------------------------------- 2019.5.26 新上传了全景展示的范例,基于panolens.js,欢迎围观
2019-05-26 - 5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)
由于小程序2020年1月10日以后改模板消息为订阅消息,所以我写了一篇新的文章来更新这个知识点 《小程序订阅消息推送(含源码)java实现小程序推送,springboot实现微信消息推送》 我们在做小程序开发时,消息推送是不可避免的。今天就来教大家如何实现小程序消息推送的后台和前台开发。源码会在文章末尾贴出来。 其实我之前有写过一篇:《springboot实现微信消息推送,java实现小程序推送,含小程序端实现代码》 但是有同学反应这篇文章里的代码太繁琐,接入也比较麻烦。今天就来给大家写个精简版的,基本上只需要几行代码,就能实现小程序模版消息推送功能。 老规矩先看效果图 [图片] 这是我们最终推送给用户的模版消息。这是用户手机微信上显示的推送消息截图。 本节知识点 1,java开发推送后台 2,springboot实现推送功能 3,小程序获取用户openid 4,小程序获取fromid用来推送 先来看后台推送功能的实现 只有下面一个简单的PushController类,就可以实现小程序消息的推送 [图片] 再来看下PushController类,你没看错,实现小程序消息推送,就需要下面这几行代码就可以实现了。 [图片] 由于本推送代码是用springboot来实现的,下面就来简单的讲下。我我们需要注意的几点内容。 1,需要在pom.xml引入一个三方类库(推送的三方类库) [图片] pom.xml的完整代码如下 [代码]<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.qcl</groupId> <artifactId>wxapppush</artifactId> <version>0.0.1-SNAPSHOT</version> <name>wxapppush</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--微信小程序模版推送--> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>3.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> [代码] 其实到这里我们java后台的推送功能,就已经实现了。我们只需要运行springboot项目,就可以实现推送了。 下面贴出完整的PushController.java类。里面注释很详细了。 [代码]package com.qcl.wxapppush; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.bean.WxMaTemplateData; import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage; import cn.binarywang.wx.miniapp.config.WxMaInMemoryConfig; import me.chanjar.weixin.common.error.WxErrorException; /** * Created by qcl on 2019-05-20 * 微信:2501902696 * desc: 微信小程序模版推送实现 */ @RestController public class PushController { @GetMapping("/push") public String push(@RequestParam String openid, @RequestParam String formid) { //1,配置小程序信息 WxMaInMemoryConfig wxConfig = new WxMaInMemoryConfig(); wxConfig.setAppid("XXX");//小程序appid wxConfig.setSecret("xxx");//小程序AppSecret WxMaService wxMaService = new WxMaServiceImpl(); wxMaService.setWxMaConfig(wxConfig); //2,设置模版信息(keyword1:类型,keyword2:内容) List<WxMaTemplateData> templateDataList = new ArrayList<>(2); WxMaTemplateData data1 = new WxMaTemplateData("keyword1", "获取老师微信"); WxMaTemplateData data2 = new WxMaTemplateData("keyword2", "2501902696"); templateDataList.add(data1); templateDataList.add(data2); //3,设置推送消息 WxMaTemplateMessage templateMessage = WxMaTemplateMessage.builder() .toUser(openid)//要推送的用户openid .formId(formid)//收集到的formid .templateId("eDZCu__qIz64Xx19dAoKg0Taf5AAoDmhUHprF6CAd4A")//推送的模版id(在小程序后台设置) .data(templateDataList)//模版信息 .page("pages/index/index")//要跳转到小程序那个页面 .build(); //4,发起推送 try { wxMaService.getMsgService().sendTemplateMsg(templateMessage); } catch (WxErrorException e) { System.out.println("推送失败:" + e.getMessage()); return e.getMessage(); } return "推送成功"; } } [代码] 看代码我们可以知道,我们需要做一些配置,需要下面信息 1,小程序appid 2,小程序AppSecret(密匙) 3,小程序推送模版id 4,用户的openid 5,用户的formid(一个formid只能用一次) 下面就是小程序部分,来教大家如何获取上面所需的5个信息。 1,appid和AppSecret的获取(登录小程序管理后台) [图片] 2,推送模版id [图片] 3,用户openid的获取,可以看下面的这篇文章,也可以看源码,这里不做具体讲解 小程序开发如何获取用户openid 4,获取formid [图片] 看官方文档,可以知道我们的formid有效期是7天,并且一个form_id只能使用一次,所以我们小程序端所需要做的就是尽可能的多拿些formid,然后传个后台,让后台存到数据库中,这样7天有效期内,想怎么用就怎么用了。 所以接下来要讲的就是小程序开发怎么尽可能多的拿到formid了 [图片] 看下官方提供的,只有在表单提交时把report-submit设为true时才能拿到formid,比如这样 [代码] <form report-submit='true' > <button form-type='submit'>获取formid</button> </form> [代码] 所以我们就要在这里下功夫了,既然只能在form组件获取,我们能不能把我们小程序里用到最多的地方用form来伪装呢。 下面简单写个获取formid和openid的完整示例,方便大家学习 效果图 [图片] 我们要做的就是点击获取formid按钮,可以获取到用户的formid和openid,正常我们开发时,是需要把openid和formid传给后台的,这里简单起见,我们直接用获取到的formid和openid实现推送功能 下面来看小程序端的实现代码 1,index.wxml [图片] 2,index.js [图片] 到这里我们小程序端的代码也实现了,接下来测试下推送。 [代码]formid: 6ee9ce80c1ed4a2f887fccddf87686eb openid o3DoL0Uusu1URBJK0NJ4jD1LrRe0 [代码] [图片] 可以看到我们用了上面获取到的openid和formid做了一次推送,显示推送成功 [图片] [图片] 到这里我们小程序消息推送的后台和小程序端都讲完了。 这里有两点需要大家注意 1,推送的openid和formid必须对应。 2,一个formid只能用一次,多次使用会报一下错误。 [代码]{"errcode":41029,"errmsg":"form id used count reach limit hint: [ssun8a09984113]"} [代码] 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 这里就不单独贴出源码下载链接了,大家感兴趣的话,可以私信我,或者在底部留言,我会把源码下载链接贴在留言区。 单独找我要源码也行(微信2501902696) 视频讲解:https://edu.csdn.net/course/detail/23750 源码链接:https://github.com/qiushi123/wxapppush
2020-01-08 - WeHalo爱敲代码的猫(简洁风格的个人博客小程序)
[图片] 简介 WeHalo [wiˈheɪloʊ],意为我们的光环,嘻嘻。 配合 Halo 轻快,简洁,功能强大的博客系统而开发出来的 简约风 微信小程序版博客 [图片] 展示 [图片] 感谢 WeHalo的诞生离不开下面这些项目: Halo:轻快,简洁,功能强大,使用Java开发的博客系统 iView Weapp:一套高质量的微信小程序 UI 组件库 Painter:微信小程序生成图片库,绘制一张可以发到朋友圈的图片 html2wxml:用于微信小程序的HTML和Markdown格式的富文本渲染组件,支持代码高亮 一言·古诗词:Hitokoto API,随机返回一条古诗词名句。采用 Vert.x + Redis 全异步开发,毫秒级稳定响应。 后续功能 生成海报(微信朋友圈装X) 自定义导航栏(个人觉得好看可自定义) 个人名片(可宣传自己) 文章弹幕式评论展示 文字评论功能 用户回复评论追评功能 想到就写... 开源地址 GitHub:https://github.com/aquanlerou/WeHalo Gitee:https://gitee.com/Aquan_LeRou/WeHalo
2019-02-27