- openSetting getUserInfo 要不要废弃,腾讯你出来说个清楚
如题,要废弃,又不废弃,线下废弃,线上又不废弃,已上线的不废弃,新更新的废弃。你倒是给废弃啊,现在弄得成了你家的能用不是你家的就不能用,大公司能用,小公司就不能用了,告诉我一个解释
2018-08-13 - 你不知道的小程序系列之生命周期执行顺序
再次开始之前先问几个问题: 你是否知道[代码]Page[代码]生命周期 与 [代码]pagelifetimes[代码] 生命周期执行顺序? 你是否知道[代码]behaviors[代码]中的生命周期与组件生命周期执行顺序? 你是否知道[代码]Page[代码]生命周期 与 组件[代码]pagelifetimes[代码]生命周期执行顺序? 要回答上面的问题,首先我们看看小程序生命周期有哪些: App onLaunch onShow onHide Page onLoad onShow onReady onHide onUnload Component created attached ready moved detached 想一下加载一个页面(包含组件)的加载顺序,按照直觉小程序加载顺序应该是这样的加载顺序(以下列子中[代码]Component[代码]都是同步组件): App(onLaunch) -> Page(onLoad) -> Component(created) 但其实并不然,小程序的加载顺序是这样的: 首先执行 [代码]App.onLaunch[代码] -> [代码]App.onShow[代码] 其次执行 [代码]Component.created[代码] -> [代码]Component.attached[代码] 再执行 [代码]Page.onLoad[代码] -> [代码]Page.onShow[代码] 最后 执行 [代码]Component.ready[代码] -> [代码]Page.onReady[代码] 其实也不难理解微信这么设计背后的逻辑,我们先看下官方的的生命周期: [图片] 可以看到,在页面[代码]onLoad[代码]之前会有页面[代码]create[代码]阶段,这其中就包含了组件的初始化,等组件初始化完成之后,才会执行页面的[代码]onLoad[代码], 之后页面[代码]ready[代码]事件也是在组件[代码]ready[代码]之后才触发的。 下面我们来看看 [代码]Behavior[代码], [代码]Behavior[代码] 与 [代码]Vue[代码]中的 [代码]mixin[代码] 类似,猜想下其中的执行顺序: Behavior.created => Component.created 测试下来和预期相符,其实在[代码]Vue[代码]的文档中有一段这样的描述: 另外,混入对象的钩子将在组件自身钩子之前调用。 这样的设计和主流设计保持一致。接下来我们看看 [代码]pageLifetimes[代码],有[代码]show[代码]和[代码]hide[代码]生命周期对应页面的展示与隐藏,预期的执行顺序: pageLifetime.show => Page.onShow 测试下来也和预期相符,那么我们可以推断出如下的结论: 当页面中包含组件时,组件的生命周期(包括pageLifetimes)总是优先于页面,[代码]Behaviors[代码]生命周期优先于组件的生命周期。但其实有个例外:页面退出堆栈,当页面[代码]unload[代码]时会执行如下顺序: Page.onUnload => Component.detached 看了以上的分析你应该知道了答案,最后做个总结(demo): [图片] 最后的最后布置个作业 异步组件(异步渲染的组件,通常是通过if条件判断是否渲染)的生命周期执行顺序是怎样的,pagelifetimes会不会执行?
2020-01-10 - 集成微信对话开放平台(openaiwid)的小程序插件后,如何集成其他的语音识别和语音合成接口,多谢
已经集成了openaiwidget小程序插件,但是插件自带的语音合成不能满足业务要求,请问该如何集成其他语音合成接口,多谢
2020-04-20 - 用开发者工具控制开发环境和生产环境
最近开发过程中遇到一个尴尬的情况,发版前调试一下,API地址改到了测试环境,改完OK发版。。。结果尴尬了,测试地址忘了改回来。审核等待前功尽弃。。。。 这种事情对于匆匆忙忙写bug,匆匆忙忙发版的程序猿来说肯定会发生第二次,第三次。遇到问题,解决问题。 开始翻看找小程序的api文档,于是找到了 wx.getSystemInfo 但是这个API只能区分是pc还是移动端并没有其他有用的信息 接下来去开发社区 https://developers.weixin.qq.com/community/develop/article/doc/000ec87cdd8070c3ba89fe00051813 突然看到这篇文章很有道理,虽然是想直接把作者的方法拿来用,但是想到要在运行时环境通过一个try cath判断控制变量心里有点发虚,再想想。 不过作者提到了灵感来自于node,那是不是可以直接写一个node脚本进行预编译呢。想想小程序的开发者工具还是很open的,兴许可以呢。毕竟还提供了命令行调用,可以考虑一下从这里入手(这太麻烦了。。。。) https://developers.weixin.qq.com/miniprogram/dev/devtools/cli.html 又玩弄了一番开发者工具,终于发现了一个有趣的功能,开发者工具竟然提供了自定义处理命令(编译前的钩子) [图片] 在执行相应的操作前钩子可以执行shell命令,那不是说预览和上传之前都可以调用node脚本了吗。 流程 :预览,上传前》执行调用node脚本命令,带上环境参数》node脚本运行 获取命令行参数》根据参数生成不同环境中使用的配置代码》重新写入api配置文件》预览,上传 [图片] api.template.js 用作config文件模板,需要根据开发环境动态生成的变量可以用占位符的形式写在里面 [代码]var WxApiRoot = '%%API_URL%%'; module.exports=WxApiRoot; [代码] env.config.js 用于配置不同开发环境的配置文件 [代码]const envConfig = { DEV: { API_URL: 'https://dev.com', }, PROD: { API_URL: 'https://pro.com', }, }; module.exports = envConfig; [代码] 接下来是node脚本的入口文件: 首先在项目根目录安装: [代码]cnpm install node-notifier --save-dev [代码] 选择安装,目的是为了编译之后提醒开发者配置文件预编译结果 脚本内容: [代码]const fs = require('fs'); const exec = require('child_process').exec; const envConfig = require('./config/env.config'); const _arguments = process.argv.splice(2); const path = require('path'); const [MINIENV = 'DEV'] = _arguments; const notifier = require('node-notifier'); const templatePath = './config/api.template.js'; const configPath = './config/api.js'; const config = envConfig[MINIENV]; //读取配置文件模板 let templateConfig = fs.readFileSync(templatePath, 'utf8'); //拿到那些变量需要动态编译 const configKeys = Object.keys(config); //通过遍历替换的方式替换模板中的占位符 configKeys.forEach((key) => { const regText = `%%${key}%%`; const regObj = new RegExp(regText, 'g'); templateConfig = templateConfig.replace(regObj, config[key]); toast(`编译环境:${MINIENV}--${key}`, config[key]); }); //把替换后的模板字符串写入api文件中 fs.writeFileSync(configPath, templateConfig); /** * @description 警告提示,方便开发者看到编译过后的配置文件的变量信息 * @author songs * @date 2020-04-27 * @param {*} title * @param {*} content */ function toast(title, content) { console.log(path.join(__dirname, './', 'login-bg.png')); notifier.notify({ title: title, message: content, icon: path.join(__dirname, './', 'login-bg.png'), }); } [代码] 这样 每次预览或者上传之前都可以执行一次node脚本了,config文件预编译完成,再上传。虽然还是没有从根本上解决问题,但是如果配上一个上线流程规范,应该可以应付一大半的环境区分问题吧。 预编译用到的文件不需要上传,可以再project.config里设置忽略上传 [代码] "packOptions": { "ignore": [ { "type": "file", "value": "config/api.template.js" }, { "type": "file", "value": "config/env.config.js" }, { "type": "file", "value": "login-bg.png" } ] }, [代码] 结束!!
2020-04-29 - 用typescript开发的能不能隐藏与ts文件同名的js文件呢?
用typescript开发的能不能隐藏与ts文件同名的js文件呢? 刚刚误点点到js文件没发现,改了好久,然后点一下编译,白写了
2020-06-03 - 原生小程序全局状态管理方案
原生小程序状态管理: mpsm 具备vue+react开发体验 整合了【vue的属性监听watch、计算属性computed】和【 dvajs 的全局状态管理模式】,解决跨页通信问题, 提出【组件圈子】概念,摆脱各种父子、兄弟、姐妹、街坊邻居、远房表兄弟等组件间的通信浆糊困扰。 数据流 不管是页面间,还是组件间,嵌套组件内部,都可以通过简单的dispach来管理全局状态或圈子状态(局部)。 [图片] [图片] 使用介绍 完全兼容原生代码,已有的业务逻辑代码,即便不适配也可使用此库,不影响已有业务逻辑。 原生: [代码]Page()[代码] 变为 [代码]page()()[代码] 原生: [代码]Component()[代码] 变为 [代码]component()()[代码] 初始化 [代码]//app.js import {page} from './mpsm/index' import models from './models/index' page.init(models, {}, {}) [代码] 第二、三参数分别为页面和组件的公共options,会在每个页面或组件生效,对于同名的对象和函数,会进行合并处理。 页面或组件注册 小:即只需将Page、Component首字母小写。 尾巴:即尾部多调用一次: [代码]page({ //component // ... })() [代码] [代码]// 数据更新有两种方式 // 第一种,直接修改this.data, 需要更新时调用this.update() this.data.a = 1 this.update() // 第二种 this.$setData this. $setData({ list: list }) //第一种更新方式,根据需要自己确定数据更新的时机,调用this.update() //第二种更新方式,会进行批处理,待函数执行结束后统一批量更新,异步操作立即更新,类似react中的setState [代码] 方法 方法 说明 备注 page 用于注册页面 page()() component 用于注册组件 component()() dispatch 状态分发 参数object, {type, action, lazy}, 默认lazy: true, 懒更新,表示只更新当前展示页面的状态,其它页面待onShow触发后再更新 getComponentOps 获取公共组件配置选项 不包含函数,只是简单的JSON格式数据 getOps 获取公共页面配置选项 不包含函数,只是简单的JSON格式数据 subscribe 订阅单一数据源 参数: ‘userInfo/setup’ unsubscribe 取消订阅单一数据源 参数: ‘userInfo/setup’ 单一数据源的订阅取消 方法: unsubscribe, subscribe 取消订阅不使用dva的[代码]app.unmodel()[代码],同时,subscription 必须返回 unlisten 方法,用于取消数据订阅。 [代码]subscribe('userInfo/setup'); unsubscribe('userInfo/setup'); [代码] 页面注册、组件注册示例 [代码]import {dispatch, page, component} from '../../mpsm/index' page({ // 或者 component watch: { // 属性监听 isLogin(newState, oldState) { } }, computed: { // 计算属性 countComputed(data) { return data.count * 2 } }, data: { count: 2 }, onLoad() {}, login() { dispatch({ type: 'userInfo/save', payload: { isLogin: true } }) }, changeGroupState() { this.dispatch({ type: 'group/index-a-1', payload: { nameA: 'name' } }) }, })(({userInfo}) => {//订阅全局状态 return { isLogin: userInfo.isLogin } }, (groups) => {//订阅圈子状态,page为全局圈子,component为组件所在圈子 return { nameA: groups.nameA && groups.nameA.name || '--' } }) [代码] tips: dispatch用于分发全局状态,风格与dva保持一致, 不过多了一个lazy字段, 默认true(懒更新,只更新当前页面,其他页面onShow时再更新),false的话,就是全量更新; Page和Component实例内置this.dispatch方法,用于分发局部状态。 3、组件可监听page的生命周期函数,无需做版本兼容,只需将想要监听的函数名与page内一直即可,即 [代码]// 原生的pageLifetimes可监听的周期太少,且版本要求高,监听了onShow,就不需要监听show了,避免执行两次 component({ pageLifetimes: { onShow: function () { }, onHide: function () { }, onPageScroll: function () { }, }, })() [代码] 目前可监听的生命周期 [代码]['onShow', 'onHide', 'onResize', 'onPageScroll', 'onTabItemTap', 'onPullDownRefresh', 'onReachBottom'][代码] 有了这个功能,就可以编写很多自控组件了,比如吸顶效果的导航栏等。 models 全局状态 参考 dvajs [代码]export default { namespace: '', state: { }, subscriptions: { }, effects: { }, reducers: { } } [代码] model 方法 详情参阅 dva select、put、history tips: put只用于简单的model内部的action分发,未封装put.resolve等方法,可使用async/await实现同等效果; 依据小程序特性,history回调中两个参数分别为当前页面路由信息和上一页路由信息,为一个对象,格式如。 [代码] export default { subscriptions: { setup({dispatch, history, select}) { const callback = (current, prev) => { dispatch({ type: 'save', payload: { prev, // {route: 'pages/index/index',options: {id: 123}} current } }) } history.listen(callback) return () => history.unlisten(callback) } } } [代码] 组件圈子 传统的组件数据流 对于嵌套组件间的数据通信,往往存在所谓的父子组件关系,内层组件想要向外层组件或其它分支上的组件传递数据,往往通过外层的组件通过监听函数来接收并派发,层层传递,这种 这种层层转接的模式,繁琐且需要专门的函数去维护,不利于组件的拓展和移植。 [图片] 小程序的官方组件数据流 而对于小程序的原生组件系统来说,behaviors, relations, definitionFilter, triggerEvent, getRelationNodes等编写组件需要使用的属性或方法, 真的是辣眼睛!小程序官方组件系统基本丧失了作为组件的意义, 虽然它还在努力地更新升级, 时不时文档中出现[代码]"不推荐使用这个字段,而是使用另一个字段代替,它更加强大且性能更好"[代码]的字眼, 大家不要信!!! [图片] [图片] 什么叫组件?相对独立,具有明确约定的接口,依赖语境,可自由组合,可拓展,可独立部署,亦可复合使用!!!你说你除了相对独立,你说你还有啥,你说你还有个啥!!! [图片] 组件圈子数据流 对于一个组件来说,在最初编写时,不应该去关心自己会被挂载到哪,自己只需要为圈子提供数据, 甚至是数据键名也不用关心,放到圈子里,谁爱用谁用,无需回调函数来协助传递数据, 这才是作为组件可移植可复用的意义,在一个圈子里,不存在父子兄弟的概念,也没有哪个组件生来是给人当儿子的,组件间的往来只有最纯粹的数据通信。 如下图中的组件C与组件F之间的数据通信,一个提供数据,另一个直接去使用就行了,不管层级多少,直接通信,没有多余的步骤。 [图片] [代码] <!--组件a in page--> <a group-name="index-a-1" group-keys="{{ {name: 'nameA1'} }" group-data="{{ {a: 1} }}"></a> [代码] 只需给组件赋值一个[代码]group-name[代码]属性,便给组件分配了一个圈子, 组件内[代码]this.dipatch({})[代码]分发地[代码]payload[代码]便是该组件给圈子贡献的数据, 也是该组件对外约定的数据值。 [代码]group-keys[代码]是用于避免字段名称冲突的,示例中可将a贡献地数据键值更改为[代码]nameA1[代码], 而值不变,所以圈子中地组件很纯粹,只向所在圈子提供必要的数据值, 至于你将组件放置何处,数据给谁用,名称怎么修改,都与我无关。 [代码]group-data[代码]是组件所依赖的外部值,是一个内置属性,可在组件的[代码]watch[代码]配置中进行监听。 页面、组件实例对象 注意: 对配置中的函数,以及setData进行了简单封装,在页面注册或组件注册options的函数中, setData的数据并不会马上更新,而是合并收集,待当前函数执行完毕后, 先模拟计算出computed,与收集的结果合并,再diff,最后setData一次。 与react更新机制类似,对于函数中出现的异步操作,不会进行数据收集,而是直接模拟计算值,diff,接着setData 页面 对圈子状态的管理拥有最高权限 属性 属性 说明 备注 $groups 获取当前页面内的所有圈子状态值 object, 只读 [代码] this.$groups['index-a-1'] [代码] 方法 dispatch 用于强制更新某个圈子中的状态 [代码] this.dispatch({ type: 'data/index-a-1', payload: { nameA1: 'name' } }) [代码] 参数 说明 备注 type 格式: 更新类型/圈子名称,更新类型:data 或 group,data表示更新圈子数据的同时,也将payload中的数据更新至this.data, 即this.setData(payload) string payload 需要更新的状态值 object 组件 只更新所在圈子状态 属性 属性 说明 备注 $groups 获取所在页面内的所有圈子状态值 object, 只读 $group 获取所在圈子状态值 object, 只读 [代码] this.$group['nameA1'] [代码] 方法 dispatch 用于更新所在圈子中的状态 [代码] this.dispatch({ type: 'data', // group payload: { nameA1: 'name' } }) [代码] 参数 说明 备注 type 格式: 更新类型,更新类型:data 或 group,data表示更新圈子数据的同时,也将payload中的数据更新至this.data, 即this.setData(payload) string payload 需要更新的状态值 object tips 更新类型 之所以会有data更新类型,是因为组件提供的数据名称,有可能会被改写,所以组件不要去监听自己的数据,那是给外人用的。 有时候组件向圈子贡献了数据,但自身并不需要这些数据更新到data,所以会有group更新类型 状态更新机制 [图片] 定制diff 腾讯官方的westore库,为小程序定制了一个diff,本想用在自己库里, 但使用中发现diff结果不太对,他会将删除的属性值重置为null,我并不需要为null, 当去遍历对象的属性值,就会多出一个为null的属性,莫名其妙,删了就是删了,不必再保留, 并且其内部实现,在进行diff前先进行一次同步下key键,这个完全没必要,浪费性能。 westore的diff应该是用在全局状态新旧状态上,其结果违反setData的数据拼接规则, 也不符合原生数据更新的习惯, 所以我得自己写一个diff,先列出基本情况: 1、不改变原生setData的使用习惯; 2、明确diff使用的场景,针对具体场景定制:属性更新时需要diff,setData时需要diff; 3、diff返回结果的格式,setData会用到,需要满足 [代码]a.b[0].a[代码]这样的格式,另外如果属性变化,还需要根据键值调用watch; 4、参与对比的两个参数都有一个共同特点,就是都是拿局部变化的数据,去对比全量旧数据,属性的话,两者的键值对更是对等的,所以无需同步键值; 定制的diff: [代码] diff({ a: 1, b: 2, c: "str", d: { e: [2, { a: 4 }, 5] }, f: true, h: [1], g: { a: [1, 2], j: 111 } }, { a: [], b: "aa", c: 3, d: { e: [3, { a: 3 }] }, f: false, h: [1, 2], g: { a: [1, 1, 1], i: "delete" }, k: 'del' }) [代码] diff结果 [代码] const diffResult = { result: { a: 1, b: 2, c: "str", 'd.e[0]': 2, 'd.e[1].a': 4, 'd.e[2]': 5, f: true, g: { a: [1,2], j: 111 }, h: [1] }, rootKeys: { a: true, b: true, c: true, d: true, g: true, h: true, } } [代码] 记录rootKeys是因为属性监听不需要 'a.b’这样的格式,在diff中记录下来,便少了一次遍历筛选拆分。
2019-12-16 - 自定义tabbar 【恋爱小清单开发总结】
看官方demo的小伙伴知道,自定义tabbar需要在小程序根目录底下建一个名叫custom-tab-bar的组件(我有试过,如果放在components目录里面小程序会识别不了),目前我自己实现的效果是:通过在配置可以切换tab,也可以点击tab后重定向到新页面,支持隐藏tabbar,同时也可以显示右上角文本和小红点。 官方demo里面用的是cover-view,我改成view,因为如果页面有弹窗的话我希望可以盖住tabbar 总结一下有以下注意点: 1、tabbar组件的目录命名需要是custom-tab-bar 2、app.json增加自定义tabbar配置 3、wx.navigateTo不允许跳转到tabb页面 4、进入tab页面时,需要调用tabbar.js手动切换tab 效果图: [图片] 可以扫码体验 [图片] 代码目录如下: [图片] 代码如下: app.json增加自定义tabbar配置 "tabBar": { "custom": true, "color": "#7A7E83", "selectedColor": "#3cc51f", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/love/love", "text": "首页" }, { "pagePath": "pages/tabbar/empty", "text": "礼物说" }, { "pagePath": "pages/tabbar/empty", "text": "恋人圈" }, { "pagePath": "pages/me/me", "text": "我" } ] }, 自定义tabbar组件代码如下 index.js //api.js是我自己对微信接口的一些封装 const api = require('../utils/api.js'); //获取应用实例 const app = getApp(); Component({ data: { isPhoneX: false, selected: 0, hide: false, list: [{ showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/love/love", iconPath: "/images/tabbar/home.png", selectedIconPath: "/images/tabbar/home-select.png", text: "首页" }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/gifts/giftList", iconPath: "/images/tabbar/gift.png", selectedIconPath: "/images/tabbar/gift-select.png", text: "礼物说", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/moments/moments", iconPath: "/images/tabbar/lover-circle.png", selectedIconPath: "/images/tabbar/lover-circle-select.png", text: "恋人圈", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/me/me", iconPath: "/images/tabbar/me.png", selectedIconPath: "/images/tabbar/me-select.png", text: "我" }] }, ready() { // console.error("custom-tab-bar ready"); this.setData({ isPhoneX: app.globalData.device.isPhoneX }) }, methods: { switchTab(e) { const data = e.currentTarget.dataset; console.log("tabBar参数:", data); api.vibrateShort(); if (data.hideTabBar) { api.navigateTo(data.navigatePath); } else { /*this.setData({ selected: data.index }, function () { wx.switchTab({url: data.path}); });*/ /** * 改为直接跳转页面, * 因为发现如果先设置selected的话, * 对应tab图标会先选中,然后页面再跳转, * 会出现图标变成未选中然后马上选中的过程 */ wx.switchTab({url: data.path}); } }, /** * 显示tabbar * @param e */ showTab(e){ this.setData({ hide: false }, function () { console.log("showTab执行完毕"); }); }, /** * 隐藏tabbar * @param e */ hideTab(e){ this.setData({ hide: true }, function () { console.log("hideTab执行完毕"); }); }, /** * 显示小红点 * @param index */ showRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = true; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏小红点 * @param index */ hideRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = false; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 显示tab右上角文本 * @param index * @param text */ showBadge(index, text, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: true, badgeText: text}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏tab右上角文本 * @param index */ hideBadge(index, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: false, badgeText: ""}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } } } }); index.html <view class="footer-tool-bar flex-center {{isPhoneX? 'phx_68':''}}" hidden="{{hide}}"> <view class="tab flex-full {{selected === index ? 'focus':''}}" wx:for="{{list}}" wx:key="index" data-path="{{item.pagePath}}" data-index="{{index}}" data-navigate-path="{{item.navigatePath}}" data-hide-tab-bar="{{item.hideTabBar}}" data-open-ext-mini-program="{{item.openExtMiniProgram}}" data-ext-mini-program-app-id="{{item.extMiniProgramAppId}}" bindtap="switchTab"> <view class="text"> <view class="dot" wx:if="{{item.showRedDot}}"></view> <view class="badge" wx:if="{{item.showBadge}}">{{item.badgeText}}</view> <image class="icon" src="{{item.selectedIconPath}}" hidden="{{selected !== index}}"></image> <image class="icon" src="{{item.iconPath}}" hidden="{{selected === index}}"></image> </view> </view> </view> index.json { "component": true, "usingComponents": {} } index.wxss @import "/app.wxss"; .footer-tool-bar{ background-color: #fff; height: 100rpx; width: 100%; position: fixed; bottom: 0; z-index: 100; text-align: center; font-size: 24rpx; transition: transform .3s; border-radius: 30rpx 30rpx 0 0; /*padding-bottom: env(safe-area-inset-bottom);*/ box-shadow:0rpx 0rpx 18rpx 8rpx rgba(212, 210, 211, 0.35); } .footer-tool-bar .tab{ color: #242424; height: 100%; line-height: 100rpx; } .footer-tool-bar .focus{ color: #f96e49; font-weight: 500; } .footer-tool-bar .icon{ width: 44rpx; height: 44rpx; margin: 18rpx auto; } .footer-tool-bar .text{ line-height: 80rpx; height: 80rpx; position: relative; display: inline-block; padding: 0rpx 40rpx; box-sizing: border-box; margin: 10rpx auto; } .footer-tool-bar .dot{ position: absolute; top: 16rpx; right: 16rpx; height: 16rpx; width: 16rpx; border-radius: 50%; background-color: #f45551; } .footer-tool-bar .badge{ position: absolute; top: 8rpx; right: 8rpx; height: 30rpx; width: 30rpx; line-height: 30rpx; border-radius: 50%; background-color: #f45551; color: #fff; text-align: center; font-size: 20rpx; font-weight: 450; } .hide{ transform: translateY(100%); } app.wxss(这里的样式文件是我用来存放一些公共样式) /**app.wxss**/ page { background-color: #f5f5f5; height: 100%; -webkit-overflow-scrolling: touch; } .container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box; } .blur { filter: blur(80rpx); opacity: 0.65; } .flex-center { display: flex; align-items: center; justify-content: center; } .flex-column { display: flex; /*垂直居中*/ align-items: center; /*水平居中*/ justify-content: center; flex-direction: column; } .flex-start-horizontal{ display: flex; justify-content: flex-start; } .flex-end-horizontal{ display: flex; justify-content: flex-end; } .flex-start-vertical{ display: flex; align-items: flex-start; } .flex-end-vertical{ display: flex; align-items: flex-end; } .flex-wrap { display: flex; flex-wrap: wrap; } .flex-full { flex: 1; } .reset-btn:after { border: none; } .reset-btn { background-color: #ffffff; border-radius: 0; margin: 0; padding: 0; overflow: auto; } .loading{ opacity: 0; transition: opacity 1s; } .load-over{ opacity: 1; } .phx_68{ padding-bottom: 68rpx; } .phx_34{ padding-bottom: 34rpx; } 另外我还对tabbar的操作做了简单的封装: tabbar.js const api = require('/api.js'); /** * 切换tab * @param me * @param index */ const switchTab = function (me, index) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { console.log("切换tab:", index); me.getTabBar().setData({ selected: index }) } }; /** * 显示 tabBar 某一项的右上角的红点 * @param me * @param index */ const showRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showRedDot(index, success, fail); } }; /** * 隐藏 tabBar 某一项的右上角的红点 * @param me * @param index */ const hideRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideRedDot(index, success, fail); } }; /** * 显示tab右上角文本 * @param me * @param index * @param text */ const showBadge = function (me, index, text, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showBadge(index, text, success, fail); } }; /** * 隐藏tab右上角文本 * @param me * @param index */ const hideBadge = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideBadge(index, success, fail); } }; /** * 显示tabbar * @param me * @param success */ const showTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showTab(success); } }; /** * 隐藏tabbar * @param me * @param success */ const hideTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideTab(success); } }; module.exports = { switchTab, showRedDot, hideRedDot, showBadge, hideBadge, showTab, hideTab }; 最后,进入到tab对应页面的时候要手动调用一下swichTab接口,然tabbar聚焦到当前tab /** * 生命周期函数--监听页面显示 */ onShow: function () { tabbar.switchTab(this, this.data.tabIndex);//tabIndex是当前tab的索引 }
2021-11-09 - 小程序分享朋友圈功能,getUpdateManager使用不了吗
[图片][图片]小程序分享朋友圈功能,通过分享的页面点进去getUpdateManager报错,数据无法加载。 。 。 。 。 。 。 。
2020-07-10 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 小程序蓝牙开发框架
wx-simple-bluetooth 名称:wx-simple-bluetooth 适用平台:微信小程序 蓝牙:低功耗蓝牙 这个项目从蓝牙连接、蓝牙协议通信、状态订阅及通知三个层面进行设计,可以很方便的定制您自己的小程序的蓝牙开发。主要功能如下: !!!需要先开启微信开发工具的增强编译!!! 这个项目从蓝牙连接、蓝牙协议通信、状态订阅及通知三个层面进行设计,可以很方便的定制您自己的小程序的蓝牙开发。主要功能如下: 以下是在手机开启了蓝牙功能、GPS以及微信定位权限的情况下: 执行[代码]getAppBLEManager.connect()[代码]会自动扫描周围的蓝牙设备,每[代码]350ms[代码]扫描一次,在该次内连接信号最强的设备。 可设置扫描周边蓝牙设备时,主 [代码]service[代码] 的 [代码]uuid[代码] 列表,以及对应的用于通信的服务id;还可额外添加蓝牙设备名称来进一步筛选设备。 可订阅蓝牙连接状态更新事件,并同步通知前端。 可订阅获取接收到的蓝牙协议事件。依据您的配置,框架内部会自行处理,并只返回最需要的数据给前端。 [代码]注意:目前在发送数据时大于20包的数据会被裁剪为20包[代码]。 已更新为2.x.x版本! 新的版本包括多种新的特性: 提供了完整的示例及较为详细的注释。 重构了蓝牙连接以及重连的整个流程,使其更加稳定和顺畅,也提高了部分场景下重连的速度。[代码](有些蓝牙连接问题是微信兼容或是手机问题,目前是无法解决的。如错误码10003以及部分华为手机蓝牙连接或重连困难。如果您有很好的解决方案,还请联系我,十分感谢)[代码] 只有在连接到蓝牙设备并使用特征值注册了read、write、notify监听后才算连接成功。 在扫描周围设备时,可按自定义的规则过滤多余设备,连接指定设备。详见示例[代码]lb-example-bluetooth-manager.js[代码]。 新增蓝牙协议配置文件,可以很方便的发送和接收蓝牙协议。 优化了蓝牙状态更新和蓝牙协议更新的订阅方式。现在可以更清晰的区分是蓝牙的状态更新还是接收到了新的协议(以及接收到的协议数据是什么),并且会过滤掉与上一条完全相同的通知。 可随时获取到最新的蓝牙连接状态。 各个业务均高度模块化,在深入了解后,可以很方便的拓展。 微信小程序官方蓝牙的部分说明: 小程序不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。 若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。 wx.writeBLECharacteristicValue并行调用多次会存在写失败的可能性。 所以基于这方面的考虑,本框架有以下约束: 必须按照约定的协议格式来制定协议,才能正常使用该框架。 协议一包数据最多20个字节。该框架不支持大于20个字节的协议格式。如果数据超出限制,建议拆分为多次发送。 建议以串行方式执行写操作。 建议先了解清楚小程序的官方蓝牙文档,方便理解框架的使用。 [代码]协议约定格式:[...命令字之前的数据(非必需), 命令字(必需), ...有效数据(非必需 如控制灯光发送255,255,255), 有效数据之后的数据(非必需 如协议结束标志校、验位等) 协议格式示例:[170(帧头), 10(命令字), 1(灯光开启),255,255,255(三个255,白色灯光),233(协议结束标志,有的协议中没有这一位),18(校验位,我胡乱写的)] 有效数据是什么: 在刚刚的这个示例中,帧头、协议结束标志是固定的值,校验位是按固定算法生成的,这些不是有效数据。而1,255,255,255这四个字节是用于控制蓝牙设备的,属于有效数据。 [代码] 该项目如有帮助,希望在GitHub上给个star! 快速集成示例 使用前必须检查手机是否开启蓝牙功能、定位功能、微信一定要给予定位权限(较新的iOS版微信要给予蓝牙权限) 导入项目下的[代码]modules[代码]文件夹到你的项目 1、在小程序页面中设置事件订阅及蓝牙连接、断开示例 [代码]import Toast from "../../view/toast"; import UI from './ui'; import {ConnectState} from "../../modules/bluetooth/lb-bluetooth-state-example"; import {getAppBLEProtocol} from "../../modules/bluetooth/lb-example-bluetooth-protocol"; import {getAppBLEManager} from "../../modules/bluetooth/lb-example-bluetooth-manager"; const app = getApp(); Page({ /** * 页面的初始数据 */ data: { connectState: ConnectState.UNAVAILABLE }, /** * 生命周期函数--监听页面加载 */ onLoad(options) { this.ui = new UI(this); console.log(app); //监听蓝牙连接状态、订阅蓝牙协议接收事件 //多次订阅只会在最新订阅的函数中生效。 //建议在app.js中订阅,以实现全局的事件通知 getAppBLEManager.setBLEListener({ onConnectStateChanged: async (res) => { const {connectState} = res; console.log('蓝牙连接状态更新', res); this.ui.setState({state: connectState}); switch (connectState) { case ConnectState.CONNECTED: //在连接成功后,紧接着设置灯光颜色和亮度 //发送协议,官方提醒并行调用多次会存在写失败的可能性,所以建议使用串行方式来发送 await getAppBLEProtocol.setColorLightAndBrightness({ brightness: 100, red: 255, green: 0, blue: 0 }); break; default: break; } }, /** * 接收到的蓝牙设备传给手机的有效数据,只包含你最关心的那一部分 * protocolState和value具体的内容是在lb-example-bluetooth-protocol.js中定义的 * * @param protocolState 蓝牙协议状态值,string类型,值是固定的几种,详情示例见: * @param value 传递的数据,对应lb-example-bluetooth-protocol.js中的{effectiveData}字段 */ onReceiveData: ({protocolState, value}) => { console.log('蓝牙协议接收到新的 protocolState:', protocolState, 'value:', value); } }); //这里执行连接后,程序会按照你指定的规则(位于getAppBLEManager中的setFilter中指定的),自动连接到距离手机最近的蓝牙设备 getAppBLEManager.connect(); }, /** * 断开连接 * @param e * @returns {Promise<void>} */ async disconnectDevice(e) { // closeAll() 会断开蓝牙连接、关闭适配器 await getAppBLEManager.closeAll(); this.setData({ device: {} }); setTimeout(Toast.success, 0, '已断开连接'); }, /** * 连接到最近的设备 */ connectHiBreathDevice() { getAppBLEManager.connect(); }, async onUnload() { await getAppBLEManager.closeAll(); }, }); [代码] 2、接下来是如何定制你自己的蓝牙业务: 1. 设置你自己的[代码]setFilter[代码]函数参数。 文件位于[代码]./modules/bluetooth/lb-example-bluetooth-manager.js[代码] [代码] import {LBlueToothManager} from "./lb-ble-common-connection/index"; import {getAppBLEProtocol} from "./lb-example-bluetooth-protocol"; /** * 蓝牙连接方式管理类 * 初始化蓝牙连接时需筛选的设备,重写蓝牙连接规则 */ export const getAppBLEManager = new class extends LBlueToothManager { constructor() { super(); //setFilter详情见父类 super.setFilter({ services: ['0000xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'],//必填 targetServiceArray: [{ serviceId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',//必填 writeCharacteristicId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxE',//必填 notifyCharacteristicId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxF',//必填 readCharacteristicId: '',//非必填 }], targetDeviceName: '目标蓝牙设备的广播数据段中的 LocalName 数据段,如:smart-voice',//非必填,在判断时是用String.prototype.includes()函数来处理的,所以targetDeviceName不必是全称 scanInterval: 350//扫描周围设备,重复上报的时间间隔,毫秒制,非必填,默认是350ms }); super.initBLEProtocol({bleProtocol: getAppBLEProtocol}); super.setMyFindTargetDeviceNeedConnectedFun({ /** * 重复上报时的过滤规则,并返回过滤结果 * 在执行完该过滤函数,并且该次连接蓝牙有了最终结果后,才会在下一次上报结果回调时,再次执行该函数。 * 所以如果在一次过滤过程中或是连接蓝牙,耗时时间很长,导致本次连接结果还没得到,就接收到了下一次的上报结果,则会忽略下一次{scanFilterRuler}的执行。 * 如果不指定这个函数,则会使用默认的连接规则 * 默认的连接规则详见 lb-ble-common-connection/utils/device-connection-manager.js的{defaultFindTargetDeviceNeedConnectedFun} * @param devices {*}是wx.onBluetoothDeviceFound(cb)中返回的{devices} * @param targetDeviceName {string}是{setFilter}中的配置项 * @returns {{targetDevice: null}|{targetDevice: *}} 最终返回对象{targetDevice},是数组{devices}其中的一个元素;{targetDevice}可返回null,意思是本次扫描结果未找到指定设备 */ scanFilterRuler: ({devices, targetDeviceName}) => { console.log('执行自定义的扫描过滤规则'); const tempFilterArray = []; for (let device of devices) { if (device.localName?.includes(targetDeviceName)) { tempFilterArray.push(device); } } if (tempFilterArray.length) { const device = tempFilterArray.reduce((pre, cur) => { return pre.RSSI > cur.RSSI ? pre : cur; }); return {targetDevice: device}; } return {targetDevice: null}; } }) } /** * 获取本机蓝牙适配器状态 * @returns {Promise<*>} 返回值见小程序官网 wx.getBluetoothAdapterState */ async getBLEAdapterState() { return await super.getBLEAdapterState(); } /** * 获取最新的蓝牙连接状态 * @returns {*} */ getBLELatestConnectState() { return super.getBLELatestConnectState(); } }(); [代码] 2. 按约定的的蓝牙协议格式组装你自己的收发数据。 文件位于[代码]./modules/bluetooth/lb-ble-example-protocol-body[代码] [代码]//send-body.js import {IBLEProtocolSendBody} from "../lb-ble-common-protocol-body/index"; import {HexTools} from "../lb-ble-common-tool/index"; /** * 组装蓝牙协议发送数据示例 * 该框架的蓝牙协议必须按照约定格式来制定,最多20个字节 */ export default class SendBody extends IBLEProtocolSendBody { getDataBeforeCommandData({command, effectiveData} = {}) { //有效数据前的数据 该示例只返回了帧头110 return [110]; } getDataAfterEffectiveData({command, effectiveData} = {}) { //协议结束标志 const endFlag = 233; //该示例中checkSum的生成规则是计算协议从第0个元素累加到结束标志 let checkSum = endFlag + HexTools.hexToNum(command); for (let item of this.getDataBeforeCommandData()) { checkSum += item; } for (let item of effectiveData) { checkSum += item; } //生成有效数据之后的数据 return [endFlag, checkSum]; } } [代码] [代码]//receive-body.js import {IBLEProtocolReceiveBody} from "../lb-ble-common-protocol-body/index"; /** * 组装蓝牙协议接收数据示例 * 该框架的蓝牙协议必须按照约定格式来制定,最多20个字节 */ export default class ReceiveBody extends IBLEProtocolReceiveBody { constructor() { //commandIndex 命令字位置索引 //effectiveDataStartIndex 有效数据开始索引,比如:填写0,{getEffectiveReceiveDataLength}中返回20,则会在{LBlueToothProtocolOperator}的子类{getReceiveAction}实现中,在参数中返回所有数据 super({commandIndex: 1, effectiveDataStartIndex: 0}); } /** * 获取有效数据的字节长度 * 该长度可根据接收到的数据动态获取或是计算,或是写固定值均可 * 有效数据字节长度是指,在协议中由你的业务规定的具有特定含义的值的总字节长度 * 有效数据更多的说明,以及该长度的计算规则示例,见 IBLEProtocolReceiveBody 类的 {getEffectiveReceiveData}函数 * * @param receiveArray 接收到的一整包数据 * @returns {number} 有效数据的字节长度 */ getEffectiveReceiveDataLength({receiveArray}) { return 20; } } [代码] 3. 实现对有效数据的发送、接收处理 位于[代码]modules/bluetooth/lb-example-bluetooth-protocol.js[代码] [代码]import {LBlueToothProtocolOperator} from "./lb-ble-common-protocol-operator/index"; import SendBody from "./lb-ble-example-protocol-body/send-body"; import ReceiveBody from "./lb-ble-example-protocol-body/receive-body"; import {ProtocolState} from "./lb-bluetooth-state-example"; /** * 蓝牙协议管理类 * 在这个类中,以配置的方式来编写读操作和写操作 * 配置方式见下方示例 */ export const getAppBLEProtocol = new class extends LBlueToothProtocolOperator { constructor() { super({protocolSendBody: new SendBody(), protocolReceiveBody: new ReceiveBody()}); } /** * 写操作(仅示例) */ getSendAction() { return { /** * 0x01:设置灯色(写操作) * @param red 0x00 - 0xff * @param green 0x00 - 0xff * @param blue 0x00 - 0xff * @returns {Promise<void>} */ '0x01': async ({red, green, blue}) => { return await this.sendProtocolData({command: '0x01', effectiveData: [red, green, blue]}); }, /** * 0x02:设置灯亮度(写操作) * @param brightness 灯亮度值 0~100 对应最暗和最亮 * @returns {Promise<void>} */ '0x02': async ({brightness}) => { //data中的数据,填写多少个数据都可以,可以像上面的3位,也可以像这条6位。你只要能保证data的数据再加上你其他的数据,数组总长度别超过20个就行。 return await this.sendProtocolData({command: '0x02', effectiveData: [brightness, 255, 255, 255, 255, 255]}); }, } } /** * 读操作(仅示例) * {dataArray}是一个数组,包含了您要接收的有效数据。 * {dataArray}的内容是在lb-ble-example-protocol-body.js中的配置的。 * 是由您配置的 dataStartIndex 和 getEffectiveReceiveDataLength 共同决定的 */ getReceiveAction() { return { /** * 获取设备当前的灯色(读) * 可return蓝牙协议状态protocolState和接收到的数据effectiveData, * 该方法的返回值,只要拥有非空的protocolState,该框架便会同步地通知前端同protocolState类型的消息 * 当然是在你订阅了setBLEListener({onReceiveData})时才会在订阅的地方接收到消息。 */ '0x10': ({dataArray}) => { const [red, green, blue] = dataArray; return {protocolState: ProtocolState.RECEIVE_COLOR, effectiveData: {red, green, blue}}; }, /** * 获取设备当前的灯亮度(读) */ '0x11': ({dataArray}) => { const [brightness] = dataArray; return {protocolState: ProtocolState.RECEIVE_BRIGHTNESS, effectiveData: {brightness}}; }, /** * 接收到设备主动发送的灯光关闭消息 * 模拟的场景是,用户关闭了设备灯光,设备需要主动推送灯光关闭事件给手机 */ '0x12': () => { //你可以不传递effectiveData return {protocolState: ProtocolState.RECEIVE_LIGHT_CLOSE}; }, /** * 接收到蓝牙设备的其他一些数据 */ '0x13': ({dataArray}) => { //do something //你可以不返回任何值 } }; } /** * 设置灯亮度和颜色 * @param brightness * @param red * @param green * @param blue * @returns {Promise<[unknown, unknown]>} */ async setColorLightAndBrightness({brightness, red, green, blue}) { //发送协议,小程序官方提醒并行调用多次会存在写失败的可能性,所以建议使用串行方式来发送,哪种方式由你权衡 //但我这里是并行发送了两条0x01和0x02两条协议,仅演示用 return Promise.all([this.sendAction['0x01']({red, green, blue}), this.sendAction['0x02']({brightness})]); } }(); [代码] 4.(非必须)拓展蓝牙连接和协议状态。 文件位于[代码]modules/bluetooth/lb-bluetooth-state-example.js[代码] [代码]import {CommonConnectState, CommonProtocolState} from "./lb-ble-common-state/index"; //特定的蓝牙设备的协议状态,用于拓展公共的蓝牙协议状态 //使用场景: //在手机接收到蓝牙数据成功或失败后,该框架会生成一条消息,包含了对应的蓝牙协议状态值{protocolState}以及对应的{effectiveData}(effectiveData示例见 lb-example-bluetooth-protocol.js), //在{setBLEListener}的{onReceiveData}回调函数中,对应参数{protocolState}和{value}(value就是effectiveData) const ProtocolState = { ...CommonProtocolState, RECEIVE_COLOR: 'receive_color',//获取到设备的颜色值 RECEIVE_BRIGHTNESS: 'receive_brightness',//获取到设备的亮度 RECEIVE_LIGHT_CLOSE: 'receive_close',//获取到设备灯光关闭事件 }; export { ProtocolState, CommonConnectState as ConnectState }; [代码] 深入了解框架 业务 对应文件夹 示例文件 蓝牙连接 [代码]lb-ble-common-connection[代码](连接、断连、重连事件的处理) [代码]abstract-bluetooth.js[代码](最简单的、调用平台API的连接、断开蓝牙等处理) [代码]base-bluetooth.js[代码](记录连接到的设备的deviceId、特征字、连接状态等信息,处理蓝牙数据的发送、蓝牙重连) [代码]base-bluetooth-imp.js[代码](对蓝牙连接结果的捕获,监听蓝牙扫描周围设备、连接、适配器状态事件并给予相应处理) 蓝牙协议的组装 [代码]lb-ble-common-protocol-body[代码](实现协议收发格式的组装) [代码]i-protocol-receive-body.js[代码] [代码]i-protocol-send-body.js[代码] 蓝牙协议的收发 [代码]lb-ble-common-protocol-operator[代码](处理发送数据和接收数据的代理) [代码]lb-bluetooth-protocol-operator.js[代码] 蓝牙协议的重发 [代码]lb-ble-common-connection[代码] [代码]lb-bluetooth-manager.js[代码](详见[代码]LBlueToothCommonManager[代码]) 蓝牙状态及协议状态 [代码]lb-ble-common-state[代码] [代码]lb-bluetooth-state-example.js[代码],可额外拓展新的状态 蓝牙连接和协议状态事件的订阅 [代码]lb-ble-common-connection/base[代码] [代码]base-bluetooth-imp.js[代码] 下面讲下蓝牙连接和协议状态的分发 蓝牙连接状态事件的分发 文件位于[代码]lb-ble-common-connection/base/base-bluetooth.js[代码] 某一时刻连接状态改变,将新的状态赋值给[代码]latestConnectState[代码]对象。 触发其[代码]setter[代码]函数[代码]set latestConnectState[代码]。 执行[代码]setter[代码]内部的[代码]_onConnectStateChanged[代码]函数回调。 在[代码]getAppBLEManager.setBLEListener[代码]的[代码]onConnectStateChanged({connectState})[代码]函数中接收到连接状态。 蓝牙协议状态事件的分发 [代码]onBLECharacteristicValueChange[代码]位于[代码]lb-ble-common-connection/abstract-bluetooth.js[代码] [代码]receiveOperation[代码]位于[代码]lb-ble-common-protocol-operator/lb-bluetooth-protocol-operator.js[代码] 在[代码]onBLECharacteristicValueChange[代码]函数中,我在接收到数据后,将数据按[代码]receive-body.js[代码]来截取有效数据,并按[代码]lb-example-bluetooth-protocol.js[代码]中[代码]getReceiveAction[代码]的配置方式来处理有效数据,生产出对应的[代码]value, protocolState[代码]。 [代码]filter[代码]是在接收到未知协议时会生成。 [代码] onBLECharacteristicValueChange((res) => { console.log('接收到消息', res); if (!!valueChangeListener) { const {value, protocolState, filter} = this.dealReceiveData({receiveBuffer: res.value}); !filter && valueChangeListener({protocolState, value}); } }); [代码] 这段代码看起来简单,但背后要经历很多流程。 最关键的是这一行[代码]const {value, protocolState, filter} = this.dealReceiveData({receiveBuffer: res.value});[代码]。 下面我详细的讲一下这一行做了哪些事儿: 执行[代码]dealReceiveData[代码]函数处理协议数据。这里的[代码]dealReceiveData[代码],最终交由[代码]lb-bluetooth-manager.js[代码]中的[代码]dealReceiveData[代码]函数来处理数据。 在[代码]dealReceiveData[代码]中执行[代码]this.bluetoothProtocol.receive({receiveBuffer})[代码]来生成有效数据和协议状态。这个[代码]receive[代码]最终交由[代码]receiveOperation[代码]函数执行。 [代码]receiveOperation[代码]在执行时会引用到[代码]LBlueToothProtocolOperator[代码]的子类的配置项[代码]getReceiveAction[代码](子类是[代码]lb-example-bluetooth-protocol.js[代码])。 [代码]getReceiveAction[代码]按开发者自己的实现最终返回约定对象[代码]{protocolState,effectiveData}[代码],该对象返回给[代码]receiveOperation[代码]后进行一次检查(对未在[代码]getReceiveAction[代码]中配置的协议[代码]protocolState[代码]按[代码]CommonProtocolState.UNKNOWN[代码]处理),将该约定对象返回给[代码]dealReceiveData[代码]函数中的局部变量[代码]effectiveData, protocolState[代码]。 [代码]protocolState!==CommonProtocolState.UNKNOWN[代码]的对应对象,会被标记为[代码]filter:true[代码];否则将约定对象返回给[代码]onBLECharacteristicValueChange[代码]函数中的局部变量[代码]value, protocolState[代码]。 以上是这一行代码所做的所有事情。 约定对象,会作为参数传入[代码]valueChangeListener({protocolState, value})[代码]并执行回调。 之后前端就能接收到订阅的事件啦,即在[代码]getAppBLEManager.setBLEListener[代码]的[代码]onReceiveData({protocolState, value})[代码]函数中接收到协议类型和[代码]value[代码]对象。 LINK Document LICENSE 交流 技术交流请加QQ群:821711186 目前该项目已在GitHub开源:点击前往 欢迎打赏 [图片][图片]
2020-03-27 - 探坑之旅 小程序线上source map 文件使用
官网上看的一脸懵逼… 贴个官方连接 https://developers.weixin.qq.com/miniprogram/dev/devtools/debug.html#source-map 划重点 划重点:小程序线上source map文件 json转化失败 需要手动修改 把修改好的文件直接上传 就可还原出错原始位置的 https://www.cnblogs.com/wozho/p/10700869.html [图片]
2019-11-06 - 微信小程序订阅消息订单通知体验,因为是一次性订阅,所以需要用户每次的同意订阅,然后用户拒绝授权之后及处理
[图片] 微信小程序消息推送,模板消息已经不能用了,只能使用订阅消息,一来要基础库高,二来,用户还是一次性订阅,每次都要授权同意,我们要做的就是每次都弹窗出来,并且要做用户拒绝后的处理,我只能做到这样了。被拒绝后的授权回调,打开设置,还是有点处理太强硬。这里应该可以引导用户授权,引导用户勾选总是同意这个订阅消息。希望给一个永久性订阅的,然后被拒绝后,可以更换的引导用户。
2020-03-03 - 自己开发的小程序可不可以跳转到“饿了么”或者“美团外卖”里自己的店铺?需要appid嘛?
如果不能怎么才能引流到外卖平台上自己的店铺呢?
2020-05-07 - 微信开发工具直接编译scss文件
前言 以前微信开发工具不支持打卡scss文件,每次一个小改动,都要切换其他编辑器编译,感觉比较麻烦.最近 RC Build (1.02.2001191) 的开发工具支持打开scss文件,这算是一个福音,然而如何编译成wxss却没有说明,所以自己利用gulp在开发工具中直接编译,很方便. 使用 1、在项目与app.js同级目录中新建文件gulpfile.js 加入内容如下: var gulp = require('gulp'); var sass = require('gulp-sass'); var rename = require('gulp-rename') var changed = require('gulp-changed') var watcher = require('gulp-watch') //自动监听 gulp.task('default', gulp.series(function() { watcher('./pages/**/*.scss', function(){ miniSass(); }); })); //手动编译 gulp.task('sass', function(){ miniSass(); }); function miniSass(){ return gulp.src('./pages/**/*.scss')//需要编译的文件 .pipe(sass({ outputStyle: 'expanded'//展开输出方式 expanded })) .pipe(rename((path)=> { path.extname = '.wxss' })) .pipe(changed('./pages'))//只编译改动的文件 .pipe(gulp.dest('./pages'))//编译 .pipe(rename((path)=> { console.log('编译完成文件:' + 'pages\\' + path.dirname + '\\' + path.basename + '.scss') })) } 复制代码 2、打开命令行,进入gulpfile.js所在目录,执行如下命令 [代码]npm install[代码][代码]gulp-sass[代码][代码]gulp-rename[代码][代码]gulp-changed[代码]3、执行监听命令 [代码]gulp[代码] 会监听pages目录所有的scss文件变动,保存后会自动编译.
2020-02-24 - 手把手带你入门微信小程序新框架---Kbone
作者:Horace ps:新人小白一枚,如有错误,欢迎指出~ Kbone 框架 前些天在微信上收到了微信开发者公众号的文章推送《揭开微信小程序Kbone的神秘面纱》,心想:微信小程序有新框架了?抱着学习的态度点进去看了一眼,看过之后觉得这框架也太宠开发者了吧,不愧是微信团队出品。 原来这个框架早在去年就已经发布了,看完只恨自己没有早点知道消息开始学习这个框架。我写本文的目的也是为了跟个风,想要让更多的人能够知道这个框架,感受它的便利,希望好学的你可以停下脚步看看~ Kbone 是什么? 看到这里我也不多说了,简单介绍一下 Kbone 是什么。用官方高大上的话来说: Kbone 是一个致力于微信小程序和 Web 端同构的解决方案,在适配层里模拟出浏览器环境,让 Web 端的代码可以不做什么改动便可运行在小程序里。 用简单粗暴一点的话来说,Kbone 这个框架可以让你只需要写一份代码,就能够在两端运行,只需要进行一些配置,轻松跑小程序和 Web 两个端。 Kbone 初探 — todoList 吹了这么多,也该上手写代码了。刚开始入门 Kbone,我们从一个简单的 todoList 开始,当然,官方也提供了一系列的demo,我也参考了官方给的 demo。Talk is cheap,let’s see the code ~ 预览 正式开始之前我们先看看效果图,感受一下 Kbone 框架一份代码跑两端的神奇 [图片] 开发准备 安装脚手架/初始化项目 [代码]npm install -g kbone-cli[代码] [代码]kbone init to-do-list[代码] 代码构建 [代码]npm run build[代码] (具体的页面介绍后面会讲到) Coding 来到 src/home/index.vue,项目的首页入口放在这里(至于为什么是这里,后面同样会介绍到) 在这里直接写业务代码就可以了,为了不使文章显得臃肿,有兴趣的可以看我的源码。 项目运行 小程序端:[代码]npm run mp[代码] Web端: [代码]npm run web[代码] 通过两个命令把项目运行起来你就会发现 Kbone 的神奇之处,通过一份代码(这里我是基于 Vue)你就可以拥有两端的效果,再也不用担心同时维护两份代码了。 Kbone 进阶 — 多页开发 刚才做了一个比较简单的 todoList,对 Kbone 进行了一个简单的了解,到这里正式进入重点,接下来我们就来详细的讲讲它的使用和多页开发。 Kbone 目录了解 [代码]├─ build │ ├─ miniprogram.config.js // mp-webpack-plugin 配置 │ ├─ webpack.base.config.js // Web 端构建基础配置 │ ├─ webpack.dev.config.js // Web 端构建开发环境配置 │ ├─ webpack.mp.config.js // 小程序端构建配置 │ └─ webpack.prod.config.js // Web 端构建生产环境配置 ├─ dist │ ├─ mp // 小程序端目标代码目录,使用微信开发者工具打开,用于生产环境 │ └─ web // web 端编译出的文件,用于生产环境 ├─ src │ ├─ common // 通用组件 │ ├─ mp // 小程序端入口目录 │ │ ├─ home // 小程序端 home 页面 │ │ │ └─ main.mp.js // 小程序端入口文件 │ │ └─ other // 小程序端 other 页面 │ │ └─ main.mp.js // 小程序端入口文件 │ ├─ detail // detail 页面 │ ├─ home // home 页面 │ ├─ list // list 页面 │ ├─ router // vue-router 路由定义 │ ├─ store // vuex 相关目录 │ ├─ App.vue // Web 端入口主视图 │ └─ main.js // Web 端入口文件 └─ index.html // Web 端入口模板 [代码] 通过官方给我们的这个目录结构,我们可以很清晰的看到每个目录下各个文件的作用。这里我就对其中的一些文件进行解释一下。 miniprogram.config.js 这个文件是关于小程序端的一些配置,类似于原生的 [代码]json[代码] 配置 webpack.mp.config.js 小程序端构建配置,也就是构建小程序端代码的 [代码]webpack[代码] 配置,多页开发中会用到其中的一部分配置。 src/mp & main.mp.js [代码]mp[代码] 用来存放小程序端的入口文件,这里设置小程序的一些页面,[代码]main.mp.js[代码] 相当于一个挂载操作,把它看成 [代码]mpvue[代码] 里面的 [代码]main.js[代码] 比较好理解,设置页面路由和挂载映射 Vue 里面的页面。 (其他的比较好理解,我就不一一赘述了) Kbone 多页开发 因为作者之前写了一个微信小程序的高仿项目,有兴趣的可以去看看: 👉《京东优选小程序快车等你上车~》 看了官方给的多页开发的 demo之后,就想把自己做的小程序项目简单的用 Kbone 做几个页面尝尝鲜,于是花了点时间动了动手,先看看两端跑起来是什么效果: [图片] 从图片上来看 Web 端和小程序端还是有细微的差别,不过只是在于样式上,毕竟想做到完全一样还是比较困难的。话不多说,接下来我们就来解构分页开发。 Vue 路由配置 Vue 的路由配置比较简单,直接在 [代码]src/router/index.js[代码] 下配置就好了,比较简单,不多说。 [代码]import Vue from 'vue' import Router from 'vue-router' const Index = () => import('@/index/Index.vue') const Explore = () => import('@/explore/Index.vue') const Cart = () => import('@/cart/Index.vue') const Me = () => import('@/me/Index.vue') Vue.use(Router) export default new Router({ mode: 'history', routes: [ { path: '/(home|index)?', name: 'Home', component: Index, }, { path: '/index.html', name: 'HomeHtml', component: Index, }, { path: '/explore', name: 'Explore', component: Explore, }, { path: '/cart', name: 'Cart', component: Cart, }, { path: '/me', name: 'Me', component: Me, } ], }) [代码] 页面建立 根据路由建立需要的四个页面:index、explore、cart、me 并给它们写上相应的代码。 我只写了 [代码]index[代码] 页面的代码,结构比较简单,为了看效果放的是假数据,有兴趣的参考一下看我的源码 小程序端页面建立/挂载 之前已经介绍过 [代码]src/mp[代码] 下存放的是小程序端的入口文件,也就是相当于小程序端页面的对于 Vue 页面的映射,每个文件夹下很简单,就一个 [代码]main.mp.js[代码] [代码]import Vue from 'vue' import Router from 'vue-router' import { sync } from 'vuex-router-sync' import App from '../../App.vue' import store from '../../store' import Index from '../../index/Index.vue' Vue.use(Router) const router = new Router({ mode: 'history', routes: [{ path: '/index', name: 'Index', component: Index, }], }) export default function createApp() { const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) Vue.config.productionTip = false sync(store, router) return new Vue({ el: '#app', router, store, render: h => h(App) }) } [代码] (每个页面的配置都差不多,只是路由不一样,我选取了 [代码]index[代码] 页面的) 这其中引入了 Vue 的路由并配置了小程序端每个页面对应的 Vue 页面进行渲染,有一点 Vue 基础的还是比较好看懂的。 小程序入口 配置到了上一步,你可能觉得已经差不多了,因为在 Web 端已经可以通过路由看到效果了,然而在小程序端还看不到具体的效果甚至还在报错,这是因为少了关键的一步 — 小程序页面入口文件的设置。 举个例子来说,上一步我们是给小程序的页面配好了钥匙,但是还没有把它拿过来去开相应的锁,现在我们就要拿它来开相应的的锁(小程序入口配置) — [代码]webpack.mp.config.js[代码] [代码]entry: { // js 入口 index: path.resolve(__dirname, '../src/mp/index/main.mp.js'), explore: path.resolve(__dirname, '../src/mp/explore/main.mp.js'), cart: path.resolve(__dirname, '../src/mp/cart/main.mp.js'), me: path.resolve(__dirname, '../src/mp/me/main.mp.js'), }, [代码] 在这里配置一下小程序的入口就能在小程序看到首页(/index)的效果了 tabBar 配合 配置好了入口仅仅只能看到首页(/index)的效果,这就需要使用 tabBar 了。 之前在说页面的作用的时候,我特意提了一下 [代码]miniprogram.config.js[代码] 是关于小程序的一些配置,作用就是在这里。 简单提一嘴 [代码]miniprogram.config.js[代码] 里面待会儿需要用到的配置项: entry:入口页面路由(一定要主页配置了tabBar之后的入口路由) router:各个页面自己的路由,页面之间跳转用的 generate:输出小程序配置(tabBar配置在这里) app:小程序窗口配置,相当于原生 [代码]app.json[代码] 中的 [代码]window[代码] 配置 pages:每个页面单独的配置,相当于原生中每个页面对应的 [代码]json[代码] 文件 开始配置(只列出我修改了的配置) [代码]entry: '/index', router: { index: ['/(home|index)?','/index.html'], explore: ['/explore'], cart: ['/cart'], me: ['/me'], }, redirect: { notFound: 'index', accessDenied: 'index', }, generate: { tabBar: { color: '#000000', selectedColor: '#DE554F', backgroundColor: '#ffffff', list: [{ pageName: 'index', text: '优选', iconPath: path.resolve(__dirname, '../src/img/home.png'), selectedIconPath: path.resolve(__dirname, '../src/img/home-active.png'), }], }, }, pages: { explore: { extra: { navigationBarTextStyle: 'white' } } }, [代码] 由于这里每一项的配置都是同样的方法,所以我就只拿一项举例子。 Web 端完善 做到上一步的时候,小程序端的效果已经完全出来了,但是 Web 端运行起来没有 tabBar,这就需要自己做一个 tabBar 放在页面上了,这里把它抽出来作为一个组件放在需要的页面上。 我的页面结构大致是这样的: [代码]<template> <div class="tabBar for-web"> <div class="tabBar_border"></div> <div class="tabBar_item" v-for="(item, index) in list" :key="index" :data-path="item.pagePath" :data-index="index" @click="switchTab"> <img :src="selected === index?item.selectedIconPath:item.iconPath"> <span :class="selected === index ? 'selected' : ''">{{item.text}}</span> </div> </div> </template> [代码] 接下来就是比较关键的一点,就是这个tabBar怎么让它隐藏起来不再小程序端显示。这里有三种方法: vue-improve-loader(给容器加上check-reduce) reduce-loader(引入的时候在路径前加上reduce-loader!) 通过样式隐藏 前两种在构建的时候就会被自动干掉,这里我个人倾向的是第三种通过样式,给容器加一点样式。 [代码].miniprogram-root { .for-web { display: none; } } [代码] 做到这一步的时候分页开发加 tabBar 就已经实现了,剩下的就是往每个页面上添加自己的业务内容。 小结 总的来说使用 Kbone 进行多页开发的步骤就是: 设置 Vue 路由 建立对应页面并进行小程序页面挂载注册 修改小程序入口并配置对应的路由(如果有需要可以继续配置 tabBar) 踩坑记录 开发中用到的图片等静态资源 在写 demo 的时候发现一个问题,自定义 tabBar 的图片和页面需要的图片文件构建的时候始终带不过去,查了一下官方提供的文档,目前暂不支持相对路径,静态资源可以考虑转成 base64 或者使用网络地址,这里用了一个比较笨的办法,把图片上传到微博然后保存在线地址。 关于样式 rpx 在 kbone 中好像不支持,尝试过 vue+kbone 对 web 端采用px适配,在构建小程序时希望能转成rpx,但可惜的是不会这样,去微信开放社区看了一下说要用 rem 做适配(要在 mp-webpack-plugin 这个插件的配置中的 global 字段内补充 rem 配置) 构建 npm 相关 开发者工具报错 [代码]Uncaught Error: module "pages/ home/miniprogram-render" is not defined[代码] 解决方案:开发者工具重新构建 npm 如果还是无法解决,删除打包出来的小程序文件,重新打包 swiper swiper编译后在小程序中无法滚动,会直接并列平铺展示出来。根据文档的说法在前面加上了 wx- 的前缀貌似也没用,还需要进一步探索一下 (更多的踩坑记录可以看我的readme~) 最后一点 最后还是想说,这个框架对于开发者还是比较友好的,解决了长久以来微信小程序和 Web 两个端的代码问题,在实际中可以少写一份代码,极大的减轻了开发和维护的工作量,虽然目前还存在一些 bug,但是我相信开发团队一定会努力的完善它。如果你觉得有用的话也用起来吧~ (Kbone 官方技术文档👉文档) 附上github地址: to-do-list multipages 文章转自本人掘金原文
2020-02-26 - 微信小程序swiper控件卡死的解决方法
微信小程序swiper控件,在使用过程中会偶尔出现卡死现象(不能再左右滑动),跟踪一下,归结原因可能是swiper组件内部问题,当无法响应用户快速翻动动作时,当前页变量current无法变更为正确页码索引,而是被置为0,所以,解决这个问题的思路如下: [代码]swiperchange: function (event) { if (event.detail.source == "touch") { //防止swiper控件卡死 if (this.data.current == 0 && this.data.preIndex>1 ) {//卡死时,重置current为正确索引 this.setData({ current: this.data.preIndex }); } else {//正常轮转时,记录正确页码索引 this.setData({ preIndex: this.data.current }); } } } [代码]
2020-02-16 - 升级开发者工具到最新,种种原因需要恢复旧版,旧版安装包找不到应急处理方法
刚需 新的开发者工具出来发现跟现有项目有兼容性问题。 而项目一天不能停,要急着修改上架,所以要恢复最新的开发者工具到旧版本 旧版本安装包没备份找不到了怎么办,2个方案: 解决方案 1、使用自带的回退功能 开发者工具菜单,回退功能,如果这里回退的版本不行或者不能正常回退,那么使用方案2 2、稳定版更新日志页面选择回退 如果是稳定版,可以在稳定版的更新日志页面找到对应旧版本的历史下载链接: 稳定版 Stable Build 更新日志 | 微信开放文档 https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html 3、根据规律找出历史版本下载链接 分析规律 找出指定旧版本的下载链接,直接打开链接下载旧版本。 应急处理的下载链接格式: [代码]https://servicewechat.com/wxa-dev-logic/download_redirect?type=x64&from=mpwiki&download_version=1021910120&version_type=1 [代码] 这个链接:后面版本号(1910120)改成你想要的版本就为所欲为的下载旧版本了。 版本号1910120的分拆:前6位代表发布的年月日(YYMMDD)格式后面的一位代表当天的第几个版本,从0开始 另外说说url里面的type是代表平台(x64/ia32/darwin) version_type代表版本类型:正式版/预览版/开发版(推测) 下载链接demo 经过上面分析后,可以得出1910120的X64版本下载链接为: https://servicewechat.com/wxa-dev-logic/download_redirect?type=x64&from=mpwiki&download_version=1021910120&version_type=1 MacOs版本下载链接为: https://servicewechat.com/wxa-dev-logic/download_redirect?type=darwin&from=mpwiki&download_version=1021910120&version_type=1
2020-09-24 - 自定义可配置format的日期时间选择组件DateTimePicker
背景 由于在项目中遇到需要同时选择日期和时间的需求,所以自己写了一个可以动态配置format格式的时间选择器 效果图 [图片] wxml [代码]<view class="view-body"> <text class='item-key'>{{title}}<text style="color:red" wx:if="{{isRequired}}">*</text></text> <view class="item-value"> <picker mode="multiSelector" bindchange="bindPickerChange" bindcolumnchange="bindPickerColumnChange" value="{{multiIndex}}" range="{{multiArray}}"> <input disabled="{{true}}" value='{{value}}' name='{{name}}' /> <image class="img-arrow" src='/images/date_icon.png' /> </picker> </view> </view> [代码] js [代码]Component({ behaviors: ['wx://form-field'], properties: { title: { type: String }, name: { type: String }, isRequired: { type: Boolean }, format: { type: String } }, data: {}, lifetimes: { attached: function() { //当前时间 年月日 时分秒 const date = new Date() const curYear = date.getFullYear() const curMonth = date.getMonth() + 1 const curDay = date.getDate() const curHour = date.getHours() const curMinute = date.getMinutes() const curSecond = date.getSeconds() //记录默认年份 后面加载二月份天数 this.setData({ chooseYear: curYear }) //初始化时间选择轴 this.initColumn(curMonth) //不足两位的前面好补0 因为后面要获取在时间轴上的索引 时间轴初始化的时候都是两位 let showMonth = curMonth < 10 ? ('0' + curMonth) : curMonth let showDay = curDay < 10 ? ('0' + curDay) : curDay let showHour = curHour < 10 ? ('0' + curHour) : curHour let showMinute = curMinute < 10 ? ('0' + curMinute) : curMinute let showSecond = curSecond < 10 ? ('0' + curSecond) : curSecond //当前时间在picker列上面的索引 为了当打开时间选择轴时选中当前的时间 let indexYear = this.data.years.indexOf(curYear + '') let indexMonth = this.data.months.indexOf(showMonth + '') let indexDay = this.data.days.indexOf(showDay + '') let indexHour = this.data.hours.indexOf(showHour + '') let indexMinute = this.data.minutes.indexOf(showMinute + '') let indexSecond = this.data.seconds.indexOf(showSecond + '') let multiIndex = [] let multiArray = [] let value = '' let format = this.properties.format; if (format == 'yyyy-MM-dd') { multiIndex = [indexYear, indexMonth, indexDay] value = `${curYear}-${showMonth}-${showDay}` multiArray = [this.data.years, this.data.months, this.data.days] } if (format == 'HH:mm:ss') { multiIndex = [indexHour, indexMinute, indexSecond] value = `${showHour}:${showMinute}:${showSecond}` multiArray = [this.data.hours, this.data.minutes, this.data.seconds] } if (format == 'yyyy-MM-dd HH:mm') { multiIndex = [indexYear, indexMonth, indexDay, indexHour, indexMinute] value = `${curYear}-${showMonth}-${showDay} ${showHour}:${showMinute}` multiArray = [this.data.years, this.data.months, this.data.days, this.data.hours, this.data.minutes] } if (format == 'yyyy-MM-dd HH:mm:ss') { multiIndex = [indexYear, indexMonth, indexDay, indexHour, indexMinute, indexSecond] value = `${curYear}-${showMonth}-${showDay} ${showHour}:${showMinute}:${showSecond}` multiArray = [this.data.years, this.data.months, this.data.days, this.data.hours, this.data.minutes, this.data.seconds] } this.setData({ value, multiIndex, multiArray, curMonth, chooseYear: curYear, }) } }, /** * 组件的方法列表 */ methods: { //获取时间日期 bindPickerChange: function(e) { this.setData({ multiIndex: e.detail.value }) const index = this.data.multiIndex let format = this.properties.format var showTime = '' if (format == 'yyyy-MM-dd') { const year = this.data.multiArray[0][index[0]] const month = this.data.multiArray[1][index[1]] const day = this.data.multiArray[2][index[2]] showTime = `${year}-${month}-${day}` } if (format == 'HH:mm:ss') { const hour = this.data.multiArray[0][index[0]] const minute = this.data.multiArray[1][index[1]] const second = this.data.multiArray[2][index[2]] showTime = `${hour}:${minute}:${second}` } if (format == 'yyyy-MM-dd HH:mm') { const year = this.data.multiArray[0][index[0]] const month = this.data.multiArray[1][index[1]] const day = this.data.multiArray[2][index[2]] const hour = this.data.multiArray[3][index[3]] const minute = this.data.multiArray[4][index[4]] showTime = `${year}-${month}-${day} ${hour}:${minute}` } if (format == 'yyyy-MM-dd HH:mm:ss') { const year = this.data.multiArray[0][index[0]] const month = this.data.multiArray[1][index[1]] const day = this.data.multiArray[2][index[2]] const hour = this.data.multiArray[3][index[3]] const minute = this.data.multiArray[4][index[4]] const second = this.data.multiArray[5][index[5]] showTime = `${year}-${month}-${day} ${hour}:${minute}:${second}` } this.setData({ value: showTime }) this.triggerEvent('dateTimePicker', showTime) }, //初始化时间选择轴 initColumn(curMonth) { let years = [] let months = [] let days = [] let hours = [] let minutes = [] let seconds = [] for (let i = 1990; i <= 2099; i++) { years.push(i + '') } for (let i = 1; i <= 12; i++) { if (i < 10) { i = "0" + i; } months.push(i + '') } if (curMonth == 1 || curMonth == 3 || curMonth == 5 || curMonth == 7 || curMonth == 8 || curMonth == 10 || curMonth == 12) { for (let i = 1; i <= 31; i++) { if (i < 10) { i = "0" + i; } days.push(i + '') } } if (curMonth == 4 || curMonth == 6 || curMonth == 9 || curMonth == 11) { for (let i = 1; i <= 30; i++) { if (i < 10) { i = "0" + i; } days.push(i + '') } } if (curMonth == 2) { days=this.setFebDays() } for (let i = 0; i <= 23; i++) { if (i < 10) { i = "0" + i; } hours.push(i + '') } for (let i = 0; i <= 59; i++) { if (i < 10) { i = "0" + i; } minutes.push(i + '') } for (let i = 0; i <= 59; i++) { if (i < 10) { i = "0" + i; } seconds.push(i + '') } this.setData({ years, months, days, hours, minutes, seconds }) }, /** * 列改变时触发 */ bindPickerColumnChange: function(e) { //获取年份 用于计算改年的2月份为平年还是闰年 if (e.detail.column == 0 && this.properties.format != 'HH:mm:ss') { let chooseYear = this.data.multiArray[e.detail.column][e.detail.value]; this.setData({ chooseYear }) if (this.data.curMonth == '02' || this.data.chooseMonth == '02') { this.setFebDays() } } //当前第二为月份时需要初始化当月的天数 if (e.detail.column == 1 && this.properties.format != 'HH:mm:ss') { let num = parseInt(this.data.multiArray[e.detail.column][e.detail.value]); let temp = []; if (num == 1 || num == 3 || num == 5 || num == 7 || num == 8 || num == 10 || num == 12) { //31天的月份 for (let i = 1; i <= 31; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp }); } else if (num == 4 || num == 6 || num == 9 || num == 11) { //30天的月份 for (let i = 1; i <= 30; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp }); } else if (num == 2) { //2月份天数 this.setFebDays() } } let data = { multiArray: this.data.multiArray, multiIndex: this.data.multiIndex }; data.multiIndex[e.detail.column] = e.detail.value; this.setData(data); }, //计算二月份天数 setFebDays() { let year = parseInt(this.data.chooseYear); let temp = []; if (year % (year % 100 ? 4 : 400) ? false : true) { for (let i = 1; i <= 29; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp, chooseMonth: '02' }); } else { for (let i = 1; i <= 28; i++) { if (i < 10) { i = "0" + i; } temp.push("" + i); } this.setData({ ['multiArray[2]']: temp, chooseMonth: '02' }); } return temp; } }, }) [代码] wxss [代码].view-body { display: flex; align-items: center; padding: 4% 0%; border-bottom: 1px solid #eee; } .item-key { font-size: 30rpx; color: #666; width: 25%; } .item-value { flex-grow: 1; font-size: 31rpx; position: relative; margin-left: 3%; } .img-arrow { width: 45rpx; height: 45rpx; padding-right: 10rpx; position: absolute; top: 50%; transform: translateY(-50%); right: 5rpx; } [代码] 使用 在你页面中的json中添加引用,路径根据你的实际工程目录来写。 [代码]{ "usingComponents": { "DateTimePicker": "/components/datetimepicker/datetimepicker" } } [代码] wxml中添加 [代码]<DateTimePicker title='选择时间' isRequired='true' bind:dateTimePicker='onDateTimePicker' name='time' format='yyyy-MM-dd HH:mm:ss'/> [代码] 说明 title:表单组件的名称 isRequired:是否必填项 dateTimePicker:为选中确认回调 name:为表单点击 form 表单中 form-type 为 submit 的 button 组件时,会将表单组件中的 value 值进行提交,需要在表单组件中加上 name 来作为 key format:时间格式化参数 支持:‘yyyy-MM-dd HH:mm:ss’,‘HH:mm:ss’,‘yyyy-MM-dd HH:mm’,‘yyyy-MM-dd’ 四种 开发者可以根据自己的需求调整组件样式 最后代码片段如下: https://developers.weixin.qq.com/s/PzmDvcmi79fI
2020-02-17