个人案例
- 代办帮帮注册公司
在线下单,服务自助
代办帮帮扫码体验
- 一炸到底
BOOM
一炸到底扫码体验
- 基础审批应用和自建应用的关系
基础审批应用开放数据权限给自建应用 能否只配置自建应用的事件服务器,不配置基础审批应用的事件服务器,从而达到审批事件回调通知吗
2020-10-23 - 【能力体验】“后台持续定位”接口的使用与踩坑
在小程序基础库 v2.8.0 版本中,新增了小程序后台持续定位功能,没错就是这个接口 wx.startLocationUpdateBackground(Object object)。对于一些有这方面需求的项目来说,这无疑是个好消息!刚好我有个项目刚好需要用到这个功能,这里就分享一下使用的体验吧。 [图片] 一、使用前提醒 要使用这个能力,官方文档有以下提醒: 1、基础库 2.8.0 开始支持,低版本需做兼容处理。 2、调用前需要 用户授权 scope.userLocationBackground (需要写一个button供用户点击) 除此之外,你还需要知道: 这个接口仅支持在真机上调试! 因此写代码时可以先用wx.startLocationUpdate(前台时接收位置消息)替代,在运行没问题后再换回并在真机测试。 二、使用时感受 微信团队在公众号里说“满足线路导航、路线记录等服务场景下,小程序需要长时间持续定位来提供服务”,但在实际情况中是,小程序很难实现所谓的“长时间”(有时甚至不到5分钟)。这样就会导致我们记录的数据还没来得及上传就丢失了。 当我发现这个问题,第一时间是抱怨为什么小程序销毁前不能给我们返回一个提醒。但思考后才理解:微信APP自身都不能确保“长时间”在后台运行,还怎么样保证小程序的“长时间”运行呢?(如被一些软件清理了) 三、使用后经验 既然无法避免突发性关闭,又不宜频繁上传地点数据。那么,我们就只能用到缓存了!以实现类似断线重连的功能。 [图片] 实现效果: [图片] 代码:(仅供参考) [代码] var points_map = [ ] // 实时绘制地图 var points_yun = [ ] // 云开发需要的格式 var point Page({ data: { polyline: [{ points: points_map, color: '#FFA500', width: 3 }], }, onLoad: function (options) { var that = this wx.getStorage({ key: 'TimeStamp', success(res) { console.log('有缓存') wx.getStorage({ key: 'points_yun', success(res) { points_yun = res.data } }) wx.showModal({ content: '检测到您有一个未完成的巡护记录!', cancelText: '不保存', cancelColor: '#DC143C', confirmText: '继续巡护', confirmColor: '#228B22', success(k) { if (k.confirm) { console.log('用户点击确定-继续巡护') that.setData({ TimeStamp: res.data }) that.GoContinue() } else if (k.cancel) { console.log('用户点击取消-不保存') wx.removeStorage({ key: 'TimeStamp' }) //删缓存 wx.removeStorage({ key: 'points_yun' }) points_yun = [] } } }) }, fail(res) { console.log('无缓存') points_yun = [] } }) }, Go: function () { // 开始巡护(首次) var that = this wx.startLocationUpdateBackground({ success(res) { var TimeStamp = (new Date()).valueOf() that.GetNowGeo() wx.setStorage({ //设置缓存(TimeStamp) key: "TimeStamp", data: TimeStamp }) }, fail() { // 这里弹窗引导用户授权使用地理位置 } }) }, GoContinue: function () { // 开始巡护(再续) var that = this wx.startLocationUpdateBackground({ success(res) { that.GetNowGeo() }, fail() { // 这里弹窗引导用户授权使用地理位置 } }) }, End: function () { var that = this wx.stopLocationUpdate() clearInterval(this.data.setInter) wx.showModal({ title: '', content: '是否要上传数据?', success(res) { if (res.confirm) { that.updateGeo() } } }) }, GetNowGeo: function () { var that = this wx.onLocationChange(function (res) { point = { latitude: res.latitude, longitude: res.longitude } }) this.data.setInter = setInterval(function () { if (points_map.length == 0) { points_yun.push([]) } points_map.push(point); //画地图 that.setData({ 'polyline[0].points': points_map }) var n = [point.longitude, point.latitude] // 云开发数据库需要的格式 var r = points_yun.length - 1 points_yun[r].push(n) wx.setStorage({ // 设置缓存(路程数据) key: "points_yun", data: points_yun }) }, 6000); }, updateGeo: function () { var that = this wx.showLoading({ title: '数据上传中', }) db.collection('patrol_geo').add({ // 云开发上传 data: { location: { type: 'MultiLineString', coordinates: points_yun } }, success: res => { wx.showToast({ title: '上传成功' }) wx.removeStorage({ key: 'TimeStamp' }) // 清理缓存 wx.removeStorage({ key: 'points_yun' }) }, fail: res => { wx.showToast({ title: '上传失败,请重新操作!' }) console.log(res) } }) }, }) [代码]
2019-09-26 - 获取的图片为什么方向不对
服务器上的图片是这样的 [图片] 浏览器直接获取是这样的 [图片] 小程序获取变成了这样 [图片]
2018-05-17 - 微信导入图片方向问题,是否bug?
[图片] 腾讯相册小程序,微信导入照片,上传过程方向显示不对,是bug吗? 我看腾讯相册传完会把方向转过来。 我们自己开发的小程序也是因为选择的时候方向被转了,然后上传完在列表里方向也跟着不对,这个算不算bug呢? 理论上在文件上传窗口就应该跟微信聊天显示的方向一致吧? 如我们公司产品: 第一步选择微信图片, [图片] 第二步:上传窗口 [图片] 第三步:传完列表显示,现在需要我们在服务器上面把方向给处理一下,增加了开发成本 [图片]
2019-04-16 - 有没有可以获取当前网速的接口?
调用uploadFile的api时,设置timeout,我们希望优化用户体验,根据网络速度动态设置timeout。但是目前来看,我没有找到可以获取网速的接口。 初步有一个想法,uploadTask使用onProgressUpdate来监听下载进度。 目前存在的问题: onProgressUpdate什么时候回触发callback有没有其他的比较好的思路
2020-02-10 - 云开发 逆地址解析(无须注册腾讯位置服务的应用 key)的云函数方法
云开发 云函数方法 逆地址解析 (无须注册腾讯位置服务的应用 key)的云函数方法 1.服务市场 【逆地址解析】API https://fuwu.weixin.qq.com/service/detail/00046c6eed0df09552990112551815 2.调用服务平台提供的服【调用方式】serviceMarket.invokeService https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/service-market/serviceMarket.invokeService.html 3.注意:无须 注册 腾讯位置服务的应用 key 4.云函数 代码 const res = await cloud.openapi({ convertCase: false }).serviceMarket.invokeService({ service: 'wxc1c68623b7bdea7b', //服务 ID(逆地址解析 Service ID) api: 'rgeoc', //接口名(逆地址解析 API Name) data: { 'location': event.latitude + ',' + event.longitude, //位置坐标,格式为纬度在前,经度在后,用半角逗号分隔,例 "40.040437,116.273623" // 'get_poi': 1, //可选项。是否返回周边地点(POI)列表,取值:1 返回;0不返回(默认) }, "client_msg_id" : "id123", //必填。随机字符串 ID,调用方请求的唯一标识 }) return res //返回结果 建议把 JSON.parse() 写在开发工具【小程序端】。不要写在云函数里(不然获取对象中的值,存储到数据库时,会「爆」各种奇奇怪怪的难搞的问题) var resOBJ = JSON.parse(res.result.data) //转为json对象格式 console.log('resOBJ为', resOBJ) 开发工具打印结果如下 [图片]
2021-07-17 - 微信小程序中实现定位以及逆地址解析
前言 在微信小程序开发中,我们可以提前获取用户的地理位置,为用户提供更好的服务,因此我们今天就来实现一下。 一、原理 通过微信小程序的开发文档,我们可以发现 wx.getLoaction 能够获取到用户所在位置的经纬度,并且通过腾讯地图提供的逆地址解析中可以将经纬度信息还原成城市名称。 在实际开发当中,我们可以分为两个部分,一个是腾讯地图key的获取,另一个是微信开发端的编码。 二、腾讯地图key 创建一个腾讯地图的账号。(需要手机号和邮箱号)腾讯地图官网 登录成功之后可以点击右上角的控制台就会出现下图的界面,点击创建应用数量,进入到应用的管理页面。 [图片] 创建一个应用.。(应用名称、应用类型如实填写即可) [图片] 随即在我的应用中会显示刚刚创建的,点击添加key [图片] 信息如实填写就可以了,[代码]注意:启用产品选项要勾选 WebServiceAPI 和 微信小程序[代码],如果忘记勾选的也可以在创建key之后重新编辑配置。[代码]APPID需要到微信小程序网站查阅[代码] [图片] 添加成功之后,在创建好的应用可以看到key。 [图片] 二、编码 1. App.json [代码]"permission": { "scope.userLocation": { "desc": "为了更好的服务体验,我们希望获取你的位置" } } [代码] 2. JavaScript [代码]// 这里我选择在onShow中触发,可以根据具体情况设置触发事件 data: { locationObj: {} } onShow: function () { // 调用定位方法 this.getUserLocation(); }, // 定位方法 getUserLocation: function () { let _this = this wx.getLocation({ type: 'gcj02', // type有两中类型,gcj02 是腾讯地图所能解析的 success: res => { _this.setData({ locationObj: res }) // 调用获取城市名称方法 _this.getCity() } }) }, // 获取定位城市名称方法 getCity: function () { var _this = this wx.request({ url: `https://apis.map.qq.com/ws/geocoder/v1/?key=key填写的位置&location=`+ _this.data.locationObj.latitude + ',' +_this.data.locationObj.longitude, success: res => { console.log(res) // 此处返回的就是需要查询的城市名称 } }) }, [代码] 3. 返回值 逆地址解析之后的返回值如下: [图片] 总结 综上所述,便是今天介绍的微信小程序中定位及逆地址解析的实现方式。更多的参数信息,可以查阅本文末的开发文档链接。 [代码]最后,如果您有更好的方法,欢迎在留言区中分享;或者实际操作中遇到什么问题均可留言或者私信我,感谢您的观看![代码] 微信开发文档:wx.getLocation(Object object) 腾讯开发文档:逆地址解析 原 文 链 接 :JhouXu博客
2021-03-02 - 微信小程序定位授权,获取经纬度并转换为实际地址
一、准备工作 参考 https://lbs.qq.com/miniProgram/jsSdk/jsSdkGuide/jsSdkOverview 1.1注册腾讯位置服务账号 腾讯位置服务为微信小程序提供了基础的标点能力、线和圆的绘制接口等地图组件和位置展示、地图选点等地图API位置服务能力支持,使得开发者可以自由地实现自己的微信小程序产品。 在此基础上,腾讯位置服务微信小程序JavaScript SDK是专为小程序开发者提供的LBS数据服务工具包,可以在小程序中调用腾讯位置服务的POI检索、关键词输入提示、地址解析、逆地址解析、行政区划和距离计算等数据服务,让您的小程序更强大! 本文要介绍的是其中的逆地址解析 https://lbs.qq.com/ 1.2.申请开发者密–钥 [图片] 1.3.开通webserviceAPI服务 [图片] 控制台 -> key管理 -> 设置(使用该功能的key)-> 勾选webserviceAPI -> 保存 (小程序SDK需要用到webserviceAPI的部分服务,所以使用该功能的KEY需要具备相应的权限) 1.4.下载微信小程序JavaScriptSDK http://3gimg.qq.com/lightmap/xcx/jssdk/qqmap-wx-jssdk1.2.zip 1.5.安全域名设置 微信公众平台登录你的小程序->开发->开发设置->服务器域名->将[代码]https://apis.map.qq.com[代码]填入request合法域名 这样在微信开发者工具就可以看到了:[图片] 腾讯位置服务是有免费额度的,每个key的每个服务接口的调用量如下: 日调用量:1万次 / Key 并发数:5次 / key / 秒 用来学习足够了。 至此,准备工作已经全部完成。 二、实践 2.1加入JavaScriptSDK 理论上可以随便放入一个文件夹。但是程序员做事应该有条理一点。 创建一个工具类文件夹 untils,将qqmap-wx-jssdk.js放入。 [代码]//在要使用服务的页面 var QQMapWX = require('../../untils/qqmap-wx-jssdk.js'); var qqmapsdk; Page({ onLoad: function () { // 实例化API核心类 qqmapsdk = new QQMapWX({ key: '你在腾讯位置服务申请的key' }); }, [代码] 2.2.获取用户定位授权 wx.authorize(Object object) https://developers.weixin.qq.com/miniprogram/dev/api/open-api/authorize/wx.authorize.html 提前向用户发起授权请求。调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口。如果用户之前已经同意授权,则不会出现弹窗,直接返回成功。更多用法详见 用户授权。 [代码]function () { var that = this wx.authorize({ scope: 'scope.userLocation',//发起定位授权 success: function () { console.log('有定位授权') //授权成功,此处调用获取定位函数 }, fail() { //如果用户拒绝授权,则要告诉用户不授权就不能使用,引导用户前往设置页面。 console.log('没有定位授权') wx.showModal({ cancelColor: 'cancelColor', title: '没有授权无法获取位置信息', content: '是否前往设置页面手动开启', success: function (res) { if (res.confirm) { wx.openSetting({ withSubscriptions: true, }) } else { wx.showToast({ icon: 'none', title: '您取消了定位授权', }) } }, fail: function (e) { console.log(e) } }) } }) } [代码] 2.3. 获取定位信息并进行逆地址解析 如果用户同意了授权,就可以获取定位信息了,调用wx.getLocation(Object object) 调用成功就会返回位置信息: [图片] 然后调用SDK的reverseGeocoder(options:Object)进行逆地址解析:[图片] 代码如下: [代码]//此函数在用户定位授权成功后调用 function () { wx.getLocation({//获取地址 type: 'gcj02', success(res) { const latitude = res.latitude const longitude = res.longitude const speed = res.speed const accuracy = res.accuracy console.log(latitude, longitude, speed, accuracy) qqmapsdk.reverseGeocoder({//SDK调用 location: { latitude, longitude }, success: function (res) { console.log(res) } }) } }) } [代码] 完成。 水平有限,欢迎交流。 觉得有用请点个赞。
2020-11-12 - 微信小程序获取openid的两种方法
第一种:使用云开发 这种比较简单,只需要开通云开发,创建云函数,调用云函数就可获得。 调用云函数 Promise Cloud.callFunction(Object object) 返回一个Promise对象,所以不用考虑异步问题。 callFunction说明 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/functions/Cloud.callFunction.html具体代码如下: 我这里云函数名为helloCloud // helloCloud-index.js 云函数入口函数 exports.main = async (event, context) => { let{ APPID,OPENID}=cloud.getWXContext() return { APPID, OPENID } //------------------------------------------------------ //云函数调用 wx.cloud.callFunction({ name:'helloCloud', data:{ message:'helloCloud', } }).then(res=>{ console.log(res)//res就将appid和openid返回了 //做一些后续操作,不用考虑代码的异步执行问题。 }) 第二种:不使用云开发 这种方式就需要开发者有自己的后台了。 首先需要在微信小程序调用登录开放接口 wx.login() 获取用户登陆凭证code。 wx.login()接口说明 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html 然后,向自己的服务器发送请求,并将code一起发送过去。 wx.login({ success (res) { if (res.code) { //发起网络请求 wx.request({ url: '自己的服务器请求接口', data: { code: res.code } }) } else { console.log('登录失败!' + res.errMsg) } } }) 接下来,在自己的服务端调用auth.code2Session接口,我这里是用Java后台。 auth.code2Session接口说明 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html @RequestMapping("/testopenid") public String getUserInfo(@RequestParam(name = "code") String code) throws Exception { System.out.println("code" + code); String url = "https://api.weixin.qq.com/sns/jscode2session"; url += "?appid=xxxxxxxxxxxxx";//自己的appid url += "&secret=xxxxxxxxxxxxxxxxxxx";//自己的appSecret url += "&js_code=" + code; url += "&grant_type=authorization_code"; url += "&connect_redirect=1"; String res = null; CloseableHttpClient httpClient = HttpClientBuilder.create().build(); // DefaultHttpClient(); HttpGet httpget = new HttpGet(url); //GET方式 CloseableHttpResponse response = null; // 配置信息 RequestConfig requestConfig = RequestConfig.custom() // 设置连接超时时间(单位毫秒) .setConnectTimeout(5000) // 设置请求超时时间(单位毫秒) .setConnectionRequestTimeout(5000) // socket读写超时时间(单位毫秒) .setSocketTimeout(5000) // 设置是否允许重定向(默认为true) .setRedirectsEnabled(false).build(); // 将上面的配置信息 运用到这个Get请求里 httpget.setConfig(requestConfig); // 由客户端执行(发送)Get请求 response = httpClient.execute(httpget); // 从响应模型中获取响应实体 HttpEntity responseEntity = response.getEntity(); System.out.println("响应状态为:" + response.getStatusLine()); if (responseEntity != null) { res = EntityUtils.toString(responseEntity); System.out.println("响应内容长度为:" + responseEntity.getContentLength()); System.out.println("响应内容为:" + res); } // 释放资源 if (httpClient != null) { httpClient.close(); } if (response != null) { response.close(); } JSONObject jo = JSON.parseObject(res); String openid = jo.getString("openid"); System.out.println("openid" + openid); return openid; } 部分参考 https://blog.csdn.net/qq_42940875/article/details/82706638?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task 这样就获得openid了。 但是在实际应用场景中,往往需要在界面展示之前获得openid来做一些操作或者什么。 用以上代码会发现,openid后台虽然获取到了,但是小程序端页面刚展示时好像并没有获取到openid,但是之后查看数据能看到openid。 这是因为wx.request()是异步请求。也就是在请求的过程中,小程序的其他工作没有因为请求而停止。 所以,我们需要将请求封装成一个返回Promise对象的函数。 廖雪峰老师讲的Promise使用 https://www.liaoxuefeng.com/wiki/1022910821149312/1023024413276544 这样就能在请求完做一些后续操作。 代码如下: //封装wx.request() function request(requestMapping, data, requestWay, contentType) { wx.showLoading({ title: '请稍后', }) return new Promise(function(resolve, reject) { console.log('请求中。。。。。') wx.request({ url: '自己的服务器地址' + requestMapping, data: data, header: { 'content-type': contentType // 默认值 }, timeout: 3000, method: requestWay, success(res) { //console.log(res) if (res.data.success == false || res.data.statusCode == 404) { reject(res) } else { resolve(res) } }, fail: (e) => { wx.showToast({ title: '连接失败', icon: 'none' })}, complete: () => { wx.hideLoading() } }) }) } //获取openid function getOpenId(app, that){ return new Promise(function (resolve, reject) { wx.login({ success: function (yes) { // 发送 res.code 到后台换取 openId, sessionKey, unionId var requestMapping = '/testopenid' var data = { code: yes.code } var requestWay = 'GET' var contentType = 'application/json' var p =request(requestMapping, data, requestWay, contentType) p.then(res => { //console.log(res) 做一些后续操作 app.globalData.openId = res.data; resolve(res) }).catch(e => { reject(e) }) }, fail(e) { console.log(e) } }) }) } 这样就解决了因为异步获取不到数据的问题。 技术有限,欢迎交流。 觉得有用请点个赞。
2020-12-05 - 微信扫码登录
微信扫码登录 1. 使用背景 如今开发业务系统,已不是一个单独的系统。往往需要同多个不同系统相互调用,甚至有时还需要跟微信,钉钉,飞书这样平台对接。目前我开发的部分业务系统,已经完成微信公众平台对接。作为知识总结,接下来,我们探讨下对接微信公众平台的一小部分功能,微信扫码登录。其中的关键点是获取openid。我仔细查找了微信提供的开发文档,主要有以下三个方式可实现。 通过微信公众平台生成带参数的二维 通过微信公众平台微信网页授权登录 通过微信开发平台微信登录功能 2. 开发环境搭建 2.1 内网穿透 微信所有的接口访问,都要求使用域名。但多数开发者是没有域名,给很多开发者测试带来了麻烦。不过有以下两种方案可以尝试: 使用公司域名,让公司管理员配置一个子域名指向你公司公网的一个ip的80端口。然后通过Nginx或者通过nat命令,将改域名定位到您的开发环境 使用内网穿透工具,目前市面有很多都可以使用免费的隧道。不过就是不稳定,不支持指定固定子域名或者已经被微信限制访问。经过我大量收集资料,发现钉钉开发平台提供的内网穿透工具,比较不错。用阿里的东西来对接微信东西,想想都为微信感到耻辱。你微信不为开发者提供便利,就让对手来实现。 那钉钉的内网穿透工具具体怎么使用用的呢? 首先使用git下载钉钉内网穿透工具,下载好后找到[代码]windows_64[代码]目录,在这里新建一个[代码]start.bat[代码]文件,内容为 [代码]ding -config=ding.cfg -subdomain=pro 8080 [代码] 其中[代码]-subdomain[代码] 是用来生成子域名[代码]8080[代码]表示隐射本地8080端口 双击[代码]start.bat[代码]文件,最终启动成功界面如下 [图片] 经过我测试,这个相当稳定,并且可以指定静态子域名。简直就是业界良心 2.2 公众号测试环境 访问公众平台测试账号系统,可以通过微信登录,可快速得到一个测试账号。然后我们需要以下两个配置 接口配置信息 [图片] 在点击提交按钮时,微信服务器会验证我们配置的这个URL是否有效。这个URL有两个用途 通过签名验证地址是否有效 接收微信推送的信息,比如用户扫码后通知 签名生成逻辑是用配置的[代码]token[代码]结合微信回传的[代码]timestamp[代码],[代码]nonce[代码],通过字符串数组排序形成新的字符串,做SHA签名,再将签名后的二进制数组转换成十六进制字符串。最终的内容就是具体的签名信息。对应的java代码如下 [代码]// author: herbert 公众号:小院不小 20210424 public static String getSignature(String token, String timestamp, String nonce) { String[] array = new String[] { token, timestamp, nonce }; Arrays.sort(array); StringBuffer sb = new StringBuffer(); for (String str : array) { sb.append(str); } try { MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(sb.toString().getBytes()); byte[] digest = md.digest(); StringBuffer hexStr = new StringBuffer(); String shaHex = ""; for (int i = 0; i < digest.length; i++) { shaHex = Integer.toHexString(digest[i] & 0xFF); if (shaHex.length() < 2) { hexStr.append(0); } hexStr.append(shaHex); } return hexStr.toString(); } catch (NoSuchAlgorithmException e) { logger.error("获取签名信息失败", e.getCause()); } return ""; } [代码] 对应GET请求代码如下 [代码]// author: herbert 公众号:小院不小 20210424 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { logger.info("微信在配置服务器传递验证参数"); Map<String, String[]> reqParam = request.getParameterMap(); for (String key : reqParam.keySet()) { logger.info(" {} = {}", key, reqParam.get(key)); } String signature = request.getParameter("signature"); String echostr = request.getParameter("echostr"); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); String buildSign = WeixinUtils.getSignature(TOKEN, timestamp, nonce); logger.info("服务器生成签名信息:{}", buildSign); if (buildSign.equals(signature)) { response.getWriter().write(echostr); logger.info("服务生成签名与微信服务器生成签名相等,验证成功"); return; } } [代码] 微信服务器验证规则是原样返回[代码]echostr[代码],如果觉得签名麻烦,直接返回[代码]echostr[代码]也是可以的。 JS接口安全域名 [图片] 这个配置主要用途是解决H5与微信JSSDK集成。微信必须要求指定的域名下,才能调用JSSDK 3. 测试项目搭建 为了测试扫码登录效果,我们需要搭建一个简单的maven工程。工程中具体文件目录如下 [图片] 用户扫描二维码得到对应的[代码]openid[代码],然后在[代码]userdata.json[代码]文件中,根据[代码]openid[代码]查找对应的用户。找到了,就把用户信息写入缓存。没找到,就提醒用户绑定业务账号信息。前端通过定时轮询,从服务缓存中查找对应扫码的用户信息 [代码]userdata.json[代码]文件中的内容如下 [代码][{ "userName": "张三", "password":"1234", "userId": "000001", "note": "扫码登录", "openId": "" }] [代码] 从代码可以知道,后端提供了5个Servlet,其作用分别是 WeixinMsgEventServlet 完成微信服务器验证,接收微信推送消息。 WeixinQrCodeServlet 完成带参数二维码生成,以及完成登录轮询接口 WeixinBindServlet 完成业务信息与用户openid绑定操作 WeixinWebQrCodeServlet 完成网页授权登录的二维码生成 WeixinRedirectServlet 完成网页授权接收微信重定向回传参数 需要调用微信接口信息如下 [代码] // author: herbert 公众号小院不小 20210424 private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}"; private static final String QRCODE_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={0}"; private static final String QRCODE_SRC_URL = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={0}"; private static final String STENDTEMPLATE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={0}"; private static final String WEB_AUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid={0}&redirect_uri={1}&response_type=code&scope=snsapi_base&state={2}#wechat_redirect"; private static final String WEB_AUTH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code"; [代码] 前端对应的三个页面分别是 login.html 用于展现登录的二维码,以及实现轮询逻辑 index.html 用于登录成功后,显示用户信息 weixinbind.html 用于用户绑定业务信息 最终实现的效果如下 [图片] 已绑定openid直接跳转到首页 [图片] 未绑定用户,在手机到会收到邀请微信绑定链接 [图片] 4. 带参数二维码登录 生成带参数的二维码主要通过以下三个步骤来实现 使用APPID和APPSECRET换取ACCESSTOKEN 使用ACCESSTOKEN换取对应二维码的TICKET 使用TICKET获取具体的二维图片返回给前端 4.1 获取公众号ACCESSTOKEN 换取ACCESSTOKEN 代码如下 [代码]// author: herbert 公众号小院不小 20210424 public static String getAccessToken() { if (ACCESSTOKEN != null) { logger.info("从内存中获取到AccessToken:{}", ACCESSTOKEN); return ACCESSTOKEN; } String access_token_url = MessageFormat.format(ACCESS_TOKEN_URL, APPID, APPSECRET); logger.info("access_token_url转换后的访问地址"); logger.info(access_token_url); Request request = new Request.Builder().url(access_token_url).build(); OkHttpClient httpClient = new OkHttpClient(); Call call = httpClient.newCall(request); try { Response response = call.execute(); String resBody = response.body().string(); logger.info("获取到相应正文:{}", resBody); JSONObject jo = JSONObject.parseObject(resBody); String accessToken = jo.getString("access_token"); String errCode = jo.getString("errcode"); if (StringUtils.isBlank(errCode)) { errCode = "0"; } if ("0".equals(errCode)) { logger.info("获取accessToken成功,值为:{}", accessToken); ACCESSTOKEN = accessToken; } return accessToken; } catch (IOException e) { logger.error("获取accessToken出现错误", e.getCause()); } return null; } [代码] 4.2 获取二维码TICKET 根据ACCESSTOKEN获取二维码TICKET代码如下 [代码]// author: herbert 公众号:小院不小 20210424 public static String getQrCodeTiket(String accessToken, String qeCodeType, String qrCodeValue) { String qrcode_ticket_url = MessageFormat.format(QRCODE_TICKET_URL, accessToken); logger.info("qrcode_ticket_url转换后的访问地址"); logger.info(qrcode_ticket_url); JSONObject pd = new JSONObject(); pd.put("expire_seconds", 604800); pd.put("action_name", "QR_STR_SCENE"); JSONObject sence = new JSONObject(); sence.put("scene", JSONObject .parseObject("{\"scene_str\":\"" + MessageFormat.format("{0}#{1}", qeCodeType, qrCodeValue) + "\"}")); pd.put("action_info", sence); logger.info("提交内容{}", pd.toJSONString()); RequestBody body = RequestBody.create(JSON, pd.toJSONString()); Request request = new Request.Builder().url(qrcode_ticket_url).post(body).build(); OkHttpClient httpClient = new OkHttpClient(); Call call = httpClient.newCall(request); try { Response response = call.execute(); String resBody = response.body().string(); logger.info("获取到相应正文:{}", resBody); JSONObject jo = JSONObject.parseObject(resBody); String qrTicket = jo.getString("ticket"); String errCode = jo.getString("errcode"); if (StringUtils.isBlank(errCode)) { errCode = "0"; } if ("0".equals(jo.getString(errCode))) { logger.info("获取QrCodeTicket成功,值为:{}", qrTicket); } return qrTicket; } catch (IOException e) { logger.error("获取QrCodeTicket出现错误", e.getCause()); } return null; } [代码] 4.3 返回二维图片 获取二维码图片流代码如下 [代码]// author: herbert 公众号:小院不小 20210424 public static InputStream getQrCodeStream(String qrCodeTicket) { String qrcode_src_url = MessageFormat.format(QRCODE_SRC_URL, qrCodeTicket); logger.info("qrcode_src_url转换后的访问地址"); logger.info(qrcode_src_url); Request request = new Request.Builder().url(qrcode_src_url).get().build(); OkHttpClient httpClient = new OkHttpClient(); Call call = httpClient.newCall(request); try { Response response = call.execute(); return response.body().byteStream(); } catch (IOException e) { logger.error("获取qrcode_src_url出现错误", e.getCause()); } return null; } [代码] 最终二维码图片通过[代码]servlet[代码]中的get方法返回到前端,需要注意的地方就是为当前session添加key用于存储扫码用户信息或[代码]openid[代码] [代码]// author: herbert 公众号:小院不小 20210424 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { accessToken = WeixinUtils.getAccessToken(); String cacheKey = request.getParameter("key"); logger.info("当前用户缓存key:{}", cacheKey); WeixinCache.put(cacheKey, "none"); WeixinCache.put(cacheKey + "_done", false); if (qrCodeTicket == null) { qrCodeTicket = WeixinUtils.getQrCodeTiket(accessToken, QRCODETYPE, cacheKey); } InputStream in = WeixinUtils.getQrCodeStream(qrCodeTicket); response.setContentType("image/jpeg; charset=utf-8"); OutputStream os = null; os = response.getOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while ((len = in.read(buffer)) != -1) { os.write(buffer, 0, len); } os.flush(); } [代码] 4.4 前端显示二维图片 前端可以使用[代码]image[代码]标签,[代码]src[代码]指向这个[代码]servlet[代码]地址就可以了 [代码]<div class="loginPanel" style="margin-left: 25%;"> <div class="title">微信登录(微信场景二维码)</div> <div class="panelContent"> <div class="wrp_code"><img class="qrcode lightBorder" src="/weixin-server/weixinqrcode?key=herbert_test_key"></div> <div class="info"> <div id="wx_default_tip"> <p>请使用微信扫描二维码登录</p> <p>“扫码登录测试系统”</p> </div> </div> </div> </div> [代码] 4.5 前端轮询扫码情况 pc端访问[代码]login[代码]页面时,除了显示对应的二维码,也需要开启定时轮询操作。查询到扫码用户信息就跳转到[代码]index[代码]页面,没有就间隔2秒继续查询。轮询的代码如下 [代码]// author: herbert 公众号:小院不小 20210424 function doPolling() { fetch("/weixin-server/weixinqrcode?key=herbert_test_key", { method: 'POST' }).then(resp => resp.json()).then(data => { if (data.errcode == 0) { console.log("获取到绑定用户信息") console.log(data.binduser) localStorage.setItem("loginuser", JSON.stringify(data.binduser)); window.location.replace("index.html") } setTimeout(() => { doPolling() }, 2000); }) } doPolling() [代码] 可以看到前端访问了后台一个POST接口,这个对应的后台代码如下 [代码]// author: herbert 公众号:小院不小 20210424 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String cacheKey = request.getParameter("key"); logger.info("登录轮询读取缓存key:{}", cacheKey); Boolean cacheDone = (Boolean) WeixinCache.get(cacheKey + "_done"); response.setContentType("application/json;charset=utf-8"); String rquestBody = WeixinUtils.InPutstream2String(request.getInputStream(), charSet); logger.info("获取到请求正文"); logger.info(rquestBody); logger.info("是否扫码成功:{}", cacheDone); JSONObject ret = new JSONObject(); if (cacheDone != null && cacheDone) { JSONObject bindUser = (JSONObject) WeixinCache.get(cacheKey); ret.put("binduser", bindUser); ret.put("errcode", 0); ret.put("errmsg", "ok"); WeixinCache.remove(cacheKey); WeixinCache.remove(cacheKey + "_done"); logger.info("已移除缓存数据,key:{}", cacheKey); response.getWriter().write(ret.toJSONString()); return; } ret.put("errcode", 99); ret.put("errmsg", "用户还未扫码"); response.getWriter().write(ret.toJSONString()); } [代码] 通过以上的操作,完美解决了二维显示和轮询功能。但用户扫描了我们提供二维码,我们系统怎么知道呢?还记得我们最初配置的URL么,微信会把扫描情况通过POST的方式发送给我们。对应接收的POST代码如下 [代码]// author: herbert 公众号:小院不小 20210424 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String rquestBody = WeixinUtils.InPutstream2String(request.getInputStream(), charSet); logger.info("获取到微信推送消息正文"); logger.info(rquestBody); try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false); DocumentBuilder db = dbf.newDocumentBuilder(); StringReader sr = new StringReader(rquestBody); InputSource is = new InputSource(sr); Document document = db.parse(is); Element root = document.getDocumentElement(); NodeList fromUserName = document.getElementsByTagName("FromUserName"); String openId = fromUserName.item(0).getTextContent(); logger.info("获取到扫码用户openid:{}", openId); NodeList msgType = root.getElementsByTagName("MsgType"); String msgTypeStr = msgType.item(0).getTextContent(); if ("event".equals(msgTypeStr)) { NodeList event = root.getElementsByTagName("Event"); String eventStr = event.item(0).getTextContent(); logger.info("获取到event类型:{}", eventStr); if ("SCAN".equals(eventStr)) { NodeList eventKey = root.getElementsByTagName("EventKey"); String eventKeyStr = eventKey.item(0).getTextContent(); logger.info("获取到扫码场景值:{}", eventKeyStr); if (eventKeyStr.indexOf("QRCODE_LOGIN") == 0) { String cacheKey = eventKeyStr.split("#")[1]; scanLogin(openId, cacheKey); } } } if ("text".equals(msgTypeStr)) { NodeList content = root.getElementsByTagName("Content"); String contentStr = content.item(0).getTextContent(); logger.info("用户发送信息:{}", contentStr); } } catch (Exception e) { logger.error("微信调用服务后台出现错误", e.getCause()); } } [代码] 我们需要的扫码数据是 [代码]MsgType=="event" and Event=="SCAN"[代码],找到这条数据,解析出我们在生成二维码时传递的[代码]key[代码]值,再写入缓存即可。代码中的 [代码]scanLogin(openId, cacheKey)[代码]完成具体业务逻辑,如果用户已经绑定业务账号,则直接发送模板消息登录成功,否则发送模板消息邀请微信绑定,对应的代码逻辑如下 [代码]// author: herbert 公众号:小院不小 20210424 private void scanLogin(String openId, String cacheKey) throws IOException { JSONObject user = findUserByOpenId(openId); if (user == null) { // 发送消息让用户绑定账号 logger.info("用户还未绑定微信,正在发送邀请绑定微信消息"); WeixinUtils.sendTempalteMsg(WeixinUtils.getAccessToken(), openId, "LWP44mgp0rEGlb0pK6foatU0Q1tWhi5ELiAjsnwEZF4", "http://pro.vaiwan.com/weixin-server/weixinbind.html?key=" + cacheKey, null); WeixinCache.put(cacheKey, openId); return; } // 更新缓存 WeixinCache.put(cacheKey, user); WeixinCache.put(cacheKey + "_done", true); logger.info("已将缓存标志[key]:{}设置为true", cacheKey + "_done"); logger.info("已更新缓存[key]:{}", cacheKey); logger.info("已发送登录成功微信消息"); WeixinUtils.sendTempalteMsg(WeixinUtils.getAccessToken(), openId, "MpiOChWEygaviWsIB9dUJLFGXqsPvAAT2U5LcIZEf_o", null, null); } [代码] 以上就完成了通过场景二维实现微信登录的逻辑 5. 网页授权登录 网页授权登录的二维码需要我们构建好具体的内容,然后使用二维码代码库生成二维码 5.1 生成网页授权二维码 [代码]// author: herbert 公众号:小院不小 20210424 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String cacheKey = request.getParameter("key"); logger.info("当前用户缓存key:{}", cacheKey); BufferedImage bImg = WeixinUtils.buildWebAuthUrlQrCode("http://pro.vaiwan.com/weixin-server/weixinredirect", cacheKey); if (bImg != null) { response.setContentType("image/png; charset=utf-8"); OutputStream os = null; os = response.getOutputStream(); ImageIO.write(bImg, "png", os); os.flush(); } } [代码] 可以看到,我们这里缓存[代码]key[代码]值,通过[代码]state[代码]方式传递给微信服务器。微信服务器会将该值原样返回给我我们的跳转地址,并且附带上授权码。我们通过二维码库生成二维码,然后直接返回二维码图。前端直接指向这个地址就可显示图片了。对应前端代码如下 [代码] <div class="loginPanel"> <div class="title">微信登录(微信网页授权)</div> <div class="panelContent"> <div class="wrp_code"><img class="qrcode lightBorder" src="/weixin-server/weixinwebqrcode?key=herbert_test_key"></div> <div class="info"> <div id="wx_default_tip"> <p>请使用微信扫描二维码登录</p> <p>“扫码登录测试系统”</p> </div> </div> </div> </div> [代码] 5.2 获取openid并验证 用户扫描我们生成的二维码以后,微信服务器会发送一个GET请求到我们配置的跳转地址,我们在这里完成[代码]openid[代码]的验证和业务系统用户信息获取操作,对应代码如下 [代码]// author: herbert 公众号:小院不小 20210424 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String code = request.getParameter("code"); String state = request.getParameter("state"); logger.info("获取到微信回传参数code:{},state:{}", code, state); JSONObject webTokenInfo = WeixinUtils.getWebAuthTokenInfo(code); if (webTokenInfo != null && !webTokenInfo.containsKey("errcode")) { String openId = webTokenInfo.getString("openid"); logger.info("获取到用opeind", openId); JSONObject user = findUserByOpenId(openId); if (user == null) { //用户未绑定 将openid存入缓存方便下一步绑定用户 WeixinCache.put(state, openId); response.sendRedirect("weixinbind.html?key=" + state); return; } WeixinCache.put(state, user); WeixinCache.put(state + "_done", true); logger.info("已将缓存标志[key]:{}设置为true", state + "_done"); logger.info("已更新缓存[key]:{}", state); response.setCharacterEncoding("GBK"); response.getWriter().print("扫码成功,已成功登录系统"); } } [代码] 用户扫描这个二维码后,逻辑跟场景二维码一样,找到用户信息就提示用户已成功登陆系统,否则就跳转到微信绑定页面 6. 开发平台登录 开放平台登录需要认证过后才能测试,认证需要交钱。对不起,我不配测试。 7. 总结 扫描登录主要逻辑是生成带key值二维,然后一直轮询服务器查询登录状态。以上两个方式各有优劣,主要区别如下 带参数二维码方式,微信负责生成二维。网页授权需要我们自己生成二维 带参数二维扫码成功或邀请绑定采用模板消息推送,网页授权可以直接跳转,体验更好 带参数二维码用途更多,比如像ngork.cc网站,实现关注了公众号才能加隧道功能 这里涉及到的知识点有 Oauth认证流程 二维码生成逻辑 内网穿透原理 Javaservlet开发 开发过程中,需要多查帮助文档。开发过程中的各种环境配置,对开发者来说,也是不小的挑战。做微信开发也有好多年,从企业微信,到公众号,到小程序,到小游戏,一直没有总结。这次专门做了一个微信扫码登录专题。先写代码,再写总结也花费了数周时间。如果觉得好,还望关注公众号支持下,您的点赞和在看是我写作力量的源泉。对微信集成和企业微信集成方面有问题的,也欢迎在公众号回复,我看到了会第一时间力所能及的为您解答。需要文中提及的项目,请扫描下方的二维码,关注公众号[小院不小],回复wxqrcode获取. [图片]
2021-04-27 - 微信小程序答题页——swiper渲染优化及swiper分页实现
前言 swiper的加载太多问题,网上资料好像没有一个特别明确的,就拿这个答题页,来讲讲我的解决方案 这里实现了如下功能和细节: 保证swiper-item的数量固定,加载大量数据时,大大优化渲染效率记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验示例动图 [图片] 截图 [图片] [图片] 问题原因 当swiper-item数量很多的时候,会出现性能问题 我实现了一个答题小程序,在一次性加载100个swipe-item的时候,低端手机页面渲染时间达到了2000多ms 也就是说在进入答题页的时候,会卡顿2秒多去加载这100个swiper-item 思考问题 那我们能不能让他先加载一部分,然后滑动以后再去改变item的数据,让swiper一直保持一定量的swiper-item? 注意到官方文档有这么两个属性可以利用,我们可以开启衔接滑动,然后再bindchange方法中去修改data [图片] 1、保证swiper-item的数量固定,加载大量数据时,优化渲染效率 假设我们请求到的数据的为list,实际渲染的数据为swiperList 我们现在给他就固定3个swiper-item,前后滑动的时候去替换数据 正向滑动的时候去替换滑动后的下一页数据,反向滑动的时候去替换滑动后的上一页数据 当我们知道了要替换的条件,我们便可以去替换数据了 但是我们应该考虑到临界值的问题,如果当前页是list第一项和最后一项该怎么办,向左向右滑是不是得禁止啊 这边是判断没数据会让它再弹回去 2、记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页 有很多时候,我们是从某一项直接进来的,比如说上次答题答到了第五题,我这次进来要直接做第六题 那么我们需要去初始化这个swiperList,让它当前页、上一页、下一页都有数据 3、答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验 从答题卡选择index,那就不仅仅是滑动上下页了,它可以跳转到任何页,所以也采用类似初始化swiperList的方法 swiper切换动画我这边是默认250ms,但是发现有时候从答题卡点击回来,你在答题卡点击的下一项不知道会从左还是从右滑过来 体验真的很差,一开始不知道怎么禁掉动画,其实在跳转到答题卡页的时候把duration设为0就可以了 然后在答题卡页的unload方法中恢复 关键点: 在固定3个swiper-item的同时,要保证我们可以有办法来替代微信自带swiper的current属性和change方法 swiper-limited-load使用方法及说明: 将components中的swiper-limited-load复制到您的项目中在需要的页面引用此组件,并且创建自己的自定义组件item-view在初始化数据时,为你的list的每一项指定index属性具体可以参照项目目录start-swiper-limited-load中的用法说明:其它属性和swiper无异,你们可以自己单独添加你们需要的属性总结 一开始很头疼,为什么微信小程序提供的这个swiper,没去考虑这方面 然后在网上和社区找也没有一个特别好的解决方案。 后来想想,遇到需求就静下来解决吧。 项目地址:https://github.com/pengboboer/swiper-limited-load 如果错误,欢迎指出。 如有新的需求也可以提出来,如果有时间的话,我会帮你们完善。 如果能帮到你们,记得给一个star,谢谢。 ---补充 有很多朋友在评论区提到了分页的需求,抽时间写了一个分页的Demo和大家分享一下。 还是以答题为例,比如我们一共有500条数据,一页20条,可能需要如下功能,乍一看不就加了个分页,挺简单的,其实实现起来挺麻烦的,下面说一下思路和一些需要特别注意的点: 1、从其他页面跳转到答题页时,不光只能默认在第一题,可以是任意一题,比如第80题。 跳转到任意一题,那么需要我们根据index算出该数据在第几页,然后需要请求该页数据,最后显示对应的index。我的思路更注重用户体验,不可能是上滑或者下滑才开始去请求数据,一定是要用户滑动前提前请求好数据。所以起码要保证左右两侧在初始化那一刻都有数据。如果此题和它的上一题下一题都在同一页,那么我们只需要请求一页数据(第15题,那么只需请求第1页数据)。如果此题和它的上一题或者下一题不在同一页,那么我们可能需要请求两页数据。(第20题,那么需要请求第1页和第2页数据) 2、左滑、右滑没数据时,都可以加载新数据。直到滑到第一题或者最后一题。 如果我们初始化时是第24题,那么我们左滑到第21题时,就应该去请求第一页的数据。那么用户在看完21题时,再滑到20题,可能就根本不会感知到通过网络请求了数据。但是如果用户此刻滑动特别快:滑到21题时请求了网络,请求还没成功,就又向左滑了。那么我们需要限制用户的滑动,给用户一个提示:数据正在加载中。 3、从答题卡点击任意一题可以跳转到相应的题目,并且左右滑动显示正常数据 比如我们初始化是跳转到了第80题,不一会点击答题卡又要跳转到200题,一会又跳转到150题。各种无序操作,你也不知道用户要往哪里点。 一开始是想着维护一个主list,点到哪道题往list中添加这道题所在的当页的数据,但是还得判断这一页或者左滑右滑请求新一页的数据得往list的哪个位置添加。这来回来去乱七八糟的判断就很麻烦了,很容易出bug。而且list长度太长了以后insert的性能也不好。 后来就去想,要不答题卡点击任意一题都清空旧的list,然后请求新的数据,左右滑动没数据了再请求新的数据呗。但是这样很浪费资源,并且用户体验也不好,用户已经从第1题答到第200题了,这时用户从答题卡选择了一个25题,还得重新请求网络。而且200道题的数据都没了,那再选个26题,再重新请求网络?网络有延时不说,还浪费资源。 最后转念一想,这时候就需要弄一个缓存了。所以最终的解决方法就出来了:我们维护一个map,在网络请求成功后,在map中保存对应页的数据,同时我们维护一个主list来显示对应的题目。当我们在答题卡选择某一题目,就清空list,然后判断map中有没有该页的数据,如果有就直接拿来,没有就再去网络请求。这个处理方式,写法相对来说简单,不需要乱七八糟的判断,也不浪费资源,用户体验也很不错。 总结 以上就是一些思路和要注意的地方。这个Demo断断续续花了好几天时间写出来的。可能我说的比较啰嗦比较细,只是想让需要用到这个分页Demo的同学能理解我是如何实现的。 如果觉得能帮到你,记得给一个star,谢谢。同时如果这个demo有bug或者你们有新想法,欢迎提出来。
2021-01-07 - 小程序上传多张图片工具方法
//上传图片 const upLoadFileList=(fileList,callBack)=>{ if(fileList.length==0){ typeof callBack == "function" && callBack([]) return; } wx.showLoading({ title: '上传中', }); const upImgList=[]; const length=fileList.length; const timestamp = Date.parse(new Date()); for (let i = 0; i < length; i++) { const filePath = fileList[i]; const cloudPath = `upLoadFiles/images/${timestamp+i+filePath.match(/\.[^.]+?$/)[0]}`; wx.cloud.uploadFile({ cloudPath, filePath, success: res => { upImgList.push(res.fileID); if(upImgList.length==length){ typeof callBack == "function" && callBack(upImgList) } }, fail:res=>{ wx.showToast({ icon: 'none', title: '上传失败', }); typeof callBack == "function" && callBack([]); return; } }) } } module.exports = { upLoadFileList }
2021-03-13 - camera 实现扫码拍照蒙版效果
[图片] 直接上代码 <cu-custom bgColor="bg-gradual-white" isBack="{{true}}"> <view slot="backText"></view> <view slot="content" style="font-weight: bold;">扫描VIN码</view> </cu-custom> <camera wx:if="{{isShowCamera}}" class="camera-box" devic-position="width" flash="{{flash}}"> <cover-view class="page-flex" style="width:{{info.windowWidth}}px; height:{{info.windowHeight}}px;"> <cover-view class="page-mask" style="height:{{topHeight}}px; width:100%"> <cover-view class="cameraTips" style="margin-top: {{topHeight -35 }}px">可以扫描前挡风,行驶证,车辆铭牌车架号</cover-view> </cover-view> <cover-view class="cameraregion" style="height:75px;width:100%"> <cover-view class="page-mask" style="height:75;width:{{edgewidth}}px"></cover-view> <cover-image class="active-image" src="../../images/0716073342969601272.png"></cover-image> <cover-view class="page-mask" style="height:75;width:{{edgewidth}}px"></cover-view> </cover-view> <cover-view class="page-mask cameraBgView" style="width:100%;height:{{buttomHeight}}px"> <cover-view style="width:100%;height:150px;display: flex;justify-content: center; margin-top:{{buttomHeight-200}}px"> <cover-view style="width:100px;height:150px;display: flex;align-items: center;justify-content: center;flex-direction: column;"> <cover-image class="takephoto" style="" src='../../images/cameraOK.png' bindtap='takePhotoAction'> </cover-image> <over-view class="ttext">立即识别</over-view> </cover-view> </cover-view> <cover-view style="position: absolute; width:100px;height:150px;display: flex;align-items: center;justify-content: center;flex-direction: column;margin-left:15px; margin-top:{{buttomHeight-200}}px;"> <cover-image class="cancelphoto" src='{{sdtsrc}}' bindtap='actionflash' style=""></cover-image> <over-view class="ttext">{{sgtext}}</over-view> </cover-view> </cover-view> </cover-view> </camera> <canvas wx:if='{{isShowImage}}' canvas-id="image-canvas" style='width:{{windowWidth}}px; height:{{windowHeight}}px;'></canvas> /* pages/camera/camera.wxss */ page { width: 100%; height: 100%; /* background-color: #fff; */ overflow: hidden; } .camera-box { width: 100vw; height: 100vh; } .page-flex { /* display: flex; */ } /* 形成遮罩 */ .page-mask { background-color: rgba(0, 0, 0, 0.5); } .cameraTips { width: 100%; text-align: center; font-size: 14px; color: #FFF; } .cameraregion { display: flex; } .ttext { font-size: 28rpx; margin-top: 12rpx; color:#fff; font-weight: bold; margin-top: 10px; /* z-index: 999; */ } /* 背景图像view */ .camerabgImage-view { height: 100%; width: 100%; position: absolute; z-index: 999; } /* 提示文本2 */ .cameraTips2 { margin-top: 22%; text-align: center; color: #fff; } .cameraactive { margin-top: 12px; position: absolute; width: 100%; display: flex; justify-content: center; } .active-image { display: block; width: 350px; height: 75px; } /* 拍照背景view */ .cameraBgView { position: relative; display: flex; } /* 按钮view*/ .cameraButton-view { height: 200rpx; width: 100%; position: absolute; display: flex; justify-content: space-around; } /* 取消按钮 */ .cancelphoto { width: 100rpx; height: 100rpx; } /* 拍照按钮 */ .takephoto { border-radius: 999px; box-shadow: 0 0 50rpx #ffd8a2; width: 152rpx; height: 152rpx; } 核心就是 /* 形成遮罩 */ .page-mask { background-color: rgba(0, 0, 0, 0.5); } 形成遮罩 cover-view 标签 计算 canvas 的裁剪部分
2021-02-27 - 小程序使用原生的camera组件拍照压缩上传示例
wxml文件: <view> <!–相机组件–> <camera device-position=’{{status}}’ model=‘normal’ flash=‘off’ style=‘width:{{w}}px;height:{{h}}px’ binderror=‘error’> [代码]<cover-view style="position:absolute" hidden='{{hideTakeButton}}' bindtap='test'> <button type='primary' plain='true'>测试</button> </cover-view> <!--头像框--> <cover-view style="position:absolute;width:100%;height:{{h}}px;z-index:-99" hidden='{{hideMask}}'> <cover-image src='{{config.imgBasePath}}/img/face_mask.png'></cover-image> </cover-view> <!--拍摄按钮--> <cover-view style="position:absolute;bottom:17%;left:{{(w-66)/2}}px;" hidden='{{hideTakeButton}}' bindtap='takePhoto'> <cover-image src='{{config.imgBasePath}}/img/takephoto_take.png'style="width:132rpx;"></cover-image> </cover-view> <!--定格图片--> <cover-view style='position:absolute;width:100%;height:{{h}}px;z-index:-1' hidden='{{hideCoverImage}}'> <cover-image src="{{showPath}}"></cover-image> </cover-view> <!--重拍按钮--> <cover-view class="complete" hidden='{{hideCoverButton}}'> <button class="reset" bindtap='takeAgain'>重拍</button> <button class="submit" bindtap='confirm'>完成</button> </cover-view> [代码] </camera> <!-- <view style=“position:fixed;top:999999999999999999999rpx;”> <canvas style=“width:{{cw}}px;height:{{ch}}px;” canvas-id=‘firstCanvas’> </canvas> </view> --> </view> js相关: /** 页面的初始数据 */ data: { filePath: ‘’, showPath:’’, status: ‘front’, w: app.globalData.winWidth, h: app.globalData.screenHeight, cw: 300, ch: 200, hideTakeButton: false, hideCoverImage: true, hideCoverButton: true, hideMask:false }, 压缩方法: //处理页面绑定事件 takePhoto() { let that = this; const ctx = wx.createCameraContext() ctx.takePhoto({ quality: ‘high’, success: (res) => { let tempPath = res.tempImagePath that.setData({ showPath : tempPath }) let zipedPath = ‘’; //直接压缩开始 wx.compressImage({ src: tempPath, quality:60, success:function®{ zipedPath= r.tempFilePath //console.log(“压缩后:” + zipedPath); that.setData({ filePath: zipedPath, hideTakeButton: true, hideMask:true, hideCoverButton: false, hideCoverImage: false }) } }) //直接压缩结束 }, fail: function() { wx.showToast({ title: ‘照片拍摄失败,请检查摄像头’, icon: ‘none’ }) } }) }, //点击确定才开始上传文件 confirm: function() { let that = this; that.setData({ hideCoverButton: true }); wx.showLoading({ title: ‘图片上传中…’, mask: true }) [代码]//调用文件处理方法 //that.zipImage(); //上传压缩过的文件 let uploadfilePath = that.data.filePath; console.log("获取上传图片:"+uploadfilePath) that.uploadzipBase64(uploadfilePath); [代码] }, //重拍按钮 takeAgain: function() { let that = this; that.setData({ hideTakeButton: false, hideMask:false, hideCoverImage: true, hideCoverButton: true, filePath: ‘’ }); }, //图片压缩后直接上传BASE64格式 zipImage: function() { let that = this; let filePath = that.data.filePath; var uploadFile = ‘’; //文件压缩 //获得原始图片大小 wx.getImageInfo({ src: filePath, success(res) { var originWidth = res.width; var originHeight = res.height; console.log(“原始宽高比” + originWidth + “:” + originHeight); //设置压缩比例,最大尺寸限度 var maxWidth = 1200; var maxHeight = 600; //目标尺寸 var targetWidth = originWidth; var targetHeight = originHeight; //等比例压缩,如果宽度大于高度,则宽度优先,否则高度优先 if (originWidth > maxWidth || originHeight > maxHeight) { if (originWidth / originHeight > maxWidth / maxHeight) { //宽度*原生图片比例=新图片尺寸 targetWidth = maxWidth; targetHeight = Math.round(maxWidth * (originHeight / originWidth)); } else { targetHeight = maxHeight; targetWidth = Math.round(maxHeight * (originWidth / originHeight)); } } //尝试压缩文件,创建canvas var ctx = wx.createCanvasContext(‘firstCanvas’); ctx.clearRect(0, 0, targetWidth, targetHeight); ctx.drawImage(filePath, 0, 0, targetWidth, targetHeight); //canvas压缩后图片处理要写在draw方法的回调里 ctx.draw(false, function callback() { [代码] //更新canvas大小 that.setData({ cw: targetWidth, ch: targetHeight }); //保存图片 setTimeout(function() { wx.canvasToTempFilePath({ canvasId: 'firstCanvas', fileType: 'jpg', success: (res) => { uploadFile = res.tempFilePath; //上传 that.uploadzipBase64(uploadFile); } }, this) }, 500); }); }, fail() { wx.showToast({ title: '获取图片失败', icon: 'none' }) } }) [代码] }, //转BASE64并上传 uploadzipBase64(uploadFile) { let that = this; //通过文件系统管理器重新编码文件 const fs = wx.getFileSystemManager(); fs.readFile({ filePath: uploadFile, encoding: “base64”, success: function(res) { var base64Data = res.data; console.log(“开始上传”) //调用你的ajax请求上传后台 ajaxUpload(); }, fail: function() { wx.showToast({ title: ‘读取图片失败’, icon: ‘none’ }) } }); }, //绑定出错事件 error(e) { wx.showToast({ title: ‘照片拍摄失败,请检查摄像头’, icon: ‘none’ }) } 总结: 以上示例,图片压缩可以通过2种方式: 1.官方的api:wx.compressImage({}) 2.利用canvas进行压缩重绘 转化为BASE64编码方式:通过文件系统管理器,读文件转化 const fs = wx.getFileSystemManager();
2020-03-10 - 03.getUserInfo和getUserProfile 对比
最近动态 wx.getUserProFile() 在2.16.0成功回调有iv、encryptedData,具体看这里https://developers.weixin.qq.com/community/develop/doc/000c04d0490118d8a6ebf675a56c00 调整背景 很多开发者在打开小程序时就通过组件方式唤起 getUserInfo 弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。详情可以点击官方调整链接(https://developers.weixin.qq.com/community/develop/doc/000cacfa20ce88df04cb468bc52801) 调整前后API功能的对比[图片] [图片] 能力检测 两个前提条件: 1.开发者工具版本不低于 1.05.21030222.基础库版本不低于 2.10.4[图片] 代码片段: https://developers.weixin.qq.com/s/odMs3wmX7Ko3 测试过程 step1: 在开发工具设置清除全部缓存step2: 点击 getUserInfo 按钮,会弹出用户授权,允许后会得到这些信息,见截图[图片] step3: 在终端输入下面代码,也可以获取上面截图数据(今天还不到截止时间,还能获取完整的用户头像和昵称)wx.getUserInfo({ complete: (res) => { console.log(res) } }) step4: 点击 getUserProfile 按钮,会弹出用户授权,允许后会得到这些信息,见截图(只有用户昵称和头像信息)[图片] step5: 通用在终端输入下面代码,获取不到任何信息,符合`若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,且开发者每次通过该接口获取用户个人信息均需用户确认`wx.getUserProfile({ complete: (res) => { console.log(res) } }) step6: 可以重复点击 getUserInfo 按钮和 getUserProfile 按钮进行测试。功能对比讲解 1.4月13日前未发布的,wx.getUserInfo 能力 wx.getUserInfo(Object object) 会返回 encryptedData、signature、rawData,通过将返回的数据传递给服务器,服务端能解析出用户的身份标识,即 unionId(unionId 获取机制:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html) 【对我们业务来说】 从 wx.getUserInfo 就是要两样东西:unionId和用户信息(头像和昵称)。 但从 2021年2月23日起,可以通过 wx.login 接口获取的登录凭证可直接换取 unionID,可以替代一部分wx.getUserInfo 的功能了。 2.新增 getUserProfile 能力 wx.getUserProfile 能获取到头像和昵称,可以替代 wx.getUserInfo 的另外一部分功能。 3.小结 从这里是不是可以得出,wx.login + wx.getUserProfile 基础可以替代之前的 4月13日前未发布的,wx.getUserInfo 能力。其实不然,如果真是这样的,官方是不是没必要这样搞,咱们接着看。 4.wx.getUserInfo 和 wx.getUserProfile 区别 1.功能上是 wx.getUserInfo 不在返回用户授权的头像昵称,只返回匿名信息,但 wx.getUserProfile 会返回用户授权的头像昵称。2.wx.getUserInfo 授权成功后,当下次调用时,可以直接获取授权成功返回数据,不需要每次都需要用户确认,但 wx.getUserProfile 每次都需要用户确认允许后才能拿到用户信息3.对于业务来说,可以通过 wx.getUserProfile 获取用户信息和昵称后,要存在自己服务器,不能像之前那样每次都通过 wx.getUserInfo 方式获取,否则体验会比较差疑问 1.4月13日后发布的新版本小程序,如果用户未更新到新版本,此时调用 wx.getUserInfo 会不会返回用户授权的头像昵称(如果不确定,业务可能需要兼容处理)2.4月13日后发布的新版本小程序,用户更新到新版本,调用 wx.getUserInfo 返回匿名的头像昵称支持服务器解密吗? 常见问题汇总 1.wx.canIUse 判断getUserProfile结果是false,可以通过直接判断 wx.getUserProfile 即可,类似问题可以查看官方知识库(https://developers.weixin.qq.com/community/develop/doc/000cac40cf0eb8d3e429647c351c09?_at=1614912876047)
2021-04-02 - 社区每周 |位置接口增加频率限制、服务商小程序新能力、新版众测及上周问题反馈(3.08-3.12)
各位微信开发者: 以下是getLocation增加调用频率限制、服务商小程序风险用户扫码能力公测启动、IOS和安卓新版众测及上周我们在社区收到的问题反馈的处理进度,希望同大家一同打造小程序生态。 getLocation增加调用频率限制 当前小程序频繁调用wx.getLocation接口会导致用户手机电量消耗较快,请开发者改为使用持续定位接口wx.onLocationChange,该接口会固定频率回调,使用效果与跟频繁调用getLocation一致。 从基础库2.17.0版本起(预计发布时间2021.4.9),将对getLocation接口增加频率限制,包括: 在开发版或体验版中,30秒内调用getLocation,仅第一次有效,剩余返回与第一次定位相同的信息。正式版中,为保证小程序正常运行同时不过度消耗用户电量,一定时间内(根据设备情况判断)调用getLocation,仅第一次会返回实时定位信息,剩余返回与第一次定位相同的信息。未做好兼容调整可能会影响用户体验,请开发者尽快适配。 服务商小程序风险用户扫描能力公测启动 为提高微信开放平台生态安全性,针对小程序各应用场景中可能存在的恶意注册、营销作弊等黑产风险和安全问题,平台将通过开放API的方式向服务商提供快速查询风险用户的接口,协助服务商保障小程序正常安全运营。 目前风险用户扫描接口支持以下两种应用场景: 1. 营销作弊场景:在首单优惠和特价优惠等营销活动中有效识别刷单、虚假交易、恶意骗保骗补贴等破坏运营秩序和安全的行为。 2. 恶意注册:识别并拦截机器批量注册、垃圾小号、伪造身份等恶意注册行为。 接口具体功能介绍请参考《小程序风险用户扫描功能介绍》。 接入指引及详细信息参考原公告:《服务商小程序风险用户扫描能力公测启动》 微信团队邀请开发者参与内部体验(安卓微信8.0.2) 本次更新概要如下小程序 video组件相关优化需关注地理位置等授权是否正常需关注临时文件相关功能是否正常需关注文件存储空间限制相关是否正常live-pusher组件相关bugfix,需关注麦克风相关功能是否正常蓝牙相关bugfix,需关注蓝牙扫描相关功能是否正常部分重构WebGL组件,需关注WebGL组件渲染是否正常小游戏 (重要)灰度期间尝试支持etc2和astc压缩格式,需关注游戏渲染是否正常优化uniformMatrix效率,需关注游戏运行性能部分重构渲染组件,需关注Touch事件等是否正常请基于以下提供的资源体验。使用过程中若发现问题,欢迎点击进入微信开放社区 #微信客户端内测 主页发表标题包含「微信8.0.2」的问答帖子反馈交流。 [图片] (扫描二维码下载) 如有需要,可查看并转发原公告:《微信团队邀请开发者参与内部体验(安卓微信8.0.2)》 微信团队邀请开发者参与内部体验(iOS微信8.0.3) 本次更新概要如下小程序: 文件系统底层重构,请关注相关接口是否受影响;小游戏: ATSC压缩纹理的支持;* 体验需识别下方二维码报名,若报名成功,则一天内会收到内测推送,内测名额8000人 [图片] 请基于以上提供的资源体验。使用过程中若发现问题,欢迎点击进入微信开放社区 #微信客户端内测 主页发表标题包含「微信iOS 8.0.3」的问答帖子反馈交流,发帖时建议提供以下信息方便定位问题: 1.手机型号 2.手机操作系统版本 3.必要时可提供代码片段 如有需要,可查看并转发原公告:《微信团队邀请开发者参与(iOS微信8.0.3内部体验)》 上周问题反馈和处理进度(3.08-3.12) 已修复的问题云开发-内容管理-短信统计分析数据一直为空的问题 查看详情 关联小程序提示:系统繁忙,请稍后重试(200003)的问题 查看详情 开发工具后控制台报(define,require)2个错误的问题 查看详情 修复中的问题 textarea maxlength 未阻止在中间输入 查看详情 previewImage 在安卓机无法预览,一直 loading 查看详情 ios 直接调hideLoading 没有触发回调 查看详情 安卓机下input输入框收起键盘之类的操作会多触发一次input事件 查看详情 iOS video 小窗模式 收不到bindended和bindtimeupdate的事件 查看详情 安卓performance中的firstRenderrender耗时偶现负数 查看详情 ios wx.previewMedia本地视频点击无法播放 查看详情 微信团队 2021.3.19
06-13 - 企业微信自建小程序登录时需要传入小程序关联应用的secret,小程序发布前无法关联企业微信...
企业微信自建小程序登录时需要传入小程序关联应用的secret,小程序发布前无法关联企业微信,那么我要如何获取access_token,使用测试企业的corpid和secret无法满足后期创建直播,创建会话等功能的测试,请问如何解决此问题 [图片] [图片]
2021-03-11 - 小程序关联多个企业微信,如何解决登录问题?
是不是要提前获得每个公司的corpid,secret, 写死在小程序代码里,然后循环传到后台进行验证比对corpid,知道判断出当前用户,是这个意思么?
2020-05-21 - 别人公司的企业微信可以关联我们的小程序吗?(企业微信和小程序不是同一主体)
别人公司的企业微信可以关联我们的小程序吗?(企业微信和小程序不是同一主体)
2019-12-04 - 如何从零实现上拉无限加载瀑布流组件
代码已优化请查看另外一篇文章 https://developers.weixin.qq.com/community/develop/article/doc/00026c521ece40c2d2db97f7156013 小程序瀑布流组件 前言:为了实现这个组件也花费了些时间,以前也做过瀑布流的功能,不过是利用 js 去 计算图片的高度,然后通过 css 的绝对定位去改变位置。不过这种要提前加载完一个列 表的图片,然后通过排列的算法生成排序的数组。总之就是太复杂了,后来在网上也看到 纯 css 实现,比如 flex 两列布局,columns 等,不做过多的阐述,下面分享下自己项 目中实现的瀑布流过程。 Css Grid 布局 Css3 变量属性 Js 动态修改 css 变量属性 Wxs 小程序脚本语言 Wxml 节点 Api Component 自定义组件 效果图 代码片段 [图片] Css Grid 网格布局实现多列多行布局 [代码]<view class="c-waterfall"> <view wx:for="{{ 10 }}" wx:key="item" class="view-container" > {{ item }} </view> </view> [代码] [代码].c-waterfall { display: grid; grid-template-columns: repeat(2, 1fr); grid-auto-flow: row dense; grid-auto-rows: 10px; grid-gap: 10px; } .view-container { width: 100%; grid-row: auto / span 20; } [代码] Css3 变量,可以通过[代码]js动态[代码]改变 [代码].c-waterfall { --grid-span: 10; --grid-column: 2; --grid-gap: 10px; --grid-rows: 10px; width: 100%; display: grid; grid-template-columns: repeat(var(--grid-column), 1fr); grid-auto-flow: row dense; grid-auto-rows: var(--grid-rows); grid-gap: var(--grid-gap); } .view-container { width: 100%; grid-row: auto / span var(--grid-span); } [代码] 动态修改 css 变量,实现遍历的节点都有独立的样式 [代码]<view class="c-waterfall" style="{{ style }}"> <view wx:for="{{ 10 }}" wx:key="item" class="view-container style="grid-row: auto / span var(--grid-row-{{ index }})" > {{ item }} </view> </view> [代码] [代码]Page({ data: { span: 20, style: '' }, onReady() { this.setData({ style: '--grid-row-0: 10;--grid-row-1: 10;' // 0-9... }) } }) [代码] 显然通过这种方式去修改emmm,有点不尽人意,当view渲染的时候,通过[代码]index[代码]下标给每个view都设置独立的[代码]grid-row[代码]样式,然后在修改view父级的style,将[代码]--grid-row-xxx[代码]变量写进去实现子类继承,虽然比直接去修改每个view的样式要优雅些,但是一旦views的节点多了,100个、1000个、没上限呢,那这个父级的style真的惨不忍睹。。比如100个view,那么style将会是下面这样,所以需要换个思路还是得单独去设置view的样式。 [代码]const views = [...99].map((v, k) => `--grid-row-${k}: 10;`) console.log(views) // ["--grid-row-0: 10;", "--grid-row-1: 10;", ... "--grid-row-2: 10;", "--grid-row-3: 10;", "--grid-row-98: 10;", "--grid-row-99: 10;"] [代码] 通过Wxs脚本语言来修改view的样式,相比较通过[代码]setData[代码]去修改view的样式,wxs的性能绝对比js强。 WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。 WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。 WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。 WXS 函数不能作为组件的事件回调。 由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。 一般在对wxs的使用场景上大多数用来做[代码]computed[代码]计算,因为在[代码]wxml[代码]模板语法里只能进行简单的三元运算,所以一些复杂的运算、逻辑判断等都会放到wxs里面去处理,然后返回给wxml。 [代码]// index.wxs var format = function(string) { return string + 'px' } module.exports = { format: format } [代码] [代码]<!-- index.wxml --> <wxs src="./index.wxs" module="wxs"></wxs> <view>{{ wxs.format('100') }}</view> <view>{{ wxs.format(span) }}</view> <button bind:tap="modifySpan">修改span的值</button> [代码] [代码]// index.js page({ data: { span }, modifySpan() { this.setData({ span: '200' }) } }) [代码] 通过WXS响应事件来修改视图层[代码]Webview[代码],跳过逻辑层[代码]App Service[代码],减少性能开销,比如一些频繁响应的事件监听,滚动条位置,手指滑动位置等,通过wxs来做视图层的修改,大大提升了流畅度。 通过wxs响应原生组件的事件,[代码]image[代码]组件的[代码]bind:load[代码]事件 [代码]<!-- index.html --> <wxs src="./index.wxs" module="wxs"></wxs> <image class="image" src="https://hbimg.huabanimg.com/ccf4a904deaebc25990a47471c61ea1c765694f82633b-71iPZs_/fw/480/format/webp" bind:load="{{ wxs.loadImg }}" /> [代码] [代码]// index.wxs var loadImg = function(event, ownerInstance) { // image组件load加载完返回图片的信息 var image = event.detail // 获取image的实例 var imageDom = ownerInstance.selectComponent('.image') // 设置image的样式 imageDom.setStyle({ height: image.height + 'px', background: 'red' // ... }) // 给image添加class imageDom.addClass('.loaded') // 更多的功能请参考文档 // https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html } module.exports = { loadImg: loadImg } [代码] wxs监听data的值 [代码]<!-- index.html --> <wxs src="./index.wxs" module="wxs"></wxs> <view class="container"> <view change:text="{{ wxs.changeText }}" text="{{ text }}" class="text" data-options="{{ options }}" > {{ text }} </view> <view class="child-node"> this is childNode </view> <!-- 某个自定义组件 --> <test-component class="other-node" /> </view> [代码] [代码]// index.wxs var changeText = function(newValue, oldValue, ownerInstance, instance) { // 获取修改后的text var text = newValue // 获取data-options var options = instance.getDataset() // 获取当前页面的任意节点实例 var childNode = instance.selectComponent('.container .child-node') // 修改childNode样式 childNode.setStyle({ color: 'gree' }) // 获取页面的自定义组件 var otherNode = instance.selectComponent('.container .other-node') // 获取自定义组件内的节点实例 // 通过css选择器 > var otherChildNode = instance.selectComponent('.container .other-node >>> .other-child-node') // 获取自定义组件内部节点的样式 var style = otherChildNode.getComputedStyle(['width', 'height']) // 更多功能看文档 } module.exports = { changeText: changeText } [代码] 通过[代码]createSelectorQuery[代码]获取节点的信息,用来后续计算[代码]grid-row[代码]的参数 [代码]Page({ onReady() { wx.createSelectorQuery(this) .select('.view-container') .fields({size: true}) .exec((res) => { console.log(res) // [{width: 375, height: 390}] }) } }) [代码] 创建waterfall自定义组件 waterfall组件的职责,做成组件有什么好处,不做成组件又有什么好处,以及通过抽象节点来实现多组件复用。 prop的基本设置参数 [代码]Component({ properties: { views: Array, // 需要渲染的瀑布流视图列表 options: { // 瀑布流的参数定义 type: Object, default: { span: 20, // 节点高度比 column: 2, // 显示几列 gap: [10, 10], // xy轴边距,单位px rows: 2, // 网格的高度,单位px }, } } }) [代码] 组件内部默认的样式 [代码].c-waterfall { --grid-span: 10; --grid-column: 2; --grid-gap: 10px; --grid-rows: 10px; width: 100%; display: grid; grid-template-columns: repeat(var(--grid-column), 1fr); grid-auto-flow: row dense; grid-auto-rows: var(--grid-rows); grid-gap: var(--grid-gap); } .view-container { width: 100%; grid-row: auto / span var(--grid-span); } [代码] 组件的骨架 [代码]<wxs src="./index.wxs" module="wx" ></wxs> <!-- 样式承载节点 --> <view class="c-waterfall" change:loadStatus="{{ wx.load }}" loadStatus="{{ childNode }}" data-options="{{ options }}" style="{{ wx.setStyle(options) }}" > <!-- 抽象节点 --> <selectable class="view-container" id="view-{{ index }}" wx:for="{{ views }}" wx:key="item" value="{{ item }}" index="{{ index }}" bind:load="load" > </selectable> </view> [代码] 抽象节点 [代码]{ "component": true, "usingComponents": {}, "componentGenerics": { "selectable": true } } [代码] 抽象节点应该遵循什么 [代码]Component({ properties: { value: Object, // 组件自身需要的数据 index: Number, // 下标值 }, methods: { load(event) { // load节点响应事件 this.triggerEvent('load', { ...this.data, // value必填参数 {width,height} value: { ...event.detail }, }) }, }, }) [代码] 组件wxs响应事件 [代码].c-waterfall[代码]样式承载节点,主要是设置options传入的参数 [代码] var _getGap = function (gaps) { return gaps .map(function (v) { return v + 'px' }) .join(' ') } var setStyle = function (options) { if (!options) return var style = [ '--grid-span: ' + options.span || 10, '--grid-column: ' + options.column || 2, '--grid-gap: ' + _getGap(options.gap || [10, 10]), '--grid-rows: ' + (options.rows || 10) + 'px', ] return style.join(';') } [代码] 获取瀑布流样式承载节点实例 [代码] var _getWaterfall = function (dom) { var waterfallDom = dom.selectComponent('.c-waterfall') return { dom: waterfallDom, options: waterfallDom.getDataset().options, } } [代码] 获取事件触发的节点实例 [代码] var _getView = function (index, dom) { var viewDom = dom.selectComponent('.c-waterfall >>> #view-' + index) return { dom: viewDom, style: viewDom.getComputedStyle(['width', 'height']), } } [代码] 获取虚拟节点自定义组件load节点实例,初始化渲染时,节点是未知的,比如image组件,图片的宽高是未知的,需要等到image加载完成才会知道宽高,该节点用于存放异步视图展示,然后通过事件回调计算出节点高度。 [代码] var _getLoadView = function (index, dom) { return { dom: dom.selectComponent( '.c-waterfall >>> #view-' + index + '>>>.waterfall-load-node' ), } } [代码] 获取虚拟节点自定义组件other节点实例,初始化渲染就存在节点,比如一些文字就放在该节点,具体由组件的创造者去自定义。 [代码] var _getOtherView = function (index, dom) { var other = dom.selectComponent( '.c-waterfall >>> #view-' + index + '>>> .waterfall-load-other' ) return { dom: other, style: other.getComputedStyle(['height', 'width']), } } [代码] 已知瀑布流样式承载节点的宽度,等load节点异步视图回调时,获取到load节点的实际高度,比如一张400*800的图片,如果要显示在一个宽度180px的视图里,注意:[代码]image[代码]组件会有默认高度240px,或者用户自己设置了高度。如果要实现瀑布流,还是需要通过计算图片的宽高比例得到图片在视图中宽高,然后再通过计算grid布局的span值实现填充。 [代码] var fix = function (string) { if (typeof string === 'number') return string return Number(string.replace('px', '')) } var computedContainerHeight = function (node, view) { var vW = fix(view.width) var nW = fix(node.width) var nH = fix(node.height) var scale = nW / vW return { width: vW, height: nH / scale, } } [代码] 通过公式计算span的值,这个公式也是花了我不少时间去研究的,对grid布局使用也不多,很多潜在用法并不知道,所以通过大量的随机数据对比查找规律所在。[代码]gap为数组[x, y][代码],我们要取y计算,已知gap、rows求视图中节点高度[代码](gap[y] + rows) * span - gap[y] = height[代码],有了求height的公式,那么求span就简单了,[代码](height + gap[y]) / (gap[y] + rows) = span[代码],最终视图里的高度会跟计算出来的结果几个像素的误差,因为[代码]grid-row[代码]设置span不能为小数,只能为整数,而我们瀑布流的高度是未知的,通过计算有多位浮点数,所以只能向上取整了导致有几个像素的误差。 [代码] var computedSpan = function (height, options) { var rows = options.rows var gap = options.gap[1] var span = Math.ceil((height + gap) / (gap + rows)) return span } [代码] 最后我们能得到[代码]span[代码]的值了,只需要将[代码]load完成的视图修改样式即可[代码] [代码] var load = function (node, oldNode, dom) { if (!node.value) return false var index = node.index var waterfall = _getWaterfall(dom) // 获取虚拟组件,通过index下标确认是哪个,获取宽度高度 var view = _getView(index, dom) var otherView = _getOtherView(index, dom) var otherViewHeight = fix(otherView.style.height) // 计算虚拟组件的高度,其实就是计算图片在当前视图节点里的宽高比例 // image组件的mode="widthFix"也是这样计算的额 var virtualStyle = computedContainerHeight(node.value, view.style) // span取值,此处计算的高度应该是整个虚拟节点视图的高度 // load事件回调里,我们只传了load视图节点的宽高 // 后续通过selectComponent获取到了other视图节点的高度 var span = computedSpan( otherViewHeight + virtualStyle.height, waterfall.options ) // 设置虚拟组件的样式 view.dom.setStyle({ 'grid-row': 'auto / span ' + span, }) // 获取重新渲染后的虚拟组件高度 var viewHeight = view.dom.getComputedStyle(['width', 'height']) viewHeight = fix(viewHeight.height) // 上面说了因为浮点数的计算会导致有几个像素的误差 // 为了视图美观,我们将load视图节点的高度设置成虚拟视图节点的总高度减去静态节点的高度 var loadView = _getLoadView(index, dom) loadView.dom.setStyle({ width: virtualStyle.width + 'px', height: parseInt(viewHeight - otherViewHeight) + 'px', opacity: 1, visibility: 'visible', }) return false } module.exports = { load: load, setStyle: setStyle, } [代码] 抽离成虚拟节点自定义组件的利弊 利: 符合观察者模式的设计模式 降低代码耦合度 扩展性强 代码清晰 弊: 节点增加,如果视图节点过多会造成小程序性能警告 样式编写不便捷,需要写过多的判断代码去实现外部样式覆盖 wxs只能监听原生组件的事件,所以image的load事件触发时本可以直接去修改页面视图节点样式,不需要传回给父组件,然后父组件setData下标,wxs监听事件触发在去修改视图样式,多了一次setData的开销。 合: 时间有限没有扩展样式覆盖了,可以开启自定义组件的外部样式引入 节点过多的问题,在我自己电脑上,开发工具插入100个组件时,出现了卡顿,样式错乱,真机上目前还没发现上限。 后续想实现长列表功能,有回收机制,这样视图内的节点有限了,降低了性能开销,因为之前版本的长列表组件是通过[代码]createSelectorQuery[代码]获取节点信息,然后记录高度,通过创建[代码]createIntersectionObserver[代码]监听视图节点是否在视图来判断是否渲染。但是瀑布流有异步视图,初次渲染的高度跟异步加载完的高度是不一样,所以创建监听事件高度会不准确,若等到load完再创建监听事件,父级容器的高度又要经过计算,因为子节点会去填充空白区域实现瀑布流,目前项目中为了避免节点过大造成性能警告,加了item的个数限制,如果超过100或者1000个就清空数组,类似分页的功能。不过上面总结的思路可以去试试。 等把功能完善了,发布npm依赖包安装。 后续有时间会将项目里比较实用的组件抽离出来。。 自定义tabbar 自定义navbar 长列表 下拉刷新 上拉加载 购物车sku … Demo page调用页面 [代码]<view class="container"> <waterfall wx:if="{{ _type === 0 }}" generic:selectable="test-view" views="{{ views }}" options="{{ options }}" /> <waterfall wx:else generic:selectable="image-view" views="{{ images }}" options="{{ options }}" /> </view> <view class="btns"> <button bind:tap="loadView">模拟节点</button> <button bind:tap="loadImage">远程图片</button> </view> [代码] [代码]Page({ data: { views: [], loading: false, options: { span: 30, column: 2, gap: [10, 10], rows: 2, }, images: [], _page: 1, _type: 0, }, onLoad() { // 生成随机数据 // this.generateViews() // this.getHuaBanList() }, loadView() { this.data._page = 1 this.setData({ images: [], _type: 0 }) this.generateViews() }, loadImage() { this.data._type = 1 this.setData({ views: [], _type: 1 }) this.getHuaBanList() }, getHuaBanList() { let { images, _page } = this.data wx.request({ url: `https://huaban.com/search/?q=随机&page=${_page}&per_page=10&wfl=1`, header: { accept: 'application/json', 'accept-language': 'zh-CN,zh;q=0.9', 'x-request': 'JSON', 'x-requested-with': 'XMLHttpRequest', }, success: (res) => { res.data.pins.map((v) => { images.push({ url: `https://hbimg.huabanimg.com/${v.file.key}_/fw/480/format/webp`, title: v.raw_text, }) }) this.setData({ images, _page: ++_page }) wx.hideLoading() }, }) }, generateViews() { const { views } = this.data for (let i = 0; i < 10; i++) { views.push({ width: this._randomNum(150, 500) + 'px', height: this._randomNum(200, 600) + 'px', }) } this.setData({ views, }) }, _randomNum(minNum, maxNum) { switch (arguments.length) { case 1: return parseInt(String(Math.random() * minNum + 1), 10) break case 2: return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10) break default: return 0 break } }, onReachBottom() { let { loading, _type } = this.data if (!loading) { wx.showLoading({ title: 'loading...', }) loading = true setTimeout(() => { _type === 0 ? this.generateViews() : this.getHuaBanList() wx.hideLoading() loading = false }, 1000) } }, }) [代码] [代码]{ "usingComponents": { "waterfall": "/components/waterfall/index", "test-view": "/components/test-view/index", "image-view": "/components/image-view/index" } } [代码] 模拟load异步的自定义组件 [代码]<view class="c-test-view"> <view class="waterfall-load-node"> {{value.width}}*{{value.height}} </view> <view class="waterfall-load-other">模拟加载图片</view> </view> [代码] [代码]Component({ properties: { value: Object, index: Number, }, lifetimes: { ready() { const { index } = this.data const timer = 1000 + 300 * String(index).charAt(index.length - 1) setTimeout(() => this.load(), timer) }, }, methods: { load() { this.triggerEvent('load', { ...this.data, }) }, }, }) [代码] [代码].c-test-view { width: 100%; height: 100%; display: flex; flex-flow: column; justify-content: center; align-items: center; background: white; } .c-test-view .waterfall-load-node { height: 50%; flex-grow: 1; transition: all 0.3s; display: inline-flex; flex-flow: column; justify-content: center; align-items: center; background: #eeeeee; width: 100%; opacity: 0; } .c-test-view .waterfall-load-other { width: 100%; height: 80rpx; display: inline-flex; justify-content: center; align-items: center; background: cornflowerblue; color: white; } [代码] 随机获取花瓣网图片的自定义组件 [代码]<view class="c-image-view"> <view class="waterfall-load-node"> <image class="load-image" src="{{ value.url }}" bind:load="load" /> </view> <view class="waterfall-load-other">{{ value.title }}</view> </view> [代码] [代码]Component({ properties: { value: Object, index: Number, }, lifetimes: { ready() {}, }, methods: { load(event) { this.triggerEvent('load', { ...this.data, value: { ...event.detail }, }) }, }, }) [代码] [代码].c-image-view { width: 100%; display: inline-flex; flex-flow: column; background: white; border-radius: 10px; overflow: hidden; height: 100%; } .c-image-view .waterfall-load-node { width: 100%; height: 50%; display: inline-flex; flex-grow: 1; background: gainsboro; transition: opacity 0.3s; opacity: 0; overflow: hidden; visibility: hidden; } .c-image-view .waterfall-load-node .load-image { width: 100%; height: 100%; overflow: hidden; } .c-image-view .waterfall-load-other { font-size: 30rpx; background: white; min-height: 60rpx; padding: 10px; display: flex; align-items: center; } [代码] 代码片段 https://developers.weixin.qq.com/s/Q02FETmW7ind
2021-03-19 - 如何实现一个6位数的密码输入框
背景: 因为公司业务调整需要做用户支付这一块 开发者需要在小程序上实现一个简单的6位数密码输入框 [图片] 首先想下如何实现该效果: 1.使用input覆盖在框上面,设置letter-spacing达到数字之间间距的效果,实现时发现在input组件上使用letter-spacing无效果 2.循环六个view模拟的框,光标使用动画模拟,一个隐藏的input,点击view框时触发input的Focus属性弹起键盘,同时模拟的光标展示出来,输入值后,input的value长度发生变化,设置光标位置以及模拟的密码小黑圆点 好了,废话不多数,咱们直接上手。 wxml [代码]<view class='container'> <!-- 模拟输入框 --> <view class='pay-box {{focusType ? "focus-border" : ""}}' bindtap="handleFocus" style='width: 604rpx;height: 98rpx'> <block wx:for="{{boxList}}" wx:key="{{index}}"> <view class='password-box {{index === 0 ? "b-l-n":""}}'> <view wx:if="{{(dataLength === item - 1)&& focusType}}" class="cursor"></view> <view wx:if="{{dataLength >= item}}" class="input-black-dot"></view> </view> </block> </view> <!-- 隐藏input框 --> <input value="{{input_value}}" focus="{{isFocus}}" maxlength="6" type="number" class='hidden-input' bindinput="handleSetData" bindfocus="handleUseFocus" bindblur="handleUseFocus" /> </view> [代码] wxss [代码]/* 第一个格子输入框 */ .container .b-l-n { border-left: none; } .pay-box { margin: 0 auto; display: flex; flex-direction: row; border-left: 1px solid #cfd4d3; } /* 支付密码框聚焦的时候 */ .focus-border { border-color: #0c8; } /* 单个格式样式(聚焦的时候) */ .password-box { flex: 1; border: 1px solid #0c8; margin-right: 10rpx; display: flex; align-items: center; justify-content: center; } /* 模拟光标 */ .cursor { width: 2rpx; height: 36rpx; background-color: #0c8; animation: focus 1.2s infinite; } /* 光标动画 */ @keyframes focus { from { opacity: 1; } to { opacity: 0; } } /* 模拟输入的password的黑点 */ .input-black-dot { width: 20rpx; height: 20rpx; background-color: #000; border-radius: 50%; } /* 输入框 */ .hidden-input { margin-top: 200rpx; position: relative; } [代码] JS [代码]Component({ data: { //输入框聚焦状态 isFocus: false, //输入框聚焦样式 是否自动获取焦点 focusType: true, valueData: '', //输入的值 dataLength: '', boxList: [1, 2, 3, 4, 5, 6] }, // 组件属性 properties: { }, // 组件方法 methods: { // 获得焦点时 handleUseFocus() { this.setData({ focusType: true }) }, // 失去焦点时 handleUseBlur() { this.setData({ focusType: false }) }, // 点击6个框聚焦 handleFocus() { this.setData({ isFocus: true }) }, // 获取输入框的值 handleSetData(e) { // 更新数据 this.setData({ dataLength: e.detail.value.length, valueData: e.detail.value }) // 当输入框的值等于6时(发起支付等...) if (e.detail.value.length === 6) { // 通知用户输入数字达到6位数可以发送接口校验密码是否正确 this.triggerEvent('initData', e.detail.value) } } } }) [代码] 实现方式很简单,有点小问题,还有一些后续准备做的优化点,等完善后上线后再来修改一波。 最后附上代码片段: https://developers.weixin.qq.com/s/8CtRqJmT7W8k
2020-07-06 - 纵向 slider
因为官方的 slider 只有横向的,所以按照官方的slider写了个纵向的,用 wxs 处理的事件(出了这么久了,却没有怎么用过。。)。代码片段如下 https://developers.weixin.qq.com/s/qYLgcDmC7Air 没什么难点,本来想着几行就写完了。。意料之外的多了些代码。。主要有几点想吐槽的 wxs 里的getState是用来存公共临时变量的。为什么不直接在 wxs里定义一个变量呢?因为如果有多个组件的话,这些变量会共享。另外还要注意下面的情况 [代码] var state = getState() state.value = 1 // 正确 state = {value: 1} // 错误 [代码] 点击事件是加到线条上的,线条本身太窄了,加了padding扩大点。另外,头尾两个位置应该是用户最常用的点击,但是却不怎么好点到,甚至根本点不到,所以在两头各加了2个view来直接设置最大最小值。 想点着圆点滑动的时候,偶尔会点到滚动条上,结果造成页面滑动,所以在最外面加了 touchmove 绑定了空方法。 为什么这个组件需要有 show-value 的配置。。感觉特别鸡肋。。 早先发到群里的那一版不太好,没优化,而且value的设置忘了减去 min了。。哈哈哈。。抱歉。。
2020-07-06 - 该死的颜色选择器!!
目标 看到这个很酷的网站 所以也想看看怎么弄? 先来挑战入门版… 颜色坐标系 首先要解决一个 误解 [图片] 我们所看到的颜色面板, 其实就是一个固定的样式, 而我们获取的颜色其实是从 坐标模型中 计算出来的。 坐标模型有很多, 在此使用的是 HSV颜色模型 Q: 为什么使用 HSV ? A: HSV色系对用户来说是一种直观的颜色模型, 主要由 色调(Hue, 简H)、饱和度(Saturation, 简S)、色明度(Value, 简V) 将 HSV六角锥体模型 转为 直观的数学坐标系 [图片] 需要注意, document中元素节点 坐标原点是右上角, 而数学坐标原点为右下角 数学坐标系: y、x、h HSV坐标系: v、s、h 确认坐标系的范围 色调H: 取值范围为0°~360° 饱和度S:取值范围为0.0~1.0 亮度V:取值范围为0.0(黑色)~1.0(白色) 通过document节点上元素的宽高, 计算步长, 达到取值范围为 0~100(转为百分制) 颜色转换 通过 触摸 坐标系 获取 y(v)、x(s)、h 的值, 然后利用算法公式转换成 rgb 颜色 hsv转rgb公式 rgb转hsv公式 还有 rgb转hex、 rgb转hsl; 都在这里 实例用法 详细注释在 代码中… 代码片段: https://developers.weixin.qq.com/s/rRHvfdmx79mR github: https://github.com/angxuejian/moto.wxui/tree/main/UI/colorPicker 1. 将 colorPicker 组件 引入到项目中。 [代码]// index.json { "usingComponents": { "color-picker": "../../components/colorPicker/colorPicker" } } // index.html <view> <color-picker></color-picker> </view> [代码] 2. Attributes 属性 类型 默认值 必填 说明 width number 35 否 宽度; 单位px height number 35 否 高度; 单位px predefined string #409EFF 否 预览颜色; 支持HEX和RGB; 只支持英文字符 3. Events 事件名称 回调参数 说明 change 当前颜色 当修改绑定值时触发 4. 示例 [图片] 参考文献 MakerGYT 看了MakerGYT写的mini-color-picker源码, 非常强🤙🤙🤙 颜色公式转换 在线测试工具,校验计算是否正确 hsv百度百科 Element的color-picker
2020-11-29 - 小程序怎么申请一个长期订阅消息模板?
目的:想申请一个长期订阅消息的模板。 小程序名:微连心 用途:政法委授权,群众发现身边事,例如消防隐患、交通安全等问题,将身边问题报给负责问题收集、问题排查的网格员, 网格将事情受理,办结或转交到职能部门,实现提前预防和及时处理群众身边的事。 希望网格员在办理事情的进度,通过小程序订阅消息实时的通知报事群众。 例如:XXXX年XX月XX日 XX时XX分。网格员XXX已经受理您的报事。 XXXX年XX月XX日 XX时XX分。网格员XXX已经办结您的报事。
2020-06-11 - 小程序申请长期订阅消息模板?
小程序APPid:wx8242f2b7d78bb0f2 主体信息:惠州市行政服务中心(惠州市12345投诉受理中心) 服务类目:政务民生 > 政务服务大厅 申请模板名称:中介超市业务通知 使用场景:采购单位在政府主导建设的公平公开平台发布采购公告后,通过小程序发消息通知中介机构踊跃参与报名 模板字段:通知类型、项目名称、采购方名称、提醒内容、注意事项 消息示例: 通知类型:新项目报名通知 项目名称:石坝河截污管网建设工程采购预算编制服务 项目业主:博罗县石坝镇人民政府 提醒内容:有新项目可报名 注意事项:请留意是否符合条件及报名截止时间
2021-01-08 - 小程序后台如何申请长期订阅消息模板?
各位大佬好。小弟的问题是这样的,公司是医疗行业的,现需要弄一个小程序消息推送功能。我就去后台申请了一个消息模板,但是审核通过之后发现申请的消息模板是一次性订阅的,不满足需求,需要弄一个长期订阅的。但是在小程序后台找了半天也没找到申请长期订阅消息的入口。所以小弟有两个问题: 1、小程序后台如何申请长期订阅消息模板? 2、已经申请的一次性订阅的消息模板可以调整为长期订阅消息模板吗?
2020-12-25 - 小程序长期订阅申请
【小程序id】wx8487569a079b8382 【小程序主体】腾讯医疗健康(深圳)有限公司 【申请模板类目】工具 > 健康管理 【申请模板名称】问诊提醒 【使用场景】每当订单状态为“订单完成待评价”时,立即向用户发送一条小程序提醒,告知订单状态更新,并邀请ta对服务医生进行评价 【模板字段】问诊医生、问诊内容、提示说明 【消息示例】 问诊医生:张三医生 问诊内容:我晚上血压升高,是否要加药? 提示说明:您的订单已完成,点击这里即可评价医生
2020-12-11 - 纪录一个增加订阅次数的交互方案
普通小程序没有长期订阅消息,只能按照用户订阅次数发送订阅消息。如果订阅次数用完了怎么办呢? 可以让用户多点几次订阅,就可以做到了,但是这个操作对用户来说还是不太方便。怎样设计交互,让用户做起来不那么麻烦,是需要好好考虑的事情。 最近正好看到ReadHub的订阅方式,截图纪录一下。 [图片][图片][图片] 首先提醒用户,通知次数用完了,需要手动点击订阅,增加订阅次数。第一次会申请两个订阅,“每日早报提醒”和“订阅次数耗尽提醒”。用户点击允许以后,会发现可以接收的次数变成1了。 [图片][图片] 再次点击增加通知次数,从第二次开始,只申请一个“每日早报提醒”,每操作一次,订阅次数+1。点多了以后,嫌麻烦索性选中“总是保持以上选择,不再询问”,然后狂按增加订阅次数,会发现增加的次数并没有那么快,应该是每次订阅服务器端都要纪录一下才能展示出来,也就是说每次点击都落到数据库了。这样大概每秒1次,我点这21下也花了接近一分钟时间,不是一个可以忽略的操作。 更多参考: 云开发·多次订阅一次性订阅消息后定时发送
2021-03-08 - 小程序的web-view支持 wx.navigateToMiniProgram 吗?
也就是说 我小程序内的web-view 引入H5页面 H5页面引入了微信的JSSDK 1.3.2 调用wx.navigateToMiniProgram报错是还没有支持此功能吗?
2021-01-21 - 关于同主体下两个小程序单点登陆问题?
公司有两个小程序,希望其中一个小程序有个入口可以直接跳转到第二个小程序,两个小程序都是同一个主体,并且在开放平台上有绑定,两个小程序都可以单独使用。如果用户没有关注公众号,需要在第一个小程序登陆后,进入第二个小程序不需要再次登陆。 有没有办法做到其中一个小程序登陆后,另外一个小程序不用登陆? 想用unionid来区分唯一,如果第一个小程序已经登陆过了,用户第一次从第一个小程序跳转到第二个小程序,第二个小程序里是拿不到unionID的,请问有这种问题的解决办法吗? 现在想到的方式是:如果直接在跳转(navigateToMiniProgram)到第二个小程序的时候将第一个小程序的登陆过后的unionID和session传给第二个小程序,做成无状态session,但是会有一个问题,下次用户直接进入第二个小程序,就没法拿到unionID,还是需要重新登陆。想在第二个小程序里用wx.setStorage设置缓存将传过来的unionid保存下来,下次进来就能拿到unionid,不知道是不是合理的解决方法,感觉安全风险很高。 大家有没有好的解决方案,官方有没有好的方案?感激不尽! [图片]
2019-08-01 - 微信开放平台绑定微信小程序,是不是必须要认证300块钱,才可以?
我的微信小程序已交300已认证。现在我想使用unionid,在开放平台注册关联(同一公司主体),又要我交300,是必须要交的吗? 另外,我个人的账号,没认证也可以关联上小程序。到底是怎么回事?
2021-01-14 - 个人开发者不能注册微信开放平台帐号?
个人主体可以注册,但个人主体注册后无法认证。
2019-12-30 - 如何注册小程序?
1、注册方法 在微信公众平台官网首页(mp.weixin.qq.com)点击右上角的“立即注册”按钮。 [图片] 2、选择注册的帐号类型 选择“小程序”,点击“查看类型区别”可查看不同类型帐号的区别和优势。 3、填写邮箱和密码 请填写未注册过公众平台、开放平台、企业号、未绑定个人号的邮箱。 4、激活邮箱 登录邮箱,查收激活邮件,点击激活链接。 5、填写主体信息 点击激活链接后,继续下一步的注册流程。请选择主体类型选择,完善主体信息和管理员信息。 选择主体类型: [图片] 主体类型说明如下: 帐号主体 范围 个人 18岁以上有国内身份信息的微信实名用户 企业 企业、分支机构、企业相关品牌。 企业(个体工商户) 个体工商户。 政府 国内、各级、各类政府机构、事业单位、具有行政职能的社会组织等。目前主要覆盖公安机构、党团机构、司法机构、交通机构、旅游机构、工商税务机构、市政机构等。 媒体 报纸、杂志、电视、电台、通讯社、其他等。 其他组织 不属于政府、媒体、企业或个人的类型。 填写主体信息并选择验证方式 企业类型帐号可选择两种主体验证方式。 方式一:支付验证 需要用公司的对公账户向腾讯公司打款来验证主体身份,打款信息在提交主体信息后可以查看到。 请根据页面提示,向指定的收款帐号汇入指定金额。 温馨提示:请在10天内完成汇款,否则将注册失败。 方式二:微信认证 通过微信认证验证主体身份,需支付300元认证费。认证通过前,小程序部分功能暂无法使用。 如需了解主体验证方式请“点击这里” 填写管理员信息 [图片] 确认主体信息不可变更 [图片]
2020-03-18 - 小程序登录、用户信息相关接口调整说明
公告更新时间:2021年04月15日考虑到近期开发者对小程序登录、用户信息相关接口调整的相关反馈,为优化开发者调整接口的体验,回收wx.getUserInfo接口可获取用户授权的个人信息能力的截止时间由2021年4月13日调整至2021年4月28日24时。为优化用户的使用体验,平台将进行以下调整: 2021年2月23日起,若小程序已在微信开放平台进行绑定,则通过wx.login接口获取的登录凭证可直接换取unionID2021年4月28日24时后发布的小程序新版本,无法通过wx.getUserInfo与<button open-type="getUserInfo"/>获取用户个人信息(头像、昵称、性别与地区),将直接获取匿名数据(包括userInfo与encryptedData中的用户个人信息),获取加密后的openID与unionID数据的能力不做调整。此前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。新增getUserProfile接口(基础库2.10.4版本开始支持),可获取用户头像、昵称、性别及地区信息,开发者每次通过该接口获取用户个人信息均需用户确认。具体接口文档:《getUserProfile接口文档》由于getUserProfile接口从2.10.4版本基础库开始支持(覆盖微信7.0.9以上版本),考虑到开发者在低版本中有获取用户头像昵称的诉求,对于未支持getUserProfile的情况下,开发者可继续使用getUserInfo能力。开发者可参考getUserProfile接口文档中的示例代码进行适配。请使用了wx.getUserInfo接口或<button open-type="getUserInfo"/>的开发者尽快适配。开发者工具1.05.2103022版本开始支持getUserProfile接口调试,开发者可下载该版本进行改造。 小游戏不受本次调整影响。 一、调整背景很多开发者在打开小程序时就通过组件方式唤起getUserInfo弹窗,如果用户点击拒绝,无法使用小程序,这种做法打断了用户正常使用小程序的流程,同时也不利于小程序获取新用户。 二、调整说明通过wx.login接口获取的登录凭证可直接换取unionID 若小程序已在微信开放平台进行绑定,原wx.login接口获取的登录凭证若需换取unionID需满足以下条件: 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用2月23日后,开发者调用wx.login获取的登录凭证可以直接换取unionID,无需满足以上条件。 回收wx.getUserInfo接口可获取用户个人信息能力 4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo或<button open-type="getUserInfo"/>将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。 具体变化如下表: [图片] 即wx.getUserInfo接口的返回参数不变,但开发者获取的userInfo为匿名信息。 [图片] 此外,针对scope.userInfo将做如下调整: 若开发者调用wx.authorize接口请求scope.userInfo授权,用户侧不会触发授权弹框,直接返回授权成功若开发者调用wx.getSetting接口请求用户的授权状态,会直接读取到scope.userInfo为true新增getUserProfile接口 若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,该接口从基础库2.10.4版本开始支持,该接口只返回用户个人信息,不包含用户身份标识符。该接口中desc属性(声明获取用户个人信息后的用途)后续会展示在弹窗中,请开发者谨慎填写。开发者每次通过该接口获取用户个人信息均需用户确认,请开发者妥善保管用户快速填写的头像昵称,避免重复弹窗。 插件用户信息功能页 插件申请获取用户头像昵称与用户身份标识符仍保留功能页的形式,不作调整。用户在用户信息功能页中授权之后,插件就可以直接调用 wx.login 和 wx.getUserInfo 。 三、最佳实践调整后,开发者如需获取用户身份标识符只需要调用wx.login接口即可。 开发者若需要在界面中展示用户的头像昵称信息,可以通过<open-data>组件进行渲染,该组件无需用户确认,可以在界面中直接展示。 在部分场景(如社交类小程序)中,开发者需要在获取用户的头像昵称信息,可调用wx.getUserProfile接口,开发者每次通过该接口均需用户确认,请开发者妥善处理调用接口的时机,避免过度弹出弹窗骚扰用户。 微信团队 2021年4月15日
2021-04-15 - wx.onGetWifiList在ios中不进入回调
这里需要注意: 1、Wi-Fi 相关接口的调用时序,参考:https://developers.weixin.qq.com/miniprogram/dev/framework/device/wifi.html 2、需要跳转到系统 WiFi 列表,等到列表刷新出 WiFi,在微信前台的小程序才会收到 onGetWifiList 的回调;这是苹果系统的限制,暂时无法规避
2019-09-24 - 微信证件OCR小程序应用案例分享
应用场景 身份证/银行卡识别 用户身份认证 - 应用于政务、金融、企业服务等应用下的远程用户身份认证,自动识别并录入各字段信息,降低用户输入 成本,有效提升用户体验。 商户身份核验 - 应用于电商、外卖、运输服务等场景下的商户身份认证、资质文件审核,提高平台服务质量,规避恶意违规等业务风险。 身份证识别: [图片] 银行卡识别: [图片] 营业执照识别 企业信息电子化存档 - 准确识别营业执照的关键字段,快速核验企业资质,完成企业信息的快速录入,提升企业信息化管理水平,有效节约人力成本 。 商家资质审查 - 通过对供应商企业信息的结构化识别和审核核验,提高合作伙伴的管理效率 银行金融信贷服务 - 适用于企业银行开户、信贷评估等金融服务场景,通过对企业信息的自动化审查,提升银行业务效率,有效控制业务风险。 [图片] 驾驶证/行驶证识别 车主身份认证 - 只需拍照即可快速上传本人证件信息,帮助车主快速完成身份认证,降低车主输入成本,广泛应用于 ETC 办理、打车、租车、车险投保理赔等场景。 车主信息服务 - 在汽车保险理赔、二手车交易、车辆租借和年审等场景,帮助用户快速录入车辆相关信息,提高业务人员的办公效率和服务准确性。 [图片] 通用印刷体识别 纸质文档电子化- 使用通用文字识别技术,助您完成大量的文档整理工作,从书籍、纸质论文、档案、PPT 课件等印刷资料,到课堂笔记、作业作文等手写内容,均可实现拍照自动识别文字,方便用户进行文本录入和文档管理,提高产品易用性和用户体验。 [图片] 补充:微信证件OCR的接口文档 银行卡识别 营业执照识别 驾驶证识别 身份证识别 通用印刷体识别 行驶证识别 服务平台 OCR 接口
2020-10-10 - 小程序页面通信、数据刷新、事件总线 、event bus 终极解决方案之 iny-bus
#### 背景介绍 在各种小程序中,我们经常会遇到 这种情况 有一个 列表,点击列表中的一项进入详情,详情有个按钮,删除了这一项,这个时候当用户返回到列表页时, 发现列表中的这一项依然存在,这种情况,就是一个 `bug`,也就是数据不同步问题,这个时候测试小姐姐 肯定会找你,让你解决,这个时候,你也许会很快速的解决,但过一会儿,测试小姐姐又来找你说,我打开了 四五个页面更改了用户状态,但我一层一层返回到首页,发现有好几个页面数据没有刷新,也是一个 bug, 这个时候你就犯愁了,怎么解决,常规方法有下面几种 #### 解决方法 1. 将所有请求放到 生命周期 `onShow` 中,只要我们页面重新显示,就会重新请求,数据也会刷新 2. 通过用 `getCurrentPages` 获取页面栈,然后找到对应的 页面实例,调用实例方法,去刷新数据 3. 通过设置一个全局变量,例如 App.globalData.xxx,通过改变这个变量的值,然后在对应 onShow 中检查,如果值已改变,刷新数据 4. 在打开详情页时,使用 redirectTo 而不是 navigateTo,这样在打开新的页面时,会销毁当前页面, 返回时就不会回到这个里面,自然也不会有数据不同步问题 #### 存在的问题 1. 假如我们将 所有 请求放到 onShow 生命周期中,自然能解决所有数据刷新问题,但是 onShow 这个生命周期,有两个问题 第一个问题,它其实是在 onLoad 后面执行的,也就是说,假如请求耗时相同,从它发起请求到页面渲染, 会比 onLoad 慢 第二个问题,那就是页面隐藏、调用微信分享、锁频等等都会触发执行,请求放置于 `onShow` 中就会造成 大量不需要的请求,造成服务器压力,多余的资源浪费、也会造成用户体验不好的问题 2. 通过 `getCurrentPages` 获取页面栈,然后找到对应的 页面实例,调用实例方法,去刷新数据,这也 不失为一个办法,但是就如微信官方文档所说 > 不要尝试修改页面栈,会导致路由以及页面状态错误。 > 不要在 App.onLaunch 的时候调用 `getCurrentPages()`,此时 page 还没有生成。 同时、当需要通信的页面有两个、三个、多个呢,这里去使用 `getCurrentPages` 就会比较困难、繁琐 3. 通过设置全局变量的方法,当需要使用的地方比较少时,可以接受,当使用的地方多的时候,维护起来 就会很困难,代码过于臃肿,也会有很多问题 4. 使用 redirectTo 而不是 navigateTo,从用来体验来说,很糟糕,并且只存在一个页面,对于 tab 页面,它也无能为力,不推荐使用 #### 最佳实践 在 Vue 中, 可以通过 new Vue() 来实现一个 event bus作为事件总线,来达到事件通知的功能,在各大 框架中,也有自身的事件机制实现,那么我们完全可以通过同样的方法,实现一个事件中心,来管理我们的事件, 同时,解决我们的问题。iny-bus 就是这样一个及其轻量的事件库,使用 typescript 编写,100% 测试覆 盖率,能运行 js 的环境,就能使用 传送门 [源码](https://github.com/landluck/iny-bus) [NPM](https://www.npmjs.com/package/iny-bus) [文档](https://landluck.github.io/iny-bus/docs/) #### 简单使用 iny-bus 使用及其简单,在需要的页面 onLoad 中添加事件监听, 在需要触发事件的地方派发事件,使监 听该事件的每个页面执行处理函数,达到通信和刷新数据的目的,在小程序中的使用可以参考以下代码 [代码] // 小程序[代码] [代码] import bus from [代码][代码]'iny-bus'[代码] [代码] // 添加事件监听[代码] [代码] // 在 onLoad 中注册, 避免在 onShow 中使用[代码] [代码] onLoad () {[代码] [代码] this[代码][代码].eventId = bus.on([代码][代码]'事件名'[代码][代码], (a, b, c, d) => {[代码] [代码] // 支持多参数[代码] [代码] console.log(a, b, c, d)[代码] [代码] this[代码][代码].setData({ a [代码]}) [代码] // 调用页面请求函数,刷新数据[代码] [代码] this[代码][代码].refreshPageData()[代码] [代码] })[代码] [代码] // 添加只需要执行一次的 事件监听[代码] [代码] this[代码][代码].eventIdOnce = bus.once([代码][代码]'事件名'[代码][代码], () => {[代码] [代码] // do some thing[代码] [代码] })[代码] [代码] }[代码] [代码] // 移除事件监听,该函数有两个参数,第二个事件id不传,会移除整个事件监听,传入ID,会移除该[代码] [代码] // 页面的事件监听,避免多余资源浪费, 在添加事件监[代码][代码]/// 听后,页面卸载(onUnload)时建议移除[代码] [代码] onUnload () {[代码] [代码] bus.remove([代码][代码]'事件名'[代码][代码], [代码][代码]this[代码][代码].eventId)[代码] [代码] }[代码] [代码] // 派发事件,触发事件监听处更新视图[代码] [代码] // 支持多参传递[代码] [代码] onClick () {[代码] [代码] bus.emit([代码][代码]'事件名'[代码][代码], a, b, c)[代码] [代码] }[代码] 更详细的使用和例子可以参考 [Github iny-bus 小程序代码](https://github.com/landluck/iny-bus/tree/master/examples) #### iny-bus 具体实现 * 基本打包工具,这里使用非常优秀的开源库 [typescript-library-starter](https://github.com/alexjoverm/typescript-library-starter),具体细节不展开 * 测试工具 使用 facebook 的 [jest](https://github.com/facebook/jest) * build ci 使用 [travis-ci](https://www.travis-ci.org/) * 测试覆盖率上传使用 [codecov](https://codecov.io/) * 具体的其他细节大家可以看源码中的 [package.json](https://github.com/landluck/iny-bus/blob/master/package.json),这里就一一展开讲了 iny-bus 的核心代码,其实就这么多,总的来说,非常少,但是能解决我们在小程序中遇到的大量 通信 和 数据刷新问题,是采用 各大平台小程序 原生开发时,页面通信的不二之选,同时,100% 的测试覆盖率,确保了 iny-bus 在使用中的稳定性和安全性,当然,每个库都是从简单走向复杂,功能慢慢完善,如果 大家在使用或者源码中发现了bug或者可以优化的点,欢迎大家提 pr 或者直接联系我 最后,如果 iny-bus 给你提供了帮助或者让你有任何收获,请给 作者 点个赞,感谢大家 [点赞](https://github.com/landluck/iny-bus)
2019-08-04 - 图片报403?
由秀米富文本编辑器,编辑好的内容,在小程序中图片不能正常显示 [图片]
2020-06-11 - iphone真机上部分图片无法显示?模拟器上可以,安卓可以
https://cdn2db.jys.site/file/png/202009/7747ad42d8563184f1327212e418b88b2ffoGKniCB.png 图片路径
2020-09-03 - 自定义TabBar注意事项
官方文档对于自定义的TabBar描述太少了,只有个代码片段可以参考,试了半天才成功,特地记录下。 自定义TabBar必须放在代码根目录下,文件夹的名字必须是custom-tab-bar,否则tabbar不显示。不需要在当前page的json或者app.json里引用custom-tab-bar的组件,小程序会自动识别。如果按照自定义组件形式在json里的using-component里引用,会导致this.getTabBar()失效,我也不知道为什么。文档地址 https://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html 如有别的问题,欢迎补充。
2020-06-19 - 自定义tabbar 【恋爱小清单开发总结】
看官方demo的小伙伴知道,自定义tabbar需要在小程序根目录底下建一个名叫custom-tab-bar的组件(我有试过,如果放在components目录里面小程序会识别不了),目前我自己实现的效果是:通过在配置可以切换tab,也可以点击tab后重定向到新页面,支持隐藏tabbar,同时也可以显示右上角文本和小红点。 官方demo里面用的是cover-view,我改成view,因为如果页面有弹窗的话我希望可以盖住tabbar 总结一下有以下注意点: 1、tabbar组件的目录命名需要是custom-tab-bar 2、app.json增加自定义tabbar配置 3、wx.navigateTo不允许跳转到tabb页面 4、进入tab页面时,需要调用tabbar.js手动切换tab 效果图: [图片] 可以扫码体验 [图片] 代码目录如下: [图片] 代码如下: app.json增加自定义tabbar配置 "tabBar": { "custom": true, "color": "#7A7E83", "selectedColor": "#3cc51f", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/love/love", "text": "首页" }, { "pagePath": "pages/tabbar/empty", "text": "礼物说" }, { "pagePath": "pages/tabbar/empty", "text": "恋人圈" }, { "pagePath": "pages/me/me", "text": "我" } ] }, 自定义tabbar组件代码如下 index.js //api.js是我自己对微信接口的一些封装 const api = require('../utils/api.js'); //获取应用实例 const app = getApp(); Component({ data: { isPhoneX: false, selected: 0, hide: false, list: [{ showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/love/love", iconPath: "/images/tabbar/home.png", selectedIconPath: "/images/tabbar/home-select.png", text: "首页" }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/gifts/giftList", iconPath: "/images/tabbar/gift.png", selectedIconPath: "/images/tabbar/gift-select.png", text: "礼物说", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/moments/moments", iconPath: "/images/tabbar/lover-circle.png", selectedIconPath: "/images/tabbar/lover-circle-select.png", text: "恋人圈", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/me/me", iconPath: "/images/tabbar/me.png", selectedIconPath: "/images/tabbar/me-select.png", text: "我" }] }, ready() { // console.error("custom-tab-bar ready"); this.setData({ isPhoneX: app.globalData.device.isPhoneX }) }, methods: { switchTab(e) { const data = e.currentTarget.dataset; console.log("tabBar参数:", data); api.vibrateShort(); if (data.hideTabBar) { api.navigateTo(data.navigatePath); } else { /*this.setData({ selected: data.index }, function () { wx.switchTab({url: data.path}); });*/ /** * 改为直接跳转页面, * 因为发现如果先设置selected的话, * 对应tab图标会先选中,然后页面再跳转, * 会出现图标变成未选中然后马上选中的过程 */ wx.switchTab({url: data.path}); } }, /** * 显示tabbar * @param e */ showTab(e){ this.setData({ hide: false }, function () { console.log("showTab执行完毕"); }); }, /** * 隐藏tabbar * @param e */ hideTab(e){ this.setData({ hide: true }, function () { console.log("hideTab执行完毕"); }); }, /** * 显示小红点 * @param index */ showRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = true; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏小红点 * @param index */ hideRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = false; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 显示tab右上角文本 * @param index * @param text */ showBadge(index, text, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: true, badgeText: text}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏tab右上角文本 * @param index */ hideBadge(index, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: false, badgeText: ""}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } } } }); index.html <view class="footer-tool-bar flex-center {{isPhoneX? 'phx_68':''}}" hidden="{{hide}}"> <view class="tab flex-full {{selected === index ? 'focus':''}}" wx:for="{{list}}" wx:key="index" data-path="{{item.pagePath}}" data-index="{{index}}" data-navigate-path="{{item.navigatePath}}" data-hide-tab-bar="{{item.hideTabBar}}" data-open-ext-mini-program="{{item.openExtMiniProgram}}" data-ext-mini-program-app-id="{{item.extMiniProgramAppId}}" bindtap="switchTab"> <view class="text"> <view class="dot" wx:if="{{item.showRedDot}}"></view> <view class="badge" wx:if="{{item.showBadge}}">{{item.badgeText}}</view> <image class="icon" src="{{item.selectedIconPath}}" hidden="{{selected !== index}}"></image> <image class="icon" src="{{item.iconPath}}" hidden="{{selected === index}}"></image> </view> </view> </view> index.json { "component": true, "usingComponents": {} } index.wxss @import "/app.wxss"; .footer-tool-bar{ background-color: #fff; height: 100rpx; width: 100%; position: fixed; bottom: 0; z-index: 100; text-align: center; font-size: 24rpx; transition: transform .3s; border-radius: 30rpx 30rpx 0 0; /*padding-bottom: env(safe-area-inset-bottom);*/ box-shadow:0rpx 0rpx 18rpx 8rpx rgba(212, 210, 211, 0.35); } .footer-tool-bar .tab{ color: #242424; height: 100%; line-height: 100rpx; } .footer-tool-bar .focus{ color: #f96e49; font-weight: 500; } .footer-tool-bar .icon{ width: 44rpx; height: 44rpx; margin: 18rpx auto; } .footer-tool-bar .text{ line-height: 80rpx; height: 80rpx; position: relative; display: inline-block; padding: 0rpx 40rpx; box-sizing: border-box; margin: 10rpx auto; } .footer-tool-bar .dot{ position: absolute; top: 16rpx; right: 16rpx; height: 16rpx; width: 16rpx; border-radius: 50%; background-color: #f45551; } .footer-tool-bar .badge{ position: absolute; top: 8rpx; right: 8rpx; height: 30rpx; width: 30rpx; line-height: 30rpx; border-radius: 50%; background-color: #f45551; color: #fff; text-align: center; font-size: 20rpx; font-weight: 450; } .hide{ transform: translateY(100%); } app.wxss(这里的样式文件是我用来存放一些公共样式) /**app.wxss**/ page { background-color: #f5f5f5; height: 100%; -webkit-overflow-scrolling: touch; } .container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box; } .blur { filter: blur(80rpx); opacity: 0.65; } .flex-center { display: flex; align-items: center; justify-content: center; } .flex-column { display: flex; /*垂直居中*/ align-items: center; /*水平居中*/ justify-content: center; flex-direction: column; } .flex-start-horizontal{ display: flex; justify-content: flex-start; } .flex-end-horizontal{ display: flex; justify-content: flex-end; } .flex-start-vertical{ display: flex; align-items: flex-start; } .flex-end-vertical{ display: flex; align-items: flex-end; } .flex-wrap { display: flex; flex-wrap: wrap; } .flex-full { flex: 1; } .reset-btn:after { border: none; } .reset-btn { background-color: #ffffff; border-radius: 0; margin: 0; padding: 0; overflow: auto; } .loading{ opacity: 0; transition: opacity 1s; } .load-over{ opacity: 1; } .phx_68{ padding-bottom: 68rpx; } .phx_34{ padding-bottom: 34rpx; } 另外我还对tabbar的操作做了简单的封装: tabbar.js const api = require('/api.js'); /** * 切换tab * @param me * @param index */ const switchTab = function (me, index) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { console.log("切换tab:", index); me.getTabBar().setData({ selected: index }) } }; /** * 显示 tabBar 某一项的右上角的红点 * @param me * @param index */ const showRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showRedDot(index, success, fail); } }; /** * 隐藏 tabBar 某一项的右上角的红点 * @param me * @param index */ const hideRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideRedDot(index, success, fail); } }; /** * 显示tab右上角文本 * @param me * @param index * @param text */ const showBadge = function (me, index, text, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showBadge(index, text, success, fail); } }; /** * 隐藏tab右上角文本 * @param me * @param index */ const hideBadge = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideBadge(index, success, fail); } }; /** * 显示tabbar * @param me * @param success */ const showTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showTab(success); } }; /** * 隐藏tabbar * @param me * @param success */ const hideTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideTab(success); } }; module.exports = { switchTab, showRedDot, hideRedDot, showBadge, hideBadge, showTab, hideTab }; 最后,进入到tab对应页面的时候要手动调用一下swichTab接口,然tabbar聚焦到当前tab /** * 生命周期函数--监听页面显示 */ onShow: function () { tabbar.switchTab(this, this.data.tabIndex);//tabIndex是当前tab的索引 }
2021-11-09 - canvas生成图片在安卓手机上画的不全,缺失东西,ios下完好?
[图片] 这是系统的原因吗? 还是代码有问题
2020-09-24 - 未完成微信认证的组织类型小程序怎么注销?
目前小程序组织类型的帐号仅支持有对公账户的注销,用户需要继续完成认证后操作注销。 更多小程序注销相关内容(包含个人类型、组织类型、政府无对公账户类型)可参考:https://kf.qq.com/faq/181226yieaEv181226r6nuMr.html
2019-10-25 - 小程序主体公司注销了,是否支持注销小程序?
目前小程序企业类型的帐号仅支持有对公账户的注销。
2019-08-29 - 小程序注销问题汇总
1、注销成功后,流程不可逆? 帐号一旦成功注销,流程不可逆。 2、小程序注销过程中,是否可以撤销注销,恢复使用? 冻结期间可登录小程序后台,点击“取消注销”,可恢复帐号正常使用。 3、组织类型如何修改打款信息? 在发起注销申请后,需自行填写帐号主体正确的对公账户信息。若您不小心填写错,建议等待验证失败或者超时未验证后再次重新填写。 4、组织类型支付验证打款后,多久验证成功? 若打款账户信息和金额正确,1个工作日内会验证成功。 5、小额打款验证成功后,退款到账时间多久? 验证成功后,打款金额退回具体到账时间视银行而定,一般为3-10个工作日内,原路退回。 6、管理员手机端的注销帐号确认,不小心删除,是否支持补发? 不支持。目前管理员确认注销有效期为7天,且每天都会下发提醒,建议您留意下一次的消息提醒。 注:若已确认,将不会再下发消息提醒。 7、注销流程中是否会通知小程序项目成员? 在进入冻结期、取消注销、注销成功时会下发模板消息通知小程序项目成员。 8、小程序注销成功后已关联该小程序的的公众号是否会收到通知? 在冻结期、注销成功时会下发模板消息通知关联该小程序的公众号管理员。 9、小程序冻结期间,用户可以正常访问吗? 已发布小游戏冻结期间用户可正常访问小游戏,每日首次访问,弹窗提示用户该小游戏即将下架;其余类型小程序用户无法访问。 10、已发布的小游戏,在注销期间,虚拟支付如何结算? 虚拟支付延期30天结算。 11、已发布的小游戏,在注销期间,用户可以进行支付吗? 不可以。 12、小程序注销条件是什么? ①小程序必须是已注册成功的帐号。 ②已开通广告主服务的小程序广告投放账户余额须为零。 ③须自主暂停线上小程序版本服务(除已发布小游戏帐号外)。 温馨提示: 海外小程序不支持注销。 小程序帐号内已发布插件线上版本的不支持注销。 13、小程序注销冻结期内,帐号还可以使用吗? 普通小程序、未发布的小游戏: 冻结期7天,帐号所有功能不可用,资源仍为占用 已发布的小游戏: ①冻结期为30天,资源仍为占用 ②已发布的小游戏冻结期内部分功能可用,但需关闭充值功能 ③虚拟支付延期30天结算 温馨提示:冻结期间可登录小程序后台,点击“取消注销”,可恢复帐号正常使用。 14、注销成功后可以释放哪些资源? ①管理员确认注销后,立即释放以下资源: 绑定邮箱、主体名称、管理员信息(姓名、身份证号、手机号码、微信号)、项目成员信息、关联关系。 ②成功注销后,原来的昵称有2*24小时(即2天)的保护期,在此期间,符合命名唯一规则情况下,同一主体下的其他帐号可以使用该名称,主体不一致的,则需要在保护期满后才能申请使用该名称。 温馨提示:冻结期内,资源仍处于占用状态
2020-04-23 - 公众号已冻结,管理员如何解绑?
1、个人主体公众号不支持单方面解绑管理员,建议进行帐号找回后再申请注销,则会释放管理员信息。 找回帐号流程参考:https://kf.qq.com/faq/161221y2uIZV161221Zjm6ZB.html 注销流程参考:https://kf.qq.com/faq/180306JVvii2180306RvmEzy.html 2、企业主体需要解绑管理员请前往微信关注公众平台安全助手,在“绑定查询-微信号绑定帐号” 这里就有解绑的入口。(此方法仅限于非封禁状态的公众号使用)
2020-04-23 - 【笔记】解决用户头像过期无法显示问题
小根据官方规则,用户如果修改了头像,那么一段时间之后,用户原始的头像链接会失效。而因为我们一般用户资料储存的时候只储存了链接,就会造成失效,因此需要把用户头像转换成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 - 如何实现一个自定义数据版省市区二级、三级联动
社区可能有其他的方案了,但是再分享下吧,给有需要的童鞋。 效果图: [图片] 额,这个视频转GIF因为社区上传不了大图,所以剪了一部分,具体的效果还是直接工具打开代码片段预览吧~ 第一步:你的页面JSON引入该组件: [代码]{ "usingComponents": { "city-picker": "/components/cityPicker/index" } } [代码] 第二步:你的页面WXML引入该组件 [代码]<city-picker visible="{{visible}}" column="2" bind:close="handleClick" bind:confirm="handleConfirm" /> [代码] 第三步:你的页面JS调用 [代码]// 显示/隐藏picker选择器 handleClick() { this.setData( visible: !this.data.visible }) }, // 用户选择城市后 点击确定的返回值 handleConfirm(e) { const { detail: { provinceName = '', provinceId = '', cityName, cityId='', areaName = '', areaId = '' } = {} } = e this.setData({ cityId, cityName, areaId, areaName, provinceId, provinceName }) } [代码] 组件属性 属性 默认值 描述 visible false 是否显示picker选择器 column 3 显示几列,可选值:1,2,3 values [0, 0, 0] 必填,默认回填的省市区下标,可选择具体省市区后查看AppData的regionValue字段 close function 点击关闭picker弹窗 confirm function 点击选择器的确定返回值 confirm: 属性 默认值 描述 provinceName 北京市 省份名称 provinceId 110000 省份ID cityName 市辖区 城市名称 cityId 110100 城市ID areaName 东城区 区域名称 areaId 110000 区域Id 至于怎么获取你想默认城市的下标,可以滑动操作下选中省市区后,点击确定后查看appData里的regionValue的值。 以上就是一个自定义数据版本的省市区二级、三级联动啦,老规矩,结尾放代码片段。 https://developers.weixin.qq.com/s/F9k9cTmT7LAz
2022-07-20