- 小程序帐号登录规范要求与修改指引
为更好地保护用户隐私信息,优化用户体验,平台对小程序内的帐号登录功能进行规范。“帐号登录功能”是指开发者在小程序内提供帐号登录功能,包括但不限于进行的手机号登录,getuserinfo形式登录、邮箱登录等形式。一、登录规范规则,你需要了解:[图片] 一、「体验范围开放」与「体验范围特定」区分与对应整改建议:1、“体验范围开放”定义:①直接打开即可体验 ②有账号限制,但有注册流程是对外开放 整改建议: ①授权个人信息功能后置,给与用户充分了解、体验功能服务后,再由用户主动点击进一步功能触发登录授权 ②授权同时,亦同时支持给与用户取消登录的权利 案例解析: ①范围开放-登录规范违规案例解析 违规点:服务范围开放,首页打开即要求授权登录,用户未体验功能服务强制要求授权登录,登录规范不合规。 [图片] ②范围开放-登录环节无取消/拒绝登录按钮案例解析 违规点:体验范围开放,用户体验功能服务后自主触发登录,提供取消/拒绝登录按钮,但点击取消/拒绝登录按钮无响应,仍强制要求登录,无法取消/拒绝登录。 [图片] 2、体验范围特定 定义: 体验范围提供给特定人员使用、对外无开放注册流程,例如为学校系统、员工系统、社保卡信息系统等提供特定服务的小程序 整改建议: ①首页有明显的使用范围(特定范围)说明 ②首页即要求授权来拉取身份鉴定类,需要为用户提供暂不登录/取消登录选项按钮 案例解析: ①特定范围-登录违规案例解析 违规点:打开首页即要求授权登录,无任何说明,不符合登录规范要求 [图片] 这是一份动态更新的文档,辅助开发者了解登录规范要求,避免开发者因登录规范问题审核失败导致无法按期发布上线,开发者如有其他疑问,可以通过目前开放的咨询渠道反馈: 1、微信开放社区-交流专区-小程序发帖咨询-提出问题-运营相关问题 2、驳回站内信通知-客服咨询入口(MP代码审核客服入口正处于灰度开放中,若未获得灰度测试入口,开发者可前往社区发帖咨询) 我们会根据新出现的问题、相关法律法规更新或产品运营的需要及时对其内容进行修改并更新,制定新的规则,保证微信用户的体验。建议开发者反复查看以便获得最新信息,定期了解更新情况。
2020-12-24 - wx.openOfflinePayView接口 还能申请吗?
wx.openOfflinePayView接口 还能申请吗?
2024-07-12 - taiwindcss在小程序中的应用
css 工程化思想有很多(sass,less,css-in-js…),基于原子类书写思想的 taiwindcss(以下用 tw 代替)风头很盛,去年底 tw3 上线,搭配新的引擎让[代码]just-in-time[代码]模式平稳落地。 tw 的思想和好处这里不在赘述,官网介绍的很清楚(tw2 中文官网)。 这里主要描述下如何在小程序中使用tw书写样式。 题外话:很难想象前端最佳实践到来的那一天,但 css 最佳实现或许已初见苗头… tw书写小程序 tw3 是即时(实时)生成类样式。tw 会根据您的配置文件,匹配您指定文件中包含的关键字,自动生成对应配置的样式。 在小程序中,只需要在 app.wxss 中引入 tw 生成的样式文件(记得组件中配置 addGlobalClass:true),就不必在书写组件时,关心 wxss 文件了。 base类 base类好比预设一些样式 (由于官方默认配置基于web端,小程序中只能自己定义了) [代码] /* wxss */ page { height: 100%; line-height: 1.2; -webkit-text-size-adjust: 100%; font-family: "fontFamily.sans", ui-sans-serif, system-ui" } [class], view, image, ::before, ::after{ box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; } /* variables */ page{ --color-primary:#22c55e; --color-primary-dark: #16a34a; /* ... */ } @media (prefers-color-scheme: dark) { page { --color-primary:#ef4444; --color-primary-dark: #dc2626; /* ... */ } } [代码] utilities(工具类) tw 中最核心的思想(工具类 css,也有叫原子化 css),大多数内部模块可以基于官方默认配置,有些还需要自定义配置。 Demo1 [代码] <view> <text class="text-red text-40 ml-10 px-20 "> 我的字体颜色是红色,大小为40rpx,左外边距10rpx,内边距x轴20rpx</text > </view> <!-- wxss新增 .text-red{ color:red }, .text-40{ font-size: 40rpx }, .ml-10{ margin-left:10rpx } .px-20{ padding: 0 20rpx } --> [代码] components(组件类) 有时几个工具类样式会经常出现,我们可以把他们自定义为组件类(比如下面 Demo3 中的[代码]ellipsis[代码]类) Demo2 [代码] <view> <button class="bg-primary hover_bg-primary-dark inline-block w-100 h-60 whitespace-nowrap overflow-hidden overflow-ellipsis" > 工具类写法--按钮1 </button> </view> <view> <button class="btn ellipsis">组件类写法--按钮2</button> </view> <!-- wxss新增 .bg-primary{ color:var(--color-primary) } .hover_bg-primary-dark:hover{ color:var(--color-primary-dark) } .inline-block { display: inline-block; } .w-100 { width: 100rpx; } .h-60 { height: 60rpx; } .whitespace-nowrap{ white-space: nowrap; } .overflow-hidden{ overflow: hidden; } .overflow-ellipsis{ text-overflow: ellipsis; } .btn{ // 组件类 color:var(--color-primary); display: inline-block; width: 100rpx; height: 60rpx; } .btn:hover{ // 组件类 color:var(--color-primary-dark); } .ellipsis { // 组件类 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } --> [代码] 不必纠结这么多工具类样式很难记,因为这些类都基于原生样式且可以自定义的,搭配官方插件 Tailwind CSS IntelliSense还可以得到代码提示(小程序编辑器也支持)。 先写这么多,不知道是否有人关注tw在小程序中的应用,下篇预期发具体核心类编译配置,如果您感兴趣请点赞支持。
2022-01-29 - wxml-to-canvas 使用规则
先说结论: 仅适用生成简单布局的页面,复杂页面会有大量性能开销。 复杂页面可以其他使用webview嵌入的方式用原生H5代替。 安装使用见官方文档,本文主要提及重要使用规则。 需要为wxml-to-canvas指定width和height属性,默认为400*300,当该尺寸与wxss最大容器尺寸不一致时会有异常只能使用view、text、image标签view只用于布局、text只用于文字、image只用于图片(可绝对定位作为背景)text、image必须指定width、heightview可以不指定height、只指定width,由子元素动态撑高。渲染高度限制:IOS实际像素 < 4096,实际高度需 < 4096/3 = 1356动态数据可通过函数结合模板字符串实现画布高度最好是确定的数字(<1356)lineHeight可使text文本居中,lineHeight值等于height即可多个absolute元素时,因为没有z-index,template元素自上而下渲染,对应z-index依次增高元素默认为flex布局,子元素会继承父元素的flex布局属性(row或column)背景颜色仅可使用backgroundColor,而非background。性能优化指南 在canvas绘制完成后,可以使用this.widget.canvasToTempFilePath来保存并获取本地临时路径,但是图片可能会比较大,通常会1-2M,实际我们应该尽可能在保证清晰的情况下减小文件大小,我们可以通过canvasToTempFilePath的参数配置来进行调整,如下通常可以保持较小的体积及基本合适的清晰度。更多的配置可详细参考canvasToTempFilePath文档{ destWidth: canvas组件的宽度, destHeight: canvas组件的高度, fileType: 'jpg', quality: 0.9 } 以下是动态数据与动态高度及基本使用的示例: //动态wxml : poster-data.js const getWxml = (info)=>{ return `${info.a?`${info.b}`:''}` } //动态css const getStyle = (info)=>{ return { textTest: { width: 100, height: 20, fontSize: info.fontSize } } } //页面使用 : index.js const { wxml, style } = require('./poster-data.js'); //... data:{ canvasHeight: 0, info:{ a:1, b:2, fontSize: 16 } } onLoad(){ const tempHeight = this.computeContentHeight() this.setData({ canvasHeight:tempHeight },()=>{ this.widget = this.selectComponent('.widget');//数据就绪后canvas高度已确定,再获取canvas组件实例 }) } computeContentHeight(){ //do something 根据数据动态现实隐藏而修改canvas高度 return 500; } saveImage(){ wx.showLoading({ title: '生成中请稍后.', mask: false, }); const template = getWxml(this.data.info);//获取template const css = getStyle(this.data.info)//获取canvas const p1=this.widget.renderToCanvas({wxml:template,style:css}); p1.then((res)=>{ const p2 =this.widget.canvasToTempFilePath();//canvas图片保存到本地临时路径 p2.then(res=>{ wx.hideLoading(); this.data.poster = res.tempFilePath // 使用微信分享 wx.showShareImageMenu({ path: res.tempFilePath }); // 或保存到本地 // wx.saveImageToPhotosAlbum({ // filePath: res.tempFilePath, // success(res){ // console.log(res); // }, // fail(res){ // console.log(res); // } // }); }); }); } //隐藏canvas .widget { position: absolute; left: -999px; } 参考 小程序中实现页面截图 | 微信开放社区wxml-to-canvas 组件中高度不能超过 4096?,否则无法生成图片,报错 | 微信开放社区微信扩展组件wxml-to-canvas支持动态传入数据吗?急 | 微信开放社区小程序使用wxml-to-canvas截屏_培根芝士的博客-CSDN博客_小程序 截屏
2023-08-01 - 小程序中使用npm安装vant组件实现按需引入减少代码包大小,避免触发用户隐私协议
在小程序中使用 vant 组件库主要有以下两种方式: 下载源代码包放入项目中,可以自己删掉没用到的组件,不过后期只能自己手动更新,会不太好维护 通过 npm 的方式安装管理依赖,后期更新可以直接交给 npm 来管理,方便维护 正常项目中我们可能都会选择 npm 的方式,但是这种方式 vant 和小程序并不支持像我们一般的前端项目中的按需引入,小程序开发工具构建 npm 时会把整个 vant 的组件编译到 miniprogram_npm 目录中,即使我们在项目中没有通过 usingComponents 申明引用的组件也会被打包进代码包中。 [图片] 减少代码包大小 因为小程序主包有 2M 的限制,如果我们本身只用到了几个组件,最终却打包进了整个组件库,这样不仅不合理也额外占用了咱小程序的包大小。想要按需引入的办法只能自己手动去把 miniprogram_npm 目录中没用到的组件删掉,然后再打包上传。不过每次我们提交版本都要这样去操作的话,不光容易出错也很费时间,这里我们就可以借助 node 和 npm 的 script 脚本来自动处理。 大体思路就是,先直接用 node 去自动扫描读取项目中所有 json 文件 中的 usingComponents,找出项目中实际有用到的 vant 组件,然后再去 miniprogram_npm/@vant/weapp 目录下将没有用到的多余组件删除掉就行了,最后直接把相关代码放到项目中的 script 脚本中操作,这样通过类似 npm run vant 这样一条命令 1 秒钟就可以删除掉未使用到的多余组件,实现了按需引入了。 未使用到的 vant 组件也会触发隐私协议 除了减少代码包大小这一项外,其实还有一个更大的痛点,vant 的部分组件会自动触发小程序的隐私协议,比如上传组件 uploader 中用到的:收集你选中的照片或视频信息(wx.chooseImage、wx.chooseMedia、wx.chooseVideo)、收集你选中的文件(wx.chooseMessageFile),这类 api 会自动触发隐私协议授权。 即使你的项目中压根没使用这类组件,上传版本提审的时候小程序还是会自动扫描你 miniprogram_npm 目录下的所有文件,只要代码中有相关的 api 代码就会认为你用到了,然后霸道地强制要求你填写和更新相关隐私说明,随便瞎填一个 99% 会被拒,也不能填写项目中未使用,这样那你自己说未使用就会让你先去把项目中相关的代码删掉再来提审。 vant-tree-shaking 为了方便使用,脚本代码已经封装成了一个 npm 包 vant-tree-shaking(https://www.npmjs.com/package/vant-tree-shaking )上传到了 npm 公共仓库中,大家可以直接通过 npm 来下载使用: 全局安装 [代码]npm install -g vant-tree-shaking [代码] 在小程序开发者工具中上传小程序代码前,直接在项目根目录终端中运行命令:vant-tree-shaking,成功后会在控制台打印出:vant-tree-shaking success。 本地安装 [代码]npm install -D vant-tree-shaking [代码] 需要自己在 package.json 配置文件中配置 script 脚本命令,如直接配置自定义命令 vant: [代码]{ "name": "miniapp", "version": "1.0.0", "main": "app.js", "scripts": { "vant": "vant-tree-shaking", }, "dependencies": { "@vant/weapp": "^1.11.5" }, "devDependencies": { "vant-tree-shaking": "^1.0.0" } } [代码] 在小程序开发者工具中上传小程序代码前,直接在项目根目录终端中运行命令:npm run vant,成功后会在控制台打印出:vant-tree-shaking success。 核心代码 主要用到了 node 的 fs、path 这两个模块,来处理文件、目录读取删除和路径的处理,其实也很简单,完整的源码可以参考 github 仓库(https://github.com/cafehaus/vant-tree-shaking ),有什么使用问题或建议也欢迎大家积极反馈。 [代码]// 1、扫描项目中的所有 json 文件,找出项目中使用到的所有 vant 组件 function readFile(filePath) { if (!filePath || !fs.existsSync(filePath)) return if (fs.statSync(filePath).isDirectory()) { let files = fs.readdirSync(filePath) || [] files.map(m => { if (!IGNORE_DIRS.includes(m)) { let curPath = path.join(filePath, m) readFile(curPath) } }) } else { getUsingComponents(filePath, vantSet) } } // 2、项目中使用到的 vant 组件依赖的其他 vant 组件 function readVantDir() { const files = fs.readdirSync(VANT_PATH) || [] files.map(m => { if (!COMMON_DIRS.includes(m) && vantSet.has(`van-${m}`)) { const curPath = path.join(VANT_PATH, m, 'index.json') getUsingComponents(curPath, dependentSet) } }) // 3、删除未使用到 vant 组件目录 const usedVant = new Set([...vantSet, ...dependentSet]) for (let i = files.length - 1; i >= 0; i--) { const cur = files[i] if (!COMMON_DIRS.includes(cur) && !usedVant.has(`van-${cur}`)) { const curPath = path.join(VANT_PATH, cur) deleteDir(curPath) } } console.log('vant-tree-shaking success') } /** * 读取 json 文件中的 usingComponents 属性值 * @param {*} filePath 路径 * @param {*} setList 存符合条件的 vant 组件名的列表 */ function getUsingComponents(filePath, setList) { if (!filePath || !fs.existsSync(filePath) || !isJsonFile(filePath)) return try { const data = fs.readFileSync(filePath, 'utf8') const json = JSON.parse(data) const usingComponents = json.usingComponents || {} for (const key in usingComponents) { if (key.startsWith('van-')) { setList.add(key) } } } catch (err) { console.error(err) } } [代码] 实际测试的一个项目,其中只使用到了 vant 的自定义导航栏 van-nav-bar 组件,没有按需引入时整个代码包大小 544KB,按需引入之后只有 156KB。除了代码包减少了以外,也不用再担心其他未使用到的组件默认触发隐私协议而被拒审了。
2024-05-25 - 小程序基础库 3.4.1 更新
各位微信开发者: 小程序基础库 3.4.1 已经开始灰度开发者,请大家基于业务情况关注相关变更。如遇问题请及时在该帖下方留言或在小程序交流专区发表标题包含「基础库3.4.1」的帖子反馈。本次更新如下: 更新 框架 getSystemInfo 接口不再维护,建议开发者使用 getSystemSetting / getAppAuthorizeSetting / getDeviceInfo / getWindowInfo / getAppBaseInfo 详情更新 框架 xr-frame 点击事件迁移更新 框架 wx.downloadFile、wx.uploadFile 接口新增 useHighPerformanceMode 参数 详情更新 框架 授权弹窗上耦合隐私勾选 详情更新 组件 skyline canvas 触摸事件支持 x,y 详情更新 API 支持插件代宿主调用订阅消息接口 详情更新 API TCP接口 onConnect 和 onMessage 回调参数增加 remoteInfo 和 localInfo 详情修复 组件 修复 canvas 默认滚动修复 组件 修复安卓同层 input 微信团队 2024年04月09日
2024-04-09 - Webview、Skyline 混用切换耗时吗?
在学习 Skyline 的过程中,许多开发者会有一个疑问:是否可以将小程序的部分页面迁移到 Skyline? 对 Skyline 感兴趣但还没有完全决定是否要使用的开发者来说,可能只想先尝试一下 Skyline 的功能。 实际上,Skyline 支持最小粒度的页面配置,意味着我们可以为某个页面单独开启 Skyline,而不必将整个小程序迁移到 Skyline 上。 开发者可以更加灵活地使用 Skyline,并逐步将小程序迁移到 Skyline 上,从而获得更好的性能和用户体验。 我们知道 Webview 和 Skyline 是两个渲染引擎,对于 Webview 和 Skyline 混用,大家又有新的疑问:当进行页面切换的时,混用是否会增加耗时? 这里需要分三种情况: 1、Skyline -> Webview:这种情况取决于 app.json 里配置的全局 renderer,即小程序设置的默认渲染引擎 如果全局 renderer 是 Skyline,那么 Webview 不会被预加载,此时 Skyline 跳转 Webview 耗时会增加,开发者需要手动调用 wx.preloadWebview 做预加载。如果全局 renderer 是 Webview,由于 Webview 默认会预加载,所以 Skyline -> Webview 和 Webview -> Webview 耗时一样,不会增加耗时。2、Webview -> Skyline:Skyline 默认都不会被预加载,开发者需要手动调用 wx.preloadSkylineView 做预加载。 3、Skyline -> Skyline:速度变快,因为多个页面复用同一个 Skyline 实例。 根据上述三种情况的分析,为了保证混用渲染引擎的页面切换耗时最短,我们需要在以下时机进行预加载。 wx.preloadWebview 当 Skyline 页面跳转到 Webview 页面时并且全局 renderer 是 Skyline 由于 Skyline 不影响渲染线程,所以预加载 Webview 的时机只需要在主要逻辑完成后即可 // Skyline page.js Page({ onShow() { // 等待执行完主要逻辑后进行预加载 wx.preloadWebview() } }) wx.preloadSkylineView 当 Webview 页面跳转 Skyline 页面时,因为 Skyline 默认不预加载,所以我们需要手动预加载。 建议大家在 Skyline 页面的 onShow 生命周期里延迟一段时间后调用,这样可以保证在 Skyline 页面被返回时也能够重新预加载。 注意:预加载会影响当前页面的渲染,建议异步延迟去执行预加载操作 // Webview page.js Page({ onShow() { // 延迟 200ms 预加载 Skyline // 建议这个延迟时机在页面渲染完成之后 setTimeout(() => { wx.preloadSkylineView() }, 200) } }) 做好预加载是提高 Webview 和 Skyline 混用体验的有效方式,需要根据实际情况进行调整和优化,以达到最佳的预加载效果。
2024-03-07 - 收集用户隐私行为规范与修改指引
为更好地保护用户隐私信息,优化用户体验,平台对小程序内的收集用户隐私行为进行规范,开发者可自查所涉及的违规内容,并参照修改指引进行相应整改。 常见违规内容 一、隐私政策协议默示同意 小程序在收集用户数据前,应提示用户阅读所提供的隐私政策,向用户如实披露数据用途、使用范围等相关信息,并确保小程序的用户个人信息处理活动已依法取得用户同意。 案例1:隐私政策协议勾选框已默认勾选,默示用户同意隐私政策协议。 [图片] 案例2:显示“用户登录即代表同意”或其他相同语义,默示用户同意隐私政策协议。 [图片] 案例3:隐私政策协议弹窗仅提供同意选项,默示用户同意隐私政策协议。 [图片] 修改指引:隐私政策协议需以明示同意的方式征得用户同意,如隐私政策协议弹窗提供明确的同意(允许)和不同意(拒绝)选项进行选择,或在登录界面提供主动勾选的方式,不应设置为默认勾选。 [图片] 二、在用户未体验具体功能前,就要求进行授权 小程序应在提供使用权限或收集用户个人信息对应的相关功能或服务时,向用户申请授权。若用户未触发需使用权限或用户个人信息的相关功能或服务时,就要求用户进行授权,这种行为涉及违规收集用户个人信息。 案例1:在用户未实际体验小程序对应权限的功能或服务时,就要求用户进行授权。 修改指引:用户使用小程序时,应该在实际体验所需对应权限的功能或服务时,再弹出相应权限的授权框,让用户进行授权。 [图片] 案例2:在用户未实际体验小程序对应权限的功能或服务时,就要求用户连续进行多次授权。 修改指引:用户使用小程序时,应该在实际体验所需对应权限的功能或服务时,再弹出相应权限的授权框,让用户进行授权,不可要求用户一次性同意小程序涉及的所有权限。 [图片] 案例3:首页打开即要求用户授权登录。 修改指引:用户打开小程序后,应该先通过完整页面展示小程序的功能,在用户体验功能服务后自主触发登录。(若小程序仅提供给特定人员使用,需在首页有明显的使用范围(特定范围)说明,详情可参考:小程序账号登录规范要求与修改指引) [图片] 三、用户不同意授权则拒绝提供服务 小程序在非服务所必需或无合理应用场景下,用户拒绝相关个人信息授权申请的,不能拒绝提供小程序全部服务。若用户不同意在前述情形下进行授权时,小程序通过自动退出、关闭或其他方式拒绝提供其全部服务,导致用户完全无法进行使用的,这种行为涉及强迫用户授权个人信息。 案例1:用户点击拒绝授权后,小程序则无法继续使用。 修改指引:用户拒绝授权某权限,可暂停提供和该权限有关的功能,但和该权限无关的功能,应该可让用户继续进行使用,不可直接中断全部服务体验。 [图片] 案例2:用户点击拒绝授权后,小程序显示需授权的提示框无法消除,强迫用户必须进行授权才可继续使用小程序。 修改指引:用户拒绝授权某权限,可暂停提供和该权限有关的功能,但和该权限无关的功能,应该可让用户继续进行使用,不可直接中断全部服务体验。 [图片] 案例3:用户在登录页面,点击取消/拒绝登录按钮无响应,或未提供返回/退出按钮等,导致小程序无法继续使用。 修改指引:用户拒绝登录,应该提供明显的取消/拒绝/暂不登录/返回首页的按钮,让用户退出登录界面,不可直接中断全部服务体验。 [图片] 开发者如有其他疑问,可以通过目前开放的咨询渠道反馈: 1、微信开放社区-交流专区-小程序发帖咨询-提出问题-运营相关问题 2、站内信通知-客服咨询入口
2023-09-18 - draggable-sheet 组件:快速实现半屏拖拽
在日常应用中,半屏可拖拽交互已经成为了用户体验的重要组成部分。页面内的分段式拖拽、半屏拖拽放大等等,这些常见的场景使用半屏拖拽交互可以很好的提升用户的体验感。 目前,实现半屏拖拽交互可以使用手势协商系统来实现,不过需要理解手势协商系统并编写相关代码。 考虑到半屏拖拽交互比较常见,为了让小程序开发者更加轻松地实现半屏拖拽交互,小程序 Skyline 渲染引擎推出了 draggable-sheet 组件。这个组件自带半屏拖拽交互,可以帮助开发者省去手势协商相关的代码,只需要简单配置容器大小、吸附阈值等参数即可实现半屏拖拽交互。无论是半屏弹窗、分段式半屏还是其他场景,draggable-sheet 组件都能帮助你实现更加出色的用户体验。 快速接入 使用 draggable-sheet 组件的流程如下: Step 1. 配置 draggable-sheet 组件 在页面中引入 draggable-sheet 组件,并且配置 draggable-sheet 组件的参数,包括容器大小、吸附阈值等。 在 draggable-sheet 组件内嵌套 scroll-view 组件,且声明 associative-container="draggable-sheet" 将 scroll-view 组件与 draggable-sheet 组件进行关联即可。 <!-- page.wxml --> <draggable-sheet class="sheet" initial-child-size="0.5" min-child-size="0.2" max-child-size="0.8" snap="{{true}}" snap-sizes="{{[0.4, 0.6]}}" > <scroll-view scroll-y="{{true}}" type="list" associative-container="draggable-sheet" bounces="{{true}}" > <!-- 这里放置半屏内容 --> </scroll-view> </draggable-sheet> Step 2. 使用 DraggableSheetContext 接口控制滚动 使用 createSelectorQuery 方法获取 draggable-sheet 组件的节点,然后通过 node 方法获取 DraggableSheetContext 实例。调用 DraggableSheetContext.scrollTo 即可将 draggable-sheet 组件 滚动到指定位置。 // page.js Page({ onReady() { this.createSelectorQuery().select('.sheet').node().exec(res => { // 使用 sheetContext 控制 draggable-sheet 组件的滚动 const sheetContext = res[0].node sheetContext.scrollTo({ size: 0.7, animated: true, duration: 300, easingFunction: 'ease' }) }); }, }); 使用 draggable-sheet 组件,你可以轻松实现半屏拖拽交互,让用户体验更加自由。同时,使用 DraggableSheetContext 接口,你可以动态控制 draggable-sheet 组件的行为,实现更加灵活的交互效果。 例如,我们这里演示了使用 draggable-sheet 实现的 分享弹窗效果 和 分段式拖拽查看旅游景点列表~ [图片][图片] 与其他组件联动 除了简单的半屏弹窗之外,我们还可以通过 worklet:onsizeupdate 回调,在 draggable-sheet 组件尺寸发生变化时,将 draggable-sheet 组件与其他组件进行动画交互。 例如,我们这里在 draggable-sheet 接近搜索框时,与搜索框衔接为一体,隐藏后面的页面,以此实现更流畅的展示效果。 首先,给 draggable-sheet 配置监听函数 worklet:onsizeupdate="onSizeUpdate" <!-- page.wxml --> <view class="search-wrp"> ... </view> <draggable-sheet class="sheet" ... worklet:onsizeupdate="onSizeUpdate" > 然后,我们在 onSizeUpdate 发生变化时,通过 worklet 函数改变共享变量的值即可 Page({ // 监听 draggable-sheet 尺寸变化去改变 progress onSizeUpdate(e) { 'worklet' const distance = sheetHeight - e.pixels this.progress.value = distance >= 20 ? 1 : distance / 20 }, onLoad(options) { const progress = wx.worklet.shared(1) // 绑定 worklet 动画,progress 变化时改变搜索的背景来显示/隐藏 map this.applyAnimatedStyle('.search-wrp', () => { 'worklet' const t = progress.value return { backgroundColor: `rgb(245, 242, 241, ${1 - t})` } }) ... this.progress = progress }, }) [图片] 如果想要快速了解和实践本文中的案例,可在 PC 端点击 代码片段 进行调试。 注意:draggable-sheet 组件支持页面内的拖拽放大功能,如果需要实现页面返回的功能,可以使用 预设路由 来实现。 总结 draggable-sheet 组件通过拖拽的方式来展开或关闭一个面板,提供更加直观、自然的用户交互体验。不仅能够节省屏幕空间,让用户更加高效地利用界面,适用于展示评论列表、商品列表、展开筛选条件、展示更多信息等场景。对于需要提供更加灵活、自由的用户交互体验的小程序来说,draggable-sheet 组件是一个非常有用的功能。 因此,我们邀请大家尝试将这个能力应用到自己的小程序上,通过这个组件来提升用户体验和交互效果。结合其他组件和功能,打造出更加丰富、多样的小程序~
2024-02-26 - Skyline | 快速搞定复杂的分享海报
在小程序中生成海报是一种非常有效的推广方式 用户可以使用小程序的过程中生成小程序海报并分享给他人 通过海报的形式,用户可以直观地了解产品或服务的特点和优势 [图片] 常见绘制海报方式 目前,小程序海报有两种常见的实现方式: · canvas 绘制海报 · 服务端绘制海报 这两种方式各有千秋 canvas 绘制海报使用 canvas 绘制海报主要有以下几个步骤 1、创建 [代码]canvasContext[代码] 2、获取网络图片的本地路径 3、绘制图片、文字等到 [代码]canvas[代码] 4、调用 [代码]wx.canvasToTempFilePath[代码] 导出图片 尽管 canvas 绘制功能强大,但实际使用中,这些操作看似简单,但调试起来却比较麻烦 而且面对一些复杂的排版时,使用 canvas 绘制相较于使用 CSS 绘制来说困难许多 除此之外,canvas 的宽高有最大限制,超出限制则会绘制空白 服务端绘制 小程序也可以通过调用服务端接口,将需要生成海报的数据传递给服务端, 由服务端使用 Canvas API 等第三方库来生成图片。 然而,这种绘制方式需要走网络请求,如果量大会给服务器带来一定的成本压力。 此外,对于复杂排版的实现,使用 Canvas 绘制也有一定的难度。 尽管小程序海报虽然好用,但是当遇到要求比较高的设计稿需要还原海报时,对小程序开发者来说是一个十分让人头疼的问题 考虑到海报在小程序中使用的广泛性,我们把开发者的烦恼交给官方来处理~ 小程序官方推出了 [代码]snapshot[代码] 组件,可以直接将小程序 wxml 导出图片。 snapshot 生成海报 当使用 canvas 或 服务端绘制海报遇到复杂排版时,如 圆角、百分比、自定义字体 等等,实现比较困难。 但是使用 wxml 实现却很简单 👇 下面的例子我们使用 wxml 实现海报 <view class="snapshot-box"> <view class="poster-container"> <view class="poster-header"> <image /> ... </view> <view class="description"> ... </view> <view class="footer"> ... </view> </view> </view> [图片] 接着,我们就可以导出海报啦,使用非常简单: 1、用 [代码]snapshot[代码] 组件包裹海报的 wxml 2、调用 [代码]takeSnapshot[代码] 获取图片数据 3、调用 [代码]fs.writeFileSync[代码] 将海报数据写入本地文件 4、调用 [代码]wx.saveImageToPhotosAlbum[代码] 将海报保存到本地 <snapshot id="view"> <!-- 这里是要海报的 wxml --> </snapshot> <button bindtap="tap">保存海报</button> tap() { this.createSelectorQuery().select("#view") .node().exec(res => { const node = res[0].node // 保存海报 node.takeSnapshot({ type: 'arraybuffer', format: 'png', success: (res) => { const f = `${wx.env.USER_DATA_PATH}/hello.png` const fs = wx.getFileSystemManager(); // 将海报数据写入本地文件 fs.writeFileSync(f, res.data, 'binary') this.setData({ img: f }) // 把海报图片保存到本地 wx.saveImageToPhotosAlbum({ filePath: f }) } }) }) } 最后我们来看看使用 [代码]snapshot[代码] 组件生成海报的效果吧~ [图片] 除了普通尺寸分享海报之外,对于 canvas 无法搞定的超长海报,[代码]snapshot[代码] 后续也会支持超长海报的导出~ [图片] 你的小程序也有海报生成需求吗? 赶紧 mark 下这个 代码片段 来接入使用吧~
2023-09-06 - App跳转小程序失败?
App跳转小程序失败,开发者可先按照以下指引自查: 1、AppID和BundleId的绑定关系,还有AppID和小程序userName的绑定关系是否正确 2、参数是否正确(AppID或userName) Android开发示例: https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Launching_a_Mini_Program/Android_Development_example.html iOS开发示例: https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Launching_a_Mini_Program/iOS_Development_example.html
2020-01-14 - 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 - 试着解读一下《小程序隐私协议开发指南》3个官方demo的应用场景
隐私协议开发 关于小程序隐私协议开发, 官方最近给出了3个demo,看起来都不太一样: demo1: 演示使用 wx.getPrivacySetting 和 <button open-type=“agreePrivacyAuthorization”> 在首页处理隐私弹窗逻辑 https://developers.weixin.qq.com/s/gi71sGm67hK0 demo2: 演示使用 wx.onNeedPrivacyAuthorization 和 <button open-type=“agreePrivacyAuthorization”> 在多个页面处理隐私弹窗逻辑,同时演示了如何处理多个隐私接口同时调用。 https://developers.weixin.qq.com/s/4X7yyGmQ7EKp demo3: 演示 wx.onNeedPrivacyAuthorization、wx.requirePrivacyAuthorize、<button open-type=“agreePrivacyAuthorization”> 和 <input type=“nickname”> 组件如何结合使用 https://developers.weixin.qq.com/s/jX7xWGmA7UKa 说明也比较简单,我结合我查看demo源码说一下我理解的使用场景 demo1 这个是最简单的。调用的隐私协议接口最少,只有一个[代码]wx.getPrivacySetting[代码]。([代码]wx.openPrivacyContract[代码]不算)。 其逻辑就是在调用任何隐私接口之前检查授权状态,已授权则啥也不做,未授权则弹窗。 特点: 开发者主动检查后弹窗,而不是隐私接口调用触发。 简单,代码量最少。 流程:主动检查->弹窗->用户同意->调用隐私接口 适用场景: 小程序启动过程没有调用隐私接口,换句话说,隐私接口需要由用户动作才会调用,如用户点击按钮选择图片。 强隐私需求小程序,用户不给授权就不能用,直接退出那种。 极简小程序,比如就一个页面,或者就只调用一个隐私接口。 以上几种情况就可以使用demo1, 在小程序启动时做检查,没授权就弹窗,用户不同意就退出; 或者对于极简小程序,比如就一个页面点击按钮选个图片,可以在处理按钮事件时调用[代码]wx.getPrivacySetting[代码]。弹窗后用户不同意就啥也不干或给个toast,下次用户再点按钮就再检查再弹窗。 demo2 demo2就复杂一些了,需要用到[代码]wx.onNeedPrivacyAuthorization[代码]接口了,和demo1的区别就是,弹窗是由调用隐私接口触发的,而不是开发者主动检查触发 特点: 弹窗是由调用隐私接口触发。 复杂,代码量多。 流程:调用隐私接口->弹窗(同时隐私接口pending)->用户同意/不同意->隐私接口返回成功或失败 适用场景: 普遍场景,多个页面,多个隐私接口。 被动触发,无需关心隐私接口何时被调用。 demo3 demo3在demo2的基础上,就多了[代码]wx.requirePrivacyAuthorize[代码]调用,官方说法是这个接口用来模拟隐私接口调用。 特点: 弹窗可以由调用隐私接口触发,也可以由[代码]wx.requirePrivacyAuthorize[代码]触发 复杂,代码量多。 流程:调用隐私接口(wx.requirePrivacyAuthorize)->弹窗(同时隐私接口pending)->用户同意/不同意->隐私接口返回成功或失败 相较于demo1,相同的地方在于开发者可主动触发隐私弹窗,不同的地方在于demo3更复杂一些 相较于demo2,相同的地方在于其逻辑和调用真实隐私接口是一致的,不同之处在于给开发者提供了一种更灵活的可主动触发弹窗的模式 你可以认为 demo3=demo1 + demo2 适用场景: 同demo2。 无接口的隐私组件,如<input type=“nickname”> 比demo2更灵活的需求. 对于[代码]wx.requirePrivacyAuthorize[代码]接口,大家不要把这个接口局限于仅在开发时调试用。这个接口也是可以用在线上的。可以用来实现更灵活的逻辑。 以上就是我对这3个demo的理解,大家有什么样的想法欢迎在评论区留言大家一起讨论。
2023-08-24 - 关于新版隐私协议接口wx.onNeedPrivacyAuthorization的解读以及实现代码(二)
官方公告地址: https://developers.weixin.qq.com/community/develop/doc/00042e3ef54940ce8520e38db61801 目前,开发工具或者体验版的小程序,调试基础库改成3.0.0可以适配测试,线上版本9月15日之后生效,所以这之前需要尽快改完,发布一版,否则到了9月15号之后 线上就会生效报错了。 这是官方的Demo地址,也是每个页引用一个全局的组件,个人感觉太过于繁琐了,可以参考以下我整理的代码。 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html 其实改起来也很简单,以下是实现步骤和代码: 1、首先看一下这个网址,里边包含涉及到的隐私的接口,这些接口都要适配一下 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html [图片] 在以上接口用到的页面,需要画一下类似上边的弹窗(这个弹窗可以全局定义个组件,方便多个页面共用),然后里边蓝字可以点击后调用wx.openPrivacyContract(Object object)接口即可,会自动跳转打开隐私协议页面。 拒绝按钮可以加一个点击事件,然后在事件里这样写,官方demo里也没有加id。 <button class="btn-refuse" catch:tap="clickRefuse">拒绝</button> refuse() { this.resolvePrivacyAuthorization({ event: 'disagree' }) }, 同意按钮比较特殊,布局需要用button这样写,记得给button加一个Id <button id="agree-btn" class="btn-agree" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyAuthorization">同意</button> 然后在handleAgreePrivacyAuthorization里就可以获取到点击事件,这样写 handleAgreePrivacyAuthorization() { this.resolvePrivacyAuthorization({ buttonId: 'agree-btn', event: 'agree' }) }, 2、然后每个有隐私接口的页面引用自己的组件,代码如下 { "usingComponents": { "agreement": "/components/agreement/agreement" } } 布局引用 <!-- 隐私授权弹窗 --> <agreement id="agreement" frameTitle="温馨提示" bind:refuse="refuse" bind:agree="agree"></agreement> 让组件弹窗显示,这样写: this.selectComponent('#agreement').show(); 3、最后需要在用到隐私接口的页面的onShow里加上以下监听代码,在这里边让自定义的隐私弹窗显示出来即可。 onShow: function () { let that = this; if (wx.onNeedPrivacyAuthorization) { wx.onNeedPrivacyAuthorization(resolve => { this.selectComponent('#agreement').show();//这里是让组件弹窗显示 this.resolvePrivacyAuthorization = resolve }) } } 以上代码加上就可以了,如果业务逻辑用到了需要判断是否授权过,可以加上 wx.getPrivacySetting(Object object)去获取是否授权过,用不到可以不加这个判断。 //=======================以下是针对昵称输入框input的type="nickname"的代码=========================== 注意:以下代码只是针对input另外加的,上边提到的代码也都要加上才能正常弹出弹窗 布局,在input的外层View加touch事件,然后加一个focus动态控制焦点。 <view catch:touchstart="handleTouchInput"> <input type="nickname" class="text" bindinput="listenerContent" placeholder="输入名称" maxlength="10" value='{{name}}' focus="{{focus}}" /> </view> 然后,touch事件: handleTouchInput() { let that = this if (wx.getPrivacySetting) { wx.getPrivacySetting({//获取是否需要弹出 success: res => { if (res.needAuthorization) { wx.requirePrivacyAuthorize({//该接口请求隐私协议,会自动触发上边的wx.onNeedPrivacyAuthorization success: () => { that.setData({ focus: true, }) }, fail: () => {}, complete: () => {} }) } else { that.setData({ focus: true, }) } }, fail: () => {}, complete: () => {} }) } else { this.setData({ focus: true }) } }, 这样就可以了,记得把上边提到的监听代码都加上。
2023-08-23 - canvas绘制水印的问题?
[图片] 上面是代码 [图片] 下面是效果 我想实现全屏铺满 就是屏幕里多有几个文字 但是没办法实现
2023-05-22 - 小程序页面加水印,防止用户截图分享隐私数据
为了防止用户将小程序内的隐私数据进行截图或者录屏分享导致信息泄露,我们会在小程序全局添加一个水印浮层。这样即使被截图或者拍照,也能轻松地确定泄露的源头。 小程序防止用户截屏的方法有很多,其中一种常见的方法就是在小程序的页面中添加水印。具体实现方法如下: 在小程序中的页面中添加水印浮层,一般通过绝对定位来实现,这样可以使水印在页面的最上层,无法被其他元素覆盖。设计水印的样式和位置,通常可以在小程序的样式文件中设置,例如设置水印的位置为右下角,样式为半透明的字体,以免影响正常的内容展示。对于不同类型的页面,可以根据需要添加不同的水印,例如在敏感信息页面添加比较醒目的水印,而在其他普通页面只添加轻微的水印。如果需要防止用户截屏或者拍照,可以在小程序中添加截屏监听事件,并在用户进行截屏或者拍照操作时,自动添加水印。 总的来说,小程序防止用户截屏的方法还有很多,例如使用安全键盘等,但是添加水印仍然是最为常见的一种方法。通过添加水印,可以有效地防止用户截屏和分享敏感信息,保护用户和单位的信息安全。 下面是一个简单的示例,position选择固定定位fixed,固定定位会固定在浏览器窗口某个位置,不会随滚动条滚动。用z-index将元素的层级设置为最低,将view旋转45度,效果就出来啦 [图片] <view style="position: fixed;top: -10vh; left:-100vw;width: 250vw; height: 100vh; z-index: -999;transform: rotate(-45deg);"> <block wx:for="{{30}}" wx:key="index" wx:if="{{userInfo}}"> <view style="color:gray; margin:30rpx; padding:20rpx; opacity: 0.15;"> {{userInfo.user_name + ' ' + userInfo.user_phone}} {{userInfo.user_name + ' ' + userInfo.user_phone}} {{userInfo.user_name + ' ' + userInfo.user_phone}} {{userInfo.user_name + ' ' + userInfo.user_phone}} </view> </block> </view>
2023-02-23 - 在工具内从开发板切稳定版后Mock数据丢失
由于前几天用着开发版的工具,选择元素无法选中自定义组件内元素,于是在工具这个位置点击了切换最新稳定版本,稳定版没有这个问题了。 [图片] 但之后发现原先工具内的mock数据就不见了 [图片] 请问这些mock的数据有没有存在哪里?
2022-05-10 - 微信小程序生成海报图片导出相册
前言 小程序内通过静态模板和样式绘制 canvas ,导出图片,可用于生成分享图等场景 一、效果预览 点击生成海报 [图片] 这个是生成的保存到相册的图片 [图片] 二、使用步骤 1.安装引入wxml-to-canvas Step1.运行小程序npm安装命令 [代码]npm install --save wxml-to-canvas [代码] Step2.JSON 组件声明 [代码]{ "usingComponents": { "wxml-to-canvas": "wxml-to-canvas", } } [代码] 文章最后会有wxml-to-canvas的其它详细教程 2.创建js 创建的js放到页面文件夹即可 [图片] 代码如下(demo.js): [代码]var calendar = require('../../../utils/calendar'); var d=new Date(); let Year = d.getFullYear(); let Month = (d.getMonth()+1).toString().padStart(2,'0'); let Day = (d.getDate()).toString().padStart(2,'0'); let time = Month+ '/' + Day; let xq = "星期"+"天一二三四五六".charAt(d.getDay()); const wxml = (hb_describe,hb_bg,hb_wx2code)=>{ return ` <view class="container" > <view class="itembox1"> <text class="time">`+time+`</text> <view class="xqview"> <text class="xq">`+xq+`</text> <text class="xq2">`+calendar.default.solar2lunar(Year,Month,Day).gzYear+'年'+calendar.default.solar2lunar(Year,Month,Day).IMonthCn+calendar.default.solar2lunar(Year,Month,Day).IDayCn+`</text> </view> </view> <view class="itembox2"> <image class="img" mode="aspectFit" src="`+hb_bg+`"></image> </view> <view class="itembox3"> <text class="text">`+hb_describe+`</text> <image class="imgcode" src="`+hb_wx2code+`"></image> </view> </view> ` } let width=wx.getSystemInfoSync().windowWidth*0.8; let widthCenter=width*0.9; let width6=widthCenter*0.6;//描述占60% let width4=widthCenter*0.4; const style = { container: { width: width, height: 450, flexDirection: 'column', backgroundColor: '#FFFFFF', alignItems: 'center' }, itembox1: { width: widthCenter, height: 130 }, itembox2: { width: widthCenter, height: 200 }, itembox3: { width: widthCenter, height: 120, flexDirection: 'row', justifyContent:'space-around', alignItems: 'center', //paddingTop:20, //paddingBottom:20 }, time:{ fontSize:88, color:'#0222AC', textAlign:'center', fontStyle:'italic' }, xqview:{ fontSize:16, color:'#0222AC', marginTop:106 }, xq:{ textAlign:'left' }, xq2:{ textAlign:'right' }, img: { width: widthCenter, minHeight: 190, maxHeight: 200, }, imgcode: { width: 80, height: 80 }, text: { width: width6, height: 60, textAlign: 'center', verticalAlign: 'middle' } } module.exports = { wxml, style } [代码] 3.小程序页面 wxml: [代码]<!-- 海报 --> <view class="hbbox" wx:if="{{maskHidden}}"> <wxml-to-canvas class="widget" height='{{canvasH}}'></wxml-to-canvas> </view> <view class="padding-sm margin-top flex flex-direction renderToCanvas" wx:if="{{hexiao}}"> <button bindtap="renderToCanvas" class="cu-btn">活动打卡</button> </view> <view class='hbmask' hidden="{{maskHidden == false}}"> <button class='hbbaocun' hidden="{{maskHidden == false}}" bindtap='extraImage'>保存相册</button> </view> <!-- 海报end --> [代码] js: 博主这里是打开一个弹窗 在弹窗里展示生成海报,js代码全部放出来,可自行取用。 [代码]const { wxml, style } = require('./demo.js') Page({ data: { //海报 hb_wx2code: "", hb_bg: "", hb_describe: '', maskHidden: false, canvasH: 0, number:0, flag:'' }, //生成海报 makeHB(){ let that = this; that.setData({ maskHidden: true, canvasH:570 }) setTimeout(()=>{ const _wxml = wxml(that.data.hb_describe,that.data.hb_bg,that.data.hb_wx2code) wx.nextTick(()=>{ that.widget = that.selectComponent('.widget') const p1 = that.selectComponent('.widget').renderToCanvas({ wxml:_wxml , style }) p1.then((res) => { wx.hideToast(); that.container = res; }).catch(err=>{ }) }) },500) }, renderToCanvas() { let that = this; if(that.data.hb_wx2code){ wx.showToast({ title: '海报生成中...', icon: 'loading', duration: 1000 }); that.makeHB(); } else{ wx.showToast({ title: '海报生成中...', icon: 'loading', duration: 1000 }); const data={ open_id : wx.getStorageSync('openId'), type: '2', video_id:that.data.id } mycomm.ajaxDataGet("/inviteFriends/ordinary", data, function (res) { that.setData({ hb_wx2code: res.val.qr_code, hb_bg:res.val.background_img, hb_describe:res.val.describe }); that.makeHB(); }, true); } }, extraImage() { const p2 = this.widget.canvasToTempFilePath() p2.then(res => { // this.setData({ // src: res.tempFilePath, // width: this.container.layoutBox.width, // height: this.container.layoutBox.height // }) this.baocun(res.tempFilePath); }) }, //点击保存到相册 baocun: function (imagePath) { var that = this wx.saveImageToPhotosAlbum({ filePath: imagePath, success(res) { wx.showModal({ content: '海报已保存到相册', showCancel: false, confirmText: '确定', confirmColor: '#333', success: function (res) { if (res.confirm) { /* 该隐藏的隐藏 */ that.setData({ maskHidden: false }) } }, fail: function (res) {} }) } }) }, }) [代码] 4.农历日期js 这个是海报上根据当前日期对应的农历日期,不需要可以自行删除掉。 [代码]var calendar = { /** * 农历1900-2100的润大小信息表 * @Array Of Property * @return Hex */ lunarInfo:[0x04bd8,0x04ae0,0x0a570,0x054d5,0x0d260,0x0d950,0x16554,0x056a0,0x09ad0,0x055d2,//1900-1909 0x04ae0,0x0a5b6,0x0a4d0,0x0d250,0x1d255,0x0b540,0x0d6a0,0x0ada2,0x095b0,0x14977,//1910-1919 0x04970,0x0a4b0,0x0b4b5,0x06a50,0x06d40,0x1ab54,0x02b60,0x09570,0x052f2,0x04970,//1920-1929 0x06566,0x0d4a0,0x0ea50,0x16a95,0x05ad0,0x02b60,0x186e3,0x092e0,0x1c8d7,0x0c950,//1930-1939 0x0d4a0,0x1d8a6,0x0b550,0x056a0,0x1a5b4,0x025d0,0x092d0,0x0d2b2,0x0a950,0x0b557,//1940-1949 0x06ca0,0x0b550,0x15355,0x04da0,0x0a5b0,0x14573,0x052b0,0x0a9a8,0x0e950,0x06aa0,//1950-1959 0x0aea6,0x0ab50,0x04b60,0x0aae4,0x0a570,0x05260,0x0f263,0x0d950,0x05b57,0x056a0,//1960-1969 0x096d0,0x04dd5,0x04ad0,0x0a4d0,0x0d4d4,0x0d250,0x0d558,0x0b540,0x0b6a0,0x195a6,//1970-1979 0x095b0,0x049b0,0x0a974,0x0a4b0,0x0b27a,0x06a50,0x06d40,0x0af46,0x0ab60,0x09570,//1980-1989 0x04af5,0x04970,0x064b0,0x074a3,0x0ea50,0x06b58,0x05ac0,0x0ab60,0x096d5,0x092e0,//1990-1999 0x0c960,0x0d954,0x0d4a0,0x0da50,0x07552,0x056a0,0x0abb7,0x025d0,0x092d0,0x0cab5,//2000-2009 0x0a950,0x0b4a0,0x0baa4,0x0ad50,0x055d9,0x04ba0,0x0a5b0,0x15176,0x052b0,0x0a930,//2010-2019 0x07954,0x06aa0,0x0ad50,0x05b52,0x04b60,0x0a6e6,0x0a4e0,0x0d260,0x0ea65,0x0d530,//2020-2029 0x05aa0,0x076a3,0x096d0,0x04afb,0x04ad0,0x0a4d0,0x1d0b6,0x0d250,0x0d520,0x0dd45,//2030-2039 0x0b5a0,0x056d0,0x055b2,0x049b0,0x0a577,0x0a4b0,0x0aa50,0x1b255,0x06d20,0x0ada0,//2040-2049 /**Add By JJonline@JJonline.Cn**/ 0x14b63,0x09370,0x049f8,0x04970,0x064b0,0x168a6,0x0ea50, 0x06b20,0x1a6c4,0x0aae0,//2050-2059 0x092e0,0x0d2e3,0x0c960,0x0d557,0x0d4a0,0x0da50,0x05d55,0x056a0,0x0a6d0,0x055d4,//2060-2069 0x052d0,0x0a9b8,0x0a950,0x0b4a0,0x0b6a6,0x0ad50,0x055a0,0x0aba4,0x0a5b0,0x052b0,//2070-2079 0x0b273,0x06930,0x07337,0x06aa0,0x0ad50,0x14b55,0x04b60,0x0a570,0x054e4,0x0d160,//2080-2089 0x0e968,0x0d520,0x0daa0,0x16aa6,0x056d0,0x04ae0,0x0a9d4,0x0a2d0,0x0d150,0x0f252,//2090-2099 0x0d520],//2100 /** * 公历每个月份的天数普通表 * @Array Of Property * @return Number */ solarMonth:[31,28,31,30,31,30,31,31,30,31,30,31], /** * 天干地支之天干速查表 * @Array Of Property trans["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"] * @return Cn string */ Gan:["\u7532","\u4e59","\u4e19","\u4e01","\u620a","\u5df1","\u5e9a","\u8f9b","\u58ec","\u7678"], /** * 天干地支之地支速查表 * @Array Of Property * @trans["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"] * @return Cn string */ Zhi:["\u5b50","\u4e11","\u5bc5","\u536f","\u8fb0","\u5df3","\u5348","\u672a","\u7533","\u9149","\u620c","\u4ea5"], /** * 天干地支之地支速查表<=>生肖 * @Array Of Property * @trans["鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪"] * @return Cn string */ Animals:["\u9f20","\u725b","\u864e","\u5154","\u9f99","\u86c7","\u9a6c","\u7f8a","\u7334","\u9e21","\u72d7","\u732a"], /** * 阳历节日 */ festival: { '1-1': {title: '元旦节'}, '2-14': {title: '情人节'}, '5-1': {title: '劳动节'}, '5-4': {title: '青年节'}, '6-1': {title: '儿童节'}, '9-10': {title: '教师节'}, '10-1': {title: '国庆节'}, '12-25': {title: '圣诞节'}, '3-8': {title: '妇女节'}, '3-12': {title: '植树节'}, '4-1': {title: '愚人节'}, '5-12': {title: '护士节'}, '7-1': {title: '建党节'}, '8-1': {title: '建军节'}, '12-24': {title: '平安夜'}, }, /** * 农历节日 */ lfestival: { '12-30': {title: '除夕'}, '1-1': {title: '春节'}, '1-15': {title: '元宵节'}, '5-5': {title: '端午节'}, '8-15': {title: '中秋节'}, '9-9': {title: '重阳节'}, }, /** * 返回默认定义的阳历节日 */ getFestival(){ return this.festival }, /** * 返回默认定义的内容里节日 */ getLunarFestival(){ return this.lfestival }, /** * * @param {Object} 按照festival的格式输入数据,设置阳历节日 */ setFestival(param={}){ this.festival = param }, /** * * @param {Object} 按照lfestival的格式输入数据,设置农历节日 */ setLunarFestival(param={}){ this.lfestival = param }, /** * 24节气速查表 * @Array Of Property * @trans["小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨","立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑","白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至"] * @return Cn string */ solarTerm:["\u5c0f\u5bd2","\u5927\u5bd2","\u7acb\u6625","\u96e8\u6c34","\u60ca\u86f0","\u6625\u5206","\u6e05\u660e","\u8c37\u96e8","\u7acb\u590f","\u5c0f\u6ee1","\u8292\u79cd","\u590f\u81f3","\u5c0f\u6691","\u5927\u6691","\u7acb\u79cb","\u5904\u6691","\u767d\u9732","\u79cb\u5206","\u5bd2\u9732","\u971c\u964d","\u7acb\u51ac","\u5c0f\u96ea","\u5927\u96ea","\u51ac\u81f3"], /** * 1900-2100各年的24节气日期速查表 * @Array Of Property * @return 0x string For splice */ sTermInfo:['9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e','97bcf97c3598082c95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f','b027097bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f','97bd0b06bdb0722c965ce1cfcc920f','b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e','97bcf97c359801ec95f8c965cc920f','97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa','9778397bd19801ec9210c965cc920e','97b6b97bd19801ec95f8c965cc920f', '97bd09801d98082c95f8e1cfcc920f','97bd097bd097c36b0b6fc9210c8dc2','9778397bd197c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e','97bd09801d98082c95f8e1cfcc920f','97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa','97b6b97bd19801ec95f8c965cc920e','97bcf97c3598082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2','9778397bd097c36c9210c9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f','97bd097bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e','97bcf97c3598082c95f8c965cc920f','97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e','97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f','97bd097bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e','97bcf97c359801ec95f8c965cc920f','97bd097bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9210c8dc2','9778397bd19801ec9210c9274c920e','97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722','7f0e397bd097c36b0b6fc9210c8dc2','9778397bd097c36c9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f','97bd07f5307f595b0b0bc920fb0722','7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa','97b6b97bd19801ec9210c965cc920e','97bd07f1487f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2','9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e','97bcf7f1487f595b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e','97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa','97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722','7f0e397bd07f595b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c9274c920e','97bcf7f0e47f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c91aa','97b6b97bd197c36c9210c9274c920e','97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722','9778397bd097c36b0b6fc9210c8dc2','9778397bd097c36c9210c9274c920e', '97b6b7f0e47f531b0723b0b6fb0722','7f0e37f5307f595b0b0bc920fb0722','7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b70c9274c91aa','97b6b7f0e47f531b0723b0b6fb0721','7f0e37f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc9210c8dc2','9778397bd097c36b0b6fc9274c91aa','97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f595b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa','97b6b7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722','9778397bd097c36b0b6fc9274c91aa','97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722','9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0787b0721','7f0e27f0e47f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c91aa','97b6b7f0e47f149b0723b0787b0721','7f0e27f0e47f531b0723b0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722','9778397bd097c36b0b6fc9210c8dc2','977837f0e37f149b0723b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722','7f0e37f5307f595b0b0bc920fb0722','7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b0721','7f07e7f0e47f531b0723b0b6fb0721','7f0e37f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc9210c8dc2','977837f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722','977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722','977837f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722','977837f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721','7f0e27f0e47f531b0b0bb0b6fb0722','7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0723b06bd','7f07e7f0e37f149b0723b0787b0721','7f0e27f0e47f531b0723b0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722','977837f0e37f14898082b0723b02d5','7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722','7f0e37f1487f595b0b0bb0b6fb0722','7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721','7f07e7f0e47f531b0723b0b6fb0722','7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5','7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f531b0b0bb0b6fb0722','7f0e37f0e37f14898082b072297c35','7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722','7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35','7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f149b0723b0787b0721', '7f0e27f1487f531b0b0bb0b6fb0722','7f0e37f0e366aa89801eb072297c35','7ec967f0e37f14998082b0723b06bd', '7f07e7f0e47f149b0723b0787b0721','7f0e27f0e47f531b0723b0b6fb0722','7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd','7f07e7f0e37f14998083b0787b0721','7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35','7ec967f0e37f14898082b0723b02d5','7f07e7f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722','7f0e36665b66aa89801e9808297c35','665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721','7f07e7f0e47f531b0723b0b6fb0722','7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b0723b02d5','7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721', '7f0e36665b66a449801e9808297c35','665f67f0e37f14898082b072297c35','7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721','7f0e26665b66a449801e9808297c35','665f67f0e37f1489801eb072297c35', '7ec967f0e37f14998082b0787b06bd','7f07e7f0e47f531b0723b0b6fb0721','7f0e27f1487f531b0b0bb0b6fb0722'], /** * 数字转中文速查表 * @Array Of Property * @trans ['日','一','二','三','四','五','六','七','八','九','十'] * @return Cn string */ nStr1:["\u65e5","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d","\u4e03","\u516b","\u4e5d","\u5341"], /** * 日期转农历称呼速查表 * @Array Of Property * @trans ['初','十','廿','卅'] * @return Cn string */ nStr2:["\u521d","\u5341","\u5eff","\u5345"], /** * 月份转农历称呼速查表 * @Array Of Property * @trans ['正','一','二','三','四','五','六','七','八','九','十','冬','腊'] * @return Cn string */ nStr3:["\u6b63","\u4e8c","\u4e09","\u56db","\u4e94","\u516d","\u4e03","\u516b","\u4e5d","\u5341","\u51ac","\u814a"], /** * 农历年份数字称呼速查表 * @Array Of Property * @trans ['零','一','二','三','四','五','六','七','八','九','十'] * @return Cn string */ nStr4:["\u96f6","\u4e00","\u4e8c","\u4e09","\u56db","\u4e94","\u516d","\u4e03","\u516b","\u4e5d","\u5341"], /** * 返回农历y年一整年的总天数 * @param lunar Year * @return Number * @eg:var count = calendar.lYearDays(1987) ;//count=387 */ lYearDays:function(y) { var i, sum = 348; for(i=0x8000; i>0x8; i>>=1) { sum += (this.lunarInfo[y-1900] & i)? 1: 0; } return(sum+this.leapDays(y)); }, /** * 返回农历y年闰月是哪个月;若y年没有闰月 则返回0 * @param lunar Year * @return Number (0-12) * @eg:var leapMonth = calendar.leapMonth(1987) ;//leapMonth=6 */ leapMonth:function(y) { //闰字编码 \u95f0 return(this.lunarInfo[y-1900] & 0xf); }, /** * 返回农历y年闰月的天数 若该年没有闰月则返回0 * @param lunar Year * @return Number (0、29、30) * @eg:var leapMonthDay = calendar.leapDays(1987) ;//leapMonthDay=29 */ leapDays:function(y) { if(this.leapMonth(y)) { return((this.lunarInfo[y-1900] & 0x10000)? 30: 29); } return(0); }, /** * 返回农历y年m月(非闰月)的总天数,计算m为闰月时的天数请使用leapDays方法 * @param lunar Year * @return Number (-1、29、30) * @eg:var MonthDay = calendar.monthDays(1987,9) ;//MonthDay=29 */ monthDays:function(y,m) { if(m>12 || m<1) {return -1}//月份参数从1至12,参数错误返回-1 return( (this.lunarInfo[y-1900] & (0x10000>>m))? 30: 29 ); }, /** * 返回公历(!)y年m月的天数 * @param solar Year * @return Number (-1、28、29、30、31) * @eg:var solarMonthDay = calendar.leapDays(1987) ;//solarMonthDay=30 */ solarDays:function(y,m) { if(m>12 || m<1) {return -1} //若参数错误 返回-1 var ms = m-1; if(ms==1) { //2月份的闰平规律测算后确认返回28或29 return(((y%4 == 0) && (y%100 != 0) || (y%400 == 0))? 29: 28); }else { return(this.solarMonth[ms]); } }, /** * 农历年份转换为干支纪年 * @param lYear 农历年的年份数 * @return Cn string */ toGanZhiYear:function(lYear) { var ganKey = (lYear - 3) % 10; var zhiKey = (lYear - 3) % 12; if(ganKey == 0) ganKey = 10;//如果余数为0则为最后一个天干 if(zhiKey == 0) zhiKey = 12;//如果余数为0则为最后一个地支 return this.Gan[ganKey-1] + this.Zhi[zhiKey-1]; }, /** * 公历月、日判断所属星座 * @param cMonth [description] * @param cDay [description] * @return Cn string */ toAstro:function(cMonth,cDay) { var s = "\u9b54\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u9b54\u7faf"; var arr = [20,19,21,21,21,22,23,23,23,23,22,22]; return s.substr(cMonth*2 - (cDay < arr[cMonth-1] ? 2 : 0),2) + "\u5ea7";//座 }, /** * 传入offset偏移量返回干支 * @param offset 相对甲子的偏移量 * @return Cn string */ toGanZhi:function(offset) { return this.Gan[offset%10] + this.Zhi[offset%12]; }, /** * 传入公历(!)y年获得该年第n个节气的公历日期 * @param y公历年(1900-2100);n二十四节气中的第几个节气(1~24);从n=1(小寒)算起 * @return day Number * @eg:var _24 = calendar.getTerm(1987,3) ;//_24=4;意即1987年2月4日立春 */ getTerm:function(y,n) { if(y<1900 || y>2100) {return -1;} if(n<1 || n>24) {return -1;} var _table = this.sTermInfo[y-1900]; var _info = [ parseInt('0x'+_table.substr(0,5)).toString() , parseInt('0x'+_table.substr(5,5)).toString(), parseInt('0x'+_table.substr(10,5)).toString(), parseInt('0x'+_table.substr(15,5)).toString(), parseInt('0x'+_table.substr(20,5)).toString(), parseInt('0x'+_table.substr(25,5)).toString() ]; var _calday = [ _info[0].substr(0,1), _info[0].substr(1,2), _info[0].substr(3,1), _info[0].substr(4,2), _info[1].substr(0,1), _info[1].substr(1,2), _info[1].substr(3,1), _info[1].substr(4,2), _info[2].substr(0,1), _info[2].substr(1,2), _info[2].substr(3,1), _info[2].substr(4,2), _info[3].substr(0,1), _info[3].substr(1,2), _info[3].substr(3,1), _info[3].substr(4,2), _info[4].substr(0,1), _info[4].substr(1,2), _info[4].substr(3,1), _info[4].substr(4,2), _info[5].substr(0,1), _info[5].substr(1,2), _info[5].substr(3,1), _info[5].substr(4,2), ]; return parseInt(_calday[n-1]); }, /** * 传入年份返回汉语通俗表示法 * @param {lunar} year * @return CN {string} * @eg:let cnYear = calendar.toChinaYear(1960);//cnYear = '一九六零' */ toChinaYear:function(y){ //年 let year = y.toString().split(""); return `${this.nStr4[year[0]]}${this.nStr4[year[1]]}${this.nStr4[year[2]]}${this.nStr4[year[3]]}`; }, /** * 传入农历数字月份返回汉语通俗表示法 * @param lunar month * @return Cn string * @eg:var cnMonth = calendar.toChinaMonth(12) ;//cnMonth='腊月' */ toChinaMonth:function(m) { // 月 => \u6708 if(m>12 || m<1) {return -1} //若参数错误 返回-1 var s = this.nStr3[m-1]; s+= "\u6708";//加上月字 return s; }, /** * 传入农历日期数字返回汉字表示法 * @param lunar day * @return Cn string * @eg:var cnDay = calendar.toChinaDay(21) ;//cnMonth='廿一' */ toChinaDay:function(d){ //日 => \u65e5 var s; switch (d) { case 10: s = '\u521d\u5341'; break; case 20: s = '\u4e8c\u5341'; break; break; case 30: s = '\u4e09\u5341'; break; break; default : s = this.nStr2[Math.floor(d/10)]; s += this.nStr1[d%10]; } return(s); }, /** * 年份转生肖[!仅能大致转换] => 精确划分生肖分界线是“立春” * @param y year * @return Cn string * @eg:var animal = calendar.getAnimal(1987) ;//animal='兔' */ getAnimal: function(y) { return this.Animals[(y - 4) % 12] }, /** * 传入阳历年月日获得详细的公历、农历object信息 <=>JSON * @param y solar year * @param m solar month * @param d solar day * @return JSON object * @eg:console.log(calendar.solar2lunar(1987,11,01)); */ solar2lunar:function (y,m,d) { //参数区间1900.1.31~2100.12.31 y = parseInt(y) m = parseInt(m) d = parseInt(d) //年份限定、上限 if(y<1900 || y>2100) { return -1;// undefined转换为数字变为NaN } //公历传参最下限 if(y==1900&&m==1&&d<31) { return -1; } //未传参 获得当天 if(!y) { var objDate = new Date(); }else { var objDate = new Date(y,parseInt(m)-1,d) } var i, leap=0, temp=0; //修正ymd参数 var y = objDate.getFullYear(), m = objDate.getMonth()+1, d = objDate.getDate(); var offset = (Date.UTC(objDate.getFullYear(),objDate.getMonth(),objDate.getDate()) - Date.UTC(1900,0,31))/86400000; for(i=1900; i<2101 && offset>0; i++) { temp = this.lYearDays(i); offset -= temp; } if(offset<0) { offset+=temp; i--; } //是否今天 var isTodayObj = new Date(), isToday = false; if(isTodayObj.getFullYear()==y && isTodayObj.getMonth()+1==m && isTodayObj.getDate()==d) { isToday = true; } //星期几 var nWeek = objDate.getDay(), cWeek = this.nStr1[nWeek]; //数字表示周几顺应天朝周一开始的惯例 if(nWeek==0) { nWeek = 7; } //农历年 var year = i; var leap = this.leapMonth(i); //闰哪个月 var isLeap = false; //效验闰月 for(i=1; i<13 && offset>0; i++) { //闰月 if(leap>0 && i==(leap+1) && isLeap==false){ --i; isLeap = true; temp = this.leapDays(year); //计算农历闰月天数 } else{ temp = this.monthDays(year, i);//计算农历普通月天数 } //解除闰月 if(isLeap==true && i==(leap+1)) { isLeap = false; } offset -= temp; } // 闰月导致数组下标重叠取反 if(offset==0 && leap>0 && i==leap+1) { if(isLeap){ isLeap = false; }else{ isLeap = true; --i; } } if(offset<0) { offset += temp; --i; } //农历月 var month = i; //农历日 var day = offset + 1; //天干地支处理 var sm = m-1; var gzY = this.toGanZhiYear(year); // 当月的两个节气 // bugfix-2017-7-24 11:03:38 use lunar Year Param `y` Not `year` var firstNode = this.getTerm(y,(m*2-1));//返回当月「节」为几日开始 var secondNode = this.getTerm(y,(m*2));//返回当月「节」为几日开始 // 依据12节气修正干支月 var gzM = this.toGanZhi((y-1900)*12+m+11); if(d>=firstNode) { gzM = this.toGanZhi((y-1900)*12+m+12); } //传入的日期的节气与否 var isTerm = false; var Term = null; if(firstNode==d) { isTerm = true; Term = this.solarTerm[m*2-2]; } if(secondNode==d) { isTerm = true; Term = this.solarTerm[m*2-1]; } //日柱 当月一日与 1900/1/1 相差天数 var dayCyclical = Date.UTC(y,sm,1,0,0,0,0)/86400000+25567+10; var gzD = this.toGanZhi(dayCyclical+d-1); //该日期所属的星座 var astro = this.toAstro(m,d); var solarDate = y+'-'+m+'-'+d var lunarDate = year+'-'+month+'-'+day var festival = this.festival var lfestival = this.lfestival var festivalDate = m+'-'+d var lunarFestivalDate = month+'-'+day return { date: solarDate, lunarDate: lunarDate, festival: festival[festivalDate] ? festival[festivalDate].title : null, lunarFestival: lfestival[lunarFestivalDate] ? lfestival[lunarFestivalDate].title : null, 'lYear':year, 'lMonth':month, 'lDay':day, 'Animal':this.getAnimal(year), 'IMonthCn':(isLeap?"\u95f0":'')+this.toChinaMonth(month), 'IDayCn':this.toChinaDay(day), 'cYear':y, 'cMonth':m, 'cDay':d, 'gzYear':gzY, 'gzMonth':gzM, 'gzDay':gzD, 'isToday':isToday, 'isLeap':isLeap, 'nWeek':nWeek, 'ncWeek':"\u661f\u671f"+cWeek, 'isTerm':isTerm, 'Term':Term, 'astro':astro }; }, /** * 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历object信息 <=>JSON * @param y lunar year * @param m lunar month * @param d lunar day * @param isLeapMonth lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可] * @return JSON object * @eg:console.log(calendar.lunar2solar(1987,9,10)); */ lunar2solar:function(y,m,d,isLeapMonth) { //参数区间1900.1.31~2100.12.1 y = parseInt(y) m = parseInt(m) d = parseInt(d) var isLeapMonth = !!isLeapMonth; var leapOffset = 0; var leapMonth = this.leapMonth(y); var leapDay = this.leapDays(y); if(isLeapMonth&&(leapMonth!=m)) {return -1;}//传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同 if(y==2100&&m==12&&d>1 || y==1900&&m==1&&d<31) {return -1;}//超出了最大极限值 var day = this.monthDays(y,m); var _day = day; //bugFix 2016-9-25 //if month is leap, _day use leapDays method if(isLeapMonth) { _day = this.leapDays(y,m); } if(y < 1900 || y > 2100 || d > _day) {return -1;}//参数合法性效验 //计算农历的时间差 var offset = 0; for(var i=1900;i<y;i++) { offset+=this.lYearDays(i); } var leap = 0,isAdd= false; for(var i=1;i<m;i++) { leap = this.leapMonth(y); if(!isAdd) {//处理闰月 if(leap<=i && leap>0) { offset+=this.leapDays(y);isAdd = true; } } offset+=this.monthDays(y,i); } //转换闰月农历 需补充该年闰月的前一个月的时差 if(isLeapMonth) {offset+=day;} //1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点) var stmap = Date.UTC(1900,1,30,0,0,0); var calObj = new Date((offset+d-31)*86400000+stmap); var cY = calObj.getUTCFullYear(); var cM = calObj.getUTCMonth()+1; var cD = calObj.getUTCDate(); return this.solar2lunar(cY,cM,cD); } }; export default calendar [代码] 附其它: wxml-to-canvas的使用 wxml 引入组件 [代码]<video class="video" src="{{src}}"> <wxml-to-canvas class="widget"></wxml-to-canvas> </video> <image src="{{src}}" style="width: {{width}}px; height: {{height}}px"></image> [代码] js 获取实例 [代码]const {wxml, style} = require('./demo.js') Page({ data: { src: '' }, onLoad() { this.widget = this.selectComponent('.widget') }, renderToCanvas() { const p1 = this.widget.renderToCanvas({ wxml, style }) p1.then((res) => { this.container = res this.extraImage() }) }, extraImage() { const p2 = this.widget.canvasToTempFilePath() p2.then(res => { this.setData({ src: res.tempFilePath, width: this.container.layoutBox.width, height: this.container.layoutBox.height }) }) } }) [代码] wxml模板支持 view、text、image 三种标签,通过 class 匹配 style 对象中的样式。 样式对象属性值为对应 wxml 标签的 class 驼峰形式。需为每个元素指定 width 和 height 属性,否则会导致布局错误。 存在多个 className 时,位置靠后的优先级更高,子元素会继承父级元素的可继承属性。 元素均为 flex 布局。left/top 等 仅在 absolute 定位下生效。
2023-06-07 - 关于跳转空白页的问题?
今天测试出来一个bug,各种方法试了都没有解决,求教大神! 问题如下:登录页登录后 navigateTo 跳转到 个人中心页,从个人中心页的一个入口 navigateTo跳转到一个 管理页面会出现一个空白页,但是重新进入小程序后就能正常的跳转!另外,PC端的开发者工具一切正常,手机打开调试安卓正常,IOS不正常,不打开调试就一直有这个问题!我把那个管理页面的 js什么都注释掉,只写了一个静态页面测试还有这个问题,使用redirectTo进行跳转也不行。。。求教大神
2022-10-19 - 小程序用户头像昵称获取规则调整公告
更新时间:2022年11月9日由于 PC/macOS 平台「头像昵称填写能力」存在兼容性问题,对于来自低于2.27.1版本的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称。 更新时间:2022年9月28日考虑到近期开发者对小程序用户头像昵称获取规则调整的相关反馈,平台将接口回收的截止时间由2022年10月25日延期至2022年11月8日24时。 调整背景在小程序内,开发者可以通过 wx.login 接口直接获取用户的 openId 与 unionId 信息,实现微信身份登录,支持开发者在多个小程序或其它应用间匿名关联同一用户。 同时,为了满足部分小程序业务中需要创建用户的昵称与头像的诉求,平台提供了 wx.getUserProfile 接口,支持在用户授权的前提下,快速使用自己的微信昵称头像。 但实践中发现有部分小程序,在用户刚打开小程序时就要求收集用户的微信昵称头像,或者在支付前等不合理路径上要求授权。如果用户拒绝授权,则无法使用小程序或相关功能。在已经获取用户的 openId 与 unionId 信息情况下,用户的微信昵称与头像并不是用户使用小程序的必要条件。为减少此类不合理的强迫授权情况,作出如下调整。 调整说明自 2022 年 10 月 25 日 24 时后(以下统称 “生效期” ),用户头像昵称获取规则将进行如下调整: 自生效期起,小程序 wx.getUserProfile 接口将被收回:生效期后发布的小程序新版本,通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。自生效期起,插件通过 wx.getUserInfo 接口获取用户昵称头像将被收回:生效期后发布的插件新版本,通过 wx.getUserInfo 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的插件版本不受影响,但如果要进行版本更新则需要进行适配。通过 wx.login 与 wx.getUserInfo 接口获取 openId、unionId 能力不受影响。「头像昵称填写能力」支持获取用户头像昵称:如业务需获取用户头像昵称,可以使用「头像昵称填写能力」(基础库 2.21.2 版本开始支持,覆盖iOS与安卓微信 8.0.16 以上版本),具体实践可见下方《最佳实践》。小程序 wx.getUserProfile 与插件 wx.getUserInfo 接口兼容基础库 2.27.1 以下版本的头像昵称获取需求:对于来自低版本的基础库与微信客户端的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称,开发者可继续使用以上能力做向下兼容。对于上述 3,wx.getUserProfile 接口、wx.getUserInfo 接口、头像昵称填写能力的基础库版本支持能力详细对比见下表: [图片] *针对低版本基础库,兼容处理可参考 兼容文档 请已使用 wx.getUserProfile 接口的小程序开发者和已使用 wx.getUserInfo 接口的插件开发者尽快适配。小游戏不受本次调整影响。 最佳实践小程序可在个人中心或设置等页面使用头像昵称填写能力让用户完善个人资料: [图片] 微信团队 2022年5月9日
2023-09-26 - 小程序性能优化指南
开发者可通过开发者工具中的性能扫描工具提前发现代码中的可优化项: 1. 代码包不包含插件大小超过 1.5 M 【建议】小程序代码包单个包大小限制为2M。因此我们建议开发者在开发时,如果遇到单包体积大于1.5M的情况,可以采取分包的方式,把部分代码拆分到分包去,降低单个包的体积,提升小程序的加载速度。具体可以查看文档《使用分包》。 2. 引用插件大小超过 200 K 【知会】小程序插件的大小是会算进小程序代码包2M体积限制中的。因此当我们发现开发者引用的插件体积大于200K时,会对开发者予以提示,避免出现上传阶段提示代码包体积超限,但是不知道为何超限的问题。 3. 图片和音频资源大小超过 200 K 【建议】小程序代码包里可以存放一些必要的静态资源(如tabbar的icon等);但其他非必要的静态资源体积过大会影响小程序代码包加载速度。因此我们建议图片、音频等静态资源体积大小超过200K时,将它们上传到CDN,用URL引入会是个更好的选择。 4. 主包存在仅被其他分包依赖的JS 【建议】当主包里存在一些JS文件只会被分包使用(而主包自己不使用)时,我们建议把这些JS文件从主包中拆分出去,放到对应的分包里,从而优化主包的加载速度。 5. 主包存在仅被其他分包依赖的组件 【建议】当主包里存在一些组件只会被分包使用(而主包自己不使用)时,我们建议把这些组件从主包拆分出去,并且可以使用 分包异步化 这个特性加载这些组件,从而优化主包的加载速度。 6. 存在无使用的插件 【必须】如果有无使用的插件,请将其从 app.json 中去除。不然它会占用代码包体积,也会延迟代码包加载的时间。 7. 存在无使用的组件 【必须】如果在对应页面JSON的 `usingComponents` 里声明的组件但是没有使用,请将其从 `usingComponents` 里去除。 8. 未开启JS压缩 【必须】在工具「详情」-「本地设置」中开启「上传代码时自动压缩脚本文件」的设置 [图片] 9. 未开启WXML压缩 【必须】在工具「详情」-「本地设置」中开启「上传代码时自动压缩wxml文件」的设置 [图片] 10. 未开启WXSS压缩 【必须】在工具「详情」-「本地设置」中开启「上传代码时自动压缩样式文件」的设置 [图片] 11. 存在无依赖文件 【必须】在「代码质量」面板,点击「建议去除」后,可以打开代码依赖分析面板的「无依赖文件」页面,这里可以看到代码包里没有被用到的文件。请在代码包中去除这部分文件,减小体积并优化加载速度。 在本地开发的过程中,会自动过滤无依赖的文件,如果出现误过滤的情况,可以在 project.config.json 的 setting 字段中添加 ignoreDevUnusedFiles 为 false,也可以在 packOptions 的 include 字段中手动将被忽略的文件引入,同时欢迎发帖反馈误报的情况提交代码片段帮助我们完善此功能 注意:页面若为配置在 app.json 中,将被识别为无依赖文件 [图片] 12. 未开启组件懒注入(按需注入) 【必须】在 app.json 中加入 `"lazyCodeLoading": "requiredComponents"` 可以开启小程序组件按需注入特性。 其他优化内容,请点击学习《小程序性能优化实践》课程 [图片]
2023-02-17 - (18)微信3D小游戏下HUD绘制的经验分享
平视显示器(head up display)简称HUD。游戏经常在三维场景上叠加文本或二维图形信息,如弹窗,血量条等,同时需要保证它们在屏幕上的位置和大小不变。 传统的H5游戏可以使用dom,或是在原本的webgl上面盖一个新的2D canvas(画布)做为HUD来实现,同时使用其接口就可以画出HUD所需要的内容。 但微信小游戏只支持一个画布,无法和传统H5游戏的绘制方式一样。因此,要在3D世界中实现HUD就必须在这个唯一的画布上实现。 我们在后台收到了许多反馈:如何用小游戏的框架来实现HUD的绘制。这一期的小故事,我们跟大家分享如何在微信3D小游戏中绘制HUD: 本文的内容包括: 1.微信小游戏只支持一个画布 2.如何使用三维平面模拟HUD? 3.相机变化导致HUD产生位移缩放 4.如何用图形渲染管线解决上述问题? 5.绘制场景时视点变化与投影阶段的问题 6.如何使用顶点着色器解决上述问题? 微信小游戏只支持一个画布 与浏览器不同,微信客户端只有一个画布,并且不能使用html。 普通H5游戏会使用html,或是创建一个新的2D canvas标签,定位在原本的webgl canvas上面,同时使用2D canvas的接口就可以画出HUD的内容。但微信小游戏不支持这样做。所以在三维世界中要实现HUD,需要在一个画布上实现。 所以在三维世界中要实现HUD,则必须在这个唯一的画布上实现。 如何使用三维平面模拟HUD? 对于图像,webgl可以通过纹理贴图来展示图像。开发者可将图片作为的纹理贴图,贴在一个三维矩形平面上,使平面一直正对相机,来模拟HUD。 [图片] 对于文字,微信小游戏三维的canvas是使用webgl作为context的。但是webgl却无法像2D的context能直接画文字。开发者如果直接用webgl画出文字,需要导入文字模型的顶点数据,但由于文字比较复杂,顶点数量多,相当于渲染了一个复杂的3D物体,这种方式无论是从文件大小还是性能上,都会有损体验。 [图片] 那么是否可以使用2D canvas 绘好文字,再作为纹理贴在三维平面上呢? 虽然微信小游戏只能渲染一个canvas,但是开发者可以创建多个的canvas实例。 Step1:开发者可创建一个离屏的2D canvas,再使用2D的接口绘制文字、图片等; Step2:开发者可将这个离屏canvas传给webgl,当成一个texture,贴到一个三维的平面物体上,使其永远都在相机的正前方,通过这样模拟HUD 。 补充 webgl支持直接将canvas作为纹理; void gl.texImage2D(target, level, internalformat, format, type, HTMLCanvasElement? pixels)。 [图片] 相机变化导致HUD产生位移缩放 游戏场景中的相机是会改变的,比如说吃鸡游戏中的第一人称和第三人称视角转化。我们发现了一个问题:当相机的可视范围变化的时候,HUD就会发生形变。 [图片] 那是因为视野看的越广,映射到屏幕上的时候,同一个物体就显得越小。 我们需要保证HUD在任何视角下位置大小都是正确无误的。那么如何才能做到呢? 要解决这个问题就需要明白计算机是如何把三维场景画到二维的屏幕上的。这个画的过程也就是计算机图形渲染管线帮我们完成的。 如何用图形渲染管线解决上述问题? 画一个三维物体到二维平面可以分为三个阶段: ●“准备数据” (应用程序阶段) ●“画点” (几何阶段) ●“画像素” (光栅化阶段) [图片] 一个HUD实际上是一个矩形的平面物体,通过矩形的4个顶点就可以描述出来一个平面的位置、大小。为了让平面的位置,大小看起来没问题,我们需要修改“画点”阶段的逻辑。这个阶段又可以进行如下的细分。 [图片] 与摄影机相关的逻辑,是视点变换还有投影阶段。我们可以通过修改这两者的逻辑来达到我们的目的。 绘制场景时视点变化与投影阶段的问题 1.视点变化阶段的问题 我们需要绘制摄像机看到的世界,而摄像机可以处在任意位置观察这个世界。视点变化本质是就是根据摄像机看的方向来旋转物体,从而让三维空间的物体正确旋转到观察者看到的样子。原本是摆正放的物体,由于观察者的视角问题(歪着看),所以显示出来物体最终也是歪的。 [图片] 通过在应用程序阶段定义相机的视点、观察目标点以及上方向等数据,我们可以得到一个叫做视图矩阵(View Matrix)的矩阵。把这个矩阵与物体的位置做矩阵乘法就可以得到物体变化后的新位置。 因为游戏世界中,摄像机的位置是不停变化的,而我们的物体却需要一直出现在摄像机正前方。所以游戏场景中的视觉矩阵(View Matrix)在每一帧的渲染中,可能都在变化。这里我们只要将HUD原本一直在变化的视觉矩阵(View Matrix)替换为我们需要的,并且保持不变就好了。 2.投影阶段的问题 投影其实是把透视摄像机原本的可视范围,压缩成一个单位立方体。 [图片] 再通过屏幕映射,就会出现如下的效果出来。 [图片] 这一个过程中,会通过摄像机定义的数据(比如长宽比,视场,近截面,远截面),来生成一个叫做投影矩阵(Projection Matrix)的矩阵。将这个矩阵与位置信息进行矩阵乘法,再进行一些归一化操作,就会得到单位立方体内的位置。 和视觉矩阵(View Matrix)一样,对于HUD的物体,我们也不能使用透视摄像机生成的矩阵,否则就会可能导致大小变化。我们替换成正视摄像机的矩阵。这样算出来的位置就是永远都是正常的,不需要担心游戏中更新了相机的数据。 如何使用顶点着色器解决上述问题? 现在我们要用顶点着色器来修改视点变换还有投影的逻辑。 顶点着色器与片元着色器都是 webgl 提供给我们用来操作渲染管线的能力。让我们可以使用glsl 这种编程语言来对 GPU 的能力进行编程。 [图片] 顶点着色器运行在“画点”阶段(几何阶段),也就是对每个三维物体的顶点进行计算。片元着色器运行在“画像素”阶段(光栅化阶段),把顶点围起来的像素(其实是片元)画上颜色。 我们可以通过顶点着色器,修改视点变换与投影的逻辑,最后达到我们的效果。 总结 由于微信小游戏支持一个单独的画布,开发者想要在任何游戏场景下绘制正常的HUD,可以通过顶点着色器的能力,去修改视点变换与投影的所用到的矩阵,最终来解决这个问题。 微信小游戏还有很多与H5游戏、客户端游戏不一样的设计理念与特点,我们会在后续的文章里继续分享微信小游戏背后的小故事。 历史文章回顾 :小程序•小故事 如果大家有想了解的小程序相关能力的故事,欢迎在评论区留言,我们后续会考虑将这些能力背后的故事分期分享给大家。
2018-08-17