- [bug反馈]wxml 中 style 指向的文件路径 在不同设备中解析不同
// 模拟器中有效, 真机无效 style="background-image: url('/images/testUseCard.png');" // 真机中有效, 模拟器无效 style="background-image: url('images/testUseCard.png');"
2021-03-12 - 【uniapp】在 uni-popup 弹出层中创建camera,在camera上覆盖cover-image,失败!
create at 2021年10月11日 19:13:04 modify at 2021.10.12 项目一直使用uniapp开发的,我接手后也只能使用这套方案; 最近接到一个新功能,就是全屏拍摄,类似微信中的拍摄功能;此功能强制只能拍摄,不能从相册取图片。 虽然 wx.chooseImage(Object object) 可以选择从相册或者从摄像头取图片,但是,此API在有些手机上失效;所以,弃用此方案。 一开始我选择使用 uni-popup ,弹出层;通过样式将其设置为全屏,然后在这个弹出层中打开摄像头;遇到一些问题,都依次解决了;但最终还是放弃此方案了(折腾了很久,可能因为uni-popup中的样式问题吧,图标显示不出来) 这个是 camera 组件,拍摄使用 wx.createCameraContext;另外, cover-view , cover-image 是用来在拍摄层上添加按钮或图标的; (1)cover-view 不显示,添加 z-index 可以解决; (2)没法关闭摄像头,uni-popup 可以 close,但是弹出层关闭了,摄像头还在;解决办法是使用data变量来控制摄像头的显示与隐藏,[代码]<camera v-if="flagShowCamera"[代码] 弹出层关闭时,将 [代码]flagShowCamera[代码] 设为 false; (3)cover-image 不显示,这个问题折腾了很久,图片总是不显示;没办法只能放弃 uni-popup 弹出层方案。 于是,我选择创建一个新的page页面,来做全屏拍摄;使用一个单独page页面,上面的问题就都没有了(这也表示:uni-popup 确实会影响 camera 等微信组件的正常使用)。但又遇到新的问题 拍摄后会得到一个 [代码]wxfile://tmp_78b87cad87ad27beab102881a9dc68d0.jpg[代码] 这样的地址,但是返回到上一页后,这个地址就失效了;仿佛这个地址只在摄像头(camera)没有销毁时存在。 解决办法好像只能是:在返回上一页前,将图片上传到服务器。但是,结合当前业务,这样会产生很多冗余的图片。 根据上面的推测,我使用wx.saveFile 将图片保存到本地(结果发现,只是一场乌龙) [代码]wxfile://tmp_78b87cad87ad27beab102881a9dc68d0.jpg[代码] 可以直接赋值给 [代码]image[代码] 的 [代码]src[代码] 属性的,也不存在 camera 销毁,临时图片就消失的现象;上面几处描述是错误的,已经添加删除线。 因为程序中的逻辑不严谨,导致低级错误,我一开始的怀疑都是错的; 也不需要 wx.saveFile 来转存到本地,直接拍摄后将得到的临时文件赋值给 image 的 src 属性就可以显示出刚刚拍摄的图片了。 至此,全屏拍摄就完成了,以上是开发过程的记录。字节跳动的双色图标是真的好用,推荐下。
2021-10-12 - 生成分享朋友圈微海报
[图片] 描码体验 文未有小程序源码地址 (另有偿提供后台数据接口服务) [图片] 模板代码参考 [代码] <view class='padding text-center'> <image src="{{shareImage}}" mode="widthFix"></image> <canvasdrawer :painting.sync="painting" @getImage.user="eventGetImage"></canvasdrawer> </view> <view class="padding"> <button wx:if="{{shareImage}}" class='cu-btn block bg-red margin-tb-sm lg' @tap="eventSave">保存分享图</button> </view> [代码] 部分js代码 [代码]event = { getImage (e) { wepy.showToast({ title: e, icon: "success", duration: 2000 }); } } async buildPoster(){ if(this.shareImage == ''){ wepy.showLoading({ title: "生成中", mask: true }); let poster = await commApi.GetArticlePoster(this.id) this.painting = poster } this.showposter = true; this.$apply(); } methods = { async eventSave() { // 保存图片至本地 const res = await wepy.saveImageToPhotosAlbum({ filePath: this.shareImage }); if (res.errMsg === "saveImageToPhotosAlbum:ok") { wepy.showToast({ title: "分享图已保存到相册", icon: "success", duration: 2000 }); } }, eventGetImage(event) { wepy.hideLoading(); const { tempFilePath, errMsg } = event; if (errMsg === "canvasdrawer:ok"){ this.shareImage = tempFilePath; }else{ wepy.showToast({ title: errMsg, icon: "success", duration: 2000 }); } }, } [代码] 后端php代码 [代码]public function defalutArticlePoster($app, $user, $article){ // $title_length = Str::length($article->title,'UTF-8'); // dd($title_length); $poster = [ 'width'=>460, 'height'=> 500, 'clear'=> true, 'views'=>[ [ 'type'=> 'rect', 'background'=> '#666', 'top'=> 0, 'width'=> 460, 'height'=> 500, 'left'=> 0 ], [ 'type'=> 'rect', 'background'=> '#ffffff', 'top'=> 2, 'width'=> 456, 'height'=> 496, 'left'=> 2 ], [ 'type'=> 'image', 'url'=> $article->cover? $article->cover:'https://wx1.wechatrank.com/base64img/20190402233111.jpeg', 'top'=> 70, 'left'=> 28, 'width'=> 400, 'height'=> 320 ], [ 'type'=> 'text', 'content'=> $article->title, 'fontSize'=> 18, 'lineHeight'=> 24, 'color'=> '#333', 'textAlign'=> 'left', 'top'=> $title_length>22?16:26, 'left'=> 28, 'width'=> 387, 'MaxLineNumber'=> 2, 'breakWord'=> true, 'bolder'=> true ], [ 'type'=> 'text', 'content'=> str_replace(array(" ", " ", "\t", "\n", "\r", "\r\n", PHP_EOL), '', $article->intro), 'fontSize'=> 18, 'lineHeight'=> 24, 'color'=> '#666', 'textAlign'=> 'left', 'top'=> 406, 'left'=> 28, 'width'=> 310, 'MaxLineNumber'=> 3, 'breakWord'=> true, 'bolder'=> true ], [ 'type'=> 'image', 'url'=> url('/qrcode/article/'.$article->qrcode), 'top'=> 406, 'left'=> 360, 'width'=> 68, 'height'=> 68 ] ], ]; return $poster; } [代码] 示例代码来源: https://github.com/yizenghui/wxcms/blob/master/src/pages/poster/index.wpy 项目地址: https://github.com/yizenghui/wxcms https://github.com/simmzl/wepy_canvas_drawer 觉得对您有帮助请点个赞,谢谢
2019-05-27 - [开盖即食]小程序Canvas官方新版API实战
[图片] [图片] 最近本人在开发一个新项目的时候,注意到官方在2.9.0开始支持了一个canvas 2D的新API,同时对webGL上支持也有了很大的改进,相信很多人用canvas的组件做一些分享海报,战绩和新闻帖功能。 [图片] 这里是新的引入方式。 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html 那么新的canvas2D API有啥好处呢? 原本的API微信有做一定的修改,现在全面支持源生H5 JS的写法,迁移H5的老代码变成更加容易,学习成本更低 修复了一些诡异的BUG,例如原本在IOS早期版本写法顺序会导致clip()图片裁切失效等~ 性能上的优化和提升,复杂动画上帧数明显 举例写法上的一些改变: 1、设置font的写法: [代码]//原本(传值的写法) ctx.setFontSize(20); ctx.fillText('MINA', 100, 100) ctx.draw() //现在(和源生H5写法一致,赋值) ctx.font = "16px"; ctx.fillStyle = 'blue'; //可以直接写颜色,原本的不支持 //不需要 ctx.draw() [代码] 2、获取并添加图片写法: [代码]//原本 //使用的是 wx.getImageInfo的方法 wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { console.log(res); ctx.drawImage(res.path, 0, 0); ctx.draw(true); }, fail: function (res) { //失败回调 } }); //现在 //可以直接img.onload调用 const headerImg = canvas.createImage(); headerImg.src = headImage;//微信请求返回头像 headerImg.onload = () => { ctx.save(); ctx.beginPath()//开始创建一个路径 ctx.arc(38, 288, 18, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.drawImage(headerImg,0,0); ctx.closePath(); ctx.restore(); } [代码] 3、将canvas生成虚拟地址便于下载(重点): [图片] 由于官方文档没有写清楚,误导了挺多人的。这里canvas对象必须通过选择器获取,并获得对应的node节点。 [代码]async saveImg() { let self = this; //这里是重点 新版本的type 2d 获取方法 const query = wx.createSelectorQuery(); const canvasObj = await new Promise((resolve, reject) => { query.select('#posterCanvas') .fields({ node: true, size: true }) .exec(async (res) => { resolve(res[0].node); }) }); console.log(canvasObj); wx.canvasToTempFilePath({ //fileType: 'jpg', //canvasId: 'posterCanvas', //之前的写法 canvas: canvasObj, //现在的写法 success: (res) => { console.log(res); self.setData({ canClose: true }); //保存图片 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '已保存到相册', icon: 'success', duration: 2000 }) // setTimeout(() => { // self.setData({show: false}) // }, 6000); }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") } else { util.showToast("请截屏保存分享"); } }, complete(res) { wx.hideLoading(); console.log(res); } }) }, fail(res) { console.log(res); } }, this) }, [代码] 分享个canvas海报的代码片段: [图片] 片段名: PoCf4emw7TgE 片段link: https://developers.weixin.qq.com/s/PoCf4emw7TgE [图片] [图片] 总结,相对之前还要看官方文档的canvas自定义API,现在写起来更加的方便,老代码迁移起来得心应手,只要你之前会canvas,那么各种效果和动画,拿来就怼,没什么大问题~ 一些奇怪的问题(注意!!!) canvas 2d 目前(2020年4月3日)还不支持真机调试,会报错!!! IDE工具 1.02.2003190 直接保存新版本canvas的API图片是打不开的,但是直接用手机保存在相册是没问题的,请更新到1.02.2003250 最新版即可解决~ 一些老款手机用新的API保存图片会有报错问题,如华为NOTE10,请更新系统到能支持的最新,且微信也是,即可解决~ 部分Android设备诡异的闪退和报错 [图片] 这种有可能是代码写法的问题,比如: [代码]//缺省写法 会导致部分Android机器 闪退 ctx.font = "bold 16px"; ctx.fillStyle = "#000" //在canvas 2D的写法中,所以写法必须规范且完整 ctx.font = "normal bold 12px sans-serif"; ctx.fillStyle = '#707070'; [代码] 所以在canvas 2D 的环境,所以写法必须原始且规范,不能用缺省写法,不然就会有诡异的闪退/报错。 后续:官方在7.0.13的Android版本已修复。 https://developers.weixin.qq.com/community/develop/doc/00088c13e1437890692afd8d85ec00 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2020-05-09 - Failed to execute 'drawImage' on 'CanvasRenderin ?
Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(CSSImageValue or HTMLImageElement or SVGImageElement or HTMLVideoElement or HTMLCanvasElement or ImageBitmap or OffscreenCanvas)';at SelectorQuery callback function TypeError: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(CSSImageValue or HTMLImageElement or SVGImageElement or HTMLVideoElement or HTMLCanvasElement or ImageBitmap or OffscreenCanvas)'
2020-01-10 - onShow
小程序第一次启动home 页面,数据加载成功 onshow 方法没走 onShow: function() { var that = this; wx.onUserCaptureScreen(function(res) { var screenshotlog = wx.getStorageSync("screenshotlog") console.log("======================onUserCaptureScreen", screenshotlog + "========"); if (screenshotlog) { that.post(); } }); }, 日志没有打印出来
2019-04-19 - 微信小程序之SelectorQuery
在开发小程序展开全文组件时需要用到节点查询API - [代码]wx.createSelectorQuery()[代码] 来查询全文内容的高度。 [图片] [代码]wx.createSelectorQuery()[代码] 返回一个 [代码]SelectorQuery[代码] 对象实例。 [代码]SelectorQuery[代码] 有五个方法([代码]in[代码],[代码]select[代码],[代码]selectAll[代码],[代码]selectViewport[代码],[代码]exec[代码]),第一个返回 [代码]SelectorQuery[代码],后四个返回 [代码]NodesRef[代码]。 [代码]NodesRef[代码] 有四个方法([代码]fields[代码],[代码]boundingClientRect[代码],[代码]scrollOffset[代码],[代码]context[代码]),第一个返回 [代码]NodesRef[代码],后三个返回 [代码]SelectorQuery[代码]。 对照官方提供的示例代码来看 [代码]const query = wx.createSelectorQuery() // query 是 SelectorQuery 对象 query.select('#the-id').boundingClientRect() // select 后是 NodesRef 对象,然后 boundingClientRect 返回 SelectorQuery 对象 query.selectViewport().scrollOffset() // selectViewport 后是 NodesRef 对象,然后 scrollOffset 返回 SelectorQuery 对象 query.exec(function (res) { // exec 返回 NodesRef 对象 res[0].top // #the-id节点的上边界坐标 res[1].scrollTop // 显示区域的竖直滚动位置 }) [代码] 问题:每行执行返回的 [代码]SelectorQuery[代码] 对象是相同的吗? 答案:是的,都是同一个对象。 问题:直接执行 [代码]query.select('#the-id').boundingClientRect().exec[代码] 也可以吗? 答案:可以,[代码]boundingClientRect()[代码] 返回就是 [代码]query[代码]。 问题:这样连写 [代码]query.select('#the-id').boundingClientRect().selectViewport().scrollOffset()[代码] 算两条查询请求吗? 答案:是两条请求。 问题:[代码]query.exec[代码] 执行后会清空前面的查询请求吗?再次执行还能拿到结果吗? 答案:可以,[代码]query[代码] 不会清空请求。 问题:[代码]boundingClientRect[代码] 和 [代码]scrollOffset[代码] 可以接受 [代码]callback[代码] 参数,它与 [代码]query.exec[代码] 执行顺序是怎样,修改 [代码]res[代码] 结果会影响到后面的 [代码]callback[代码] 吗? 答案:先执行 [代码]boundingClientRect[代码] 和 [代码]scrollOffset[代码] 的 [代码]callback[代码],再执行 [代码]query.exec[代码] 的 [代码]callback[代码];修改 [代码]res[代码] 结果会影响到后面 [代码]exec[代码] 的结果。 上面的问题通过小程序开发者工具中的 [代码]WAService.js[代码] 源码简单美化还原后可以了解 [代码]SelectorQuery[代码] 的代码逻辑 SelectorQuery.js [代码]import NodesRef from 'NodesRef'; import requestComponentInfo from 'requestComponentInfo'; export default function(pluginId) { return class SelectorQuery { constructor(plugin) { if (plugin && plugin.page) { this._component = this._defaultComponent = plugin.page; this._webviewId = this._defaultComponent.__wxWebviewId__; } else { var pages = __internalGlobal__.getCurrentPagesByDomain(''); this._defaultComponent = pages[pages.length - 1], this._component = null; this._webviewId = null; } this._queue = []; this._queueCb = []; } in(component) { if (!this._webviewId) { this._webviewId = component.__wxWebviewId__; this._component = component; } else if (this._webviewId !== component.__wxWebviewId__) { console.error('A single SelectorQuery could not work in components in different pages. A SelectorQuery#in call has been ignored.'); } else { this._component = component; } return this; } select(selector) { return new NodesRef(this, this._component, selector, true); } selectAll(selector) { return new NodesRef(this, this._component, selector, false); } selectViewport() { return new NodesRef(this, 0, '', true); } _push(selector, component, single, fields, callback) { if (!this._webviewId) { this._webviewId = this._defaultComponent ? this._defaultComponent.__wxWebviewId__ : undefined; } const rootNodeId = pluginId ? '' : r.getRootNodeId(this._webviewId); this._queue.push({ component: null != component ? (0 === component ? 0 : component.__wxExparserNodeId__) : rootNodeId, selector, single, fields, }); this._queueCb.push(callback || null); } exec(callback) { requestComponentInfo(this._webviewId, { pluginId, queue: this._queue, }, (results) => { const queueCb = this._queueCb; results.forEach((res, index) => { if ('function' == typeof queueCb[index]) { queueCb[index].call(this, res); } }); if ('function' == typeof callback) { callback.call(this, results); } }) } } } [代码] requestComponentInfo.js [代码]const subscribe = function(eventType, callback) { const _callback = function(event, webviewId, nativeInfo = {}) { const { data = {}, options } = event; const startTime = options && options.timestamp || 0; const endTime = Date.now(); if ('function' == typeof callback) { callback(data, webviewId); Reporter.speedReport({ key: 'webview2AppService', data, timeMark: { startTime, endTime, nativeTime: nativeInfo.nativeTime || 0, } }); } }; __safeway__.bridge.subscribe(eventType, _callback); } const publish = function(eventType, data, webviewIds) { const event = { data, options: { timestamp: Date.now(), } }; __safeway__.bridge.publish(eventType, event, webviewIds); } const requestCb = {}; let requestId = 1; subscribe('responseComponentInfo', function(data) { const reqId = data.reqId; const callback = requestCb[reqId]; if (callback) { delete requestCb[reqId]; callback(data.res); } }); export default function requestComponentInfo(webviewId, reqs, callback) { const reqId = requestId++; if (!webviewId) { console.warn('An SelectorQuery call is ignored because no proper page or component is found. Please considering using `SelectorQuery.in` to specify a proper one.'); return; } requestCb[reqId] = callback, publish('requestComponentInfo', { reqId, reqs, }, [webviewId], ); } [代码] NodesRef.js [代码]export default class NodesRef { constructor(selectorQuery, component, selector, single) { this._selectorQuery = selectorQuery; this._component = component; this._selector = selector; this._single = single; } fields(fields, callback) { this._selectorQuery._push(this._selector, this._component, this._single, fields, callback, ); return this._selectorQuery; } boundingClientRect(callback) { this._selectorQuery._push( this._selector, this._component, this._single, { id: true, dataset: true, rect: true, size: true, }, callback, ); return this._selectorQuery; } scrollOffset(callback) { this._selectorQuery._push( this._selector, this._component, this._single, { id: true, dataset: true, scrollOffset: true, }, callback, ); return this._selectorQuery; } } [代码]
2019-05-16 - 开发工具中提前创建的 tabbar 的 WebViewId 与页面的 WebViewId 不匹配
进行的操作如下图所示: [图片] 打开首页(ID: 3), 此时提前创建 tabbar 栏(ID: 4), 从首页(ID: 3)路由(wx.switchTab)到第二页(ID: 4), 此时提前创建的 tabbar 与第二页的页面ID匹配第二页(ID: 4)提前创建 tabbar 栏(ID: 5)从第二页(ID: 4)路由(wx.switchTab)回首页(ID: 3), 此时还留有一个提前创建 tabbar 栏(ID: 5)从首页(ID: 3)路由到(wx.switchTab)到第三页, 发现此时第三页的ID实却是6, 即跳过了5, 这导致提前创建 tabbar 栏(ID: 5)无法匹配, 进而需要重新创建[图片] 打开首页(ID: 14), 此时提前创建 tabbar 栏(ID: 15), 从首页(ID: 14)路由(wx.switchTab)到第二页(ID: 15), 此时提前创建的 tabbar 与第二页的页面ID匹配第二页(ID: 15)提前创建 tabbar 栏(ID: 16)从第二页(ID: 15)路由(wx.switchTab)回首页(ID: 14), 此时还留有一个提前创建 tabbar 栏(ID: 16)从首页(ID: 14)路由(wx.redirectTo)到一个无 tabbar 的空页面, 发现此空页面的ID为17, 同样跳过了16, 这导致提前创建 tabbar 栏(ID: 16)无法匹配
2020-10-25 - [填坑手册]小程序web-view组件实战与踩坑
[图片] 首先,根据官网文档可以知道 只有非个人 的小程序才可以使用web-view组件,如果你的个人开发者,可以跳过这篇文章。 [图片] 一、使用web-view以及它的好处 1、己方账号(第三方)与小程序openId/UnionId的关联绑定,实现免登陆 比如你是某门户网站S,你要识别自己小程序上的用户与网站用户的关系,你可以通过三种方法绑定关系,公众号,小程序源生,小程序web-view内嵌跳转三种方法 2、内嵌H5的富文本,减少重复开发 比如你是门户网站,社区,以往有大量的新闻和帖子,里面带了各种css样式的富文本,小程序源生是无法直接读取的,需要大量转化,这时候直接内嵌这些H5新闻,大大降低开发成本 3、热更新,减少发布审核 某些需要经常更新的内容、公告、活动页,内嵌H5可以减少频繁提交小程序审核 二、小程序功能赋权 为H5提供各种小程序才有的功能,比如录音,扫一扫等。 注意事项 多场景判断,建议使用官方API: wx.miniProgram.getEnv H5唤醒一些小程序API有一定的延时,0.3~1秒 请调用小程序专用的JSSDK,同一个jssdk,但是webview的功能收到限制,和之前微信打开H5有所不同 小程序自动获取加载H5的title H5中iframe的url必须也是业务域名 web-view一定是撑满全屏的,自定义顶部菜单,悬浮的都没用 三、小程序和H5之前的互相通讯 1、 从小程序 ==>> h5 小程序控制H5,可以直接用src路径传参的形式,比如 [代码]<!-- 小程序端 HTML --> <web-view src="//URL?a=param1&b=param2"></web-view> [代码] 避免在链接中带有中文字符,在 iOS 中会有打开白屏的问题,建议加一下 encodeURIComponent。 2、 从 H5 ==>> 小程序 [图片] 这里我们知道bindmessage是小程序用来监听H5的推送的内容,但是这里不大不小的坑!就是它的三个出发场景: 小程序后退:使用接口名 wx.miniProgram.navigateTo,wx.miniProgram.navigateBack,wx.miniProgram.switchTab 等切换小程序页面/场景的API时候都会出发 分享:这个就是当你点分享小程序的时候,会接受到H5之前发送的postMessage 组件销毁,web-view组件销毁,类似 wx.miniProgram.redirectTo 都会触发。 [代码]<!-- 小程序端 HTML --> <web-view bindmessage="handleGetMessage" src="{{openUrl}}"></web-view> [代码] [代码]// 小程序端 JS --> Page({ /** * 页面的初始数据 */ data: { openUrl: "", }, /** * 获取请求数据 */ handleGetMessage: function (e) { console.log(e.detail.data); } }, }) [代码] [代码]<!-- h5端 HTML和JS --> <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script> <script> wx.miniProgram.postMessage({ data: { link: "//test.com", title: "一起学习,一起进步" } }); //wx.miniProgram.redirectTo({ // url:"/pages/inner/index?source=123" //}) wx.miniProgram.navigateBack({delta: 1}) </script> [代码] 注意事项 那些H5控制小程序的跳转路径必须是“/”开头,如 “/pages/xxx/xxx”,且路径必须在app.json里有,地址错误的话,有时不报错。 postMessage的json必须是data开始,不然接收不到数据。 [图片] 四、bindmessage接收到消息有3个重要特性(重点) 接收可以是H5之前几分钟前发送postMessage,不一定是即刻发出的。 之前发出的 postMessage的DATA信息会累加,当触发bindmessage接收的时候是一个数组。 [图片] 当bindmessage 再次 接收到数据,之前发送的数据不会被清空,将累加一起返回,获取时要判断好数组的角标 [图片] 五、Tips 1、在IDE工具中如何调试H5 [图片] 可以在 web-view 组件上通过右键 - 调试,打开 web-view 组件的调试。 2、内嵌H5缓存问题 web-view加载的H5具有很重的缓存,如果需要调试,可以通过在url后面加时间戳的方式解决。 3、小程序关闭,H5背景音乐仍然在播放问题 小程序已经关闭,但是H5自带的背景音乐仍然在手机后台播放的问题。这里可以利用一个属性: visibilitychange:页面可见性状态 简单的说,浏览器标签页被隐藏或显示的时候会触发visibilitychange事件。 [代码]var statusBeforeHide = true; //初始化页面的状态 var music = document.getElementById("xxx"); // 更改音乐播放状态 function setChangeMusic() { if (document[hiddenProperty]) { // 页面隐藏 if (statusBeforeHide) { music.pause(); // 暂停 } } else { // 页面显示 if (statusBeforeHide) { music.play() //如果statusBeforeHide是true, 继续播放 } } } let hiddenProperty = ('hidden' in document) ? 'hidden' : ('webkitHidden' in document) ? 'webkitHidden' : ('mozHidden' in document) ? 'mozHidden' : null; if (hiddenProperty) { let visibilityChangeEvent = hiddenProperty.replace(/hidden/i, 'visibilitychange'); let onVisibilityChange = () => { //console.log('visibilityChange'); setChangeMusic(); }; document.addEventListener(visibilityChangeEvent, onVisibilityChange); } else { console.log("不支持这个api"); } [代码] 总结,web-view还是非常实用的组件,且用且珍惜~ 往期回顾: 小程序自定义头部导航栏“完美”解决方案 小程序Canvas生成海报(一) 小程序新版订阅消息+云开发实战与跳坑
2021-09-13 - 基于wxs实现拖拽排序,封装成了组件,拷贝组件即可使用
基于wxs封装的拖拽排序组件,开箱即用。 目前拖拽到屏幕边缘的时候未触发滚动(即当排序列表超过一个屏幕的话会比较麻烦,如果有需要实现该功能的小伙伴,请留言,作者会抽空加上该功能) 仓库地址:https://gitee.com/ayao1010/miniapp-drag-to-sort 如果喜欢的话给我一个star,我也很开心的😄😄 [图片] 更新: 增加了poschange钩子(在demo里,在该回调里调用了振动反馈)
2021-10-22 - 极致的scroll-view的下拉刷新扩展组件
不敢说是最好的,但是感觉也应该是性能和体验比较极致的下拉刷新扩展了,老规矩,代码片段放最后了~ 2020.2.22 修复了小程序基础库v2.10.2带来的不能滚动的问题,最新代码片段见scroll-view-extends 原理 其实原理很简单,和普通H5以及市面上有的下拉刷新没有特别大的区别,都是基于[代码]touch[代码]手势检测事件来实现下拉刷新的。[代码]touchstart[代码]的时候记录当前触摸点,[代码]touchmove[代码]的时候开始计算移动方向和移动距离, [代码]touchend[代码]的时候计算是否要进行下拉刷新操作。如图所示: [图片] 实现方法 调研了一些实现方法,目前大部分都是通过js计算,然后setData来改变元素的[代码]transform[代码]值实现下拉刷新。考虑到性能问题,此处使用了[代码]wxs[代码]的响应式能力来实现整个计算逻辑,不用通过逻辑层和视图层通信,直接在视图层进行渲染。具体文档请参考wxs响应事件。 这里在[代码]list[代码]组件(由[代码]scroll-view[代码]组成)下抽出了一个[代码]scroll.wxs[代码]作为响应事件的事件处理函数集合,源码基本上就在[代码]scroll.wxs[代码]和[代码]list[代码]组件。 [代码]scroll.wxs[代码]定义了如下变量和函数: [代码]var moveStartPosition = 0 //开始位置 var moveDistance = 0 //移动距离 var moveRefreshDistance = 60 //达到刷新的阈值 var moveMaxDistance = 100 //最大可滑动距离 var isRefreshMaxDown = false //是否达到了最大距离, 用来判断是否要震动提示 var loading = false //是否正在loading ... ... module.exports = { touchStart: touchStart, //手指开始触摸事件 touchMove: touchMove, //手指移动事件 touchEnd: touchEnd, //手指离开屏幕事件 loadingTypeChange: loadingTypeChange, //请求状态变化监听,监听刷新请求开始和请求完成 triggerRefresh: triggerRefresh //主动触发刷新操作,比如点击页面上一个按钮,重新刷新list,这就需要用到这个方法 } [代码] [代码]touchStart[代码]和[代码]touchMove[代码]就不用说了,代码注释都很明白,普通的监听移动和处理逻辑。 [代码]touchEnd[代码]主要是判断移动距离是否达到了阈值,然后根据结果,调用监听实例的[代码]callMethod[代码]方法触发[代码]refreshStart[代码]或者[代码]refreshCancel[代码]方法,这两个方法都是写到[代码]list[代码]组件里面的,用来触发刷新方法或者取消刷新。 [代码]loadingTypeChange[代码]方法主要是监听刷新是否完成,以此来触发动画效果。 [代码]triggerRefresh[代码]通过监听主动触发的变量来处理。如果需要主动触发刷新,则调用[代码]list[代码]组件内部的[代码]forceRefresh[代码]方法,具体使用示例在[代码]index/index/js[代码]的[代码]onLoad[代码]函数有: [代码]this.selectComponent('.list').forceRefresh()[代码] [代码]scroll.wxs[代码]里面还有一个未导出的方法,叫[代码]drawTransitionY[代码],这个方法主要是因为[代码]ios12[代码]对于[代码]transition[代码]动画效果支持的不好,所以自己写了个Y轴方向的动画([代码]linear[代码]线性的),大佬们可以自己往上添加各种[代码]ease-in-out[代码]效果。 里面具体的实现可以查看代码注释哦~ 使用 好了,前面讲了实现的原理和方法,那么在代码里面,应该怎么直接使用呢?如下代码所示: [代码]<!-- 使用示例 --> <list class="list" refresh-loading="{{refreshLoading}}" loading="{{loading}}" bindrefresh="initList" bindloadmore="loadmore"> <!-- your code --> </list> [代码] [代码]refresh-loading[代码]属性用来通过外部loading态来控制刷新动画的开始结束,因为每当变化[代码]refresh-loading[代码]的值时,会将变化同步到组件内的[代码]showRefresh[代码]属性,[代码]wxs[代码]通过监听[代码]showRefresh[代码]来处理动画逻辑。 [代码]loading[代码]属性是上拉加载更多的时候触发的loading态展示,跟刷新无关 [代码]bindrefresh[代码]是刷新触发时绑定的函数,下拉刷新动画成功开始后触发这个函数 [代码]bindloadmore[代码]透传[代码]scroll-view[代码]的加载更多方法 当然,源码里面也包含了一个[代码]list-item[代码]组件,这个跟本文没太大关系,是用来做瀑布流长列表内容太多时的内存不足问题解决方案的,具体请看解决小程序渲染复杂长列表,内存不足问题 干货 最后,上代码片段, 小程序代码片段 github地址
2020-02-22 - 高阶性能渲染-wxs
我们永远没有资格说放弃,因为这是属于我们的年华,应该开出耀眼的繁花。 这里主要讲解两点使用方式 wxs --> 数据处理使用 wxs --> 拖拽使用 wxs介绍 1.先看看官方如何介绍的 ,如下 点我可以查看更多官方文档地址 WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。 WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。 WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。 WXS 函数不能作为组件的事件回调。 由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。 总的来说,我主要看中它快和方便 (有种在wxml写函数的感觉),并且处理函数内容后渲染不需要去调用setData,可以节省在请求数据返回后写大量的逻辑函数或者遍历方法去处理事务,所以我才拿出来讲解。 wxs数据处理使用 使用我习惯现在utils文件夹里创建一个common.wxs,然后在使用的wxml引入该文件。具体使用也很简单,如下 在你定义的common.wxs文件里,写你需要处理的函数,然后使用export导出 [代码] var getMax = function(array) { var max = undefined; for (var i = 0; i < array.length; ++i) { max = max === undefined ? array[i] : (max >= array[i] ? max : array[i]); } return max; } module.exports = { getMax: getMax }; [代码] 在.wxml文件需要的地方引入该.wxs文件,其中<wxs>标签包含2个重要属性,[代码]module[代码]和[代码]src[代码]<br> module表示你要导出wxs的方法集合,在wxml里面可以使用该方法名去使用具体的函数<br> src表示你.wxs路径位置引入,代码如下 [代码]<wxs module="common" src="../../utils/common.wxs"></wxs> <view> <text>{{common.getMax(arrNumber)}}</text> </view> [代码] 上面代码里[代码]arrNumber[代码]是你页面.js文件里面定义的数据 [代码]data: { arrNumber: [1,2,9,2,1,5,7] } [代码] 就是如此简单,你已经学会了使用wxs处理数据了,接下来我们来点难的,使用wxs做一个可以随便拖动又不影响小程序性能的手势拖拽事件 wxs拖拽使用 你是否还在使用setData控制元素?为什么不能频繁使用setData,点我查看详情然后通过输出手势坐标来移动元素具体位置?如果是建议你该换个方法了,推荐使用wxs,让你小程序性能上一个档次。 解决思路如下: 我们先在页面的.wxml文件书写初始化的样式内容; 给[代码]view[代码]标签绑定手势事件,其中[代码]touchmove[代码]我使用了阻止冒泡事件绑定是为了防止在苹果机中,元素移动会带着屏幕一起移动,导致滑动手势突然中断不流畅, [代码]clickLive[代码]事件我也用了阻止冒泡事件是为了触发子元素的事件防止点击被父元素拦截而使用,代码如下: [代码]<wxs module="comm" src="../../utils/common.wxs"></wxs> <view class="enterLive liveMove" data-maxWidth="{{windowWidth}}" data-maxHeight="{{windowHeight}}" bind:touchstart="{{comm.liveTouchmove}}" catch:touchmove="{{comm.liveTouchmove}}" bind:touchend="{{comm.liveTouchmove}}"> <view class="live-block" catch:tap="{{comm.clickLive}}"> <image class="liveEnter-img" mode="aspectFit" src="https://img0.baidu.com/it/u=401284325,23907343&fm=26&fmt=auto&gp=0.jpg" /> </view> </view> [代码] 我们发现上面的代码有[代码]windowWidth,windowHeight[代码]两个参数,这是在页面的.js中,是计算当前页面的宽高,用于防止滑块滑出视野范围内,导致无法滑动回来,其中下面代码[代码]getApp().globalData.systemInfo[代码]是读取全局文件[代码]app.js[代码]里面拿值的,那里我有获取系统参数设为全局保存, 而[代码]bindEnter[代码]事件是用于待会在[代码].wxs[代码]里面可以触发.js文件内函数示例 [代码]data: { windowWidth: 375, windowHeight: 667, }, ready: function() { let systemInfo = getApp().globalData.systemInfo; let width = 150 * systemInfo.windowWidth / 750; let height = 150 * systemInfo.windowWidth / 750; if (systemInfo) { this.setData({ windowWidth: systemInfo.windowWidth - parseInt(width), windowHeight: systemInfo.windowHeight - parseInt(height) }) } }, bindEnter(e) { wx.showToast({ title: '你点击到我啦~', icon: 'none' }) }, [代码] 接下来我们将书写.wxs文件,处理手势输出内容,使页面的滑块可以平静的滑动; 认真看的同学会发现页面的[代码]touchstart,touchmove,touchend[代码]事件我都是绑定同一个函数[代码]liveTouchmove[代码]这里是我想节约函数名称,统一使用一个函数,然后在输出的回调里面判断当前是什么手势 这里的滑块滑动逻辑是:当输出是touchstart手势时,我们记住当前滑块的[代码]starLeft,starTop[代码]值及坐标[代码]starX,starY[代码]值 当我们判断当前是[代码]touchmove[代码]手势时,我们就用当前的x,y坐标值减去开始starX和starY坐标值,得到一个差值[代码]diffX,diffY[代码] 然后我们使用这个[代码]diffX,diffY[代码]差值分别加上滑块刚开始的[代码]starLeft,starTop[代码]值,即可得到我们滑块当前移动的位置 最后我们通过条件判断当前是否超出屏幕即可,我这里做多一步,就是滑块永远停留左右两边,所以要多算一次滑块停留是偏离那一边,在通过[代码]ownerInstance.selectComponent('.liveMove').setStyle[代码]可以给页面元素添加行内样式。 滑动滑动逻辑已经讲完了,就是这么简单 如果你想通过[代码].wxs[代码]函数触发[代码].js[代码]文件内的函数,这里有提供[代码]ownerInstance.callMethod("bindEnter")[代码]方法给你触发.js里面的函数,其中[代码]ownerInstance[代码]是绑定页面函数的第二个回调参数,[代码]callMethod[代码]是官方提供的一个方法,[代码]bindEnter[代码]是.js文件里自己命名的函数方法名称,整体代码如下:更多wxs内置方法,请查看官方文档 [代码]/** * 滑块计算位置 */ var starLeft = 0; var starTop = 0; var starX = 0; var starY = 0; function liveTouchmove(event, ownerInstance) { // console.log(JSON.stringify(event)) // console.log(JSON.stringify(ownerInstance)) var left = 0, top = 0; var diffX = 0, diffY = 0; if(event.type === 'touchstart') { starLeft = event.currentTarget.offsetLeft; starTop = event.currentTarget.offsetTop; starX = event.changedTouches[0].clientX; starY = event.changedTouches[0].clientY; left = starLeft; top = starTop; } else { diffX = event.changedTouches[0].clientX - starX; diffY = event.changedTouches[0].clientY - starY; left = starLeft + diffX; top = starTop + diffY; var maxWidth = event.currentTarget.dataset.maxwidth; var maxHeight = event.currentTarget.dataset.maxheight; if (left > maxWidth) { left = maxWidth; } if (top > maxHeight) { top = maxHeight; } if (left <= 0) { left = 0; } if (top <= 0) { top = 0; } } if (event.type === 'touchend') { if (maxWidth / 2 < left) { left = "calc(100% - 3% - 152rpx)"; } if (maxWidth / 2 > left) { left = '3%'; } } else { left = left + 'px'; } var instance = ownerInstance.selectComponent('.liveMove'); // 返回组件的实例 instance.setStyle({ left: left, top: top +'px', }); } /** * 点击直播入口 * @param */ function clickLive (event, ownerInstance) { ownerInstance.callMethod("bindEnter"); } module.exports = { liveTouchmove: liveTouchmove, clickLive: clickLive, }; [代码] 总结 wxs确实可以解决我们一些性能问题,和wxml函数调用方便,但是我们也要注意几个问题 目前还不支持原生组件的事件、input和textarea组件的 bindinput 事件 1.02.1901170及以后版本的开发者工具上支持交互动画,最低版本基础库是2.4.4 目前在WXS函数里面仅支持console.log方式打日志定位问题,注意连续的重复日志会被过滤掉。 wxs有自己的语法,我理解为不支持ES6及以上语法更多wxs语法可以查看官方文档 以上内容,包括之前写的文章内容,最终会上传到我的gitee里面,请留意。 文章创作不易,喜欢的记得点赞 本文同步掘金号文章 喜欢的记得去点个赞哦
2021-07-11 - vant-weapp UI组件库中的tabs组件在项目中的实际运用
按照惯例,开头先啰嗦几句: 作为一个安卓开发工程师,由于公司业务的变化我也是最近才开始接触到小程序的开发,不得不说,对比起来小程序是真的香,易上手(“其实坑还是有的,而且很多”)而且还有很多非常丰富好用的第三方组件,特别是再结合上云开发之后,基本上已经能够涵盖大多数应用场景,对于中小型的项目来说是最合适不过的了,性价比极高,加上微信有这么庞大的用户群体,推广起来也是非常容易的(“才怪”)。。。 好了切回正题,这次分享的是我在项目开发过程中所用到tabs组件,献上最终实现的效果,如果确实是你的菜的话那么可以继续往下看 [视频] (ps:视频经过速度慢放调整,实际很顺滑。。。) 这是项目需求当中要实现的一个效果,在scroll-view包裹的页面中,自定义导航栏上边的几个标签用于点击之后跳转到当前页的某个锚点,并且在页面未下拉的时候,导航栏背景是透明的,back图标和标签字体显示为白色;当页面下拉后导航栏背景呈白色,而标签字体和back图标转变成黑色。 要实现需求中所描述的那种效果,首先得搞清楚以下几点: 1:拿到页面上拉之后距顶部距离变化的数值,该数值将作为修改样式的判断条件; 2:导航栏和标签以及back图标的布局方式,导航栏那层view应该位于标签和back下,也就是z-index要低于标签和back,同时他们的position都是固定的; [图片] [图片] 3:导航栏背景透明度的设置,颜色根据项目要求的样式一致性,这里设置为白色,而透明度如果是按scroTop的距离来设置会有渐变的效果,这里将根据scrollTop变化到某个值时修改opacity为0或1,也就是无渐变效果; 4:back图标样式的切换,这里是通过指定image不同的url来实现切换的; 5:重点来了,tabs组件的样式如何修改?这个涉及到了第三方组件样式和外部界面隔离的问题,也就是无法直接通过指定一个class去修改样式,需要在其对应子元素节点中进行设置 [图片] 5.1:tabs组件背景如何设置成透明? 5.2:tabs组件标签被选中时的title颜色如何设置? 5.3:tabs组件标签未被选中时title默认颜色如何设置? 上边的三个问题,我参考了文档列出的外部样式类,但是都没有能够实现 [图片] 但是还有其他方法可以进行设置,作为程序开发人员,调试是特别重要的一环,而对于这种界面样式的处理,调试可以更为直观的看出问题所在: [图片] 比如上面的5.1:如何设置背景透明: [图片] 找到之后就可以在样式里边去设置了,下边是那三个问题的答案(不唯一) /* 设置tabs为透明背景 */ van-tabs .van-tabs__scroll { background-color: rgba(255, 255, 0, 0); } /* 指定tabs中被选中title的颜色 */ .index-tabs .van-tab--active .van-ellipsis { color: #FFC125; } /* 设置tabs标签未被选中时title的颜色 */ .index-tabs2 .van-ellipsis { color: white; } 那么如何在页面上下拉时动态修改tabs组件标签title的颜色呢?答案就是上边的 5.3:tabs组件标签未被选中时title默认颜色如何设置? 只需要修改页面当中tabs标签的样式类就好 <van-tabs ...... class="{{navigatonOpacity < 0.5 ? 'index-tabs2' : ''}}" // 就是这里,根据条件来修改样式类 ...... > <van-tab title="详情" /> <van-tab title="配套" /> <van-tab title="须知" /> <van-tab title="周边" /> </van-tabs> 到此,基本上就能够实现上边所展现的效果了,当然这里只列举出了其中一些方法,还有其他更好的方法可以实现
2020-09-19 - weui扩展组件tabs如何控制切换菜单节流?
[图片]快速切换菜单项时页面请求会无限请求,我给请求单独加了节流也没用,[图片][图片]
2021-03-26 - 在使用Tabs组件遇到swiper高度自适应问题,使用变量计算高度值解决小经验。
在社区里找了很多解决高度的问题,相对都比较麻烦,并且嵌套FOR之后问题一直没解决。最后是通过下面方法解决的。 1. 在wxss中,定义变量:width:var(--width--); 2. 在js中,定义变量:viewWidth,并给这个变量赋予想要的值 3. 在xml中,使用wxss中的变量和js中的变量:style="--width--: {{ viewWidth }}px" 方法比较简单,也可以借鉴到其他应用。
2021-02-07 - tabs扩展组件动态修改swiper高度
1.首先在需要使用tabs扩展组件的.json文件中引入"mp-tabs": "@miniprogram-component-plus/tabs"。 [图片] 打开终端,安装依赖npm install。 2.在工具栏中找到工具-构建npm [图片] 3.npm构建完成后会在项目根目录下生成miniprogram_npm文件夹,并且文件夹内有引入的tabs扩展组件。 [图片] 4.进入正题,在调用tabs扩展组件的组件、页面中传递参数swiperStyle(String)。 [图片] 5.参数传递后那么就在tabs扩展组件的目录下(第三点)的index.js接收swiperStyle参数, [图片] 6.接收参数后在tabs扩展组件目录下(第三点)的index.wxml中找到swiper并将index.js接收的参数(swiperStyle)以动态style的方式赋值给swiper标签。 [图片] 7.获取列表内容的高度('.tab-content')。因为'.tab-content'在#shadow-root虚拟Dom下无法直接使用wx.createSelectorQuery().select('')获取节点,并且返回null。在.select()前加上.in(this)。 [图片] const that = this // 获取节点的高度 const query = wx.createSelectorQuery() query.in(this).select('.tab-content').boundingClientRect(function (res) { that.setData({ tabSwiperHeight: res.height, }) }) query.exec() 参考:https://developers.weixin.qq.com/community/develop/doc/0000663a034ca08c7087f5b1957000
2020-12-09 - 如何使用scroll-view制作左右滚动导航条效果
最新:2020/06/13。修改为scroll-view与swiper联动效果,新增下拉刷新以及上拉加载效果。。具体效果查看代码片段,以下文章内容和就不改了 刚刚在社区里看到 有老哥在问如何做滚动的导航栏。这里简单给他写了个代码片段,需要的大哥拿去随便改改,先看效果图: [图片] 代码如下: wxml [代码]<scroll-view class="scroll-wrapper" scroll-x scroll-with-animation="true" scroll-into-view="item{{currentTab < 4 ? 0 : currentTab - 3}}" > <view class="navigate-item" id="item{{index}}" wx:for="{{taskList}}" wx:key="{{index}}" data-index="{{index}}" bindtap="handleClick"> <view class="names {{currentTab === index ? 'active' : ''}}">{{item.name}}</view> <view class="currtline {{currentTab === index ? 'active' : ''}}"></view> </view> </scroll-view> [代码] wxss [代码].scroll-wrapper { white-space: nowrap; -webkit-overflow-scrolling: touch; background: #FFF; height: 90rpx; padding: 0 32rpx; box-sizing: border-box; } ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .navigate-item { display: inline-block; text-align: center; height: 90rpx; line-height: 90rpx; margin: 0 16rpx; } .names { font-size: 28rpx; color: #3c3c3c; } .names.active { color: #00cc88; font-weight: bold; font-size: 34rpx; } .currtline { margin: -8rpx auto 0 auto; width: 100rpx; height: 8rpx; border-radius: 4rpx; } .currtline.active { background: #47CD88; transition: all .3s; } [代码] JS [代码]const app = getApp() Page({ data: { currentTab: 0, taskList: [{ name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, ] }, onLoad() { }, handleClick(e) { let currentTab = e.currentTarget.dataset.index this.setData({ currentTab }) }, }) [代码] 最后奉上代码片段: https://developers.weixin.qq.com/s/nkyp64mN7fim
2020-06-13 - 微信小程序通过weixin://wxpay/bizpayurl生成支付二维码
其实很简单,我开始也觉得复杂,目的很明确,后端返回前端一个短链接weixin://wxpay/bizpayurl?pr=sZz****,前端生成一个支付二维码,扫码可以直接调起微信支付,上代码: 这是最后生成的效果图,扫码可以直接支付,显示支付金额: [图片] wxml: js:var QRCode = require('../../utils/qrcode.js'); page({ data:{}, getQrcode() { // 生成二维码 app.http.getQrcode({ // 这里是请求后端接口获取短链接weixin://wxpay/bizpayurl id: this.data..id }).then(res => { let qrcode = new QRCode('canvas', { text: res.data, // weixin://wxpay/bizpayurl?pr=sZz**** width: 194, height: 194, colorDark: "black", colorLight: "white", correctLevel: QRCode.CorrectLevel.H, }); }) }, }) 这是上面引入生成二维码的js文件:…/…/utils/qrcode.js //Core code comes from https://github.com/davidshimjs/qrcodejs var QRCode; (function () { /** * Get the type by string length * * @private * @param {String} sText * @param {Number} nCorrectLevel * @return {Number} type */ function _getTypeNumber(sText, nCorrectLevel) { var nType = 1; var length = _getUTF8Length(sText); for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { var nLimit = 0; switch (nCorrectLevel) { case QRErrorCorrectLevel.L: nLimit = QRCodeLimitLength[i][0]; break; case QRErrorCorrectLevel.M: nLimit = QRCodeLimitLength[i][1]; break; case QRErrorCorrectLevel.Q: nLimit = QRCodeLimitLength[i][2]; break; case QRErrorCorrectLevel.H: nLimit = QRCodeLimitLength[i][3]; break; } if (length <= nLimit) { break; } else { nType++; } } if (nType > QRCodeLimitLength.length) { throw new Error("Too long data"); } return nType; } function _getUTF8Length(sText) { var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); return replacedText.length + (replacedText.length != sText ? 3 : 0); } function QR8bitByte(data) { this.mode = QRMode.MODE_8BIT_BYTE; this.data = data; this.parsedData = []; // Added to support UTF-8 Characters for (var i = 0, l = this.data.length; i < l; i++) { var byteArray = []; var code = this.data.charCodeAt(i); if (code > 0x10000) { byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); byteArray[3] = 0x80 | (code & 0x3F); } else if (code > 0x800) { byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); byteArray[2] = 0x80 | (code & 0x3F); } else if (code > 0x80) { byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); byteArray[1] = 0x80 | (code & 0x3F); } else { byteArray[0] = code; } this.parsedData.push(byteArray); } this.parsedData = Array.prototype.concat.apply([], this.parsedData); if (this.parsedData.length != this.data.length) { this.parsedData.unshift(191); this.parsedData.unshift(187); this.parsedData.unshift(239); } } QR8bitByte.prototype = { getLength: function (buffer) { return this.parsedData.length; }, write: function (buffer) { for (var i = 0, l = this.parsedData.length; i < l; i++) { buffer.put(this.parsedData[i], 8); } } }; // QRCodeModel function QRCodeModel(typeNumber, errorCorrectLevel) { this.typeNumber = typeNumber; this.errorCorrectLevel = errorCorrectLevel; this.modules = null; this.moduleCount = 0; this.dataCache = null; this.dataList = []; } QRCodeModel.prototype = { addData: function (data) { var newData = new QR8bitByte(data); this.dataList.push(newData); this.dataCache = null; }, isDark: function (row, col) { if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) { throw new Error(row + "," + col); } return this.modules[row][col]; }, getModuleCount: function () { return this.moduleCount; }, make: function () { this.makeImpl(false, this.getBestMaskPattern()); }, makeImpl: function (test, maskPattern) { this.moduleCount = this.typeNumber * 4 + 17; this.modules = new Array(this.moduleCount); for (var row = 0; row < this.moduleCount; row++) { this.modules[row] = new Array(this.moduleCount); for (var col = 0; col < this.moduleCount; col++) { this.modules[row][col] = null; } } this.setupPositionProbePattern(0, 0); this.setupPositionProbePattern(this.moduleCount - 7, 0); this.setupPositionProbePattern(0, this.moduleCount - 7); this.setupPositionAdjustPattern(); this.setupTimingPattern(); this.setupTypeInfo(test, maskPattern); if (this.typeNumber >= 7) { this.setupTypeNumber(test); } if (this.dataCache == null) { this.dataCache = QRCodeModel.createData(this.typeNumber, this.errorCorrectLevel, this.dataList); } this.mapData(this.dataCache, maskPattern); }, setupPositionProbePattern: function (row, col) { for (var r = -1; r <= 7; r++) { if (row + r <= -1 || this.moduleCount <= row + r) continue; for (var c = -1; c <= 7; c++) { if (col + c <= -1 || this.moduleCount <= col + c) continue; if ((0 <= r && r <= 6 && (c == 0 || c == 6)) || (0 <= c && c <= 6 && (r == 0 || r == 6)) || (2 <= r && r <= 4 && 2 <= c && c <= 4)) { this.modules[row + r][col + c] = true; } else { this.modules[row + r][col + c] = false; } } } }, getBestMaskPattern: function () { var minLostPoint = 0; var pattern = 0; for (var i = 0; i < 8; i++) { this.makeImpl(true, i); var lostPoint = QRUtil.getLostPoint(this); if (i == 0 || minLostPoint > lostPoint) { minLostPoint = lostPoint; pattern = i; } } return pattern; }, createMovieClip: function (target_mc, instance_name, depth) { var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth); var cs = 1; this.make(); for (var row = 0; row < this.modules.length; row++) { var y = row * cs; for (var col = 0; col < this.modules[row].length; col++) { var x = col * cs; var dark = this.modules[row][col]; if (dark) { qr_mc.beginFill(0, 100); qr_mc.moveTo(x, y); qr_mc.lineTo(x + cs, y); qr_mc.lineTo(x + cs, y + cs); qr_mc.lineTo(x, y + cs); qr_mc.endFill(); } } } return qr_mc; }, setupTimingPattern: function () { for (var r = 8; r < this.moduleCount - 8; r++) { if (this.modules[r][6] != null) { continue; } this.modules[r][6] = (r % 2 == 0); } for (var c = 8; c < this.moduleCount - 8; c++) { if (this.modules[6][c] != null) { continue; } this.modules[6][c] = (c % 2 == 0); } }, setupPositionAdjustPattern: function () { var pos = QRUtil.getPatternPosition(this.typeNumber); for (var i = 0; i < pos.length; i++) { for (var j = 0; j < pos.length; j++) { var row = pos[i]; var col = pos[j]; if (this.modules[row][col] != null) { continue; } for (var r = -2; r <= 2; r++) { for (var c = -2; c <= 2; c++) { if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) { this.modules[row + r][col + c] = true; } else { this.modules[row + r][col + c] = false; } } } } } }, setupTypeNumber: function (test) { var bits = QRUtil.getBCHTypeNumber(this.typeNumber); for (var i = 0; i < 18; i++) { var mod = (!test && ((bits >> i) & 1) == 1); this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod; } for (var i = 0; i < 18; i++) { var mod = (!test && ((bits >> i) & 1) == 1); this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod; } }, setupTypeInfo: function (test, maskPattern) { var data = (this.errorCorrectLevel << 3) | maskPattern; var bits = QRUtil.getBCHTypeInfo(data); for (var i = 0; i < 15; i++) { var mod = (!test && ((bits >> i) & 1) == 1); if (i < 6) { this.modules[i][8] = mod; } else if (i < 8) { this.modules[i + 1][8] = mod; } else { this.modules[this.moduleCount - 15 + i][8] = mod; } } for (var i = 0; i < 15; i++) { var mod = (!test && ((bits >> i) & 1) == 1); if (i < 8) { this.modules[8][this.moduleCount - i - 1] = mod; } else if (i < 9) { this.modules[8][15 - i - 1 + 1] = mod; } else { this.modules[8][15 - i - 1] = mod; } } this.modules[this.moduleCount - 8][8] = (!test); }, mapData: function (data, maskPattern) { var inc = -1; var row = this.moduleCount - 1; var bitIndex = 7; var byteIndex = 0; for (var col = this.moduleCount - 1; col > 0; col -= 2) { if (col == 6) col--; while (true) { for (var c = 0; c < 2; c++) { if (this.modules[row][col - c] == null) { var dark = false; if (byteIndex < data.length) { dark = (((data[byteIndex] >>> bitIndex) & 1) == 1); } var mask = QRUtil.getMask(maskPattern, row, col - c); if (mask) { dark = !dark; } this.modules[row][col - c] = dark; bitIndex--; if (bitIndex == -1) { byteIndex++; bitIndex = 7; } } } row += inc; if (row < 0 || this.moduleCount <= row) { row -= inc; inc = -inc; break; } } } } }; QRCodeModel.PAD0 = 0xEC; QRCodeModel.PAD1 = 0x11; QRCodeModel.createData = function (typeNumber, errorCorrectLevel, dataList) { var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel); var buffer = new QRBitBuffer(); for (var i = 0; i < dataList.length; i++) { var data = dataList[i]; buffer.put(data.mode, 4); buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber)); data.write(buffer); } var totalDataCount = 0; for (var i = 0; i < rsBlocks.length; i++) { totalDataCount += rsBlocks[i].dataCount; } if (buffer.getLengthInBits() > totalDataCount * 8) { throw new Error("code length overflow. (" + buffer.getLengthInBits() + ">" + totalDataCount * 8 + ")"); } if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { buffer.put(0, 4); } while (buffer.getLengthInBits() % 8 != 0) { buffer.putBit(false); } while (true) { if (buffer.getLengthInBits() >= totalDataCount * 8) { break; } buffer.put(QRCodeModel.PAD0, 8); if (buffer.getLengthInBits() >= totalDataCount * 8) { break; } buffer.put(QRCodeModel.PAD1, 8); } return QRCodeModel.createBytes(buffer, rsBlocks); }; QRCodeModel.createBytes = function (buffer, rsBlocks) { var offset = 0; var maxDcCount = 0; var maxEcCount = 0; var dcdata = new Array(rsBlocks.length); var ecdata = new Array(rsBlocks.length); for (var r = 0; r < rsBlocks.length; r++) { var dcCount = rsBlocks[r].dataCount; var ecCount = rsBlocks[r].totalCount - dcCount; maxDcCount = Math.max(maxDcCount, dcCount); maxEcCount = Math.max(maxEcCount, ecCount); dcdata[r] = new Array(dcCount); for (var i = 0; i < dcdata[r].length; i++) { dcdata[r][i] = 0xff & buffer.buffer[i + offset]; } offset += dcCount; var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1); var modPoly = rawPoly.mod(rsPoly); ecdata[r] = new Array(rsPoly.getLength() - 1); for (var i = 0; i < ecdata[r].length; i++) { var modIndex = i + modPoly.getLength() - ecdata[r].length; ecdata[r][i] = (modIndex >= 0) ? modPoly.get(modIndex) : 0; } } var totalCodeCount = 0; for (var i = 0; i < rsBlocks.length; i++) { totalCodeCount += rsBlocks[i].totalCount; } var data = new Array(totalCodeCount); var index = 0; for (var i = 0; i < maxDcCount; i++) { for (var r = 0; r < rsBlocks.length; r++) { if (i < dcdata[r].length) { data[index++] = dcdata[r][i]; } } } for (var i = 0; i < maxEcCount; i++) { for (var r = 0; r < rsBlocks.length; r++) { if (i < ecdata[r].length) { data[index++] = ecdata[r][i]; } } } return data; }; var QRMode = { MODE_NUMBER: 1 << 0, MODE_ALPHA_NUM: 1 << 1, MODE_8BIT_BYTE: 1 << 2, MODE_KANJI: 1 << 3 }; var QRErrorCorrectLevel = { L: 1, M: 0, Q: 3, H: 2 }; var QRMaskPattern = { PATTERN000: 0, PATTERN001: 1, PATTERN010: 2, PATTERN011: 3, PATTERN100: 4, PATTERN101: 5, PATTERN110: 6, PATTERN111: 7 }; var QRUtil = { PATTERN_POSITION_TABLE: [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170]], G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0), G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0), G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1), getBCHTypeInfo: function (data) { var d = data << 10; while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) { d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15))); } return ((data << 10) | d) ^ QRUtil.G15_MASK; }, getBCHTypeNumber: function (data) { var d = data << 12; while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) { d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18))); } return (data << 12) | d; }, getBCHDigit: function (data) { var digit = 0; while (data != 0) { digit++; data >>>= 1; } return digit; }, getPatternPosition: function (typeNumber) { return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]; }, getMask: function (maskPattern, i, j) { switch (maskPattern) { case QRMaskPattern.PATTERN000: return (i + j) % 2 == 0; case QRMaskPattern.PATTERN001: return i % 2 == 0; case QRMaskPattern.PATTERN010: return j % 3 == 0; case QRMaskPattern.PATTERN011: return (i + j) % 3 == 0; case QRMaskPattern.PATTERN100: return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0; case QRMaskPattern.PATTERN101: return (i * j) % 2 + (i * j) % 3 == 0; case QRMaskPattern.PATTERN110: return ((i * j) % 2 + (i * j) % 3) % 2 == 0; case QRMaskPattern.PATTERN111: return ((i * j) % 3 + (i + j) % 2) % 2 == 0; default: throw new Error("bad maskPattern:" + maskPattern); } }, getErrorCorrectPolynomial: function (errorCorrectLength) { var a = new QRPolynomial([1], 0); for (var i = 0; i < errorCorrectLength; i++) { a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0)); } return a; }, getLengthInBits: function (mode, type) { if (1 <= type && type < 10) { switch (mode) { case QRMode.MODE_NUMBER: return 10; case QRMode.MODE_ALPHA_NUM: return 9; case QRMode.MODE_8BIT_BYTE: return 8; case QRMode.MODE_KANJI: return 8; default: throw new Error("mode:" + mode); } } else if (type < 27) { switch (mode) { case QRMode.MODE_NUMBER: return 12; case QRMode.MODE_ALPHA_NUM: return 11; case QRMode.MODE_8BIT_BYTE: return 16; case QRMode.MODE_KANJI: return 10; default: throw new Error("mode:" + mode); } } else if (type < 41) { switch (mode) { case QRMode.MODE_NUMBER: return 14; case QRMode.MODE_ALPHA_NUM: return 13; case QRMode.MODE_8BIT_BYTE: return 16; case QRMode.MODE_KANJI: return 12; default: throw new Error("mode:" + mode); } } else { throw new Error("type:" + type); } }, getLostPoint: function (qrCode) { var moduleCount = qrCode.getModuleCount(); var lostPoint = 0; for (var row = 0; row < moduleCount; row++) { for (var col = 0; col < moduleCount; col++) { var sameCount = 0; var dark = qrCode.isDark(row, col); for (var r = -1; r <= 1; r++) { if (row + r < 0 || moduleCount <= row + r) { continue; } for (var c = -1; c <= 1; c++) { if (col + c < 0 || moduleCount <= col + c) { continue; } if (r == 0 && c == 0) { continue; } if (dark == qrCode.isDark(row + r, col + c)) { sameCount++; } } } if (sameCount > 5) { lostPoint += (3 + sameCount - 5); } } } for (var row = 0; row < moduleCount - 1; row++) { for (var col = 0; col < moduleCount - 1; col++) { var count = 0; if (qrCode.isDark(row, col)) count++; if (qrCode.isDark(row + 1, col)) count++; if (qrCode.isDark(row, col + 1)) count++; if (qrCode.isDark(row + 1, col + 1)) count++; if (count == 0 || count == 4) { lostPoint += 3; } } } for (var row = 0; row < moduleCount; row++) { for (var col = 0; col < moduleCount - 6; col++) { if (qrCode.isDark(row, col) && !qrCode.isDark(row, col + 1) && qrCode.isDark(row, col + 2) && qrCode.isDark(row, col + 3) && qrCode.isDark(row, col + 4) && !qrCode.isDark(row, col + 5) && qrCode.isDark(row, col + 6)) { lostPoint += 40; } } } for (var col = 0; col < moduleCount; col++) { for (var row = 0; row < moduleCount - 6; row++) { if (qrCode.isDark(row, col) && !qrCode.isDark(row + 1, col) && qrCode.isDark(row + 2, col) && qrCode.isDark(row + 3, col) && qrCode.isDark(row + 4, col) && !qrCode.isDark(row + 5, col) && qrCode.isDark(row + 6, col)) { lostPoint += 40; } } } var darkCount = 0; for (var col = 0; col < moduleCount; col++) { for (var row = 0; row < moduleCount; row++) { if (qrCode.isDark(row, col)) { darkCount++; } } } var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; lostPoint += ratio * 10; return lostPoint; } }; var QRMath = { glog: function (n) { if (n < 1) { throw new Error("glog(" + n + ")"); } return QRMath.LOG_TABLE[n]; }, gexp: function (n) { while (n < 0) { n += 255; } while (n >= 256) { n -= 255; } return QRMath.EXP_TABLE[n]; }, EXP_TABLE: new Array(256), LOG_TABLE: new Array(256) }; for (var i = 0; i < 8; i++) { QRMath.EXP_TABLE[i] = 1 << i; } for (var i = 8; i < 256; i++) { QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8]; } for (var i = 0; i < 255; i++) { QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i; } function QRPolynomial(num, shift) { if (num.length == undefined) { throw new Error(num.length + "/" + shift); } var offset = 0; while (offset < num.length && num[offset] == 0) { offset++; } this.num = new Array(num.length - offset + shift); for (var i = 0; i < num.length - offset; i++) { this.num[i] = num[i + offset]; } } QRPolynomial.prototype = { get: function (index) { return this.num[index]; }, getLength: function () { return this.num.length; }, multiply: function (e) { var num = new Array(this.getLength() + e.getLength() - 1); for (var i = 0; i < this.getLength(); i++) { for (var j = 0; j < e.getLength(); j++) { num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j))); } } return new QRPolynomial(num, 0); }, mod: function (e) { if (this.getLength() - e.getLength() < 0) { return this; } var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0)); var num = new Array(this.getLength()); for (var i = 0; i < this.getLength(); i++) { num[i] = this.get(i); } for (var i = 0; i < e.getLength(); i++) { num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio); } return new QRPolynomial(num, 0).mod(e); } }; function QRRSBlock(totalCount, dataCount) { this.totalCount = totalCount; this.dataCount = dataCount; } QRRSBlock.RS_BLOCK_TABLE = [[1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9], [1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16], [1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13], [1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9], [1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12], [2, 86, 68], [4, 43, 27], [4, 43, 19], [4, 43, 15], [2, 98, 78], [4, 49, 31], [2, 32, 14, 4, 33, 15], [4, 39, 13, 1, 40, 14], [2, 121, 97], [2, 60, 38, 2, 61, 39], [4, 40, 18, 2, 41, 19], [4, 40, 14, 2, 41, 15], [2, 146, 116], [3, 58, 36, 2, 59, 37], [4, 36, 16, 4, 37, 17], [4, 36, 12, 4, 37, 13], [2, 86, 68, 2, 87, 69], [4, 69, 43, 1, 70, 44], [6, 43, 19, 2, 44, 20], [6, 43, 15, 2, 44, 16], [4, 101, 81], [1, 80, 50, 4, 81, 51], [4, 50, 22, 4, 51, 23], [3, 36, 12, 8, 37, 13], [2, 116, 92, 2, 117, 93], [6, 58, 36, 2, 59, 37], [4, 46, 20, 6, 47, 21], [7, 42, 14, 4, 43, 15], [4, 133, 107], [8, 59, 37, 1, 60, 38], [8, 44, 20, 4, 45, 21], [12, 33, 11, 4, 34, 12], [3, 145, 115, 1, 146, 116], [4, 64, 40, 5, 65, 41], [11, 36, 16, 5, 37, 17], [11, 36, 12, 5, 37, 13], [5, 109, 87, 1, 110, 88], [5, 65, 41, 5, 66, 42], [5, 54, 24, 7, 55, 25], [11, 36, 12], [5, 122, 98, 1, 123, 99], [7, 73, 45, 3, 74, 46], [15, 43, 19, 2, 44, 20], [3, 45, 15, 13, 46, 16], [1, 135, 107, 5, 136, 108], [10, 74, 46, 1, 75, 47], [1, 50, 22, 15, 51, 23], [2, 42, 14, 17, 43, 15], [5, 150, 120, 1, 151, 121], [9, 69, 43, 4, 70, 44], [17, 50, 22, 1, 51, 23], [2, 42, 14, 19, 43, 15], [3, 141, 113, 4, 142, 114], [3, 70, 44, 11, 71, 45], [17, 47, 21, 4, 48, 22], [9, 39, 13, 16, 40, 14], [3, 135, 107, 5, 136, 108], [3, 67, 41, 13, 68, 42], [15, 54, 24, 5, 55, 25], [15, 43, 15, 10, 44, 16], [4, 144, 116, 4, 145, 117], [17, 68, 42], [17, 50, 22, 6, 51, 23], [19, 46, 16, 6, 47, 17], [2, 139, 111, 7, 140, 112], [17, 74, 46], [7, 54, 24, 16, 55, 25], [34, 37, 13], [4, 151, 121, 5, 152, 122], [4, 75, 47, 14, 76, 48], [11, 54, 24, 14, 55, 25], [16, 45, 15, 14, 46, 16], [6, 147, 117, 4, 148, 118], [6, 73, 45, 14, 74, 46], [11, 54, 24, 16, 55, 25], [30, 46, 16, 2, 47, 17], [8, 132, 106, 4, 133, 107], [8, 75, 47, 13, 76, 48], [7, 54, 24, 22, 55, 25], [22, 45, 15, 13, 46, 16], [10, 142, 114, 2, 143, 115], [19, 74, 46, 4, 75, 47], [28, 50, 22, 6, 51, 23], [33, 46, 16, 4, 47, 17], [8, 152, 122, 4, 153, 123], [22, 73, 45, 3, 74, 46], [8, 53, 23, 26, 54, 24], [12, 45, 15, 28, 46, 16], [3, 147, 117, 10, 148, 118], [3, 73, 45, 23, 74, 46], [4, 54, 24, 31, 55, 25], [11, 45, 15, 31, 46, 16], [7, 146, 116, 7, 147, 117], [21, 73, 45, 7, 74, 46], [1, 53, 23, 37, 54, 24], [19, 45, 15, 26, 46, 16], [5, 145, 115, 10, 146, 116], [19, 75, 47, 10, 76, 48], [15, 54, 24, 25, 55, 25], [23, 45, 15, 25, 46, 16], [13, 145, 115, 3, 146, 116], [2, 74, 46, 29, 75, 47], [42, 54, 24, 1, 55, 25], [23, 45, 15, 28, 46, 16], [17, 145, 115], [10, 74, 46, 23, 75, 47], [10, 54, 24, 35, 55, 25], [19, 45, 15, 35, 46, 16], [17, 145, 115, 1, 146, 116], [14, 74, 46, 21, 75, 47], [29, 54, 24, 19, 55, 25], [11, 45, 15, 46, 46, 16], [13, 145, 115, 6, 146, 116], [14, 74, 46, 23, 75, 47], [44, 54, 24, 7, 55, 25], [59, 46, 16, 1, 47, 17], [12, 151, 121, 7, 152, 122], [12, 75, 47, 26, 76, 48], [39, 54, 24, 14, 55, 25], [22, 45, 15, 41, 46, 16], [6, 151, 121, 14, 152, 122], [6, 75, 47, 34, 76, 48], [46, 54, 24, 10, 55, 25], [2, 45, 15, 64, 46, 16], [17, 152, 122, 4, 153, 123], [29, 74, 46, 14, 75, 47], [49, 54, 24, 10, 55, 25], [24, 45, 15, 46, 46, 16], [4, 152, 122, 18, 153, 123], [13, 74, 46, 32, 75, 47], [48, 54, 24, 14, 55, 25], [42, 45, 15, 32, 46, 16], [20, 147, 117, 4, 148, 118], [40, 75, 47, 7, 76, 48], [43, 54, 24, 22, 55, 25], [10, 45, 15, 67, 46, 16], [19, 148, 118, 6, 149, 119], [18, 75, 47, 31, 76, 48], [34, 54, 24, 34, 55, 25], [20, 45, 15, 61, 46, 16]]; QRRSBlock.getRSBlocks = function (typeNumber, errorCorrectLevel) { var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel); if (rsBlock == undefined) { throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel); } var length = rsBlock.length / 3; var list = []; for (var i = 0; i < length; i++) { var count = rsBlock[i * 3 + 0]; var totalCount = rsBlock[i * 3 + 1]; var dataCount = rsBlock[i * 3 + 2]; for (var j = 0; j < count; j++) { list.push(new QRRSBlock(totalCount, dataCount)); } } return list; }; QRRSBlock.getRsBlockTable = function (typeNumber, errorCorrectLevel) { switch (errorCorrectLevel) { case QRErrorCorrectLevel.L: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; case QRErrorCorrectLevel.M: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; case QRErrorCorrectLevel.Q: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; case QRErrorCorrectLevel.H: return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; default: return undefined; } }; function QRBitBuffer() { this.buffer = []; this.length = 0; } QRBitBuffer.prototype = { get: function (index) { var bufIndex = Math.floor(index / 8); return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) == 1; }, put: function (num, length) { for (var i = 0; i < length; i++) { this.putBit(((num >>> (length - i - 1)) & 1) == 1); } }, getLengthInBits: function () { return this.length; }, putBit: function (bit) { var bufIndex = Math.floor(this.length / 8); if (this.buffer.length <= bufIndex) { this.buffer.push(0); } if (bit) { this.buffer[bufIndex] |= (0x80 >>> (this.length % 8)); } this.length++; } }; var QRCodeLimitLength = [[17, 14, 11, 7], [32, 26, 20, 14], [53, 42, 32, 24], [78, 62, 46, 34], [106, 84, 60, 44], [134, 106, 74, 58], [154, 122, 86, 64], [192, 152, 108, 84], [230, 180, 130, 98], [271, 213, 151, 119], [321, 251, 177, 137], [367, 287, 203, 155], [425, 331, 241, 177], [458, 362, 258, 194], [520, 412, 292, 220], [586, 450, 322, 250], [644, 504, 364, 280], [718, 560, 394, 310], [792, 624, 442, 338], [858, 666, 482, 382], [929, 711, 509, 403], [1003, 779, 565, 439], [1091, 857, 611, 461], [1171, 911, 661, 511], [1273, 997, 715, 535], [1367, 1059, 751, 593], [1465, 1125, 805, 625], [1528, 1190, 868, 658], [1628, 1264, 908, 698], [1732, 1370, 982, 742], [1840, 1452, 1030, 790], [1952, 1538, 1112, 842], [2068, 1628, 1168, 898], [2188, 1722, 1228, 958], [2303, 1809, 1283, 983], [2431, 1911, 1351, 1051], [2563, 1989, 1423, 1093], [2699, 2099, 1499, 1139], [2809, 2213, 1579, 1219], [2953, 2331, 1663, 1273]]; // QRCode object QRCode = function (canvasId, vOption) { this._htOption = { width: 256, height: 256, typeNumber: 4, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRErrorCorrectLevel.H }; if (typeof vOption === 'string') { vOption = { text: vOption }; } // Overwrites options if (vOption) { for (var i in vOption) { this._htOption[i] = vOption[i]; } } this._oQRCode = null; this.canvasId = canvasId if (this._htOption.text && this.canvasId) { this.makeCode(this._htOption.text); } }; QRCode.prototype.makeCode = function (sText, callback) { this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); this._oQRCode.addData(sText); this._oQRCode.make(); this.makeImage(callback); }; QRCode.prototype.makeImage = function (callback) { var _oContext if (this._htOption.usingIn) { _oContext = wx.createCanvasContext(this.canvasId, this._htOption.usingIn) } else { _oContext = wx.createCanvasContext(this.canvasId) } var _htOption = this._htOption; var oQRCode = this._oQRCode var nCount = oQRCode.getModuleCount(); var nWidth = _htOption.width / nCount; var nHeight = _htOption.height / nCount; var nRoundedWidth = Math.round(nWidth); var nRoundedHeight = Math.round(nHeight); if (_htOption.image && _htOption.image != '') { _oContext.drawImage(_htOption.image, 0, 0, _htOption.width, _htOption.height) } for (var row = 0; row < nCount; row++) { for (var col = 0; col < nCount; col++) { var bIsDark = oQRCode.isDark(row, col); var nLeft = col * nWidth; var nTop = row * nHeight; _oContext.setStrokeStyle(bIsDark ? _htOption.colorDark : _htOption.colorLight) // _oContext.setStrokeStyle('yellow') _oContext.setLineWidth(1) _oContext.setFillStyle(bIsDark ? _htOption.colorDark : _htOption.colorLight) // _oContext.setFillStyle('red') // if (bIsDark) { _oContext.fillRect(nLeft, nTop, nWidth, nHeight); // } // 안티 앨리어싱 방지 처리 // if (bIsDark) { _oContext.strokeRect( Math.floor(nLeft) + 0.5, Math.floor(nTop) + 0.5, nRoundedWidth, nRoundedHeight ); _oContext.strokeRect( Math.ceil(nLeft) - 0.5, Math.ceil(nTop) - 0.5, nRoundedWidth, nRoundedHeight ); // } // _oContext.fillRect( // Math.floor(nLeft) + 0.5, // Math.floor(nTop) + 0.5, // nRoundedWidth, // nRoundedHeight // ); // _oContext.fillRect( // Math.ceil(nLeft) - 0.5, // Math.ceil(nTop) - 0.5, // nRoundedWidth, // nRoundedHeight // ); // _oContext.clearRect( // Math.floor(nLeft) + 0.5, // Math.floor(nTop) + 0.5, // nRoundedWidth, // nRoundedHeight // ); // _oContext.clearRect( // Math.ceil(nLeft) - 0.5, // Math.ceil(nTop) - 0.5, // nRoundedWidth, // nRoundedHeight // ); } } _oContext.draw(false, callback) }; // 保存为图片,将临时路径传给回调 QRCode.prototype.exportImage = function (callback) { if (!callback) { return } wx.canvasToTempFilePath({ x: 0, y: 0, width: this._htOption.width, height: this._htOption.height, destWidth: this._htOption.width, destHeight: this._htOption.height, canvasId: this.canvasId, success: function (res) { callback(res.tempFilePath) }, fail: res => { console.log(res) } }) } QRCode.CorrectLevel = QRErrorCorrectLevel; })(); module.exports = QRCode```
2021-11-02 - 使用scroll-view下拉刷新,刷新完了列表loading状态不回弹怎么办?
我的代码里有用到mp-tabs,然后每个tab下都有一个scroll-view,使用wx:for循环出来的。 现在的情况是我下拉刷新以后,能触发getItemList方法,ajax拿到数据并赋值后把triggered设为false,并且在开发者工具里看也的确是false了,ajax调用也没问题,但是界面就是不弹回去,三个点还顶在头上。各位大神麻烦帮忙看看吧~ 截图如下: [图片] 代码如下: <mp-tabs tabs="{{tabs}}" activeTab="{{activeTab}}" active-class="tab-item-actived" swiper-class="tab-swiper" bindtabclick="onTabClick" bindchange="onChange" tabClass="tab-item" tab-background-color="transparent" tab-underline-color="transparent" duration="300" > <block wx:for="{{tabs}}" wx:for-item="item" wx:for-index="index" wx:key="title"> <scroll-view class="tab-content" slot="tab-content-{{index}}" scroll-y refresher-enabled enhanced bounces bindrefresherrefresh="getItemList" refresher-triggered="{{triggered}}"> <!-- ………… --> </scroll-view> </block> </mp-tabs>
2021-03-19 - 微信小程序码(默认圆形) 与方形二维码 有什么区别?
平台:windows 64 开发工具版本:RC 1.05.2107221 基础库版本 2.19.1 手机:小米8 系统版本:android 10 在调用wx.scanCode 的时候 scanType默认为['barCode', 'qrCode'],扫描方形小程序码,圆形小程序码 都可正确识别 但是如果指定scanType:['qrCode'] 扫描方形小程序码正确识别,圆形小程序码则不识别 苹果14版本 同样指定scanType:['qrCode'] 也无法正确识别圆形小程序码 圆形小程序码与方形小程序码存在差异么?
2021-08-19 - 关于前端h5生成的base64二维码图片,保存到相册的问题
注册成功后,前端通过后端给的含有邀请码的链接,让我这边来生成二维码。 使用QRcode一下子就完成了。但是有一个问题,就是生的二维码是base64的,长按无法保存图片。 试了很多方法,就是不行。 然后我长按一个是本地图片或者网络图片,都可以保存。我猜想一定是base64的问题。 然后我将QRcode生成的base64编码,先解码,再创建缓冲数组和视图,然后转换成Blob对象,再转换成url,将url 弄到img src属性上,完美收官。 贴上代码 var url = 'xxx' // 你的链接 var div = document.createElement('div'); var code = new QRCode(div, { text: url, width: 500, height: 500, colorDark: '#000000', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H, }); var canvas = code._el.querySelector('canvas'); var base64Text = canvas.toDataURL('image/png'); var blob = getBlob(base64Text); var url_link = window.URL.createObjectURL(blob); // 将这个url赋值给img的src属性即可 function getBlob(base64) { var mimeStr = base64.split(',')[0].split(':')[1].split(';')[0]; // mime类型 var byteStr = atob(base64.split(',')[1]); //base64 解码 var arrayBuffer = new ArrayBuffer(byteSt.length); //创建缓冲数组 var intArray = new Uint8Array(arrayBuffer); //创建视图 for(var i = 0; i < byteStr.length; i += 1) { intArray[i] = byteStr.charCodeAt(i); } return new Blob([intArray], { type: mimeStr, }); }
2021-05-15 - weapp-qrcode-canvas-2d在微信小程序中生成二维码,新版canvas-2d接口
weapp-qrcode-canvas-2d weapp-qrcode-canvas-2d 是使用新版canvas-2d接口在微信小程序中生成二维码(外部二维码)的js包。canvas 2d 接口支持同层渲染且性能更佳,建议切换使用,可大幅提升生成图片的速度。 仓库地址 weapp-qrcode-canvas-2d【码云gitee】 weapp-qrcode-canvas-2d【github】 [图片] 测试环境 微信小程序基础库版本:2.10.4 开发者工具版本:Stable 1.03.2101150 Usage 先在 wxml 文件中,创建绘制的 [代码]canvas[代码],并定义好 [代码]width[代码], [代码]height[代码], [代码]id[代码] , [代码]type[代码] ,其中type的值必须为[代码]2d[代码] [代码]<canvas type="2d" style="width: 260px; height: 260px;" id="myQrcode"></canvas> [代码] 安装方法1:直接引入 js 文件 直接引入 js 文件,使用 [代码]drawQrcode()[代码] 绘制二维码 [代码]// 将 dist 目录下,weapp.qrcode.esm.js 复制到项目中。路径根据实际引用的页面路径自行改变 import drawQrcode from '../../utils/weapp.qrcode.esm.js' [代码] 安装方法2:npm安装 [代码]npm install weapp-qrcode-canvas-2d --save [代码] // 然后需要在小程序开发者工具中:构建npm [代码]import drawQrcode from 'weapp-qrcode-canvas-2d' [代码] 安装完成后调用 例子1:没有使用叠加图片 [代码]const query = wx.createSelectorQuery() query.select('#myQrcode') .fields({ node: true, size: true }) .exec((res) => { var canvas = res[0].node // 调用方法drawQrcode生成二维码 drawQrcode({ canvas: canvas, canvasId: 'myQrcode', width: 260, padding: 30, background: '#ffffff', foreground: '#000000', text: 'abc', }) // 获取临时路径(得到之后,想干嘛就干嘛了) wx.canvasToTempFilePath({ canvasId: 'myQrcode', canvas: canvas, x: 0, y: 0, width: 260, height: 260, destWidth: 260, destHeight: 260, success(res) { console.log('二维码临时路径:', res.tempFilePath) }, fail(res) { console.error(res) } }) }) [代码] 例子2:使用叠加图片(在二维码中加logo) [代码]const query = wx.createSelectorQuery() query.select('#myQrcode') .fields({ node: true, size: true }) .exec((res) => { var canvas = res[0].node var img = canvas.createImage(); img.src = "/image/logo.png" img.onload = function () { // img.onload完成后才能调用 drawQrcode方法 var options = { canvas: canvas, canvasId: 'myQrcode', width: 260, padding: 30, paddingColor: '#fff', background: '#fff', foreground: '#000000', text: '123456789', image: { imageResource: img, width: 80, // 建议不要设置过大,以免影响扫码 height: 80, // 建议不要设置过大,以免影响扫码 round: true // Logo图片是否为圆形 } } drawQrcode(options) // 获取临时路径(得到之后,想干嘛就干嘛了) wx.canvasToTempFilePath({ x: 0, y: 0, width: 260, height: 260, destWidth: 600, destHeight: 600, canvasId: 'myQrcode', canvas: canvas, success(res) { console.log('二维码临时路径为:', res.tempFilePath) }, fail(res) { console.error(res) } }) }; }) [代码] API drawQrcode([options]) options Type: Object 参数 必须 说明 示例 canvas 必须 画布标识,传入 canvas 组件实例 canvasId 非 绘制的[代码]canvasId[代码] [代码]'myQrcode'[代码] text 必须 二维码内容 ‘123456789’ width 非 二维码宽度,与[代码]canvas[代码]的[代码]width[代码]保持一致 260 padding 非 空白内边距 20 paddingColor 非 内边距颜色 默认与background一致 background 非 二维码背景颜色,默认值白色 [代码]'#ffffff'[代码] foreground 非 二维码前景色,默认值黑色 [代码]'#000000'[代码] typeNumber 非 二维码的计算模式,默认值-1 8 correctLevel 非 二维码纠错级别,默认值为高级,取值:[代码]{ L: 1, M: 0, Q: 3, H: 2 }[代码] 1 image 非 在 canvas 上绘制图片,层级高于二维码,v1.1.1+版本支持。具体使用见:例子2 [代码]{imageResource: '', width:80, height: 80, round: true}[代码]
2023-04-02 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - 云开发 逆地址解析(无须注册腾讯位置服务的应用 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 - 云函数 openapi.updatableMessage.createActivityId 正确写法(之一)
云函数代码 [图片] config.json 文件 [图片] 打印云函数返回结果: [图片] -------------------------- 代码如下 -------------------------- // 云函数入口文件 activityId const cloud = require('wx-server-sdk') //给定 DYNAMIC_CURRENT_ENV 常量:接下来的 API 调用都将请求到与该云函数当前所在环境相同的环境 cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { const activityId = cloud.openapi.updatableMessage.createActivityId() return activityId } //注意。以下3点 1.在 openapi.updatableMessage.createActivityId() 前面要加 cloud. 2.不是 createActivityId,而是 createActivityId()。带后面一定要带括号() 3.在 config.json 文件中,加接口权限 { "permissions": { "openapi": [ "updatableMessage.createActivityId" ] } }
2021-03-14 - 云函数定时器调用subscribeMessage.send发送订阅消息,如何获取openid?
用户点击授权后,收到一条订阅消息。我在云函数中设置了定时器,定时器可以按时执行,但一直获取不到openid。 这是云函数的配置: [图片] 这是云函数实现: [图片] 云函数的日志: [图片] 定时器调用的云函数,红框处都获取不到openid。 问题: 是否能够直接在此处获取到openid进行消息推送?不能的话,想要使用云函数定时推送消息怎么实现?
2020-11-25 - 订阅消息,subscribeMessage.send报43101?
从调用这个接口以来就在报43101,微信小程序端的接收通知按钮已经打开。 这个怎么解决? 还需哪里要设置一下。 在发送消息之前已经调用wx.requestSubscribeMessage。返回结果也是accept.但是接口返回一直是43101,是哪里还需要设置, 已经换了模板和用户,测试均是43101
2021-04-08 - this.setData使用改值修改其他对象问题?
[图片] 我有这个一个界面 实现 dataArray:[{id:'1',name:'一斤',items:[{id:'2',name:'大',price:0,stock:0},{id:'3',name:'中',price:0,stock:0}] },{...}] 现在有个问题 我输入几次之后 会把之前的内容覆盖 比如 :输入第一行(价格+库存) 1 2 输入 第三行(价格+库存) 4 5 后面打印输出 一三行的结果值都是 4 5 值被覆盖了 我不知道是真么原因 <view class="content-item-head"> <view class="head-item"></view> <view class="head-item">规格</view> <view class="head-item">价格(元)</view> <view class="head-item">库存(件)</view> </view> <view class="content-item-row {{ idx%2==0?'itemBackground':'' }}" wx:key="index" wx:for="{{item.items}}" wx:for-item="obj" wx:for-index="idx"> <view class="head-item ">{{idx==filters.toFix(item.items.length/3) ? item.name :''}}</view> <view class="head-item"> {{obj.name}} </view> <view class="head-item"> <input class="item-input" bindblur="onInput" data-type='price' data-pindex="{{index}}" data-index="{{idx}}" maxlength="6" placeholder="¥输入金额" type="digit" value="{{ obj.price }}" ></input> </view> <view class="head-item"> <input class="item-input" bindblur="onInput" data-type='stock' data-pindex="{{index}}" data-index="{{idx}}" maxlength="5" placeholder="输入库存" type="number" value="{{ obj.stock}}"></input> </view> </view> </view> onInput(e) { var value = e.detail.value let index = e.currentTarget.dataset.index //子容器的index let pindex = e.currentTarget.dataset.pindex //父容器的index let type = e.currentTarget.dataset.type //父容器的index if (type == 'price') { value = this.money(value)//处理金额价格 } let key = 'dataArray[' + pindex + '].items[' + index + '].' + type this.setData({ [key]: value }) },
2020-05-11 - 自定义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 - 如何从零实现上拉无限加载瀑布流组件
代码已优化请查看另外一篇文章 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 - 页面上多个input,怎么通过代码切换到下一个input?
需求:一个input输入完成,回车,激活下一个input。以下尝试都没有效果 尝试1:bindconfirm对应的JS方法里,通过id获取下一个input,设置focus。 let input = this.createSelectorQuery().select('#input' + nextIndex) input.focus = true 尝试2:套一层form,input设置confirm-type = "next"
2020-04-09 - 使用input组件的adjust-position(键盘弹起时,是否自动上推页面)出现问题如何解决?
在聊天室的场景的下和用户进行聊天, 有以下的场景: adjust-position 的属性为true 1、在聊天历史列表比较长,点击底部fixed 定位的输入框弹起手机键盘的时候,输入框上面的内容会自动向上推(涂黑部分是订单的卡片,也是聊天的历史)这个体验是可以接受的。 图一: [图片] 2、在聊天历史列表比较短的时候,点击键盘也会把内容会自动向上推 ,效果如下: 点击输入框之前: [图片] 点击输入框之后: [图片] 想到的解决方案: 能不能使用input组件的bindkeyboardheightchange(监听键盘高度变化)或者 bindfocus(聚焦)+bindblur(失去焦点) 获取键盘的高度来动态改变 adjust-position 属性的值来解决问题 过程: 这里 adjust-position 默认值分别设置两种情况: 一、.adjust-position: true 键盘弹起时,自动上推页面 1. 浏览历史长的时候不做处理,键盘唤起的时候,页面应该自动上推,体验正常 2.浏览历史比较短的时候,唤起键盘的时候, 这个需要计算浏览历史列表的安全距离 根据这个安全距离才能设置adjust-position 的开启,浏览历史的长度会有一个临界值 判断这个临界值,如果浏览历史的长度,小于临界值,则设置 adjust-position: false,唤起键盘不会向上推动页面 先测试只有一个聊天内容时候,第一次唤起键盘是如下的效果: [图片] 为什么还是会把页面的聊天内容挤上去,由于唤起键盘的时候,adjust-position 还是true 唤起键盘之后才能拿到键盘的高度,才能判断是否设置adjust-position为false. 点击第二次就正常了,由于点击第一次已经将adjust-position 设置为false,如图: [图片] 二、.adjust-position: false 键盘弹起时,不自动上推页面 1、聊天历史列表数据短的时候,就不测试了,效果和上图一样是正常的, 2、在聊天历史列表长的时候,在点击唤起键盘的时候,效果如下: [图片] 由于默认是 不自动上推页面 ,弹起键盘获取键盘高度的时候,adjust-position的值还是false 再点击第二次唤起键盘的时候,才能显示正常,如上面图一的截图。 造成这样原因归根到底是 在唤起键盘的时候,adjust-position的属性改变的操作是在 拿到键盘高度之后,键盘高度拿到之前界面就已经 被自动上推或者没有被推上去 第二次唤起键盘才能正常显示,不知道这种场景下如何解决,比如提早获取键盘的高度 计算出安全的聊天历史长度。希望各位大佬指导下。
2020-12-14 - 如何自己在小程序内做埋点数据统计
如何在自己小程序内做数据埋点 小程序后台已经有了较为完善的数据统计和基础的分析,但是功能还是比较基础的,通常我们对数据分析有较高的要求时,就需要自己做数据收集了。那小程序内如何做自动的数据埋点和手动埋点呢。 自动埋点 启动时间 (onLaunch) 系统信息 (systemInfo) 停留时长 (Page onHide - Page onShow) 来源 (query 参数) PV (Page onShow) UV (结合用户筛选PV) 预设点击数据收集 (onTap 等) 手动埋点 自定义点击收集数据 改造小程序生命周期 自动埋点是需要集成到底层内,不能对业务进行侵入,所以,我们需要改造小程序生命周期,在不同的生命周期内进行预设收集数据的功能。 对 [代码]App[代码] 进行重写 [代码]const oldApp = App; // 我们需要重写的方法 const appFn = ['onLaunch', 'onShow', 'onHide']; App = function (options) { let oldFuncs = {}; appFn.forEach((item) => { oldFuncs[item] = options[item] }) appFn.forEach((item) => { options[item] = function (options) { // todo 做各类数据收集 oldFuncs[item].apply(this, arguments) } }) oldApp.apply(this, arguments); }; [代码] 对 [代码]Page[代码] 重写 [代码]const oldPage = Page; const pageFn = ['onLoad', 'onShow', 'onHide', 'onUnload', 'onShareAppMessage', 'onAddToFavorites'] Page = function (options) { let oldFuncs = {}; pageFn.forEach((item) => { if (options[item]) { oldFuncs[item] = options[item] } }) pageFn.forEach((item) => { if (options[item]) { options[item] = function () { console.log('Page', item, ); // 收集各类数据 oldFuncs[item].apply(this, arguments) } } }) // 以下代码则是对除生命周期类的方法进行重写,做预设点击事件收集数据 const methods = getMethods(options); if (!!methods) { for (var i = 0, len = methods.length; i < len; i++) { clickProxy(options, methods[i]); } } oldPage.apply(this, arguments); } [代码] 对 [代码]Component[代码] 重写 [代码]const oldComponent = Component; Component = function (options) { // 对组建内 methods 进行重写预设点击事件埋点收集 Object.keys(options.methods).forEach((method) => { clickProxy(options.methods, method) }) oldComponent.apply(this, arguments); } [代码] 以上对 [代码]App[代码] [代码]Page[代码] [代码]Component[代码] 进行重写之后,在必要的地方,加入自己的上报代码. 以下完整代码 [代码] const mpHook = { data: 1, onLoad: 1, onShow: 1, onReady: 1, onPullDownRefresh: 1, onReachBottom: 1, onShareAppMessage: 1, onPageScroll: 1, onResize: 1, onTabItemTap: 1, onHide: 1, onUnload: 1, }; const oldApp = App; const oldPage = Page; const oldComponent = Component; const appFn = ['onLaunch', 'onShow', 'onHide'] App = function (options) { let oldFuncs = {}; appFn.forEach((item) => { oldFuncs[item] = options[item] }) appFn.forEach((item) => { options[item] = function (options) { console.log('App', item); // 收集各类数据 oldFuncs[item].apply(this, arguments) } }) oldApp.apply(this, arguments); }; const pageFn = ['onLoad', 'onShow', 'onHide', 'onUnload', 'onShareAppMessage', 'onAddToFavorites'] Page = function (options) { let oldFuncs = {}; pageFn.forEach((item) => { if (options[item]) { oldFuncs[item] = options[item] } }) pageFn.forEach((item) => { if (options[item]) { options[item] = function () { console.log('Page', item, ); // 收集各类数据 oldFuncs[item].apply(this, arguments) } } }) const methods = getMethods(options); if (!!methods) { for (var i = 0, len = methods.length; i < len; i++) { clickProxy(options, methods[i]); } } oldPage.apply(this, arguments); } Component = function (options) { Object.keys(options.methods).forEach((method) => { clickProxy(options.methods, method) }) oldComponent.apply(this, arguments); } function clickProxy(options, method) { const oldFunc = options[method]; options[method] = function () { const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const pageQuery = currentPage.options || {}; const pagePath = currentPage.route; const res = oldFunc.apply(this, arguments); let prop = {}, type = ""; if (isObject(arguments[0])) { const current_target = arguments[0].currentTarget || {}; const dataset = current_target.dataset || {}; type = arguments[0]["type"]; prop["$event_type"] = type; prop["$event_timestamp"] = Date.now(); prop["$element_id"] = current_target.id; prop["$element_type"] = dataset["type"]; prop["$element_content"] = dataset["content"]; prop["$element_name"] = dataset["name"]; prop["$page_path"] = pagePath; prop["$page_quey"] = pageQuery; if (isObject(arguments[0].event_prop)) { prop = Object.assign(prop, arguments[0].event_prop); } } console.log('type', type) if (type) { // 可以对不同事件类型进行筛选是否需要收集 post(prop) } console.log(res); return res; }; } const getMethods = function (options) { let methods = []; for (let m in options) { if (typeof options[m] === "function" && !mpHook[m]) { methods.push(m); } } return methods; }; const isObject = function (obj) { if (obj === undefined || obj === null) { return false; } else { return toString.call(obj) == "[object Object]"; } }; const post = function (data) { console.log('data', data) // 提交数据时,可以在组合下 systeminfo 用户信息等相关信息 wx.request({ url: 'https://www.example.php', method: 'post', data }) } // 手动埋点的部分,自己在需要收集的地方调用相关方法收集数据 [代码]
2021-06-24 - 新富文本组件
mp-html小程序富文本组件 news欢迎加入 QQ 交流群:699734691示例小程序添加获取组件包功能[图片] 功能介绍 支持在多个平台使用 支持丰富的标签(包括 table、video、svg 等) 支持丰富的事件效果(自动预览图片、链接处理等) 支持锚点跳转、长按复制等丰富功能 支持大部分 html 实体 丰富的插件(关键词搜索、内容编辑等) 效率高、容错性强且轻量化使用方法1. npm 方式 在项目根目录下执行 npm install mp-html 开发者工具中勾选 使用 npm 模块 并点击 工具 - 构建 npm 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}" /> 在需要使用页面的 js 文件中添加 Page({ onLoad() { this.setData({ html: 'Hello World!' }) } }) 2. 源码方式 将源码中的代码包(dist/mp-weixin)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "/components/mp-html/index" } } 后续步骤同上 获取github 链接:https://github.com/jin-yufeng/mp-html npm 链接:https://www.npmjs.com/package/mp-html 文档链接:https://jin-yufeng.gitee.io/mp-html
2022-03-04 - table表格组件,分享给各位
前言 移动端的页面本应该很少有table表格这样的展示、操作,但总归有这样的需求,然而平时用的vant和iview的小程序组件库都没有table组件,这里将自己编写的table组件展示一下供大家查看。 小程序实现table的问题在于,自定义td的实现,而小程序没办法像react一样使用[代码]jsx[代码],也没办法像vue一样用[代码]作用域插槽[代码]传row行的信息给slot,但是小程序还是留有一样东西可以完成自定义td的功能。 抽象节点 这个特性自小程序基础库版本 1.9.6 开始支持。 有时,自定义组件模板中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。 微信官方api地址 通过抽象节点我们可以做到使用自定义组件通过key值分发组件内容到不同的td里。 具体的源码地址可点击下方查看,如果对你有帮助请点个star~~ 源码地址 具体的实现效果可以扫描下方小程序码。 [图片] API prop 参数 说明 类型 默认值 是否必填 columns 表格的配置 Columns[] [] true dataList 数据 any[] [] true getListLoading 请求列表的loading boolean false true showTipImage 无数据时的提示文本图片 boolean false true rowKey 用于指明行的唯一标识符,在勾选中有使用 string id false scrollViewHeight 控制可滚动区域高度。 string 600rpx false tipTitle 无数据时的提示文本主标题 string 提示 false tipSubtitle 无数据时的提示文本副标题 string 暂无数据 false scrollX 是否需要X轴滚动。 boolean false false select 控制是否出现勾选。 boolean false false selectKeys 勾选的初始值 any[] [] false generic:action-td 当列表项内具有操作列,需要在[代码]columns[代码]内添加[代码]type:action[代码]的一项,操作列的内容往往需要自定义,小程序不提供react,vue的[代码]rander函数[代码],所以使用到了抽象节点,该属性指明抽象节点的组件。操作列位置可以不固定,点击事件由[代码]bindclickaction[代码]触发 component undefined false isExpand 控制是否点击展开。 boolean false false expandValueKey 展开信息的key值 string false initExpandValue 当展开信息为空时的默认提示语 string ‘暂无信息’ false expandStyle 展开信息的最外层的样式 string ‘’ false generic:expand-component 如果展开区域的内容需要自定义,[代码]expandValueKey[代码]设置为空字符串,则切换到组件模式,传一个组件进来,展开区域的点击事件由[代码]bindclickexpand[代码]触发 component undefined false dynamicValue 给自定义内容的动态值,用于改变状态 ,建议{value:放的数据} object {} false Events 事件 解释 类型 bindclicklistitem 点击列表行事件 Function(e); e.detail.value = {index:number(当前行序号),item: any(当前行的内容)} bindclickexpand 点击展开内容事件 Function(e); e.detail.value = {type:(这个按钮的含义字段,如‘close’),index:(当前的行),item:(当前行的数据)};(这是我这里定义的结构,具体可以自己定义在expand-component里)} bindclickaction 点击抽象节点事件 Function(e); e.detail.value = {type:(这个按钮的含义字段,如‘close’),index:(当前的行),item:(当前行的数据)};(这是我这里定义的结构,具体可以自己定义在action-td里)} bindcheckkey 勾选事件 返回被勾选项的rowKey数组 Function(e); e.detail.value = any[]//(数组内每一项是rowKey字段定义的数据的toString()结果) bindscrolltolower 滚动触底 Function() bindscrolltoupper 滚动触顶 Function() column 列描述数据对象,是 columns 中的一项,Column 使用相同的 API。 事件 解释 类型 必填 title 字段名中文含义 string true key 字段名 string true width 单元格宽度 string false type 判断字段是否是自定义组件 ‘action’/undefined false render td内内容由函数返回 (value: any, item: any, index: number, data?: 当前页面的this.data) => any,// 设置内容 function false
2022-11-24 - swiper-item超过30个时加载失败
[图片][图片]
2021-06-22 - swiper-item中的图片如何进行居中显示?
如题。
2021-03-06 - scroll-view内部的第一个盒子添加margin-top属性会出现滚动条
margin-top 垂直方向塌陷导致的,解决 margin-top 塌陷的方法有: 给父盒子添加border 给父盒子添加padding-top 给父盒子添加overflow:hidden 父盒子:position:fixed 父盒子:display:table 给子元素的前面添加一个兄弟元素
2019-10-12 - 小程序开发起步
学习 5 节课程,从 0 至 1 做第一个属于你的小程序,深入浅出了解小程序开发。本系列视频,由腾讯课堂 NEXT 学院、微信学堂联合出品。
2022-03-24