- mask-image 在 skyline 中引用 svg 存在颜色边
[图片]有淡淡的颜色边 .i-mdi-home { display: inline-block; width: 1em; height: 1em; background-color: currentColor; -webkit-mask-image: var(--svg); mask-image: var(--svg); -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8z'/%3E%3C/svg%3E") } .block { display: block } .flex { display: flex } .min-h-screen { min-height: 100vh } .justify-center { justify-content: center } .bg-\[\#43ffff\] { --tw-bg-opacity: 1; background-color: rgb(67 255 255 / var(--tw-bg-opacity, 1)) } .bg-slate-950 { --tw-bg-opacity: 1; background-color: rgb(2 6 23 / var(--tw-bg-opacity, 1)) } .p-8 { padding: 2rem } .text-2xl { font-size: 1.5rem; line-height: 2rem } .text-\[200px\] { font-size: 200px } .text-\[\#234fff\] { --tw-text-opacity: 1; color: rgb(35 79 255 / var(--tw-text-opacity, 1)) }
03-28 - 【已解决】wx.loadFontFace引入云空间文件的下载地址,华为手机字体加载报错?
[图片] onLaunch() { wx.loadFontFace({ family: 'AIzaozibaxing', source: `url('${云空间文件的下载地址}')`, global: true, success(res) { console.log('字体加载成功:', res); }, fail(err) { console.error('字体加载失败:', err); }, }); }, ios手机字体下载正常,华为手机报错 [图片]
01-23 - gesture-view 支持双指平移、缩放、旋转的小程序组件
微信小程序原生组件 movable-view 只支持平移、缩放,不支持旋转,仅支持 webview 渲染器,并且有性能问题。 `gesture-view` 组件在微信小程序中原生实现了双指平移、缩放、旋转,并且兼容 skyline 渲染器。算法上参考了 Dan Burzo 的 Pinch me, I'm zooming: gestures in the DOM 一文,使得变换中心始终处于双指中点。初期实现参考了 微信小程序单指拖拽和双指缩放旋转 专栏文章。 由于微信小程序不支持 `DOMMatrix`,并且无法通过原生 npm 构建的形式正常加载 @thednp/dommatrix 包,这里直接使用了其 cjs(CommonJS)发布文件于 components/gesture-view/dommatrix.js。 详细使用说明请见 README.md 文件。 代码仓库:https://github.com/LogCreative/wxapp-gesture-view 代码片段:https://developers.weixin.qq.com/s/wmnPFKmY7NQr [图片]
2024-04-26 - 利用前端缓存技术,大幅度降低微信小程序云开发的“调用次数”、成本、费用!
如果提到微信云开发,大家都会说“云开发好贵”,我也觉得。 不过我发现主要是贵在“调用次数”上,其他存储费用、CDN费用和其他平台也差不多,如果其他平台更便宜,也可以在使用云开发的同时把文件存在其他平台。 如果优化好“调用次数”的资源消耗,我觉得云开发的费用并不高。按照 0.5元一万次的费用计算(买资源包最低能降到0.25元一万次),如果平均一个用户每日消耗100次(通常消耗不了那么多),那么在“调用次数”的成本上,大概是每个用户每天0.5分钱。如果能控制到一个用户每日20次调用次数,成本可以降低到每个用户每天0.1分钱。 所以我个人觉得,严格控制好“调用次数”的消耗,是降低微信云开发成本的关键。因此我写了一个系列文章来讲解如何降低资源的消耗,此系列文章已经写到第五篇,其中一、二、三这3篇文章介绍了云数据库的操作函数,文章四和五则重点介绍了如何使用前端缓存来降低“调用次数”。 简单来说就是通过前端[代码]Storage[代码]存储,实现以下功能: 写入数据时,仅写入实际变化的数据,可仅消耗1次“调用次数”写入多条数据。 读取数据时,从前端[代码]Storage[代码]缓存中读取,不消耗调用次数。 所有的读写数据库操作都很简单、高效、易用,代码简洁。 样例代码: [代码]// 用户配置改为使用大号字体 utils.setUserConfig(coll, 'page.big_font', true) // 记录用户最近提交的内容 utils.setUserConfig(coll, 'post.content', ‘用户输入的文字...’) // 把用户收藏的文章添加到收藏列表中(数组) utils.pushUserConfig(coll, 'favorite_articles', article_obj) // 取消用户收藏 utils.pluckUserConfig(coll, 'favorite_articles', article_obj) // 读取用户关于页面显示的配置值 const page_config_obj = await utils.getUserConfig(coll, 'page') // 读取多个配置值 const user_config_obj = await utils.getUserConfigObj(coll, { page: {}, // {} 是默认值 'post.content': 'default_value' }) // 把要写的数据先放入缓冲区(不会消耗调用次数) utils.setUserConfigBuffer(coll, 'page.big_font', true) utils.setUserConfigBuffer(coll, 'favorite_articles', []) // 清空收藏 utils.setUserConfigBuffer(coll, 'post.content', '这是用户最近提交的内容') // 一次性写入所有缓冲区数据(仅消耗1次调用次数) utils.flushUserConfigBuffer(coll) [代码] 上面代码样例中,coll是写入数据库的表名。 所有读取操作不消耗“调用次数”,且读取操作是瞬间完成的。 由于[代码]Storage[代码]是长期存储在用户手机上的,因此就算用户改天、下周、几个月后再次打开小程序,读取数据库操作也不会消耗“调用次数” utils中还有很多有用的数据库操作函数,这里就不一一列举了。 讲解以上函数的文章:《小白变大神五:大幅降低微信小程序云数据库的调用次数和成本费用》 Gitbub开源项目地址:WxMpCloudBooster
2024-10-21 - 小程序图片裁剪功能实现
裁剪框的拖动与缩放 在裁剪框的元素部分,增加 touchstart 和 touchmove 等事件,用于在处理拖动和缩放等操作。 当我们实现裁剪框的拖动,只需要如下两个事件: touchstartM (event) { const { clipX, clipY } = this.data const { pageX, pageY } = event.touches[0] // 获取鼠标点在裁剪框的内部位置信息 clipBoxMoveInnerX = pageX - clipX clipBoxMoveInnerY = pageY - clipY }, touchmoveM (event) { const { pageX, pageY } = event.touches[0] const { panelWidth, panelHeight, clipHeight, clipWidth } = this.data // 裁剪框不能脱离面板 // X位置范围为 0 到 (面板宽度-裁剪框宽度) let clipX = pageX - clipBoxMoveInnerX clipX = Math.max(clipX, 0) const panelX = panelWidth - clipWidth clipX = Math.min(clipX, panelX) // Y位置范围为 0 到 (面板高度-裁剪框高度) let clipY = pageY - clipBoxMoveInnerY clipY = Math.max(clipY, 0) const panleY = panelHeight - clipHeight clipY = Math.min(clipY, panleY) // 裁剪框底图位置信息 const clipImgX = 0 - clipX const clipImgY = 0 - clipY this.setData({ clipX, clipY, clipImgX, clipImgY }) } 以上代码,通过计算图片移动时的相对位移,重新计算裁剪框的新的位置信息,需要注意的是,移动时不要脱离外层的面板——即不能脱离图片区域,否则无效。 缩放的操作则相对复杂一些,需要计算位移移动的距离以及当前位置下的裁剪宽高数据,并且要对每个不同的corner角进行特殊处理,这块的完整代码和注释如下所示: // 处理缩放动作中不同corner时的尺寸位置信息 getClipX (clipWidth) { switch (activeCorner) { case 'leftTop': case 'leftBottom': return clipBoxBeforeScaleClipX + (clipBoxBeforeScaleWidth - clipWidth) case 'rightTop': case 'rightBottom': return clipBoxBeforeScaleClipX default: return 0 } }, getClipY (clipHeight) { switch (activeCorner) { case 'leftTop': case 'rightTop': return clipBoxBeforeScaleClipY + (clipBoxBeforeScaleHeight - clipHeight) case 'leftBottom': case 'rightBottom': return clipBoxBeforeScaleClipY default: return 0 } }, getScaleXWidthOffset (offsetW) { switch (activeCorner) { case 'leftTop': case 'leftBottom': return -offsetW case 'rightTop': case 'rightBottom': return offsetW default: return 0 } }, getScaleYHeightOffset (offsetH) { switch (activeCorner) { case 'rightBottom': case 'leftBottom': return offsetH case 'rightTop': case 'leftTop': return -offsetH default: return 0 } }, touchstart (event) { const dragId = event.currentTarget.dataset.id const { pageX, pageY } = event.touches[0] const { clipX, clipY, clipHeight, clipWidth } = this.data // 设置缩放时临时变量初始化值 activeCorner = dragId clipBoxBeforeScalePageX = pageX clipBoxBeforeScalePageY = pageY clipBoxBeforeScaleClipX = clipX clipBoxBeforeScaleClipY = clipY clipBoxBeforeScaleWidth = clipWidth clipBoxBeforeScaleHeight = clipHeight }, touchmove (event) { const { pageX, pageY } = event.touches[0] const { panelWidth, panelHeight } = this.data // 缩放在X上的偏移 const xWidthOffset = this.getScaleXWidthOffset(pageX - clipBoxBeforeScalePageX) // 裁剪框最小宽度36 let clipWidth = Math.max(clipBoxBeforeScaleWidth + xWidthOffset, 36) // 设置缩放最大宽度,放大时不能超过面板、缩小时不能超过初始裁剪框 let tempPanelWidth = pageX > clipBoxBeforeScalePageX ? panelWidth - clipBoxBeforeScaleClipX : clipBoxBeforeScaleWidth + clipBoxBeforeScaleClipX // 设置裁剪框宽度 clipWidth = Math.min(clipWidth, tempPanelWidth) // 缩放在Y上的偏移 const yHeightOffset = this.getScaleYHeightOffset(pageY - clipBoxBeforeScalePageY) // 裁剪框最小高度36 let clipHeight = Math.max(clipBoxBeforeScaleHeight + yHeightOffset, 36) // 设置缩放最大高度,放大时不能超过面板、缩小时不能超过初始裁剪框 let tempPanelHeight = pageY > clipBoxBeforeScalePageY ? panelHeight - clipBoxBeforeScaleClipY : clipBoxBeforeScaleHeight + clipBoxBeforeScaleClipY // 设置裁剪框高度 clipHeight = Math.min(clipHeight, tempPanelHeight) // 裁剪框位置信息 let clipX = this.getClipX(clipWidth) let clipY = this.getClipY(clipHeight) // 裁剪框底图位置信息 let clipImgX = 0 - clipX let clipImgY = 0 - clipY this.setData({ clipWidth, clipHeight, clipX, clipY, clipImgX, clipImgY, croppingImageWidth: parseInt(clipWidth / xScale), croppingImageHeight: parseInt(clipHeight / yScale) }) } 至此,图片裁剪的功能基本完成了,能够加载图片、设置裁剪框、拖动和缩放裁剪框大小,计算裁剪图片的尺寸等等。 就剩下如何真正剪裁图片了。 增加canvas并裁剪图片 要剪裁图片,我们在小程序使用canvas画布组件来处理,在页面上增加一个canvas元素: <canvas id="main" canvasId="main" class="main-canvas" style="width: {{croppingImageWidth + 'px'}}; height: {{croppingImageHeight + 'px'}}"></canvas> 1 由于这个canvas只是用来对图片进行裁剪操作,并不需要显示出来,我们可以把它定位到视觉窗口以外,不影响可操作的界面元素。 给画布设置图片裁剪所需要的宽高,通过在同等宽高下绘制图片,就能导出图片: wx.showLoading({ title: '正在裁剪...' }) preCtx.clearRect(0, 0, imageWidth, imageHeight) const width = croppingImageWidth const height = croppingImageHeight const xPos = Math.abs(clipImgX / xScale) const yPos = Math.abs(clipImgY / yScale) // 绘制裁剪区内的图片到画布上 preCtx.drawImage(imagePath, xPos, yPos, width, height, 0, 0, width, height) preCtx.save() preCtx.restore() const that = this preCtx.draw(false, function () { setTimeout(() => { // 将画布导出为临时图片文件 wx.canvasToTempFilePath({ x: 0, y: 0, width, height, destWidth: width, destHeight: height, canvasId: 'main', success: (canRes) => { wx.hideLoading() that.initImage(width, height, canRes.tempFilePath) }, fail (err) { wx.hideLoading() console.log(err) } }) }, 200) }) 如上代码所示,通过裁剪图片的真实宽高以及相对位置信息,通过canvas的 drawImage 方法,把图片的裁剪区域的内容绘制到画布上,此时,该画布就对应一张裁剪后的图片,我们只需要把画布导出成图片文件即可。 导出画布,使用 wx.canvasToTempFilePath 方法,用于将画布导出为图片临时图片文件,这个图片文件可以重新加载或者进行导出。 保存图片到相册 以上过程,生成裁剪图片的临时文件后,就可以使用小程序提供的API,将图片文件保存到相册中。 只需要使用 wx.saveImageToPhotosAlbum 方法,专门用于将图片文件保存到系统相册,接收临时文件作为参数: wx.saveImageToPhotosAlbum({ filePath: imgSrc, success: () => { wx.hideLoading() wx.vibrateShort() wx.showModal({ content: '图片已保存到相册~', showCancel: false, confirmText: '好的', confirmColor: '#333' }) } }) 该方法简单方便,其中使用 wx.vibrateShort() 方法,作用是使手机发生较短时间的振动(15 ms),在小城序也是常见的功能。 图片保存到系统相册功能完成后,我们就实现了在小程序中进行图片剪裁的完整功能,包含加载图片、图片适配和裁剪框绘制、裁剪框拖动与缩放事件、图片导出和保存的过程。 总结 本文详细讲述了在小程序中实现一个图片裁剪功能的过程,可以看出和在浏览器Web端的实现差别并不大。 在核心的图片适配、裁剪框绘制与缩放事件处理上,基本两边可以通用,在小程序中的实现逻辑可以完整在移到web浏览器上,反之亦然。 区别可能只只在于图片的加载和保存上,可以使用小程序提供的多种内置接口方法,能教方便的完成。
2023-03-12 - 【必坑指南】snapshot组件使用问题,超长截图,离屏渲染,出现空白占位?截图不完整?答案在回复里
[图片] 离屏渲染按要求设置后,截图只截了一半,哪位大神遇到过?
2023-09-13 - wxml2canvas-2d:简单易用的小程序海报、战绩等分享图片生成方案
Github 地址:https://github.com/ChrisChan13/wxml2canvas-2d 介绍 当前,众多小程序的多处场景都需要能够生成分享图便于用户进行二次传播,从而提升小程序的传播率以及加强品牌效应。 比较简单的分享图,如寥寥几行文字和一张小程序码,可以通过微信的 Canvas API 绘制。旧版 Canvas API 绘制过程繁琐,且每次绘制都需要调用 draw 方法,一不小心代码就写了上百行。 新版 Canvas API 基本与 Web Canvas 对齐,使得开发效率提高、性能得到优化。虽然免去了很多繁琐操作,但面对拥有元素众多、结构复杂的分享图片,依然解决不了代码冗长的问题。 目前开源的一些小程序图片生成方案,有的年久失修、有的依然使用旧版 Canvas API、有的使用方式不够简便,于是便有了开发 wxml2canvas-2d 的想法。 wxml2canvas-2d 的图片生成方式简单直观:首先在 wxml 页面上编写元素结构,其次在 wxss 中编写元素样式,最后调用 wxml2canvas-2d 的相关方法即可生成所需的分享图片。 wxml2canvas-2d 会通过 class 类名查询元素节点的 computedStyle 和节点属性,从而将元素节点绘制到画布上。这样做的好处是在编写 wxml 结构以及样式时,可以直观的看见样式的变化,方便调整。当然也有坏处,这个用来生成图片的“wxml 模板”,必须存在于页面上。若需要隐藏这个“模板”,只可用定位将其移至屏幕外,不可以使用 wx:if 或 hidden 隐藏。 wxml2canvas-2d 已经支持大部分常用的 CSS 属性,等你来测~ 示例 克隆此 Github 仓库,运行 [代码]npm i & npm run dev[代码],将 miniprogram_dev 文件夹导入微信开发者工具 效果预览 小程序内容: [图片] 生成的图片: [图片] 安装 npm 使用 npm 构建前,请先阅读微信官方的 npm 支持 [代码]# 通过 npm 安装 npm i wxml2canvas-2d -S --production [代码] 构建 npm 包 打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件。 使用 在页面配置中引入 [代码]wxml2canvas-2d[代码] ; [代码]{ "usingComponents": { "wxml2canvas": "wxml2canvas-2d" } } [代码] 在页面中编写 wxml 结构,将要生成画布内容的根节点用名为 [代码]wxml2canvas-container[代码] 的样式类名称标记,将该根节点内部需要生成画布内容的节点用名为 [代码]wxml2canvas-item[代码] 的样式类名称标记(文字类节点需在对应节点声明 [代码]data-text[代码] 属性,并传入文字内容)。上述两个样式类名称可以自定义,只需将对应名称传入 [代码]wxml2canvas[代码] 组件的对应属性参数即可; [代码]<!-- pages/index/index.wxml --> <view class="wxml2canvas-container box"> <view class="wxml2canvas-item title" data-text="测试标题">测试标题</view> <image class="wxml2canvas-item image" src="/your-image-path.png" /> <view class="wxml2canvas-item content" data-text="测试内容,长文本。。">测试内容,长文本。。</view> </view> <button catchtap="generateSharingCard">生成画布内容</button> <wxml2canvas id="wxml2canvas" /> [代码] 补充各个节点样式; [代码]/* pages/index/index.wxss */ .box { /* 根节点(容器)的样式 */ } .title { /* 标题的样式 */ } .image { /* 图片的样式 */ } .content { /* 内容的样式 */ } [代码] 依据 wxml 结构以及 css 样式,生成画布内容,并将生成结果导出。 [代码]// pages/index/index.js Page({ async generateSharingCard() { const canvas = this.selectComponent('#wxml2canvas'); await canvas.draw(); const filePath = await canvas.toTempFilePath(); wx.previewImage({ urls: [filePath], }); }, }); [代码] 更多内容及文档 点击此处 前往查看!如果有好的建议或者想法,也欢迎提交 Issue 或 PR~
2024-11-21 - 云函数如何接收mediaCheckAsync中的异步检测结果推送?
云函数 checkContent(审核图片) 中的 index.js const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { const { value } = event; const { OPENID } = cloud.getWXContext() try { let imageR = false; //检查 图片内容是否违规 if (value) { console.log(value) imageR = await cloud.openapi.security.mediaCheckAsync({ openid: OPENID, scene: 3, version: 2, media_type: 2, media_url: value }) } return { imageR //图片检查返回值 }; } catch (err) { // 错误处理 // err.errCode !== 0 return err } } 设置消息推送: [图片] 云函数 getCheckResult: // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 使用当前云环境 // 云函数入口函数 exports.main = async (event, context) => { return { event, } } 1、我的理解:用一个新的云函数去接收消息推送。我的这种理解对吗? 2、消息推送的事件类型怎么填?我现在的情况是只能选择(空)。事件类型为空有影响吗?
2023-02-25 - 调用mediaCheckAsync并处理异步检测结果推送
仅给出我的处理方式。如果你有更好的处理方式,欢迎在评论区讨论。 本人使用云开发,图片上传到云存储。 整体思路 具体场景(便于讨论):用户发布评论[代码]comment[代码],图片上传到字段[代码]image_upload[代码]。 准备:在数据库里创建一个[代码]traceId[代码]集合; 首先,用户上传图片,触发图片审核,并将[代码]traceId[代码]作为云存储路径; 异步检测结果推送到来时,如果[代码]'suggest' != 'pass'[代码]: 将[代码]traceId[代码]和固定前缀拼出一个云存储的[代码]fileID[代码]; 在集合[代码]traceId[代码],利用[代码]fileID[代码]作为一个字段创建一个对象 利用[代码]fileID[代码]在相关集合(如[代码]comment[代码])的相关字段(如[代码]image_upload[代码])里找:如果找到则直接删除云存储中的[代码]fileID[代码],找不到则不处理; 用户点击提交,首先先创建相关对象(如一个[代码]comment[代码]对象,其中包括字段[代码]image_upload[代码]),创建成功后查询集合[代码]traceId[代码]的所有对象(违规图片集合),将现有图片列表和违规图片集合进行比较,删除违规图片,并对相关字段(如:[代码]image_upload[代码]移除违规图片的[代码]fileID[代码])进行更新。 具体步骤和代码 1、创建云函数:[代码]checkContent[代码](右键 [代码]cloudfunctions[代码]→[代码]新建 Node.js 云函数[代码] 进行创建),上传并部署:云端安装依赖 [代码]//index.js const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { const { value, scene } = event; const { OPENID } = cloud.getWXContext() try { let imageR = false; //检查 图片内容是否违规 if (value) { imageR = await cloud.openapi.security.mediaCheckAsync({ openid: OPENID, scene: scene, version: 2, media_type: 2, media_url: value }) } return { imageR, //图片检查返回值 }; } catch (err) { return err } } [代码] 2、在某个[代码]page[代码]中的[代码]js[代码]文件中: [代码]wx.chooseMedia({ count: 9, sizeType: ['original', 'compressed'], mediaType: ['image'], sourceType: ['album', 'camera'], camera: 'back', success: res => { checkAndUploadManyImages(res.tempFiles, this) }, fail: err => { console.log(err) }, }) [代码] 其中,[代码]checkAndUploadManyImages[代码] 函数(放在[代码]Page({})[代码]之外)实现: [代码]/** * 触发图片审核 * @param {待审核图片列表} tempFiles * @param {page = this} page */ function checkAndUploadManyImages(tempFiles, page) { wx.showLoading({ title: '上传中', mask: true }) for (var i = 0; i < tempFiles.length; i++) { const { tempFilePath } = tempFiles[i] /** * 1、触发审核,获取traceId */ wx.cloud.callFunction({ name: 'checkContent', data: { value: wx.cloud.CDN({ type: 'filePath', filePath: tempFilePath }), scene: 3 //场景枚举值(1 资料;2 评论;3 论坛;4 社交日志) }, success: json => { console.log(json) const { traceId } = json.result.imageR /** * 2、将traceId作为图片的云存储路径 */ wx.cloud.uploadFile({ cloudPath: traceId, // 上传至云端的路径 filePath: tempFilePath, // 小程序临时文件路径 success: res => { const { fileID } = res //data里:fileID: []。用于存放图片云存储路径。 page.data.fileID.push(fileID) page.setData({ fileID: page.data.fileID, }) wx.hideLoading() wx.showToast({ title: '上传成功 正在审核', icon: 'none' }) }, fail: err => { console.error('uploadFile err', err) wx.hideLoading() wx.showToast({ icon: 'error', title: '上传失败', }) } }) }, fail: err => { console.log('checkContent err', err) } }) } } [代码] 至此,只是触发了图片审核。接下来配置mediaCheckAsync的异步检测结果推送。 1、创建云函数 [代码]getMediaCheckResult[代码],上传并部署:云端安装依赖 [代码]//index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database(); const traceId = db.collection('traceId') //在此之前,请数据库创建traceId集合 const head = 'cloud://xxx-xxx/' //这里请观察图片云存储路径(可使用consloe.log进行输出查看),将共同的前缀替换这里的字符串 function deleteImage(fileId) { cloud.deleteFile({ fileList: [fileId], success: res => { return res }, fail: err => { return err } }) } // 云函数入口函数 exports.main = async (event, context) => { const { result, trace_id, CreateTime } = event const { suggest } = result const fileId = head + trace_id; if (suggest != 'pass') { traceId.add({ data: { fileId, CreateTime } }).then(() => { //下面根据需要替换成自己的集合 db.collection('comment').where({ image_upload: fileId }).get() .then(() => { deleteImage(fileId); }) .catch((err) => { return err }) }) } else { return { suggest } } } [代码] 2、点击“云开发”-“设置”-“其他设置”-“添加消息推送”-进行配置(下图)-“确定” [图片] 3、回到[代码]page[代码]的[代码]js[代码]文件: [代码]//用户点击“提交”触发的事件 sendContent: function() { ... comment.add({ data: { ... image_upload: that.data.fileID, }, }).then((res) => { /** * 二、图片审核:处理异步检测结果推送 */ /** * 1、拿到全部的traceId集合(违规图片集合) */ const { _id } = res traceId.orderBy('CreateTime', 'desc').get() .then((res) => { /** * 2、删除上传图片列表中违规图片 */ deleteInvalidImages(that.data.fileID, res.data).then((res) => { /** * 3、更新图片列表 */ comment.doc(_id).update({ data: { image_upload: res } }).then(() => { wx.hideLoading() }) }) }) }) } [代码] 其中,[代码]deleteInvalidImages[代码]函数实现: [代码]/** * 删除已上传图片列表中的违规图片,并移除traceId对象 * @param {已上传图片列表} fileIds * @param {违规图片集合} cloudFileIds */ async function deleteInvalidImages(fileIds, cloudFileIds) { return new Promise(async function (resolve, reject) { try { const promises = []; let fileIdsWithoutCommon = []; promises.push(new Promise((resolve, reject) => { // 找到数组 fileIds 和数组 cloudFileIds 共有的元素 const common = fileIds.filter((elementA) => cloudFileIds.some((elementB) => elementA === elementB.fileId) ); // 删除数组 fileIds 中的共有元素 fileIdsWithoutCommon = fileIds.filter((elementA) => !cloudFileIds.some((elementB) => elementA === elementB.fileId) ); /** * 删除云存储中的违规图片 */ wx.cloud.deleteFile({ fileList: common, success: res => { console.log(res) resolve(); }, fail: err => { console.log(err) reject(err); } }) /** * 移除traceId对象 */ traceId.where({ fileId: _.in(common) }).remove({ success: res => { console.log(res) resolve(); }, fail: err => { console.log(err) reject(err); } }) })); await Promise.all(promises); resolve(fileIdsWithoutCommon); } catch (error) { reject(error); } }) } [代码]
2023-02-26 - 小程序开启skyline导致富文本真机编辑出现/n,而模拟器没问题
##富文本内容 <section data-role="outer" class="article135" label="edit by 135editor"><section label="Powered by 135editor.com" style="font-family:思源黑体;"><section style="background:#fdf7e1" class=""><section style=";-o-transform:translateZ(10px);transform: translateZ(10px);-webkit-transform: translateZ(10px);-moz-transform: translateZ(10px);-o-transform: translateZ(10px);"><section style="margin:10px auto;text-align:center;margin-top:-40px;-o-transform:rotate(0deg);transform: rotate(0deg);-webkit-transform: rotate(0deg);-moz-transform: rotate(0deg);-o-transform: rotate(0deg);" class=""><section style="display:inline-block" class=""><p style="font-size:45px;letter-spacing:2px;color:#ff6d1c;text-shadow:0.044em 0.044em 0.022em #fbeb9a"><br></p><p style="font-size:45px;letter-spacing:2px;color:#ff6d1c;text-shadow:0.044em 0.044em 0.022em #fbeb9a"><strong>绘画培训班</strong></p><section style="font-size:30px;letter-spacing:1.5px;color:#ff6d1c;margin:0px 0px 10px 0px;text-shadow:0.067em 0.067em 0.033em #fbeb9a"><strong>正在火热招人中</strong></section><section style="font-size:16px;letter-spacing:1.5px;padding:4px 1em;color:#fff;background:#fea513;border-radius:6px;box-sizing:border-box" class=""><strong>高效提升专业 培养绘画兴趣</strong></section><section style="font-size:16px;letter-spacing:1.5px;padding:4px 1em;color:#ff6d1c;margin-top:15px;box-sizing:border-box;text-shadow:0.063em 0.063em 0.063em #fbeb9a" class=""><strong>2024年11月30日正式开课</strong></section></section></section></section><section style="padding:0px 15px;box-sizing:border-box" class=""><section><section style="margin:0px auto;text-align:center" class=""><section style="display:inline-block"><section style="display:flex;justify-content:center;align-items:center"><section class="assistant" style="width:40px;margin-right:-25px;box-sizing:border-box;transform: rotate(0deg);-webkit-transform: rotate(0deg);-moz-transform: rotate(0deg);-o-transform: rotate(0deg);"><br></section><section style="font-size:16px;letter-spacing:1.5px;padding:6px 1em 6px 2em;background:#ffffff;color:#f78e05;box-sizing:border-box" class=""><strong>青山艺术启蒙班</strong></section></section></section></section></section><section><section style="margin:10px auto;text-align:center"><section style="display:flex;justify-content:flex-start;align-items:center" class=""><section class="assistant" style="width:45px;box-sizing:border-box;"><br></section><section class="assistant" style="flex:1 1 0%;height:4px;background:#ffffff;margin-top:5px;margin-left:10px;overflow:hidden"></section></section><section style="background:#ffffff;padding:1em;margin-top:-5px;box-sizing:border-box" hm_fix="300:471" class=""><section style="text-align:justify;line-height:1.75em;letter-spacing:1.5px;font-size:14px;color:#1ecaf3"><section style="text-align:justify;line-height:1.75em;letter-spacing:1.5px;font-size:14px;color:#3f3f3f" class=""><p style="vertical-align:inherit">以绘画和有针对性的绘画游戏方式,设计通过视觉、听觉、触觉等多感官的协调及多样化材料的运用,使孩子从感知到运用点、线、面进行造型,感受线、形、色的神奇变化及美感</p></section></section></section></section></section><section class=""><p style="vertical-align:inherit"><br></p></section><section style="height: 0px; overflow: hidden;"></section><section><section style="margin: 10px auto; text-align: center; height: 0px; overflow: hidden;" class=""></section></section><section style="height: 0px; overflow: hidden;"></section><section><section style="margin: 10px auto; text-align: center; height: 0px; overflow: hidden;" class=""></section></section><section><section style="margin:10px auto"><section style="border:10px solid #ffffff;padding:0px 20px 20px;box-sizing:border-box;background:#fff" class=""><section style="margin-top:20px" hm_fix="338:465" class=""><section style="text-align:center" class=""><section style="display:inline-block"><section style="font-size:16px;color:#fff;letter-spacing:1.5px;text-align:justify;box-sizing:border-box;background:#fea513;padding:6px 1em" class=""><strong>美术班开课指南</strong></section></section></section><section class="box-edit" style="margin-top:1.5em"><section style="display:flex;justify-content:flex-start;align-content:flex-start" class=""><section style="width:120px;flex-shrink:0;box-sizing:border-box;"><section style="display:inline-block"><section style="font-size:14px;letter-spacing:1.5px;padding:0px 2px;color:#333333;box-sizing:border-box">入学时间:</section><section style="max-width:100%;width:100%;height:5px;background:#fea513;margin-top:-5px;overflow:hidden;box-sizing:border-box;"></section></section></section><section style="font-size:14px;letter-spacing:1.5px;line-height:1.75em;text-align:justify;color:#464646" class=""><p style="vertical-align:inherit">2024年11月30日</p></section></section></section><section class="box-edit" style="margin-top:1em"><section style="display:flex;justify-content:flex-start;align-content:flex-start" class=""><section style="width:120px;flex-shrink:0;box-sizing:border-box;"><section style="display:inline-block"><section style="font-size:14px;letter-spacing:1.5px;padding:0px 2px;color:#333333;box-sizing:border-box">入学地址:</section><section style="max-width:100%;width:100%;height:5px;background:#fea513;margin-top:-5px;overflow:hidden;box-sizing:border-box;"></section></section></section><section style="font-size:14px;letter-spacing:1.5px;line-height:1.75em;text-align:justify;color:#464646" class=""><p style="vertical-align:inherit">青山路12单元12楼-520</p></section></section></section><section class="box-edit" style="margin-top:1em"><section style="display:flex;justify-content:flex-start;align-content:flex-start" class=""><section style="width:120px;flex-shrink:0;box-sizing:border-box;"><section style="display:inline-block"><section style="font-size:14px;letter-spacing:1.5px;padding:0px 2px;color:#333333;box-sizing:border-box">入学提示:</section><section style="max-width:100%;width:100%;height:5px;background:#fea513;margin-top:-5px;overflow:hidden;box-sizing:border-box;"></section></section></section><section style="font-size:14px;letter-spacing:1.5px;line-height:1.75em;text-align:justify;color:#464646" class=""><p style="vertical-align:inherit">注意安全,带好报名手续。</p></section></section></section></section></section></section></section><section><section style="margin:10px auto;text-align:center" class=""><p style="font-size:16px;letter-spacing:1.5px;padding:4px 1em;color:#3f3f3f;box-sizing:border-box"><br></p><p style="font-size:16px;letter-spacing:1.5px;padding:4px 1em;color:#3f3f3f;box-sizing:border-box"><strong>—END—</strong></p></section></section><section class=""><p style="vertical-align:inherit"><br></p></section><section><section style="font-size:14px;letter-spacing:1.5px;padding:4px 1em;color:#d98b5c;box-sizing:border-box;text-align:center" class=""><span style="color:#3f3f3f;font-family:微软雅黑, "Microsoft YaHei";">作者:青山小书童</span></section><section style="font-size:14px;letter-spacing:1.5px;padding:4px 1em;color:#d98b5c;box-sizing:border-box;text-align:center" class=""><span style="color:#3f3f3f;font-family:微软雅黑, "Microsoft YaHei";">文案 | 网络(侵删)</span></section></section><section class=""><section><section style="margin:10px auto;display:flex;flex-direction:column"><section style="margin-bottom:-12px"><section style="width:40px;height:20px;background-color:#d5260c;overflow:hidden;box-sizing:border-box;"></section></section><section style="background-color:#ffe26e;margin-left:7px;z-index:9"><section style="background-color:#ffffff;border:1px solid #ffe26e;padding:20px 10px;box-sizing:border-box;-o-transform:translate(-3px,-4px);transform: translate(-3px,-4px);-webkit-transform: translate(-3px,-4px);-moz-transform: translate(-3px,-4px);-o-transform: translate(-3px,-4px);"><section style="display:flex;justify-content:space-between;align-items:center"><section style="width:40%;box-sizing:border-box;max-width:40% !important;"><section style="width:110px;box-sizing:border-box;"><img style="width:258px;display:block;vertical-align:inherit;box-sizing:border-box;" title="undefined" src="https://oos-zsm.oss-cn-zhangjiakou.aliyuncs.com//mbookid/mphtml/202404/17/mphtml_ur17oLXG5g49fT6xfzJ6.jpg" data-width="100%" draggable="false" data-ratio="1" data-w="258"></section></section><section style="width:60%;box-sizing:border-box;max-width:60% !important;"><section style="display:flex;justify-content:center;align-items:center;margin-bottom:25px"><section><section style="display:flex;flex-direction:column"><section style="display:flex;justify-content:center;margin-bottom:-42px;height:40px"><section class="box-edit" style="background-color:#ffe26e;border-radius:100%;margin:0 -6px;box-sizing:border-box"><section style="width:2.5em;height:2.5em;background-color:#d5260c;border-radius:100%;overflow:hidden;box-sizing:border-box;transform: translate(-3px, -3px);-webkit-transform: translate(-3px, -3px);-moz-transform: translate(-3px, -3px);-o-transform: translate(-3px, -3px);"></section></section><section class="box-edit" style="background-color:#ffe26e;border-radius:100%;margin:0 -6px;box-sizing:border-box"><section style="width:2.5em;height:2.5em;background-color:#d5260c;border-radius:100%;overflow:hidden;box-sizing:border-box;transform: translate(-3px, -3px);-webkit-transform: translate(-3px, -3px);-moz-transform: translate(-3px, -3px);-o-transform: translate(-3px, -3px);"></section></section><section class="box-edit" style="background-color:#ffe26e;border-radius:100%;margin:0 -6px;box-sizing:border-box"><section style="width:2.5em;height:2.5em;background-color:#d5260c;border-radius:100%;overflow:hidden;box-sizing:border-box;transform: translate(-3px, -3px);-webkit-transform: translate(-3px, -3px);-moz-transform: translate(-3px, -3px);-o-transform: translate(-3px, -3px);"></section></section><section class="box-edit" style="background-color:#ffe26e;border-radius:100%;margin:0 -6px;box-sizing:border-box"><section style="width:2.5em;height:2.5em;background-color:#d5260c;border-radius:100%;overflow:hidden;box-sizing:border-box;transform: translate(-3px, -3px);-webkit-transform: translate(-3px, -3px);-moz-transform: translate(-3px, -3px);-o-transform: translate(-3px, -3px);"></section></section></section><section style="height:40px;z-index:9;display:flex;justify-content:center;align-items:center"><section style="font-size:16px;color:#ffffff;text-align:center"><strong>青山小书童</strong></section></section></section></section></section><section style="font-size:12px;color:#333333;text-align:center;line-height:1.75em">微 信 号:mbookid</section><section style="font-size:12px;color:#333333;text-align:center;line-height:1.75em" class="">新浪微博:@青山小书童</section></section></section></section></section></section></section><p style="vertical-align:inherit"><br></p></section></section></section><section class=""><p><br></p></section></section></section> 开启skyline的情况下模拟器调试不出现\n [图片] 开启skyline真机调试或者发布正式使用均 真机出现\n [图片] 如果不使用skyline模式则模拟器真机均不会出现\n问题 @微信开放社区 #微信开放社区 #skyline #skyline问题
2024-06-04 - editor 组件获取焦点推起时候页面不会上推 怎么解决?
editor 组件获取焦点推起时候页面不会上推(是部分机型不会例如华为) 并且editor 组件也没有adjust-position设置 没有办法自己去设置上推界面
2023-12-25 - 新建Ts小程序模版构建npm错误 ,没有找到可以构建的 NPM 包,请确认需要参与构建的 npm 都在 `minipro
没有找到可以构建的 NPM 包,请确认需要参与构建的 npm 都在 `miniprogramRoot` 目录内,或配置 project.config.json 的 packNpmManually 和 packNpmRelationList 进行构建 录内,或配置 proje[图片]ct.config.json 的 packNpmManually 和 packNpmRelationList 进行构建 1、首先确实是先初始化过了 ``` npm init ``` 2、如果已经初始化以后,项目根目录找到project.config.json文件,在setting关键字,里面增加 "packNpmManually": true, "packNpmRelationList": [ { "packageJsonPath": "./package.json", "miniprogramNpmDistDir": "miniprogram/" } ] 3、然后在npm安装你想要的包,就可以正常构建了 npm i tdesign-miniprogram -S --production
2022-10-29 - 微信云函数如何解决@cloudbase/manager-node依赖问题?
本地调试能正常运行, 云端就不行, 删除后重新部署也没用, 目测可能是安装依赖的问题. 这两个都按了, 没效果. [图片] 求各路大佬帮帮忙, 谢谢 目的: 小程序读取云储存目录下的所有图片并展示 附云函数代码和调用的代码 //这是云函数代码 const cloud = require('wx-server-sdk') const CloudBase = require('@cloudbase/manager-node') const { storage } = new CloudBase() cloud.init({ // 初始化 env: '环境ID'//只有一个环境ID, 是正确的 }) exports.main = async (event, context) => { const list = await storage.listDirectoryFiles('SmartCampus/data/home/carousel/') console.log("获取目录下文件列表", list); return { data: { fileList: list }, } } //在home.js里面调用云函数 wx.cloud.callFunction({ name: 'getHomeCarousel', data: {} }).then(res => { console.log("test", res.result.data.fileList); var newArr = res.result.data.fileList.slice(1);//去掉第一个 that.setData({ arrPic: newArr }) }) //app.js里面也初始化了 wx.cloud.init({ traceUser: true, nev: '略' });
2022-07-12 - 一张表解决云存储的七大痛点
就是这张表: Collection: material { _id, _openid, createTime, cat,//分类。比如衣服、帽子 tag,//标签。比如产品号等 fileID,//cloud云存储路径 url,//Cloud.getTempFileURL获取的http路径,云存储权限设置为公有读, type,//img, video, file size, name,//上传前文件名 ext,//文件后缀 } 说明: 1、用一张表保存所有云存储文件的信息; 2、文件上传后,将相关信息保存在集合中。 3、任何地方引用图片src,都是使用表中的url,而不是使用fileID, 解决了以下痛点: 痛点一、云存储里有哪些文件,有哪些垃圾文件? 痛点二、云存储某文件夹下有哪些文件?怎么删除云存储文件夹?不熟悉cloud base node sdk或者manage sdk的同学,一定搞不定这个痛点; 痛点三、图片太大,我想用腾讯云图像处理进行压缩裁剪?fileID不支持,只能用url; 痛点四、跨云环境访问图片,不支持fileID,只能用url; 痛点五、在前端引用url,但是删除图片做不到。即通过url,不知道fileID是什么,删除不了云存储文件; 痛点六、前端可以统一管理图片,素材库,而不是在某流程中上传文件后,完全不管理它; 痛点七、可对所有文件图片,分类、贴标签,按openid检索,按type检索,各种姿势检索。 可能还有其他好处,不多介绍。 总之,无论如何,你应该需要这样一张表。
2022-07-12 - 不用云函数,云开发如何获取openid?
极简代码如下: app.js: App({ getOpenid: async function () { let col = 'test'//任意一个未修改过权限的集合 let res = await db.collection(col).get() if (res.data.length) return res.data[0]._openid await db.collection(col).add({ data: {} }) return (await db.collection(col).get()).data[0]._openid }, }) page.js: const app = getApp() Page({ onLoad: async function () { this.openid = await app.getOpenid() console.log('openid:', this.openid) } }) 这应该是史上最简的获取openid代码了。 并且,解决了获取openid异步问题。
2022-12-05 - 富文本editor怎么实现首行缩进?
可以通过 this.editCtx.format('textIndent', '2em') 的方式实现
2019-09-16 - 🎆我们开源啦 | 基于Skyline开发的组件库🚀
我们开源啦,希望可以给大家的开发之旅带来一些灵感。我后溪的小程序也都会基于这个组件库开发,并且会保持组件库的更新与维护。 我是第一次进行开源,肯定会有错漏,欢迎大家指正,我会以最快的时间响应修改。 Skyline UI 组件库 前言 Skyline 是微信小程序推出的一个类原生的渲染引擎,其使用更精简高效的渲染管线,性能比 WebView 更优异,并且带来诸多增强特性,如 Worklet 动画、手势系统、自定义路由、共享元素等。 使用这个组件库的前提是:通过微信小程序原生+skyline框架开发,所以目前我们不保证兼容webview框架(也就是电脑端与低版本的微信),但后续会进行系统性的兼容。 使用 Skyline UI前,请确保你已经学习过微信官方的 微信小程序开发文档 和 Skyline 渲染引擎文档 。 背景 随着Skyline 渲染引擎 1.1.0 版本发布,我们所运营的小程序也平稳的渡过了阵痛期,团队使用Skyline也越来得心应手,所以接下来,团队的开发重心全面偏向Skyline渲染框架,考虑有大量的UI交互重复,我们决定基于Skyline开发了这个UI组件库。 但团队力量有限,这个新生的组件可能有很多的不尽如人意,所以希望能以开源的方式吸引更多开发者使用Skyline框架,如果这个框架不适合你,也可以借鉴其思路。 Gitee Gitee仓库 在线预览 以下是目前两个使用该框架的小程序 SkylineUI组件库 [图片] NONZERO COFFEE [图片] 开始使用 UI库结构 Skyline UI组件库 依赖于以下四部分,具体使用参考以下的具体说明 utils工具库: 其中包含了UI库自定义的一个工具类SkyUtils,它包含了组件中所含的各种函数,非常重要。 各组件元素:sky-*(组件名) skywxss样式库:其中包含深浅色色彩、文字字体、布局等样式wxss 在小程序中引入 UI库 一、直接下载引入 点击下载组件包 将src下所有文件复制到您项目根目录下的components文件夹中,没有的话请自行新建。 二、npm引入 1.在小程序项目中,可以通过 npm 的方式引入 SkylineUI组件库 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 2.安装组件库 [代码]npm install jieyue-ui-com [代码] 3.npm 命令执行完后,需要在开发者工具的项目中点菜单栏中的 工具 - 构建 npm 两种引入方式的不同可能导致后续使用时,引用组件的路径不同,请注意区别 1.直接引入components文件夹内,引用地址通常是 ‘./components/‘ 2.npm引入,组件引用地址通常是’./miniprogram_npm/jieyue-ui-com/’ 如何使用 1.在app.js文件中初始化工具类,并且添加两个全局变量 [代码]// app.js App({ onLaunch() { ;(async ()=>{ // 全局注册工具类SkyUtils // 这里默认npm引用,地址为'./components/utils/skyUtils',如果是直接引用组件,地址可能是'./components/utils/skyUtils',后面不再说明 const SkyUtils = await import('./components/utils/skyUtils'); wx.SkyUtils = SkyUtils.default; // 初始化设备与系统数据 wx.SkyUtils.skyInit() // 小程序自动更新方法 wx.SkyUtils.versionUpdate() })() }, globalData: { sky_system:{}, sky_menu:{} }, }) [代码] 2.在app.wxss文件中引入样式文件 [代码]//wxss * _dark.wxss 是适配深色模式的色彩变量 @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor_dark.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfontline.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfont.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyother.wxss'; [代码] 3.page.json中引用组件 [代码]//page.json { "usingComponents": { "sky-text":"/miniprogram_npm/jieyue-ui-com/sky-text/sky-text" } } [代码] 4.页面中使用 [代码] // wxml <sky-text content="文本内容" max-lines="2" fade></sky-text> [代码] 5.其他组件具体使用请参考组件包中的redeme.md 适配深色模式 如果您在开发时,全部使用我们预设好的颜色变量,那么可以自动适配深色模式。 [代码].page{ background-color: var(--bg-l0); } [代码] [代码] <view style="background-color: var(--bg-l0)"></view> <view style="background-color: {{color}}"></view> [代码] [代码] Page({ data: { color: "var(--bg-l0)" } }) [代码]
2024-01-09 - 小程序渲染引擎Skyline小试牛刀--快书
今年年初,在官方文档上看到小程序团队要推出一款性能逼近原生的渲染引擎Skyline,就一直在关注。刚好最近打算做一款新的阅读小程序,作为一名独立开发者,对于性能和用户体验的追求是永无止境的,于是我决定用纯Skyline打造这款小程序。 当然,这个项目里面所用到的skyline特性只是冰山一角,并非全部,更多酷炫的特性请前往官方文档查阅。 接下来,我会结合快书小程序,从以下几个方面,逐条阐述关于skyline特性(快书项目中所用到的)的理解与应用: 效果演示。如何开启Skyline。新版组件swiper。新版组件scroll-view。全新组件snapshot。增强特性worklet动画。增强特性手势系统。增强特性自定义路由。增强特性共享元素动画。希望对于刚接触Skyline,或者想要了解Skyline的同学有所帮助。当然,如有错误或遗漏,欢迎在评论区批评指正,不胜感激。 一、效果演示 [图片] 二、如何开启Skyline 开启Skyline的方式非常简单,只需要在app.json文件中,加入以下配置即可(这里是全局Skyline,若只打算指定页面开启,则在指定页面的json文件中配置即可): "renderer": "skyline", "lazyCodeLoading": "requiredComponents", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, } }, "componentFramework": "glass-easel", 三、新版组件-Swiper 旧版的Swiper基于webview的,在性能上有所局限,特别是当swiper-item的数量动态不断增加的情况下。当然,也可以自己想办法去优化,比如做懒加载和缓存,但相对来说比较麻烦。而Skyline版本的Swiper性能会大幅度提升,首先渲染引擎本身的性能提升了,另外官方也做了缓存的功能,只需要通过定义cache-extent的值,就能轻松定义缓存区域大小,例如值为 1 则表示提前渲染上下各一屏区域。 [图片] 用法上,和webview版本没有太大区别(这里就不放代码了),只需注意不要使用某些webview独有的特性即可。 四、新版组件-Scroll-view 同样,旧版的scroll-view也基于webview的,滚动元素过多的时候会有明显卡顿,当然也是可以通过虚拟Dom的方式自行优化。然而,Skyline版本的scroll-view官方已经实现了只会渲染在屏节点的特性,大大提升了滚动的流畅度,真正做到了开箱即用。 用法上,有以下几个点要注意的。 指定type属性,有2个可选值,分别为:list和custom,对应的是列表模式和自定义模式。如是普通列表,list即可,如果是稍微复杂的列表,比如常见的瀑布流表现形式(类似小红书那样),则可使用custom。只有直接子节点才能根据是否在屏来按需渲染。即你不能把你的列表项,都放在同一个父级view中,而是应该直接放在scroll-view组件下。 // 错误的方式: <scroll-view type="list" scroll-y> <view> <view class="item" wx:for="{{dataList}}" wx:key="id"></view> </view> </scroll-view> // 正确的方式 <scroll-view type="list" scroll-y> <view class="item" wx:for="{{dataList}}" wx:key="id"></view> </scroll-view> // 正确的方式 <scroll-view type="custom" scroll-y> <list-view> <view class="item" wx:for="{{dataList}}" wx:key="id"></view> <list-view> </scroll-view> 另外,上面提到了瀑布流的问题,实现方式也很简单,官方提供了一个叫做grid-view的组件,只需定义它的type="masonry"即可,但若是在webview下,除了性能不理想以外,还会有一些小BUG,比如我在社区提的这个问题:grid-view masonry 在webview模式下经常会出现大块区域的空白。在Skyline下,就不会出现此类问题。 [图片] <scroll-view type="custom" scroll-y> <grid-view type="masonry" main-axis-gap="15" cross-axis-gap="15"> <view wx:for="{{dataList}}" wx:key="id"></view> </grid-view> </scroll-view> 五、全新组件Snapshot 我们常常会有分享精美海报的需求,但由于海报上的内容是动态,仅仅使用一张图片分享达不到我们的目的。在以往,我们可能会使用到wxml-to-canvas,通过绘制 canvas ,导出图片。现在,在Skyline下(基础库3.0.0以上),实现此需求就非常简单。只需要将我们要分享的内容包裹在snapshot组件下就行。 [图片] // wxml: <snapshot id="target"> <view>content</view> </snapshot> // page: Page({ onReady() { this.createSelectorQuery() .select("#target") .node() .exec(res => { const node = res[0].node node.takeSnapshot({ type: 'arraybuffer', format: 'png', success: (res) => { fs.writeFileSync(savePath,res.data,'binary'); //图片保存至本地 wx.showShareImageMenu({ //唤起分享图片的界面 path:savePath }) }, fail(res) {} }) } }) 六、增强特性-worklet动画 worklet动画相比传统的方式,流畅度提升了不少,但如何使用呢?常见的普通动画无非是对于页面元素的平移,缩放,旋转等变换。那么,要让一个元素动起来,只需要做以下2件事: 将页面元素的样式与某个变量进行绑定,变量值的变化会自动触发样式的更新。实时动态地改变这个变量。结合快书的例子(下拉时,让页面缩小,松手后,页面弹回),来看一下具体的实现步骤。 [图片] 首先,如何绑定样式与参数呢?通过官方提供的一个applyAnimatedStyle函数: // Wxml: <view id="#box">content</box> // Page: this.scale = shared(1); //这里是定义一个共享变量,即可在UI线程和JS线程间同步的变量。 this.applyAnimatedStyle(`#box`, () => { 'worklet'; // 声明这是一个worklet函数 return { transform: `scale(${this.scale.value})`, }; }); // 1、这里使用共享变量是为了让后续改变这个变量时,worklet的函数能捕获到。 // 2、#box你要动起来的元素 // 3、当this.scale.value变化时,会自动触发函数体的执行,从而改变#box的样式 第二步,下拉时,根据下拉的偏移量,改变这个scale的值。 this.scale.value = (evt.deltaY / 100) * 0.15; // 这里的evt.deltaY是下拉时的位置偏移量,然后根据偏移量按比例计算缩放的值 // 如何获取这个下拉偏移量?下一小节讲手势系统时会讲到 第三步,松手时,复原scale的值。 this.scale.value = timing(1, { duration: 300, easing: Easing.ease }); // timing函数表示:在300毫秒内,scale.value会逐渐变成1 // easing: Easing.ease 表示缓动的方式,具体可参考https://easings.net // 如何知道已经松手了?下一小节讲手势系统时会讲到 更多动画参考请查阅官方文档。 七、增强特性-手势系统 还是上面那个例子,我们只说了下拉时根据下拉的偏移量改变scale的值,那如何得到下拉的偏移量呢?这里就涉及到了手势系统。下面讲讲如何让一个元素能响应拖动,缩放等手势。 说回上一小结的例子,我们只需要讲#box元素包裹在手势组件vertical-drag-gesture-handler即可。更多示例可查阅官方文档 // wxml: <vertical-drag-gesture-handler worklet:ongesture="handlePan"> <view id="box"></view> <vertical-drag-gesture-handler> // page: handlePan(evt) { 'worklet' if (evt.state === GestureState.ACTIVE) { // 拖拽时 if (evt.deltaY > 0) { // 下拉 this.scale.value = Math.max(this.scale.value - (evt.deltaY / 100) * 0.15, 0.85); } else { // 上拉 this.scale.value = Math.min(this.scale.value - (evt.deltaY / 100) * 0.15, 1); } } else if (evt.state === GestureState.END || evt.state === GestureState.CANCELLED) { // 拖拽结束或取消 this.scale.value = timing(1, { duration: 300, easing: Easing.ease }); } }, 然而,当手势组件与scroll-view等可以滚动的组件嵌套时,会出现冲突的问题。比如,同上一小节的示例,为了让文章内容过长时可以滚动,我们需要将文章的内容放在scroll-view中。当scroll-view已经滚动到顶部,再继续下拉的话,应当触发手势组件的拖拽事件,即缩放页面。相反,则继续滚动scroll-view。 [图片] // wxml: <vertical-drag-gesture-handler tag="pan" worklet:ongesture="handlePan" shouldResponseOnMove="shouldPanResponse" simultaneousHandlers="{{['scroll']}}"> <vertical-drag-gesture-handler tag="scroll" native-view="scroll-view" shouldResponseOnMove="shouldScrollResponse" simultaneousHandlers="{{['pan']}}"> <scroll-view type="list" scroll-y bindscroll="handleContentScroll">文章内容</scroll-view> </vertical-drag-gesture-handler> </vertical-drag-gesture-handler> // page: // 处理scroll-view的滚动事件,获取scrollTop的值 handleContentScroll(evt) { 'worklet' this.scrollTop.value = evt.detail.scrollTop; }, // return false 则表示scroll-view不再响应滚动事件 shouldScrollResponse(evt) { 'worklet'; const { deltaY } = evt if (this.scrollTop.value <= 0 && deltaY > 0) { //scroll-view已经滚动到顶部,继续下拉时 this.pan.value = true; return false; } if (this.scale.value < 1 && deltaY < 0) { //#box已经被缩放,继续上拉时 this.pan.value = true; return false; } this.pan.value = false; return true; }, shouldPanResponse() { 'worklet' return this.pan.value; // true表示响应手势组件的拖拽事件,false则不响应 }, 八、增强特性-自定义路由 以往在webview中,路由的的过渡动画仅支持从右到左,较为单调。在skyline之后,我们可以自定义路由的过渡动画了,比如常见的淡入淡出,从底部弹起等。比如以下这个例子,从首页点击图片,会跳转到分享的页面,这里就是用自定义路由实现的淡入效果。 [图片] 自定义路由的使用相比前几个特性稍微复杂一点,这里官方讲的更为具体和清晰,可查阅官方文档。唯一要注意的一点是,只有连续的skyline页面跳转时,才会有效果。 九、增强特性-共享元素动画 还是上面的例子,当从首页点击图片跳转到分享页面时,图片看起来像是从首页飞到了分享页,这里便是使用到了共享元素动画。我同时也做Flutter的开发,所以这里看起来非常类似Flutter的hero动画或者叫飞行动画。 使用方式也类似于Flutter。将2个页面的相似组件都用share-element组件包括起来,并且使用相同的key即可。再配合自定义路由,可使得飞行动画看起来非常的丝滑。 // A页面: <share-element key="唯一key"> <image src="imagePath" mode="aspectFill" /> </share-element> // B页面: <share-element key="唯一key"> <image src="imagePath" mode="aspectFill" /> </share-element> // 有几个要注意的地方 // 1、两个个页面的share-element组件必须使用相同的key。 // 2、key是唯一的,即同一个页面中,不能出现重复的key。 // 3、image不要写死宽高,应百分比100%,具体宽高数值写在share-element组件上。 有一个常见的问题,A页面是一个列表,B页面是详情页,列表中的数据都是通过接口从后台返回的,由于共享元素的key又不能重复,那么这个key怎么定义?一般后台返回的数据都会有一个唯一标识,假设为ID,我们可以用这个ID当作Key。 但是,另一个问题来了,如果数据是后台接口返回的,然后通过setData的方式响应到页面,那么很有可能B页面的首帧获取不到这个Key,因为这时候接口请求可能还未完成,那么动画也是不会生效的。针对这种情况,官方也提供了一种方式: 共享元素动画需保证下一个页面首帧即创建好 [代码]share-element[代码] 节点,并设置了 key,用于计算目标位置。如果是通过 [代码]setData[代码] 设置的,可能会错过首帧。针对这种情况,可以 使用 Component 构造器构造下一个页面,只要在组件 [代码]attached[代码] 生命周期前(含)通过 [代码]setData[代码] 设置上去,就会在首帧渲染 十、总结 Skyline还有一些其他有趣的特性,大家感兴趣的话可以查阅官方文档。总的来说,相比起webview,skyline对性能的提升是显而易见的,并且,一些在webview很难实现的效果,在skyline的基础上,也能轻易实现,开箱即用。目前,skyline还在不断地迭代中,还有许多的新特性还在评估和开发中,相信之后的版本会更完善更好用。 最后,大家多多使用快书呀,球球了~ [图片]
2023-09-01 - 小程序app.onLaunch与page.onLoad异步问题的最佳实践
场景: 在小程序中大家应该都有这样的场景,在onLaunch里用wx.login静默登录拿到code,再用code去发送请求获取token、用户信息等,整个过程都是异步的,然后我们在业务页面里onLoad去用的时候异步请求还没回来,导致没拿到想要的数据,以往要么监听是否拿到,要么自己封装一套回调,总之都挺麻烦,每个页面都要写一堆无关当前页面的逻辑。 直接上终极解决方案,公司内部已接入两年很稳定: 1.可完美解决异步问题 2.不污染原生生命周期,与onLoad等钩子共存 3.使用方便 4.可灵活定制异步钩子 5.采用监听模式实现,接入无需修改以前相关逻辑 6.支持各种小程序和vue架构 。。。 //为了简洁明了的展示使用场景,以下有部分是伪代码,请勿直接粘贴使用,具体使用代码看Github文档 //app.js //globalData提出来声明 let globalData = { // 是否已拿到token token: '', // 用户信息 userInfo: { userId: '', head: '' } } //注册自定义钩子 import CustomHook from 'spa-custom-hooks'; CustomHook.install({ 'Login':{ name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } }, globalData) // 正常走初始化逻辑 App({ globalData, onLaunch() { //发起异步登录拿token login((token)=>{ this.globalData.token = token //使用token拿用户信息 getUser((user)=>{ this.globalData.user = user }) }) } }) //关键点来了 //Page.js,业务页面使用 Page({ onLoadLogin() { //拿到token啦,可以使用token发起请求了 const token = getApp().globalData.token }, onLoadUser() { //拿到用户信息啦 const userInfo = getApp().globalData.userInfo }, onReadyUser() { //页面初次渲染完毕 && 拿到用户信息,可以把头像渲染在canvas上面啦 const userInfo = getApp().globalData.userInfo // 获取canvas上下文 const ctx = getCanvasContext2d() ctx.drawImage(userInfo.head,0,0,100,100) }, onShowUser() { //页面每次显示 && 拿到用户信息,我要在页面每次显示的时候根据userInfo走不同的逻辑 const userInfo = getApp().globalData.userInfo switch(userInfo.sex){ case 0: // 走女生逻辑 break case 1: // 走男生逻辑 break } } }) 具体文档和Demo见↓ Github:https://github.com/1977474741/spa-custom-hooks 祝大家用的愉快,记得star哦
2023-04-23 - 关于app.js 中onLaunch执行异步函数执行完再执行页面onLoad的问题?
比如在onLaunch中执行异步登录后,首页或者其他页面需要等到这个登录成功或者失败后才能才能在页面中做具体展示,在首页中这个问题可以解决,在app.js中执行完异步操作后,可以使用this.callback,然后在首页中使用this.callback = res => {}做下一步操作,但是除了首页,其他所有页面作为加载的第一个页面调用this.callback = res => {}这个方法都会报错 _this2.callback is not a function at app.js? [sm]:77。这个问题还挺困扰的,因为除了首页,其他很多页面也会因为分享而会成为小程序加载的首页。 先在这里贴出app.js和index.js的相关代码,希望可以获得帮助。 app.js App({ onLaunch() { wx.login({ success:res=>{ this.callback(res) } }) } }) index.js Page({ data: {}, onLoad(query) { app.callback = res => { //todo do something } } })
2023-08-29 - 小程序已完成备案,注销时会不会对主体下其他小程序有影响?
小程序已完成备案,注销时会不会对主体下其他小程序有影响?
2023-09-20 - eCPM单价下降,同领域账号当家三四十甚至七八十,而我只有几块?
如题,刚开始发文eCPM还维持在三十多的水平,从去年开始一直在下降,现在只剩下三四块,明明点击率还上升了,单价却下降了?[图片]
2023-01-31 - 代码加固,提示加固无法发起,加固配置内没有合法的js文件,请问是什么原因?
如题,正常编译、调试、运行都是OK的。加固配置文件内容如下: { "desc": "关于本文件的更多信息,请参考文档 代码加固开发者文档: https://developers.weixin.qq.com/miniprogram/dev/devtools/code_obfuscation.html", "switch": true, "configs": [ { "path": "app.js", "sub_switch": true } ] }
2023-05-10 - Skyline|原生级卡片转场,小程序轻松实现
在上一篇文章《在小程序中实现原生相册》中,我们学习了自定义路由搭配共享元素实现的原生相册效果,共享元素可以让用户在体验小程序时视觉关联性更强。 除了相册实现之外,常见的卡片转场也非常适合。 [图片] ⬆️ 演示效果:默认动画 vs 卡片转场动画 👇 下面我们来看看卡片转场中通过 共享元素 + 自定义路由 来实现无痕跳转。 [图片] 这里的转场稍微有点复杂,涉及到以下 3 个点 旧卡片:图片放大、内容渐隐新页面:按比例放大、页面渐显手势搭配1、旧卡片:图片放大、内容渐隐 在本示例中,列表页采用的是 scroll-view 瀑布流布局的实现。 [图片] 这里我们的共享元素是卡片,即 grid-view 中的内容 card,卡片包括 图片、内容描述。 [图片] 默认情况下,共享元素是整个节点进行飞跃的,由于前后页面的图片元素一致但文本内容不一致, 导致在第一帧或者最后一帧会有跳动的效果。 为了让转场动画更加自然,我们需要在飞跃的过程中渐隐旧卡片的内容描述。 [图片] 在这里,我们需要先用 this.applyAnimatedStyle 来给对应的节点绑定 worklet 驱动动画。 .card_wrap 节点:整个卡片按比例放大.card_desc 节点:内容描述渐隐[图片] 关于动画执行的时机,我们可以通过配置项修改。 immediate:设置是否立即执行驱动动画flush:shareValue 更新时,applyAnimatedStyle 的 updater 函数刷新时机在本例中,需要保证共享元素的图片与目标页面图片位置重叠,所以 flush 设置 sync 在当前时间片刷新。 [图片] 绑定完驱动动画之后,我们需要给共享元素绑定帧回调事件,根据当前动画进度改变共享变量的值来驱动共享动画 [图片] 2、新页面:按比例放大、页面渐显 新页面在路由中的动画,需要在自定义路由中进行配置。关于自定义路由的更多介绍,可参考《小程序页面转场动画》 在路由动画过程中,我们将上一步的共享元素帧回调拿到 begin、end 的值,然后结合动画进度 t 计算得出新页面的位置、缩放比例。 还有根据动画进度,设置页面渐显,与前面的卡片渐隐承接。 [图片] 3、手势搭配 学习过我们前面的文章的同学都知道,自定义路由经常需要结合页面手势,来实现手势返回,关于手势的基础知识可参考《小程序页面转场动画》 [图片] 这里我们希望手势缩小整个当前页面,所以这里手势返回时只在当前页面做手势动画即可。 在页面详情页的最外层,嵌套一个手势组件 pan-gesture-handler,当手势拖动时根据手势的位置改变整个页面(通过 #fake-host 控制)的位置和大小来达到拖动的效果。 [图片] 同样绑定页面驱动动画,通过 applyAnimatedStyle 给 #fake-host 绑定驱动动画,当共享变量 transX、transY 等变化时则自动改变 transform 来驱动 #fake-host 缩小。 [图片] 接着绑定手势事件,根据手势拖动时拿到位置信息改变共享变量 transX、transY 的值。 [图片] 最后我们需要设置背景颜色透明,来达到类似把卡片拖回列表的视觉效果,更好的减少页面切换感~ [图片] 一个自定义路由的页面会有 3 层可以设置到背景色,要做到透明的效果需要将 3 个背景色都设置为透明。更多自定义路由背景色的详情参考官方文档。 [图片] 想要试试卡片转场的无恒效果~扫描 ⬇️ 下方小程序码即可体验。 如果你也想在小程序中实现卡片转场动画,mark 下这个 源码 直接接到到你的小程序吧~ [图片]
2023-08-03 - Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - wx.cloud.upLoadFIle 上传excel到云存储,IOS端报错,错误码400?
[图片][图片] 安卓端测试没问题,求解决,要疯掉了!!!
2022-05-25 - 滥用分享行为
违规内容 小程序提供的服务中,不得存在滥用分享违规行为。如强制用户分享行为;分享立即获得利益的诱导行为;通过明示或暗示的样式来达到诱导分享目的;以及蹭流量诱导分享获得红包封面的行为等。包括但不限于以下类型: ▶1 强制分享后才能继续下一步操作。包括但不限于分享后才可解锁功能或能力,分享后才可查阅、下载图片或视频等。 [图片] ▶2 以分享后无需互动或无需深度互动即可获得利益的方式诱导分享至群或好友。“深度互动“是指被分享者理解被分享内容并主动参与活动或业务流程,从而执行的进入页面、点击等一系列相关操作。“利益”包括但不限于:现金奖励、实物奖品、虚拟奖品(红包、优惠券、代金券、积分、话费、流量、信息等)。 2.1 无互动分享获利:诱导分享至群或好友,完成分享操作立即可获得续命机会/积分/金币等。 [图片] 2.2 无深度互动分享获利:诱导分享至群或好友,被分享者仅需访问小程序,分享者即可获得利益等。 [图片] ▶3 通过利益诱导分享至朋友圈,包括但不限于:现金奖励、实物奖品、虚拟奖品(红包、优惠券、代金券、积分、话费、流量、信息等)。 [图片] ▶4 客服消息推送外链内容或者微信公众帐号文章诱导分享至朋友圈。 [图片] ▶5 存在暗示性诱导分享内容,包括但不限于红点提醒等。 [图片] ▶6 虚拟业务小程序中存在被分享者可获利的诱导增加新用户行为。 [图片] ▶7 小程序中存在未经用户允许或授权,披露他人好友关系或用户隐私给陌生人关系(非微信好友关系)的诱导分享行为。 [图片] ▶8 小程序存在过度营销分享行为:如果同一主体、关联主体或者同一帐号、关联帐号或平台认为有联合行动的多个帐号下的小程序分享量过大且分享转化率过低,将会被认为可能存在过度营销分享行为,相关小程序的跳转至APP的能力、分享能力等将会被自动限制,同时小程序关联的微信开放平台开发者帐号分享小程序能力也将被自动限制。 [图片] ▶9 小程序分享活动未清晰明确活动规则,存在误导用户的行为。包括但不限于以下类型: 9.1 小程序内分享活动未明确告知用户活动完整信息,包括不限于整体规则、活动步骤和互动利益结果。在小程序分享活动流程中以概率事件或者不确定的获利结果诱导用户参与。 [图片] 9.2 小程序内分享活动存在标题党式信息进行夸大误导宣传,无法保证活动信息的真实性和准确性。 [图片] 9.3 小程序内分享活动相关信息未在页面显著标识,或使用小微字体、灰色字体等让用户难以注意、识别等方式影响用户获取信息和阅读的行为。 9.4 小程序内分享活动收益和最终收益要求用户长期参与才可获取,不满足即时性要求。 [图片] ▶10 小程序内组队组团类分享活动的参与总人数未限制在5人内。 [图片] 处理规则 一经发现将根据违规程度对该小程序阶梯封禁分享能力或朋友圈二维码识别能力直至封号处理。
2021-12-13 - 视频号重磅更新,可以直接跳转小程序了。
近期,视频号重大更新,不用自定义交易组件也可以跳转小程序了。视频号的流量将全面导入小程序。 视频号推出了服务菜单功能,只要是认证的企业号,可以添加客服和小程序任意页面。一共可以添加6个。 需要注意的是,小程序页面路径一定要加上后缀【.html】,不然可能打开就是未知页面了。 [图片] [图片]
2022-03-02