- 挑战答题小程序上线了
真是无心插柳柳成荫,我的挑战答题小程序算是第一次真正进入生产跟用户见面 ~ 说来也有点可惜, 我真的是对挑战答题小程序有太多的执念,借鉴过很多的挑战答题方案,实现的我其实自己不太满意 ~不过没有关系 在这几天的一个答题活动中,我的挑战答题方案,算是他们得分的一个必须步骤,也就是不管我的体验如何,用户是一定会参与答题累计得分的 [图片] ~ [图片] ~ [图片] ~ [图片] ~ 说实话,这个挑战答题的解决方案,在多个小程序上上线,但是没有像这个活动一样是用户必须参与的,用户参与了就会推动产品往前走,当然这次活动也不断有用户反馈修改建议 我也会在日活的开发中,重点照顾这些建议,不断打磨这个产品 通过上面的需求截图,我们可以看到这个从6月底开始做,也算"十年磨一剑",在该活动中,用户每天可以参加最多5次挑战答题,
2021-08-11 - 在线答题小程序
一、使用场景 在线答题,是一种在线练习、考试、测评的智能答题系统,适用于企业培训、学生练习、评测考核、知识竞赛、模拟考试等场景。管理员创建答题活动,答题者提交后系统自动判分。 二、功能亮点 1、多种题目类型、两种答题模式 1)题型丰富多样,支持单选、多选、判断、填空。 2)支持考试、练习两种答题模式,满足不同使用场景。 2、系统自动判分 1)系统可根据答题情况自动判分、统计数据,有效降低人力成本,提升运营效率。 2)支持答题者查询成绩、查看答案解析,一 目了然,提升使用体验。 3、组题方式 1)支持固定选题(从题库手动选题,所有答题者试题相同)。 2)题库随机出题(从题库中随机抽取,不同答题者试题不完全一样),丰富答题玩法,提升趣味性。 [图片] [图片][图片][图片] [图片]
2020-06-08 - 二人pk答题小程序
二人pk答题小程序 ~ 前一段时间支持了几个答题活动,因为有些功能是需要花精力迭代的,所以二人pk答题就有点中断更新了,昨天最后一场答题活动结束,现在又开始搞心心念念的二人pk答题了 本次由于是新开始,先回顾下之前关于二人pk答题的目前进展 其实从逻辑层面二人pk答题已经完成了,但是界面相对比较原始,所以本次重启该项目,重点在于对界面的美观改造,之前储备了一些二人pk答题的优秀案例,给我提供了很多的思路和方向 你 [图片] 笑 [图片] 起 [图片] 来 [图片] 正 [图片] 好看
2021-03-16 - 可以出题的答题小程序
新做了一款答题小程序,界面经过精心设计,功能比较多,可以出题答题考试,并且拥有好友默契出题答题等功能。 本出题答题小程使用云开发,无需服务器域名就可以安装搭建,使用操作简单方便。 并且拥有完整简单的出题界面,不同于其他出题答题小程序,此出题答题小程序出题页面进过设计可以保存后下次接着出,直到题目添加完成后进行发布问题。 小程序界面以清新,美观,简洁为主要设计,以简洁操作为前提进行开发。以下为小程序界面风格。 [图片] [图片] [图片] [图片] [图片] [图片] [图片] 成语答题小程序: https://developers.weixin.qq.com/community/develop/article/doc/0006041d778350dfbb4b0a60456c13 闯关答题小程序: https://developers.weixin.qq.com/community/develop/article/doc/0004046f568298d05d1b4fcc65b013 竞赛答题小程序: https://developers.weixin.qq.com/community/develop/article/doc/000c840fa545c87fbc4b2831256013 如果搭建过程中遇到问题可到程序员锤哥公众号提问。
2020-11-28 - 知识竞赛答题小程序
通过长期的答题小程序开发,不断完善答题小程序的各种功能,整理出知识竞赛答题活小程序活动玩法。 今天我讲解一下知识竞赛答题小程序的规则 一、知识竞赛答题小程序规则一 个人排行榜 根据分数排名,分数相同则答题时间短的排前面。共有三种计分规则,详见下面描述 活动周期内最高得分 例如:活动周期内可以多次答题,取最好的一次成绩参与排名。 累计每天最高得分 例如:每天可以多次答题。第一天最好成绩为90分,第二天最好成绩为80分,两天答题最高得分为90分。 累计每次答题得分 例如:每天可以多次答题,累计每次答题成绩。每人每天最多可以答5次。 单位排行榜 可以根据参赛人数、平均分、总分进行排名。 学习模块 例如:每天不限次数练习答题,不断学习 答题记录 例如:答题的记录都可以随时查看,查看错题,查看答案,还可以生成海报等。 [图片] [图片] 二、知识竞赛答题小程序规则二 本知识竞赛答题小程序活动主要有以下规则: 1.答题活动开始后才可以答题 2.答题活动结束后不能答题 3.每答对一题得10分,答题以每次累计最高分数为主要分数。类似于微信小游戏跳一跳计分规则 4.每天不限制答题的次数,可多次答题,以最高分数为主,破纪录后将统计最高分数 5.每次答题将有3张错题卡,答错3次后需重新开始答题 6.每次答题记录统计答题分数或次数,通过累计分数显示答题段位 7.答题可设置答题段位,答题次数越多积分越高,段位越高 8.答题排行榜分为积分榜和段位榜 9.结束答题后根据积分榜选出一等奖,二等奖,三等奖。或根据段位榜选出一等奖,二等奖,三等奖或参与奖 [图片] [图片] 如果搭建过程中遇到问题可到程序员锤哥公众号提问。
2022-04-26 - 隐藏页面右侧滚动条及页面上下空白的经验
在最外层view设置 width: 100vw; height: 100vh; overflow-x: hidden; overflow-y: auto; 在页面JSON页设置 "disableScroll":true 通过以上设置,页面滑动就不会出现滚动条了。
2021-04-25 - Leadshop是一款免费开源的商城管理系统,基于Uni-app、yii框架开发
本周更新计划【20210420-20210423】点击查看本周更新计划 更新历史点击查看V1.0.1更新内容 qq交流群号:849894135 [图片] 运行环境Leadshop微商城论坛交流地址:https://forum.leadshop.vip/ 体验后台地址:https://demo.leadshop.vip 账号:18888888888 密码:123456 运行环境Linux+Nginx+PHP7.4+MySQL(5.6|5.7) 项目介绍Leadshop是一款提供持续更新迭代服务的免费商城系统,旨在打造极致的用户体验! Leadshop由浙江禾成云计算有限公司研发,主要面向中小型企业,助力搭建电商平台,并提供专业的技术支持。免费提供长期更新服务,应用涉及可视化装修、促销转化、裂变分销、用户精细化管理、数据分析等多个维度,追求极致体验,全面赋能商家,为商家创造价值。 主要特性Leadshop 开源系统,基于Yii2开发,支持composer,优化核心,减少依赖,基于全新的架构思想和命名空间 基于命名空间和PSR-4规范,加入PHP7新特性核心功能模块化,方便开发和维护强化路由功能,采用RESTful接口标准灵活创建模型控制器,易于扩展开发配置文件可分离,方便管理重写的自动验证和完成简化扩展机制,提升开发速度API支持完善,方便二次开发内置WeChat微信开发框架,微信接入更加快捷,简单使用ORM自动创建表结构,提升开发速度支持数据库结构、数据、模板在线缓存清除,提升用户体验客户端完善的交互效果和动画,提升用户端视觉体验支持在线一键安装,方便快捷。可视化DIY店铺装修,方便、快捷、直观,可以随心所欲装扮自己的店铺高效的笛卡尔乘积运算,8000条规格秒加载拟态Windows文件夹的素材管理结构,操作更熟悉随心开源无加密基于Apache License 2.0开源协议,前后端代码开源无加密,支持二次开发,支持商用。 核心技术前端技术栈 ES6、vue、vuex、vue-router、vue-cli、axios、element-ui、uni-app 后端技术栈 Yii2、Jwt、Mysql、Easy-SMS 接口标准采用标准RESTful API ,高效的API阅读性,具有扩展性强、结构清晰的特点 数据交互采用JSON API 标准,用以定义客户端如何获取与修改资源,以及服务器如何响应对应请求。高效实现的同时,无需牺牲可读性、灵活性和可发现性 认证方式目前所有的接口使用 Oauth2 Password 方式,也就是 JWT Authorization Bearer header 进认证。支持扩语言扩展,多平台扩展。 数据表格导出采用高性能的 js-xlsx数据导出,易于扩展,兼容性强。 接口文档:http://www.leadshop.vip/api.html 页面展示[图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片]安装一键安装1.上传你的代码 2.项目目录设置755权限 3.设置伪静态规则(nginx 推荐使用) location / { try_files $uri $uri/ /index.php$is_args$args; } 注:如果使用Apache环境 需要在.htaccess 中添加 SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 否则会导致OAuth登录模式获取不到Authorization 4.设置代码执行目录为/server/web 5.在浏览器中输入你的域名或IP ( 例如:www.yourdomain.com/install ),安装程序会自动执行安装。期间系统会提醒你输入数据库信息以完成安装。 6.检查php禁用函数列表,symlink函数不能被禁用,否则后台页面无法访问。 7.后台访问地址: 域名/leadshop/panel/index 8.公众号首页访问地址: 域名/h5 重新安装清除数据库删除/server/install.lock 文件目录说明├─admin // 后台前端项目 │ ├─public │ └─src │ ├─assets │ │ ├─icons │ │ └─images │ ├─components │ ├─lib │ │ └─function │ ├─mixins │ ├─pages │ │ ├─channel // 渠道 │ │ ├─error │ │ ├─gallery │ │ ├─goods // 商品 │ │ ├─login // 登入 │ │ ├─order // 订单 │ │ ├─package // 装修组件 │ │ ├─pages // 微页面 │ │ ├─panel // 首页 │ │ ├─setup // 设置 │ │ ├─store // 店铺 │ │ ├─tabbar // 底部导航 │ │ └─users // 用户 │ ├─plugins │ ├─router │ ├─store │ ├─theme │ │ └─fonts │ └─utils ├─applet //小程序项目 │ ├─public │ ├─src │ │ ├─colorui // CSS样式 │ │ │ └─components │ │ ├─components │ │ │ ├─goods-list // 商品列表 │ │ │ ├─he-html // 富文本 │ │ │ │ └─node │ │ │ └─lime-painter │ │ ├─libs │ │ │ ├─function // 工具函数 │ │ │ └─mixin │ │ ├─pages │ │ │ ├─cart // 购物车页 │ │ │ ├─categories // 分类页 │ │ │ ├─fitment // 装修组件 │ │ │ ├─goods // 商品页 │ │ │ ├─greet │ │ │ ├─index // 首页 │ │ │ ├─order // 订单页 │ │ │ ├─other // 其他页 │ │ │ ├─page // 微页面 │ │ │ └─user // 个人中心页 │ │ ├─static // 静态资源 │ │ │ ├─css │ │ │ ├─h5 // H5资源 │ │ │ ├─images │ │ │ └─mp-weixin // 小程序资源 │ │ ├─store // vuex │ │ └─utils │ └─we7 ├─install //安装程序 │ ├─public │ └─src │ ├─assets │ │ └─css │ └─components └─server //后台接口项目 ├─api //后台接口 ├─app //客户端(微信小程序,公众号) ├─applet //打包后的微信小程序源码包 ├─components //通用组件 ├─config //配置文件目录 ├─controllers //控制器 ├─datamodel //模型 ├─forms │ └─install ├─modules //模块 ├─stores //应用配置文件 ├─system //系统核心目录 │ ├─common │ ├─config │ ├─phpqrcode │ └─wechat ├─vendor //依赖 ├─views │ ├─layouts │ └─site └─web ├─assets ├─h5 //公众号编译包目录,其中index.php不可删除 ├─install ├─leadshop //后台编译包目录 ├─static //静态文件 ├─temp └─upload //上传文件 打包建议使用cnpm,cnpm安装: npm install cnpm -g --registry=https://registry.npm.taobao.org后台页面打包发布后台前端源文件目录/admin 打包步骤 1、安装依赖包 npm install 2、运行调试 npm run serve 3、打包发布 npm run build 4、打包后把/admin/dist/build内所有文件复制到站点/server/web/leadshop目录下 公众号打包发布后台前端源文件目录/applet 打包步骤 1、安装依赖包 npm install 2、运行调试 npm run serve 3、打包发布 npm run build:h5 4、复制 打包后把/applet/dist/build/h5内所有文件复制到站点/server/web/h5目录下 小程序打包发布后台前端源文件目录/applet 打包步骤 1、安装依赖包 npm install 2、运行调试 npm run serve 3、打包发布 npm run build:mp-weixin 4、复制 打包后把/applet/dist/build/mp-weixin 打包成zip,重命名为app.zip,并放入/server/applet目录下覆盖之前的app.zip 使用须知1.允许用于个人学习、毕业设计、教学案例、公益事业; 2.支持企业/个人免费商业使用,但必须保留leadshop版权信息; 版权信息Powered By Leadshop © 2021
2021-04-22 - #小程序云开发挑战赛#快速找工作-mrhuostudio
项目概要 应用场景: 为使用C#/dotnet技术的用户展现小程序开发的统一解决方案,让c#程序员更容易的开发微信小程序。为用户在微信中快速找工作,汇聚各大招聘网站的招聘数据,提供集成的岗位查找。用户人群: 各大高校学生、职场人士使用dotnet技术的程序员首先感谢官方提供了这次比赛。刚好最近看到腾讯云云函数SCF 推出Custom Runtime定制化运行功能,正在使用C#封装Custom Runtime的云函数,需要找一个场景来进行验证工作,当前微信小程序使用js 作为开发语言, 云开发的云函数也支持多种语言,但是他们都不支持C#,腾讯云 SCF Custom Runtime的云函数可以让更多的后端程序员投身全栈开发。 本次参加这次大赛的初衷是以大赛的要求来充分验证使用C#打造全场景的云原生应用开发,参加比赛的场景是使用小程序快速查找各大招聘网站的岗位,用户在小程序种输入岗位关键词和城市【支持全部城市岗位(不选城市)】,将查询条件提交给后端云函数向各大招聘网站提交查询请求,合并查询结果返回给小程序,同时将相同查询条件的请求使用Redis缓存到服务端。用户在小程序端可以保存他感兴趣的岗位,也可以在微信小程序种发起申请岗位(调用招聘网站的小程序,这一部分理论上技术可行,更多的是商务,因此目前未实现)。用户在小程序端的相关操作进行一个访问统计,用户登录和保存感兴趣岗位数据,以及用户的访问统计数据使用云开发的云函数 封装api和数据库保存数据, 同时将云开发的数据通过Custom Runtime的云函数进行封装成API 提供给 运营站点进行展示, 运营站点使用基于WebAssembly技术的前端框架blazor 进行开发,通过云开发部署到静态网站托管。 这个项目充分调用了小程序的能力,并且结合云开发的优势,同时扩展云开发的云函数,当前云开发的云函数不支持Custom Runtime,云开发的云函数也是基于腾讯云SCF封装。微信小程序可以在ios和android上运行,解决了移动App必须去兼容多端的接口,减少工作量,开发出来的小程序稳定,用完就走,方便用户使用。 云开发进一步的简化了微信小程序的开发,真正做到云原生应用开发,当前云开发不支持C#语言,本项目的主要目的就是展示使用C#语言在云开发的应用。 项目地址本项目基于MIT协议在Github开源,开源地址https://github.com/dotnetcloudbase/findjobtclooud , 后期在这项目基础上将SCF Custom Runtime 云函数的C#版本继续发展,本项目只实现httptrigger的MVP实现。 演示视频 https://v.qq.com/x/page/x3151ge7pvs.html 项目架构功能架构项目 功能上由小程序 和运营网站构成, 用户使用微信小程序快速找工作,相关的运营数据保存在云开发的云存储中,通过运营网站进行展示。 [图片] 系统架构综合使用云开发 和腾讯云云函数打造Serverless的云原生开发。 [图片] 效果截图小程序[图片] 默认界面是职位搜索界面,输入关键词后,点击键盘上的搜索按钮。小程序将向远程 scf 云函数发起请求,返回职位列表。如下: [图片] 可以看到相关职位都已经列出来,搜索框下面有两个下拉选择,分别是地区选择,和职位来源选择。不选择则返回所有地区和所有来源网站的职位信息。点击地区选择时: [图片] 这里可以切换地区,选中地区确认后,则只筛选指定地区的职位信息。这里可以看到左侧有个“定位”按钮,定位时,将根据 wx.getLocation API 获取用户当前经纬度,然后调用 scf 云函数返回用户城市。达到定位效果。重置按钮,则重置筛选条件,将城市设置为不限制。 [图片] 来源列表里这里支持“猎聘网”、“智联招聘”、“前程无忧”、“Boos直聘”这4个来源。默认不限来源。 [图片] 进入详情界面,可以看到最下方有 “分享”、“收藏”、“申请职位” 三个功能按钮。 点击分享,可以分享职位信息给朋友。 收藏按钮,可以收藏在我的收藏列表里,在“我的收藏”界面里展示。 申请职位按钮可以通过跳转别的网站小程序来实现,但是因为需要商务方面的工作,所以暂时没有实现申请职位。将来有机会,这个功能会实现的。 此详情页面设置了可以分享到朋友圈,点击右上角的 ”...“ 就可以分享到朋友圈。 [图片] “我的”界面里主要有收藏列表功能,用户首次使用时,头像那里有个“立即登录”,登录后,数据将通过上方所示云函数保存起来。 [图片] 这里是我的收藏界面。 关于界面介绍写的很简单。不过这个小程序却不简单。能实现快速的筛选实时岗位信息,想要找工作的你,是否需要? 运营网站 运营网站首页 [图片] 用户访问统计 [图片] 申请职位统计 [图片] 搜索关键词排行榜Top10 [图片] 按城市搜索排行榜Top10 [图片] 功能代码展示SCF CustomRuntime 云函数当前版本云开发不支持C# 编写云函数,SCF云函数支持通过Custom Runtime实现云函数,云开发本身也是基于SCF 云函数实现,因此这个功能主要展示Custom Runtime开发云函数,通过API网关发布给小程序使用。代码比较多,具体内容参见 《在腾讯云云函数计算上部署.NET Core 3.1》。 代码:https://github.com/dotnetcloudbase/findjobtclooud/tree/master/scf namespace Yhd.FindJob { public class JobsHttpFunctionInvoker : HttpFunctionInvoker { private IJobsManager jobsManager; private IAmapWebApi amapWebApi; public JobsHttpFunctionInvoker(ILoggerFactory loggerFactory, IJobsManager manager, IAmapWebApi webApi) : base(loggerFactory) { jobsManager = manager; amapWebApi = webApi; } protected override async Task Handler(SCFContext context, APIGatewayProxyRequestEvent requestEvent) { if (requestEvent != null && requestEvent.RequestContext == null) { return new APIGatewayProxyResponseEvent() { ErrorCode = 410, ErrorMessage = "event is not come from api gateway", }; } if (requestEvent != null) { if (requestEvent.Path != "/api/jobs/getjobs" && requestEvent.Path != "/api/jobs/getdetailsinfo" && requestEvent.Path != "/api/geocode/regeo") { return new APIGatewayProxyResponseEvent() { ErrorCode = 411, ErrorMessage = "request is not from setting api path" }; } if (requestEvent.Path == "/api/jobs/getjobs" && requestEvent.HttpMethod.ToUpper() == "GET") { string sources = requestEvent.QueryString["sources"]; string city = requestEvent.QueryString["city"]; string searchKey = requestEvent.QueryString["searchkey"]; string pageIndex = requestEvent.QueryString["pageindex"]; var jobs = await jobsManager.GetJobsAsync(sources.Split('-').ToList(), city, searchKey, int.Parse(pageIndex)); var response = new APIGatewayProxyResponseEvent() { StatusCode = 200, ErrorCode = 0, ErrorMessage = "", Body = JsonConvert.SerializeObject(jobs), IsBase64Encoded = false, Headers = new Dictionary() }; response.Headers.Add("Content-Type", "application/json"); response.Headers.Add("Access-Control-Allow-Origin", "*"); return response; } if (requestEvent.Path == "/api/jobs/getdetailsinfo" && requestEvent.HttpMethod.ToUpper() == "GET") { string source = requestEvent.QueryString["source"]; string url = requestEvent.QueryString["url"]; var jobs = await jobsManager.GetDetailsInfo(source, url); var response = new APIGatewayProxyResponseEvent() { StatusCode = 200, IsBase64Encoded = false, Headers = new Dictionary() }; if (jobs == null) { response.ErrorCode = -1; response.ErrorMessage = "user code exception caught"; } else { response.ErrorCode = 0; response.ErrorMessage = ""; response.Body = JsonConvert.SerializeObject(jobs); } response.Headers.Add("Content-Type", "application/json"); response.Headers.Add("Access-Control-Allow-Origin", "*"); return response; } if(requestEvent.Path == "/api/geocode/regeo" && requestEvent.HttpMethod.ToUpper() == "GET") { string location = requestEvent.QueryString["location"]; ReGeoParameter reGeoParameter = new ReGeoParameter() { Location = location, Batch = false, Output = "JSON", Radius = 1000, RoadLevel = 0, Extensions = "base", Poitype = string.Empty }; var regeo = await amapWebApi.GetRegeoAsync(reGeoParameter); var response = new APIGatewayProxyResponseEvent() { StatusCode = 200, IsBase64Encoded = false, Headers = new Dictionary() }; if(regeo == null) { response.ErrorCode = -1; response.ErrorMessage = "user code exception caught"; } else { response.ErrorCode = 0; response.ErrorMessage = ""; response.Body = string.IsNullOrEmpty(regeo.ReGeoCode.AddressComponent.City) ? regeo.ReGeoCode.AddressComponent.Province : regeo.ReGeoCode.AddressComponent.City; } response.Headers.Add("Content-Type", "application/json"); response.Headers.Add("Access-Control-Allow-Origin", "*"); return response; } } return new APIGatewayProxyResponseEvent() { ErrorCode = 413, ErrorMessage = "request is not correctly execute" }; } } } namespace Yhd.FindJobStat { /// /// 统计数据云开发数据库调用 /// public class StatsHttpFunctionInvoker : HttpFunctionInvoker { private static WxCloudApi _wxCloudApi; public StatsHttpFunctionInvoker(ILoggerFactory loggerFactory, WxCloudApi wxCloudApi) : base(loggerFactory) { _wxCloudApi = wxCloudApi; } /// /// URL mapping /// private readonly Dictionary>> handlerMapper = new Dictionary>>() { ["GET /api/stat/pv"] = HandlerStatPV, ["GET /api/stat/apply"] = HandlerStatApplyButton, ["GET /api/stat/topsearch"] = HandlerStatTop10Search, ["GET /api/stat/topcity"] = HandlerStatTop10City }; /// /// 根据结果构建统一返回值 /// /// /// /// private static APIGatewayProxyResponseEvent BuildCommonResponse(T responseModel) { var response = new APIGatewayProxyResponseEvent() { StatusCode = 200, ErrorCode = 0, ErrorMessage = "", Body = JsonConvert.SerializeObject(responseModel), IsBase64Encoded = false, Headers = new Dictionary() }; response.Headers.Add("Content-Type", "application/json"); response.Headers.Add("Access-Control-Allow-Origin", "*"); return response; } /// /// 调用 get-stat-pv 云函数 /// /// /// private static async Task HandlerStatPV(APIGatewayProxyRequestEvent requestEvent) { return BuildCommonResponse(await _wxCloudApi.InvokeCloudFunction("get-stat-pv")); } /// /// 调用 get-stat-apply 云函数 /// /// /// private static async Task HandlerStatApplyButton(APIGatewayProxyRequestEvent requestEvent) { return BuildCommonResponse(await _wxCloudApi.InvokeCloudFunction("get-stat-apply")); } /// /// 调用 get-stat-top-search 云函数 /// /// /// private static async Task HandlerStatTop10Search(APIGatewayProxyRequestEvent requestEvent) { return BuildCommonResponse(await _wxCloudApi.InvokeCloudFunction("get-stat-top-search")); } /// /// 调用 get-stat-top-city 云函数 /// /// /// private static async Task HandlerStatTop10City(APIGatewayProxyRequestEvent requestEvent) { return BuildCommonResponse(await _wxCloudApi.InvokeCloudFunction("get-stat-top-city")); } /// /// 拦截并分发请求 /// /// /// /// protected override async Task Handler(SCFContext context, APIGatewayProxyRequestEvent requestEvent) { if (requestEvent == null) { return new APIGatewayProxyResponseEvent() { ErrorCode = 413, ErrorMessage = "request is not correctly execute" }; } if (requestEvent.RequestContext == null) { return new APIGatewayProxyResponseEvent() { ErrorCode = 410, ErrorMessage = "event is not come from api gateway", }; } var path = $"{requestEvent.HttpMethod.ToUpper()} {requestEvent.Path.ToLower()}"; if (!handlerMapper.ContainsKey(path)) { return new APIGatewayProxyResponseEvent() { ErrorCode = 411, ErrorMessage = "request is not from setting api path" }; } return await handlerMapper[path](requestEvent); } } } 云开发云函数代码: https://github.com/dotnetcloudbase/findjobtclooud/tree/master/cloudfunctions 用户登录云函数主要是为了保存用户基本信息数据,如果用户信息已存在,则以最新数据覆盖旧数据,否则保存当前用户基本信息。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const openId = wxContext.OPENID; var user = await db.collection('users').where({ openId: openId }).get(); if (user.data.length != 0) { var userInfo = user.data[0]; var id = userInfo._id; userInfo.avatarUrl = userInfo.avatarUrl || event.userInfo.avatarUrl; userInfo.country = userInfo.country || event.userInfo.country; userInfo.province = userInfo.province || event.userInfo.province; userInfo.city = userInfo.city || event.userInfo.city; userInfo.gender = typeof (userInfo.gender) === "number" ? userInfo.gender : event.userInfo.gender; userInfo.language = userInfo.language || event.userInfo.language; userInfo.nickName = userInfo.nickName || event.userInfo.nickName; userInfo.unionId = wxContext.UNIONID || ""; delete userInfo._id; await db.collection('users').doc(id).update({ data: userInfo }); return userInfo; } else { var userInfo = event.userInfo; userInfo.avatarUrl = userInfo.avatarUrl || null; userInfo.country = userInfo.country || null; userInfo.province = userInfo.province || null; userInfo.city = userInfo.city || null; userInfo.gender = typeof (userInfo.gender) === "number" ? userInfo.gender : 0; userInfo.language = userInfo.language || "zh_CN"; userInfo.nickName = userInfo.nickName || null; userInfo.openId = wxContext.OPENID; userInfo.unionId = wxContext.UNIONID || ""; await db.collection('users').add({ data: userInfo }); return userInfo; } } 获取收藏列表云函数列出了用户已添加的收藏列表。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { return (await db.collection('favorites').where({ openId: cloud.getWXContext().OPENID }).get()).data; } 保存用户收藏,将职位名称,地区,薪资,来源公司名称都保存在收藏列表里。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { const openId = cloud.getWXContext().OPENID; var favorite = await db.collection('favorites').where({ openId: openId, url: event.url }).get(); if (favorite.data.length == 0) { var favoriteInfo = { openId: openId, source: event.source, url: event.url, jobTitle: event.jobTitle, area: event.area, salary: event.salary, jobCompany: event.jobCompany, time: event.time, logo: event.logo }; await db.collection('favorites').add({ data: favoriteInfo }); return true } await db.collection('favorites').doc(favorite.data[0]._id).remove(); return false } 是否已收藏云函数就比较简单了,这里只是检查用户是否保存了职位详情 url。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { const openId = cloud.getWXContext().OPENID; var favorite = await db.collection('favorites').where({ openId: openId, url: event.url }).get(); return favorite.data.length > 0; } 这里我们把统计数据归了类,用 type 来分类,type 一共有:"preview" 预览, "click" 点击详情, "location" 定位, "search" 搜索, "apply" 申请职位按钮, "favorite" 收藏, "shareToFriend" 分享给朋友, "shareToTimeline" 分享朋友圈这8类数据。extra 是附加数据结构,在以下的代码中会详细介绍。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { var statInfo = { date: event.date, type: event.type, page: event.page, extra: event.extra }; await db.collection('stats').add({ data: statInfo }); return true } 6、剩下的云函数:get-stat-apply,get-stat-pv,get-stat-top-city,get-stat-top-search, 这4个云函数是获取统计数据用的。这里只贴 get-stat-top-search 作为实例,其他云函数类似。 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const $ = db.command.aggregate exports.main = async () => { return (await db.collection('stats') .aggregate() .match({ type: "search" }) .group({ _id: $.toLower('$extra.keyword'), count: $.sum(1) }) .sort({ count: 1, }) .limit(10) .end()).list; } 在这个云函数里以 {type: "search"} 作为查询条件,然后以关键词分组,数量从大到小排序,取了前10条数据作为结果。 小程序 代码: https://github.com/dotnetcloudbase/findjobtclooud/tree/master/miniprogram 小程序界面主要使用了 ColorUI 开源的 UI 框架,这个框架最大的特点是漂亮、纯 css 框架, 推荐大家使用并 star。 小程序页面比较简单,结构代码不贴了,主要贴一下 jobService.js 这个类。代码比较多。 const CONST = require("./consts") let _service = (function () { const API_BASE = "https://xxx/release" const API_GET_JOBS = API_BASE + "/api/jobs/getjobs" const API_GET_JOB_DETAIL = API_BASE + "/api/jobs/getdetailsinfo" const API_GET_LOCATED_CITY = API_BASE + "/api/geocode/regeo" const DEBUG_ENABLED = false let loggerInfo = function (msg, param) { console.log("INFO: " + msg, param) }; let loggerError = function (msg, param) { console.error("ERROR: " + msg, param) if (DEBUG_ENABLED) { wx.showModal({ showCancel: false, content: `${msg}${!param ? "" :"\n" + JSON.stringify(param)}` }) } }; let dateFormat = function (fmt, date) { date = date || new Date() let ret; let opt = { "y+": date.getFullYear().toString(), // 年 "M+": (date.getMonth() + 1).toString(), // 月 "d+": date.getDate().toString(), // 日 "H+": date.getHours().toString(), // 时 "m+": date.getMinutes().toString(), // 分 "s+": date.getSeconds().toString() // 秒 }; for (let k in opt) { ret = new RegExp("(" + k + ")").exec(fmt); if (ret) { fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0"))) }; }; return fmt; }; let getJobs = function (keyword, sources, city, pageIndex, callback) { wx.request({ url: API_GET_JOBS, method: "GET", data: { pageindex: pageIndex, sources: sources, city: city, searchkey: keyword }, success(res) { loggerInfo(`请求接口【${API_GET_JOBS}】成功`, res) var data = JSON.parse(res.data.Body || "[]") || []; var jobs = data.map(p => { var logo = "" var source = "" switch (p.Source) { case "Liepin": logo = "/images/logo_liepin.png" source = "猎聘网" break; case "ZLZhaopin": logo = "/images/logo_zhilian.png" source = "智联招聘" break; case "QC51": logo = "/images/logo_qianchengwuyou.png" source = "前程无忧" break; case "BOSS": logo = "/images/logo_boss.png" source = "BOSS直聘" break; } var sourceValue = CONST.SITES.filter(p => p.name == source)[0].value; return { "logo": logo, "source": source, "jobTitle": (p.PositionName || "").trim(), "salary": (p.Salary || "").trim(), "jobCompany": (p.CorporateName || "").trim(), "area": (p.WorkingPlace || "").trim(), "time": (p.ReleaseDate || "").trim(), "detailsUrl": (p.DetailsUrl || "").trim(), "sourceValue": sourceValue } }); callback && callback(jobs) }, fail(res) { callback && callback([]) loggerError("获取职位列表出错!", res) } }) }; let getJobDetail = function (source, url, callback) { wx.request({ url: API_GET_JOB_DETAIL, method: "GET", data: { source: source, url: url }, success(res) { loggerInfo(`请求接口【${API_GET_JOB_DETAIL}】成功`, res) if (!res.data.Body) { callback && callback(null) return; } callback && callback(JSON.parse(res.data.Body)) }, fail(res) { callback && callback(null) loggerError("获取职位详情出错!", res) } }) }; let getCityByLatlng = function (lat, lng, callback) { wx.request({ url: API_GET_LOCATED_CITY, method: "GET", data: { location: `${lng},${lat}` }, success(res) { loggerInfo(`请求接口【${API_GET_LOCATED_CITY}】成功`, res) if (!res.data.Body) { callback && callback(null) return; } callback && callback(res.data.Body) }, fail(res) { callback && callback(null) loggerError("反解析地理位置失败!", res) } }) }; let isFavorite = function (url, callback) { wx.cloud.callFunction({ name: 'is-favorite', data: { url: url } }).then(res => { loggerInfo(`调用云函数【is-favorite】成功`, res) callback && callback(res.result); }).catch(err => { callback && callback(false); loggerError("调用云函数[is-favorite]失败!", err) }); }; let saveFavorite = function (jobInfo, callback) { wx.cloud.callFunction({ name: 'save-favorite', data: jobInfo }).then(res => { loggerInfo(`调用云函数【save-favorite】成功`, res) callback && callback(res.result); }).catch(err => { callback && callback(null); loggerError("调用云函数[save-favorite]失败!", err) }); }; let deleteFavorite = function (url, callback) { wx.cloud.callFunction({ name: 'save-favorite', data: { url: url } }).then(res => { loggerInfo(`调用云函数【save-favorite】成功`, res) callback && callback(res.result); }).catch(err => { callback && callback(null); loggerError("调用云函数[save-favorite]失败!", err) }); }; let getMyFavorites = function (callback) { wx.cloud.callFunction({ name: 'user-favorite' }).then(res => { loggerInfo(`调用云函数【user-favorite】成功`, res) callback && callback(res.result || []); }).catch(err => { callback && callback([]); loggerError("调用云函数[user-favorite]失败!", err) }); }; //统计方法 const EventType = { PREVIEW: "preview", CLICK: "click", LOCATION: "location", SEARCH: "search", APPLY: "apply", FAVORITE: "favorite", SHARE_TO_FRIEND: "shareToFriend", SHARE_TO_TIME_LINE: "shareToTimeline" } let stat = function (page, event, extras) { try { setTimeout(() => { var date = dateFormat("yyyy-MM-dd") console.log(`[${date}] STAT: [${event}] ${page}`, extras) wx.cloud.callFunction({ name: 'post-stat', data: { date: date, type: event, page: page, extra: extras } }).then(res => { loggerInfo(`调用云函数【post-stat】成功`, res) }).catch(err => { loggerError("调用云函数[post-stat]失败!", err) }); }, 100); } catch (error) { console.warn("POST STATISTIC DATA ERROR, ", error); loggerError("调用云函数[post-stat]失败!", error) } } let statPreviewJobIndex = function () { stat("/pages/job/index", EventType.PREVIEW) }; let statPreviewJobDetail = function () { stat("/pages/job/detail", EventType.PREVIEW) }; let statPreviewMy = function () { stat("/pages/job/my", EventType.PREVIEW) }; let statPreviewMyFavorite = function () { stat("/pages/my/favorites", EventType.PREVIEW) }; let statPreviewAbout = function () { stat("/pages/my/about", EventType.PREVIEW) }; let statUseGetLocation = function () { stat(null, EventType.LOCATION) }; let statSearch = function (keyword, source, city) { stat(null, EventType.SEARCH, { keyword, source, city }) }; let statApplyJob = function (source, url) { stat(null, EventType.APPLY, { source, url }) }; let statFavoriteJob = function (source, url) { stat(null, EventType.FAVORITE, { source, url }) }; let statShareJobToFriend = function (source, url) { stat(null, EventType.SHARE_TO_FRIEND, { source, url }) }; let statShareJobToTimeline = function (source, url) { stat(null, EventType.SHARE_TO_TIME_LINE, { source, url }) }; let getStatPV = function (callback) { wx.cloud.callFunction({ name: 'get-stat-pv' }).then(res => { loggerInfo(`调用云函数【get-stat-pv】成功`, res) callback && callback(res.result || []); }).catch(err => { callback && callback([]); loggerError("调用云函数[get-stat-pv]失败!", err) }); }; let getStatTopSearch = function (callback) { wx.cloud.callFunction({ name: 'get-stat-top-search' }).then(res => { loggerInfo(`调用云函数【get-stat-top-search】成功`, res) callback && callback(res.result || []); }).catch(err => { callback && callback([]); loggerError("调用云函数[get-stat-top-search]失败!", err) }); }; let getStatTopCity = function (callback) { wx.cloud.callFunction({ name: 'get-stat-top-city' }).then(res => { loggerInfo(`调用云函数【get-stat-top-city`, res) callback && callback(res.result || []); }).catch(err => { callback && callback([]); loggerError("调用云函数[get-stat-top-city]失败!", err) }); }; return { getJobs, getJobDetail, getCityByLatlng, isFavorite, saveFavorite, deleteFavorite, getMyFavorites, statPreviewJobIndex, statPreviewJobDetail, statPreviewMy, statPreviewMyFavorite, statPreviewAbout, statUseGetLocation, statSearch, statApplyJob, statFavoriteJob, statShareJobToFriend, statShareJobToTimeline, getStatPV, getStatTopSearch, getStatTopCity }; })(); module.exports = _service loggerInfo,loggerError 两个函数分别是调试代码输出控制台时使用。 getJobs,getJobDetail, getCityByLatlng, isFavorite, saveFavorite, deleteFavorite, getMyFavorites, 这几个函数参与了小程序所有业务。上面截图里都有大概说明。 statPreviewJobIndex,statPreviewJobDetail,statPreviewMy,statPreviewMyFavorite,statPreviewAbout,statUseGetLocation,statSearch,statApplyJob,statFavoriteJob,statShareJobToFriend,statShareJobToTimeline,这几个云函数主要是为了提交统计数据所用。 getStatPV,getStatTopSearch,getStatTopCity,这几个云函数获取了实时的统计数据。 第一版我们把统计图表做到了小程序里,但是因为 echart 和 canvas 的一些bug,导致有时候在一个页面里渲染不了3个canvas,所以此功能就被砍掉了。不过当时我保留了一张截图。我们看下: 是不是也很帅气? 运营网站代码: https://github.com/dotnetcloudbase/findjobtclooud/tree/master/blazorsite/FindJobBlazorSite 运营网站界面主要使用了blazor框架,Blazor 借助于WebAssembly技术 改进这种前后端分离的模式,他有两种模式支持:Blazor WebAssembly 应用和Blazor Server ,个人认为Blazor Webassembly 模式的应用才是这种前后端分离的正途。 在 Blazor WebAssembly 应用程序中构建的文件将编译并发送到浏览器。然后,浏览器在浏览器的执行沙盒中运行您的 JavaScript、HTML 和 C#。它甚至运行 .NET 运行时的版本,这个运行时处理 JavaScript 互操作,并提供基本服务(如垃圾回收)和更高级别的功能(布局、路由和用户界面小部件等), Blazor 允许您使用 C# 而不是 JavaScript 构建交互式 Web UI, Blazor 应用由使用 C#、HTML 和 CSS 实现的可重用 Web UI 组件组成, 客户端和服务器代码都用 C# 编写,允许您共享代码和库. 用户访问统计 @inject WxCloudApi WxCloud @if (statPvData == null) { 图表正在加载中... } @code { LineConfig chartConfig = new LineConfig { Height = 650, Title = new Title { Visible = true, Text = "FindJob 访问统计" }, Description = new Description { Visible = true, Text = "本图表统计30天内 FindJob 访问情况", }, ForceFit = true, Padding = "auto", XField = "_id", YField = "pv", Meta = new { _id = new { Alias = "日期" }, pv = new { Alias = "PV" } }, Label = new ColumnViewConfigLabel { Visible = true, Style = new TextStyle { FontSize = 12, FontWeight = 600, Opacity = 60, } }, Interactions = new Interaction[] { new Interaction { Type = "slider", Cfg = new { start = 0, end = 1, } } }, Smooth = true }; IChartComponent chartInstance = null; List statPvData = null; protected override async Task OnInitializedAsync() { var tempPv = (await WxCloud.GetStatPv()).ToDictionary(p => DateTime.Parse(p.Date), p => p.Value); var minDate = tempPv.Min(p => p.Key); var maxDate = tempPv.Max(p => p.Key); statPvData = new List(); while (minDate.Date <= maxDate.Date) { if (tempPv.ContainsKey(minDate)) { statPvData.Add(new StatPv() { Date = minDate.ToString("yyyy-MM-dd"), Value = tempPv[minDate] }); } else { statPvData.Add(new StatPv() { Date = minDate.ToString("yyyy-MM-dd"), Value = 0 }); } minDate = minDate.AddDays(1); } await chartInstance.ChangeData(statPvData); } } 申请职位按钮点击统计 @inject WxCloudApi WxCloud @if (statApplyData == null) { 图表正在加载中... } @code { ColumnConfig chartConfig = new ColumnConfig { Height = 650, Title = new Title { Visible = true, Text = "FindJob 申请职位按钮点击统计" }, Description = new Description { Visible = true, Text = "本图表统计30天内 FindJob 申请职位按钮点击情况", }, ForceFit = true, Padding = "auto", XField = "_id", YField = "pv", Meta = new { _id = new { Alias = "日期" }, pv = new { Alias = "点击量" } }, Label = new ColumnViewConfigLabel { Visible = true, Position = "middle", Style = new TextStyle { FontSize = 12, FontWeight = 600, Opacity = 60, } }, Interactions = new Interaction[] { new Interaction { Type = "slider", Cfg = new { start = 0, end = 1, } } } }; IChartComponent chartInstance = null; List statApplyData = null; protected override async Task OnInitializedAsync() { var tempPv = (await WxCloud.GetStatApply()).ToDictionary(p => DateTime.Parse(p.Date), p => p.Value); var minDate = tempPv.Min(p => p.Key); var maxDate = tempPv.Max(p => p.Key); statApplyData = new List(); while (minDate.Date <= maxDate.Date) { if (tempPv.ContainsKey(minDate)) { statApplyData.Add(new StatApply() { Date = minDate.ToString("yyyy-MM-dd"), Value = tempPv[minDate] }); } else { statApplyData.Add(new StatApply() { Date = minDate.ToString("yyyy-MM-dd"), Value = 0 }); } minDate = minDate.AddDays(1); } await chartInstance.ChangeData(statApplyData); } } 搜索关键词Top10排行榜 @inject WxCloudApi WxCloud @if (statData == null) { 图表正在加载中... } @code { BarConfig chartConfig = new BarConfig { Height = 650, Title = new Title { Visible = true, Text = "FindJob 搜索关键词 Top10 排行榜" }, Description = new Description { Visible = true, Text = "本图表统计 FindJob 搜索次数最多的关键词", }, ForceFit = true, Padding = "auto", XField = "count", YField = "_id", Meta = new { _id = new { Alias = "关键词" }, count = new { Alias = "搜索次数" } }, Label = new BarViewConfigLabel { Visible = true, Position = "left" } }; IChartComponent chartInstance = null; IEnumerable statData = null; protected override async Task OnInitializedAsync() { statData = (await WxCloud.GetStatTopSearch()).OrderByDescending(p => p.Value); await chartInstance.ChangeData(statData); } } 搜索城市Top10排行榜 @inject WxCloudApi WxCloud @if (statData == null) { 图表正在加载中... } @code { PieConfig chartConfig = new PieConfig { Height = 650, Title = new Title { Visible = true, Text = "FindJob 搜索城市 Top10 排行榜" }, Description = new Description { Visible = true, Text = "本图表统计 FindJob 搜索次数最多的城市", }, ForceFit = true, Padding = "auto", Meta = new { _id = new { Alias = "城市" }, count = new { Alias = "搜索次数" } }, AngleField = "count", ColorField = "_id", Label = new PieLabelConfig { Visible = true, Type = "inner" } }; IChartComponent chartInstance = null; IEnumerable statData = null; protected override async Task OnInitializedAsync() { statData = (await WxCloud.GetStatTopCity()).OrderByDescending(p => p.Value); await chartInstance.ChangeData(statData); } } 运营网站部署云开发的网站网站托管,地址是 https://findjob-1gcto7ln453287ca-1257277642.tcloudbaseapp.com/ 体验小程序小程序的体验二维码: [图片] 运营网站:https://findjob-1gcto7ln453287ca-1257277642.tcloudbaseapp.com/ 团队介绍我们的团队名mrhuostudio,我们团队是两名.NET程序员组成,我们的目标是在微信小程序、云函数开发中提供一套通用的C#解决方案。 张善友,友浩达科技有限公司 CTO,.NET 技术专家,现任 微软 MVP,腾讯云TVP,华为云MVP,有20年编程经验的程序员。 霍小平,以前一直在不入流的网络公司里做外包服务,现在是全职奶爸,但正是因为做外包服务,对各种技术名词都略有涉及,比如:.net, android, java, php, nodejs, python, openresty + lua, redis, android, elasticsearch, mysql, sqlserver, vue, 各种小程序等等,当然主要以 .net 为主,一个8年的老 .neter。
2020-09-14 - 专治按钮效果不明显(扩散动画效果)
效果 [图片] 需求 背景 由于最近自家小程序用户活跃用户下滑,老板看看自家小程序,发现分享按钮不够明显,于是乎有了下面这段对话。 老板:小明,你过来下,看看这个分享按钮不明显 小明:好的,给它点颜色瞧瞧 小明给按钮来了个红色,发给了BOSS BOSS:还是不明显 小明:好的,给它点放大瞧瞧 小明把按钮从原来的60rpx放大到了230rpx,发给了BOSS BOSS:还是不明显 小明:好的,让它动起来! 需求:提高分享率,做个扩散动画效果让这个按钮成为整个页面最靓的仔。 思路分析: 从小到大的变化 从颜色从深到浅 反复进行该动作 动画代码 实用 CSS3 的 animation 属性 代码 [代码].share-btn { width: 200rpx; height: 200rpx; } .share-btn::before { // 省略无关代码 animation: wave 1.5s ease-out infinite; } @keyframes wave { 50%, 75% { width: 230rpx; height: 230rpx; } 80%, 100% { opacity: 0; } } [代码] 分析 我们先来看看 animation 参数: animation: wave 1.5s ease-out infinite; animation: 关键帧名称 动画时长 动画形式 播放次数; ease-out:动画以低速结束 infinite:无限次播放 从参数可以得出来使用了wave这个关键帧参数,1.5完成一次扩散的动画,从快到满的速度,无限重复这个动画。 然后我们再来看看 keyframes 参数: 百分比:动画持续时间的百分比。 属性:CSS样式属性 从参数可以得出来 时间 50%->75% 的时候就会改变大小从200rpx-230rpx。 时间 80%->100% 的时候会改变透明度从0-1。 第一步:原始大小(高度:200,宽度:200,透明度:1) [图片] 第二步:改变大小(高度:230,宽度:230,透明度:1) [图片] 第三步:改变透明度(高度:230,宽度:230,透明度:0) [图片] 第四步:回到第一步 总结 做动画先分析步骤,然后设置 animation 参数。如果你觉得CSS比较麻烦的话,还可以使用小程序提供 Animation 对象实现。 css3 的 animation 的所有参数不局限以上这些,了解更多点击传送门 Animation 对象,了解更多点击传送门
2020-08-17 - 生成海报很复杂?有它轻松搞定!
前言 生成海报这个需求非常常见,一般用于分享朋友圈,自定义分享到群,开发者为了生成写了很多绘制代码。绘制代码看上去都差不多,有不得不写,很难受。 直到我遇见了它! Painter:小程序生成图片库,轻松通过 json 方式绘制一张可以发到朋友圈的图片 看效果 [代码]{ background: '#eee', width: '654rpx', height: '400rpx', borderRadius: '20rpx', views: [ { type: 'image', url: 'https://qhyxpicoss.kujiale.com/r/2017/12/04/L3D123I45VHNYULVSAEYCV3P3X6888_3200x2400.jpg@!70q', css: { top: '48rpx', right: '48rpx', width: '192rpx', height: '192rpx', }, } ... ], } [代码] 绘制结果: [图片] 你没看错,就是如此简单,再也不用写绘制代码了! 功能与优势 功能全,支持文本、图片、矩形、qrcode 类型的 view 绘制 布局全,支持多种布局方式,如 align(对齐方式)、rotate(旋转) 支持圆角,其中图片,矩形,和整个画布支持 borderRadius 来设置圆角 杠杠的性能优化,我们对网络素材图片加载实现了一套 LRU 存储机制,不用重复下载素材图片。 杠杠的容错,因为某些特殊情况会导致 Canvas 绘图不完整。我们对此加入了对结果图片进行检测机制,如果绘图出错会进行重绘。 实现原理 [图片] 开发者可以根据需求构建生成图片的 Palette(调色板),然后在程序运行过程中把调色板传入给 Painter(画家)。Painter 会调用 Pen(画笔),根据 Palette 内容绘制出对应的图片后返回。 看demo [图片] 从demo我们可以看出支持的样式和组件非常丰富,不仅如此还可以进行拖拽位置、对大家进行调整。 [图片] 具体详细使用方法以及json规范可以点击下方地址项目主页查看。 上地址 地址:https://github.com/Kujiale-Mobile/Painter
2020-08-21 - 利用coolui scroll组件和百度地图api实现下拉刷新展示天气效果
前言 想给自己写的下拉刷新增加一些效果,突发奇想的想增加一个天气预报的效果,然后就去寻找天气接口。查到接口以后我发现我天真了- -,那个天气真是多到难以想象啊。。什么晴转阴。。晴转多云转大雨。。。动画实在写不过来。但是为了 我的设想,还是写了一些主要的天气的动画(如:晴,多云,小雨,雪,雷阵雨),感兴趣的朋友可以继续添加。 组件 coolui scroll https://developers.weixin.qq.com/community/develop/article/doc/000a00925744f06af6ca00a7651c13 百度地图天气api http://lbs.baidu.com/index.php?title=wxjsapi/guide/getweather 演示Demo https://developers.weixin.qq.com/s/KmYdwMmX7hjV 效果图 晴 [图片] 多云 [图片] 雨 [图片] 雪 [图片] 雷阵雨 [图片]
2020-08-24 - 小程序里一个实用的多功能相片选择器
功能特色 1.拍照拾取——拍照取景更快一步 2.微信聊天记录中选取——有了这个,不用先保存本地,再浏览上传了 3.相册中选择——取相片经典内味儿 界面演示 [图片] 布局代码 [代码]
/camera> 手机相册/view> 微信图片/view> /view> /view> [代码] 其中[代码][代码]唤起系统拍照界面,在页面再在画一个[代码][代码]绑定一个takePhoto事件,就可以回调取出刚刚所拍的相片。 JS代码 [代码] Page({ takePhoto() { const ctx = wx.createCameraContext() ctx.takePhoto({ quality: 'high', success: res => { const file = res.tempImagePath this.navi(file) } }) }, error(e) { console.log(e.detail) }, chooseMessageFile() { wx.chooseMessageFile({ count: 1, type: 'image', success: res => { // tempFilePath可以作为img标签的src属性显示图片 const tempFilePaths = res.tempFiles const file = tempFilePaths[0].path this.navi(file) } }) }, choose() { wx.chooseImage({ count: 1, sourceType: ['album'], success: res => { let file = res.tempFilePaths[0] this.setData({ file: file }) this.navi(file) } }) }, }) [代码] 除了刚刚提到的takePhoto方法,另外2个方法chooseMessageFile对应的是从好友聊天记录里的图片,而choose方法,就是从传统的相册中取出相片 wxss样式表 [代码] page { background: #111; height: 100%; } .item { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 10px; } .photo { width: 60px; height: 60px; } .footer view { color: white; } .body { display: flex; flex-direction: column; justify-content: space-between; height: 100%; } .body camera { flex: 1; width: 100%; } [代码] body这个类样式就是flex布局,目的是配合[代码]camera[代码]的[代码]flex: 1[代码]使用得[代码]camera[代码]标签能够自适应高度到最大高度,而item这个类样式则是让底部操作区上的相册选择、拍照、微信选择三个按钮均匀横排。 在线体验 [图片] 关注我 [图片] 2020-08-25 - 开源一个天气小程序:轻松天气
前言 ⛅轻松天气 这次开源的小项目是一个微信小程序: 轻松天气 [图片] 这是我 2017 年还在学校时自己做的一个小程序,也是我做的第一个微信小程序。 虽然这个小玩具挣不了钱,也没什么特别之处,但是对我来说意义重大,想了许久还是决定开源,可以给有需要的人作个参考。 这个小程序的基本特点: 简洁风的界面 城市天气查询 今日天气信息 未来 6 天天气 今日出行建议 分享当前天气 📷截图 目前这个小程序也还在运营(其实就是偶尔更新下)。 在微信搜一搜里搜索“轻松天气”,出现的第一个小程序就是了: [图片] 界面长这样: [图片] 正文 🔗项目地址 Gitee:https://gitee.com/ifaswind/miniprogram-easyweather Github:https://github.com/ifaswind/miniprogram-easyweather ⭐点个 Star 好嘛~ 🔧开发工具 我当前编辑这个项目用是: 微信开发者工具 Stable 1.03.2006090 但是理论上无论啥版本都可以正常打开运行。 📃使用说明 💳AppID 首次导入项目需要设置 AppID,没有的话可以直接使用测试号~ [图片] 🌞天气数据 本小程序里使用的天气数据来源于第三方提供的 API。 使用易源数据 我用的是阿里云云市场里面的易源数据-全国天气预报查询(这个 API 是有免费试用套餐的): [图片] 传送门:https://market.aliyun.com/products/57096001/cmapi010812.html 购买成功后在管理控制台页面获取到你的 AppCode: [图片] 然后将你的 AppCode 填到项目目录下 [ config/config.js ] 里的 [代码]request.header[代码] 中: [图片] 另外还需要注意将 API 的域名添加到项目配置里的 request 合法域名 中,否则没有办法请求数据: [图片] 或者可以勾选 [ 本地设置 ] 下的 不校验合法域名… 选项来进行本地测试: [图片] 其他数据来源 如果想要使用其他的 API 来获取天气数据也是可以的,只不过需要自行修改代码中的数据结构,问题不大~ 传送门 微信推文版本 个人博客:菜鸟小栈 开源主页:陈皮皮 Eazax-CCC 游戏开发脚手架 Eazax-CCC 示例在线预览 微信小程序:轻松天气 更多分享 《为什么选择使用 TypeScript ?》 《高斯模糊 Shader》 《一文看懂 YAML》 《Cocos Creator 性能优化:DrawCall》 《互联网运营术语扫盲》 《在 Cocos Creator 里画个炫酷的雷达图》 公众号 😺菜鸟小栈 💻我是陈皮皮,这是我的个人公众号,专注但不仅限于游戏开发、前端和后端技术记录与分享。 💖每一篇原创都非常用心,你的关注就是我原创的动力! Input and output. [图片]
2020-08-26 - 【小程序开发】推荐 5 款高仿知名应用的开源项目!
## 仿知乎日报地址:https://github.com/RebeccaHanjw/weapp-wechat-zhihu ## 仿v2ex地址:https://github.com/jectychen/wechat-v2ex ## 仿微信地址:https://github.com/liujians/WeApp ## 仿微信商城地址:https://github.com/bayetech/wechat_mall_applet ## 仿ONE地址:https://github.com/ahonn/weapp-one
2020-08-23 - #小程序云开发挑战赛#-趣婚礼-趣婚礼
<h3 align=“center”> <a href=“https://github.com/wforguo/wedding-app” title=“趣婚礼”>趣婚礼</a> </h3> <p align=“center”> <img alt=“趣婚礼 Logo” src=“https://forguo-1302175274.cos.ap-shanghai.myqcloud.com/wedding/assets/img/qrcode.jpeg” width=“180”> </p> <p align=“center”> <a href=“https://github.com/wforguo/wedding-app” title=“趣婚礼”>基于Taro2 + 云开发 打造婚礼邀请函</a> </p> 项目名称 趣婚礼 基于[代码]Taro2[代码] + 云开发 打造婚礼邀请函 Taro2 云开发 项目介绍 结婚的时候婚礼邀请函是一道必不可少的程序,但是没法去很好的留存我们的数据和回忆(除非有后端支持)。 最近刚好在学习[代码]Taro[代码],所有就尝试基于[代码]Taro[代码] + 云开发快速的搭建一个婚礼邀请函小程序。 也有人会问,使用了云开发,怎么去管理数据呢,不用担心,云开发[代码]CMS[代码]帮你搞定,支持文本、富文本、图片、文件、关联类型等多种类型的可视化编辑。 CMS 概览 项目效果截图 模块划分 邀请 =》邀请函信息 相册 =》相册展示 导航 =》婚礼举办地 祝福 =》婚礼视频及弹幕留言,留言保存至留言列表 留言 =》好友留言 目录结构 [代码]├── config # 配置文件 ├── cloud # 云函数存放 ├── dist # 打包文件 ├── node_modules # 依赖的模块包 ├── package.json # 项目基本信息 ├── src # 项目的核心组件 │ ├── service # 资源文件(css、image、config) │ ├── common # 资源文件(css、image、config) │ ├── components # 公共组件 │ ├── store # 状态管理(redux) | ├── pages # 页面文件目录 | | ├── Index # index页面目录 | | | ├── index.jsx # index页面逻辑 | | | └── index.scss # index页面样式 | | | └── index.config.js # index页面配置(小程序page.json内容) │ ├── util # 公共方法(util.js、globalData.js) │ ├── app.jsx # 入口文件 │ ├── app.scss # 公共样式 │ ├── index.html # 主页模板 ├── static # 静态资源(CDN) ├── README.md # 项目描述信息 [代码] 效果预览 [图片] 视频演示 视频演示 部署 clone [代码]clone[代码]该项目,并在[代码]project.config.json[代码]下修改自己的小程序[代码]appid[代码] 开通云开发 [图片] 首先需要你在小程序的控制台去开通云开发,并拿到环境名称 在[代码]src/service/config[代码]下修改[代码]DBID[代码]为你自己申请的环境ID`; 新建数据库并导入 表的设计 wedding_invite:婚礼信息 wedding_msgs:留言祝福 wedding_photos:相册 wedding_video:视频 数据库文件存放在[代码]static/db[代码]下,按照文件名新建数据库集合,并导入数据文件即可完成数据库创建。 项目启动 使用[代码]yarn[代码] 安装依赖 [代码]yarn [代码] 编译和打包 [代码] yarn dev:weapp yarn build:weapp [代码] 使用[代码]npm[代码] 安装依赖 [代码] npm install [代码] 编译和打包 [代码] npm run dev:weapp npm run build:weapp [代码] 具体可查看[代码]Taro[代码]教程 到此你就可以看到效果了… 云开发CMS的使用 用于管理云开发数据的CMS CMS文档 CMS截图 CMS Web端 CMS端需要建立与数据库对应的内容模型,才能在列表正常展示对应数据库数据。 可直接导入内容模型,位于 static/schema 模型管理 [图片] 内容管理 [图片] 开发者工具端 [图片] 圈出来的部分是和小程序云开发控制台数据库所对应的. 技术及框架 1.小程序 具体入门和使用,请访问官方文档。 小程序文档 小程序云开发 开发者可以使用云开发开发微信小程序、小游戏,无需搭建服务器,即可使用云端能力。包含云函数 、数据库、存储和云调用。 2.Taro2 + Redux Taro2 Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/ QQ 小程序/H5 等应用。 项目中使用了最新版本[代码]Taro2[代码],由于[代码]Taro3[代码]使用期间不是很丝滑,所以选择了[代码]Taro2[代码] 所以本项目可以作为Taro的学习入门,也可以作为小程序云开发的一个入门Demo 3.TaroUI 基于Taro2的UI框架 TaroUI 云开发的使用 官方文档 需要在控制台去开启云开发,并获取DBID(数据库初始化用到) 云开发入口 [图片] 数据库配置 [图片] 数据库 可以在项目中的service中去查看数据库的CURD代码。 有数据库基础的很容易就上手了,小程序的数据库其实就是一个[代码]JSON[代码],类似于[代码]MongoDb[代码]。 顾名思义,数据库中的每条记录都是一个 JSON 格式的对象。一个数据库可以有多个集合(相当于关系型数据中的表),集合可看做一个 JSON 数组,数组中的每个对象就是一条记录,记录的格式是 JSON 对象。 操作 初始化 在开始使用数据库 API 进行增删改查操作之前,需要先获取数据库的引用。 [代码]const db = Taro.cloud.database({ env: 'DBID' }); [代码] 数据库操作 要操作一个集合,需先获取它的引用。在获取了数据库的引用后,就可以通过数据库引用上的 collection 方法获取一个集合的引用了 [代码]db.collection('wedding_msgs') .orderBy('createTime', 'desc') // 时间升序 .skip(current * 10) .limit(pageSize) .get() [代码] 这样每次都比较麻烦,所以我做了统一的处理,都写在[代码]service/cloud/index.js[代码]当中,并export,只需要按需引入,并传入要操作的数据库名称即可。 [图片] Tips:如果发现数据库有数据,但是拿不到所有数据,那应该就是数据库的权限问题了,改成仅创建者可写,所有人可读就可以了 云函数 官方文档 使用云函数去获取用户信息就变得简单多了,具体可翻阅文档! 项目中云函数所在目录,[代码]cloud/functions.js[代码],项目中使用到了一个云函数,留言的内容过滤功能[代码]msgSecCheck[代码], 具体使用方法:security.msgSecCheck 声明 [代码]/** * @Description: 文本内容过滤; */ // 云函数入口文件 const cloud = require('wx-server-sdk'); cloud.init(); // 云函数入口函数 exports.main = async function (event, context) { console.log(event); let opts = { content: event.content || '' }; let fun = cloud.openapi.security.msgSecCheck(opts); return fun.then(res => { return res; }).catch(err => { return err; }); }; [代码] 调用 [代码] // 云调用内容安全过滤 Taro.cloud.callFunction({ name: 'msgCheck', data: { content: data.userMsg }, }).then(res => { if (res && res.result && res.result.errCode === 0) { Taro.showLoading({ title: '发送中...', mask: true }); // 数据库插入留言数据 cloud.add('wedding_msgs', data).then(msgRes => { resolve(msgRes); }, (err) => { console.log(err); reject(err); }); } else { reject(res.result); } }).catch(err => { console.log(err); reject(err); }); [代码] ToDos 朋友圈海报生成… … 这次的版本使用了Taro + 云开发,后面打算出一版[代码]Taro + Antd + koa2 + MongoDb[代码]的版本,初步内容已经差不错了,详见下面项目地址 Taro + Antd + koa2 + MongoDb Taro3的坑 redux使得下拉刷新和上拉加载冲突 无法阻止事件冒泡 无法使用 小程序的[代码]selectComponent[代码],获取组件实例 [代码]const barrageComp = this.selectComponent('.barrage')[代码] 使用 [代码]Taro.createRef();[代码] 代替 关于 <h3 align=“center”> <a href=“https://forguo-1302175274.cos.ap-shanghai.myqcloud.com/wedding/assets/img/wechart.jpg” title=“微信:iforguo”>微信:iforguo</a> </h3> <p align=“center”> <img alt=“微信:iforguo” src=“https://forguo-1302175274.cos.ap-shanghai.myqcloud.com/wedding/assets/img/wechart.jpg” width=“480”> </p> 个人主页 CSDN 掘金 本项目仅供学习和交流,部分素材来源于网络,如有侵权联系删除。 项目有需改进也请留言和Issues,如有合作请在博客留言或wx:([代码]iforguo[代码])。 编码不易,感谢各位大佬的吐槽和GitHub的Star
2021-12-10 - #小程序云开发挑战赛#-莉龙美颜工具-莉龙
应用场景: 一款用于制作各种大头贴头像的图片处理工具 目标用户: 所有用户 效果截图: 请参考作品介绍视频链接 功能代码展示: 请参考代码仓库 作品体验二维码: [图片] 团队简介: 周李龙:全栈开发人员 羊莉:前端开发人员
2020-08-27 - 【建议收藏】推荐 10 款使用云开发的开源项目
1. 美食地图 地址:https://github.com/cloudkits/miniprogram-foodmap 2. 猫叫助手 地址:https://github.com/Rychou/mpvue-cloud 3. 图书管理系统 地址:https://github.com/AmosXu/library-mini-program 4. 网易云课堂 地址:https://github.com/MarchYuanx/study163 5. 五指棋 地址:https://github.com/Rateler-Inc/five-chesses-min-cloud 6. 古诗词大全 地址:https://github.com/caochangkui/miniprogram-project 7. 吸猫小程序 地址:https://github.com/godbasin/kitty-wxapp 8. 朋友圈 地址:https://github.com/xiaozhaoqi/moments 9. 一起算账 地址:https://github.com/GzhiYi/accounting-together 10. 云开发优秀实践案例集合 地址:https://github.com/TencentCloudBase/Good-practice-tutorial-recommended
2020-08-30 - WXS仿拼多多横向滚动条ScrollView组件
Demo: 最终实现效果如下 [图片] 接下来要实现的就是下面的,滚动条,显示横向拖动的进度。 Component代码: index.js Component({ properties: { customStyle: { type:String, }, // 根据Slot中的元素计算出的ScrollView总宽度 rpx scrollViewWidth: { type: Number, value: 0 }, // ScrollView的总高度,调整高度来控制显示行数 rpx scrollViewHeight: { type: Number, value: 0 }, // 滚动条的宽度rpx scrollBarWidth: { type: Number, value: 160 }, // 滚动条的高度 rpx scrollBarHeight: { type: Number, value: 10 }, // 滚动条滑块的宽度 rpx scrollBarBlockWidth: { type: Number, value: 100 }, // 滚动条样式 scrollBarStyle: { type: String, }, // 滚动滑块样式 scrollBarBlockStyle: { type: String, } }, data: { windowWidth: 375, // px px2rpxRatio: 0, windowWidth2ScrollViewWidthRatio: 0, scrollBarBlockLeft: 0, }, lifetimes: { attached: function() { this.data.windowWidth = wx.getSystemInfoSync().windowWidth this.setData({ px2rpxRatio: Number(750 / this.data.windowWidth).toFixed(3) }) console.log('px2rpxRatio', this.data.px2rpxRatio) this.setData({ windowWidth2ScrollViewWidthRatio: Number(this.data.windowWidth * this.data.px2rpxRatio / this.data.scrollViewWidth).toFixed(3) }) let scrollBarBlockWidth = Number(this.data.scrollBarWidth * this.data.windowWidth2ScrollViewWidthRatio).toFixed() if(scrollBarBlockWidth >= this.data.scrollBarWidth) { scrollBarBlockWidth = this.data.scrollBarWidth } this.setData({ scrollBarBlockWidth: scrollBarBlockWidth }) }, }, }) index.wxml <wxs module="wxs" src="./index.wxs"></wxs> <view style="{{ customStyle }}"> <scroll-view scroll-x="{{ true }}" bind:scroll="{{ wxs.onScroll }}" data-px2rpxRatio="{{ px2rpxRatio }}" data-scrollViewWidth="{{ scrollViewWidth }}" data-scrollBarWidth="{{ scrollBarWidth }}"> <view class="scroll-view" style="height: {{ scrollViewHeight }}rpx"> <slot/> </view> </scroll-view> <view class="scrollbar" style="width: {{ scrollBarWidth }}rpx; height: {{ scrollBarHeight}}rpx; margin: 0 auto; margin-top: 10rpx; {{ scrollBarStyle }}"> <view class="scrollbar-block" id="scrollbar" style="width: {{ scrollBarBlockWidth }}rpx; left: {{scrollBarBlockLeft}}rpx; {{ scrollBarBlockStyle }}"></view> </view> </view> index.wxs var onScroll = function(e, ownerInstance) { var scrollBarBlockLeft = (e.detail.scrollLeft * e.currentTarget.dataset.px2rpxratio / (e.currentTarget.dataset.scrollviewwidth)) * e.currentTarget.dataset.scrollbarwidth ownerInstance.selectComponent('#scrollbar').setStyle({left: scrollBarBlockLeft + 'rpx'}) } module.exports = { onScroll: onScroll }; index.wxss .scroll-view{ width: 100%; box-sizing: border-box; display: flex; flex-direction: column; flex-wrap: wrap; justify-content: flex-start; align-items: flex-start; align-content: flex-start; } .scrollbar { margin: 0 auto; background: black; position: relative; border-radius: 4rpx; overflow: hidden; } .scrollbar-block { height: 100%; position: absolute; top: 0; background: darkred; border-radius: 4rpx; } 以上就是组件的代码部分,下面来看一看在Page页中如何使用,代码也很简单: index.js //index.js Page({ data: { data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] } }) index.wxml <x-scroll-bar scroll-view-width="1200" scroll-view-height="400"> <view wx:for="{{ data }}" class="block" > {{ item }} </view> </x-scroll-bar> index.wxss .block{ flex-shrink: 0; flex-grow: 0; background: black; width: 200rpx; height: 200rpx; line-height: 200rpx; text-align: center; color: white; } .block:nth-child(even) { background: red; } 以上全部内容,组件代码已经在我的Github,可直接访问下载: https://github.com/foolstack-omg/fool-weapp
2020-04-08 - 基于scroll-view横向滚动的类目表实战案例。
基于scroll-view横向滚动制作的类目表组件 在电商项目中经常用到的就是一些类别滚动条,例如在某多以及某宝中在首页非常常见。 [图片] 上面顶部下面就采用了这种滚动条的效果。 原理: 首先获取scroll-view中内容的实际宽度,然后计算出scrollLeft的最大值,然后根据用户点击的位置,判断当前点击是在左边还是右边(我们以屏幕的中点为分割线,中点左侧为左边,中点右侧为右边),同时根据点击方向判断移动方向(点击左侧向右移动,点击右侧像左移动),然后我们获取被点后当前的scrollLeft值,然后获取被点击元素的宽度以及被点击元素的left值,计算出需要移动的距离,改变现有的scollLeft值。如果向右移动后的scollLeft值超过最大的scollLeft值,我们就取scrollLeft的最大值。如果向左移动scollLeft值小于0,我们就取零。 下面是组件的制作过程: 首先创建category-x的组件目录,然后生成组件文件。相关代码WXML代码: <view class="catbar" style="height:{{height}}rpx"> <scroll-view class="categories" scroll-x="{{true}}" scroll-left="{{scrollLeft}}" scroll-with-animation="{{true}}" scroll-anchoring="{{true}}"> <block wx:for="{{categoriesData}}" wx:for-item="item" wx:for-index="index" wx:key="*this"> <view class="category" style="padding-right:{{paddingRight}}rpx;padding-left:{{index==0?firstPaddingLeft:0}}" bind:tap="onChanging" data-index="{{index}}"> <text class="category-name {{nowIndex==index?'current-category':''}}">{{item}}</text> </view> </block> </scroll-view> </view> wxss代码: /* components/category-x/category-x.wxss */ .catbar{ width:100% } .categories{ height:100%; white-space: nowrap; box-sizing: border-box; border-bottom: 2rpx solid #f6f6f6; -webkit-overflow-scrolling: touch; } .category { display: inline-block; box-sizing: border-box; height: 100%; } .category:first-child { padding-left: 20rpx; } .category-name{ display: inline-block; box-sizing: border-box; width: 100%; height: 100%; font-size: 32rpx; line-height: 80rpx; color: #353535; } .current-category { color: #e64340; border-bottom: 6rpx solid #e64340; } JS代码: // components/category-x/category-x.js Component({ /** * 组件的属性列表 */ properties: { //category bar的高度,单位为rpx height: { type: Number, value: 0 }, //设置横向滚动条位置 scrollLeft: { type: Number, value: 0 }, //数据 categoriesData: { type: Array, value: [] }, //当前索引 nowIndex: { type: Number, value: 0 }, //元素间的间距 paddingRight:{ type:Number, value:32 }, //第一个元素的左间距 firstPaddingLeft:{ type:Number, value:20 } }, /** * 组件的初始数据 */ data: { }, lifetimes: { attached: function () { this._getSystemInfo(); this._getContentWidth(); } }, /** * 组件的方法列表 */ methods: { //获取设备参数 _getSystemInfo: function () { var _this = this; wx.getSystemInfo({ success: (res) => { var pxTorpxRatio = 750 / res.screenWidth; _this.setData({ pxTorpxRatio: pxTorpxRatio, windowWidth: res.screenWidth }); } }); }, //获取内部总宽度,单位是px _getContentWidth: function () { var _this = this; _this._getCategoryInfo().then((res) => { var dataLength = _this.data.categoriesData.length; _this.setData({ contentWidth: ((res.width * dataLength * _this.data.pxTorpxRatio) + dataLength * _this.data.paddingRight + _this.data.firstPaddingLeft) / _this.data.pxTorpxRatio }); }) }, //改变点击 onChanging: function (event) { var tappedIndex = event.currentTarget.dataset.index; this.setData({ nowIndex: tappedIndex }); //获取点击元素的宽度以及left值 var _this = this; _this._getCategoryInfo().then((res) => { _this.setData({ tappedWidth: res.width, tappedLeft: res.left }, () => { _this._getCategoriesScrollLeft().then((res) => { _this.setData({ scrollLeft: res.scrollLeft }, () => { _this._onMoving(); }); }); }); }); this.triggerEvent("change",{current:tappedIndex},{}) }, //移动 _onMoving: function () { var tappedLeft = this.data.tappedLeft; var tappedWidth = this.data.tappedWidth; var scrollLeft = this.data.scrollLeft; var windowWidth = this.data.windowWidth; var contentWidth = this.data.contentWidth; var maxScrollLeft = contentWidth - windowWidth; var tappedScrollLeft; if (tappedLeft > windowWidth / 2) { var moveDis = scrollLeft + (tappedLeft - ((windowWidth / 2) - tappedWidth)); tappedScrollLeft = (moveDis > maxScrollLeft) ? maxScrollLeft : moveDis; } else { var moveDis = scrollLeft - (((windowWidth / 2) - tappedWidth) - tappedLeft); tappedScrollLeft = (moveDis < 0) ? 0 : moveDis; } this.setData({ scrollLeft: tappedScrollLeft }); }, //获取当前元素的信息 _getCategoryInfo: function () { var _this = this; return new Promise((resolve, reject) => { _this.createSelectorQuery().select(".current-category").boundingClientRect((res) => { resolve(res); }).exec(); }); }, //获取scroll-view的scroll-left值 _getCategoriesScrollLeft: function () { var _this = this; return new Promise((resolve, reject) => { _this.createSelectorQuery().select(".categories").fields({ scrollOffset: true }, (res) => { resolve(res); }).exec(); }); } } }) Tip: 在page.json中引入组件后,height值为必填,categories-data为必填值。其余为不强制。默认值通过观察js中的properties可以看出。通过绑定change事件,通过event.detail.current可以获取到当前点击索引。根据索引可以进行其他的操作。本人的制作案例: JSON文件: "usingComponents": { "cat-bar":"../../components/category-x/category-x" }, wxml文件: [图片] 原创,如需转发,请先联系作者,微信A493116703
2021-05-29 - 如何实现一个带图标的actionSheet效果
刚才有童鞋询问如何给actionSheet前加图标,然鹅官方没提供该效果,那么我们自己做一个定制化的actionSheet吧。 还是那句话,有需求,咱们有空就去实现下,也算是能锻炼自己的动手能力,提升点知识面。。 先看看实现后的效果图: [图片] 首先自定义actionSheet组件。 wxml 简单的页面布局。 [代码]<view class="qts-as-mask qts-class-mask {{ visible ? 'qts-as-mask-show' : '' }}" bindtap="handleClickMask" catchtouchmove="preventTouchmove" ></view> <view class="qts-class qts-as {{ visible ? 'qts-as-show' : '' }}" catchtouchmove="preventTouchmove"> <view class="qts-as-header qts-class-header"> <slot name="header"></slot> </view> <view class="qts-as-actions"> <view class="qts-as-action-item" wx:for="{{actions}}" wx:key="name"> <button hover-class="none" bindtap="handleClickItem" data-index="{{index}}" open-type="{{item.openType}}" class="qts-as-btn-qts ptp_exposure" > <image wx:if="{{item.icon}}" src="{{item.icon}}" class="qts-btn-image" /> <view class="qts-as-btn-text">{{item.name}}</view> </button> </view> </view> <view class="qts-as-cancel" wx:if="{{showCancel}}"> <button hover-class="none" class="qts-as-cancel-btn" bindtap="handleClickCancel">取消</button> </view> </view> [代码] js处理: 定制了三种功能: 是否需要隐藏取消按钮 是否需要点击蒙层关闭actionSheet 内容外部组件可配置 [代码]Component({ externalClasses: ['qts-class', 'qts-class-mask', 'qts-class-header'], options: { multipleSlots: true }, properties: { showCancel: { type: Boolean, value: true }, visible: { type: Boolean, value: false }, actions: { type: Array, value: [] }, maskClosable: { type: Boolean, value: true } }, methods: { handleClickMask() { if (!this.data.maskClosable) return; this.handleClickCancel(); }, preventTouchmove() {}, handleClickItem(e) { const dataset = e.currentTarget.dataset || '' this.triggerEvent('click', dataset) this.triggerEvent('cancel'); }, handleClickCancel() { this.triggerEvent('cancel'); } } }) [代码] 应用的页面引用该组件: [代码]<view class="container" bindtap="handleClick">点我弹出actionSheet</view> <!-- visible 弹窗显隐藏 show-cancel 是否显示取消按钮 bind:cancel 取消按钮点击回调 bind:click 单个item点击回调 mask-closable 点击蒙层是否关闭弹窗 --> <action-sheet visible="{{visible}}" actions="{{actions}}" show-cancel bind:cancel="handleClick" bind:click="handleClickItem" mask-closable="{{true}}" /> [代码] [代码]const app = getApp() Page({ data: { //可配置items选项,支持传入button的openType属性 actions: [{ name: '生成图片', icon: 'https://qiniu-image.qtshe.com/201809104.png' }, { name: '转发给好友或群聊天', icon: 'https://qiniu-image.qtshe.com/201809105.png', openType: 'share' } ], visible: false }, //弹窗显隐藏 handleClick() { this.setData({ visible: !this.data.visible }) }, //单个item点击 //actionsheet内的点击方法 handleClickItem({detail}) { if (detail.index === 0) { console.log(111) //第一个选项被点击 } }, }) [代码] 以上就是一个自定义actionSheet的实现方式以及引用方式。。 老规矩,附上代码片段。 https://developers.weixin.qq.com/s/Bhc0Sjmw7AgW
2020-05-26 - 小程序实现img的map area功能
问题:《小程序如何实现图片热区的功能,类似html map area的功能,求指教或者思路?》 貌似小程序现在还没有图片热区的功能,所以自己动手写了一个 图片热区组件 maparea,有需要的可以拿去改改 思路:点击坐标是否在某个区域内 点击坐标在href区域内,视为点击了热区 点击坐标在href区域内,同时也在nohref区域内,视为点击了非热区 难点: 1、点击坐标在多边形内的判断 2、图片缩放带来的坐标变化,导致热区坐标变化问题 3、目前效果最好的图片mode为widthFix,其他多少会有问题 代码片段:https://developers.weixin.qq.com/s/wLy4DvmI7ahA PS: 大佬们,有好的思路或者有更好的效果的,欢迎交流交流 [图片]
2020-07-17 - #小程序云开发挑战赛#-大学生记账本-阳光队
系统提供支出、收入、转账、余额、借贷五大记账模块,内含多种情景账本,涉及食品、交通、购物、宿舍、娱乐、学习等各种针对学生的场景,以满足不同时期的记账需要。用户可以实时查看自己的账户余额和所有账单记录。本小程序已经上线,欢迎扫码体验。 [图片] 开源代码链接 GitHub:https://github.com/ChangYanwei/accountBook Gitee:https://gitee.com/changyanwei/accountBook 以下是详细介绍。 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片][图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片]
2022-04-10 - 一眼告诉你什么是订阅消息了,看完就懂订阅消息。
消息通知有两种: 一、A的动作后,发消息给A自己,这种容易解决,不多说明; 二、A动作后,发消息给B(比如管理员、店家、楼主),如何保证B收到消息?这种是本方案要解决的问题。 一张图片一眼告诉你什么是订阅消息,产品经理的设计UI居然让人一眼就知道订阅消息是什么玩意。 [图片] 用户 B (管理员、商家、组长、楼主)在知道订阅数不足后,打开小程序来续订阅数,否则没法收到订阅消息。 [图片] 补充一: 关于勾选按钮,请注意话述是:“总是保持以上选择,不再询问”,而不是:“总是同意接收订阅消息”,不要幻想就成了永久性订阅消息; 相当于你打电话订外卖,对店家说“老样子”,店家只会马上送一次外卖,而不是会以后每天自动给你送外卖了。 勾选和不勾选的区别是什么呢? 区别仅仅是:不勾选时,必须点击订阅10次,弹窗10次;勾选后,仍然必须点击订阅10次,但是不弹窗。无论如何“订阅”这个点击n次的动作少不了。 补充二: 一旦勾选后,就不可逆了,没有任何办法恢复或取消勾选了,除非你小程序MP后台换一次消息模板号(删除模板,重新添加一次)。 补充三: 关于如何保存订阅数。 保存在数据库中,笔者用的是云开发,数据库表user结构如下: { _id:'openid1', nickName:'老张', msg:{ "tempId1":5, "tempId2":7, } } 补充四: 关于如何获取订阅数。两种方式: 一、wx.requestSubscribeMessage的回调success里获取; 二、消息推送机制获取;https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html
2022-09-21 - 订阅消息一直提示47001的错误,如何解决?
在postman的调用是正常的,完全可以接收到订阅消息,但是在小程序上,前端传值后HTTS调用就一直提示47001的错误了 [图片] (上图一为postman的调用) [图片] (上图二是前端HTTPS调用后传递的参数) [图片]
2020-07-10 - 抽奖类小程序,具体抽奖逻辑如何实现
本文背景本人运营一个抽奖类小程序已步入正轨,期间虽然也出过大大的问题,好在吃一堑长一智,现在一切都比较稳定,特别是在抽奖环节。 本文内容本文依托我运营的小程序,来分享下在具体抽奖环节的逻辑是如何实现的 首先要说下目前小程序的实现机制,目前抽奖小程序主要有三步 (1)开~奖、所谓开奖就是将当前奖项根据时间,从未开奖,标记为可开奖状态 (2)抽~奖、所谓抽奖就是,在可开奖的奖项里面,根据当前奖项参与的用户,以及奖品设置,把具体的奖项给对应的某个参与用户 (3)推~送、所谓推送就是在开奖完成后,推送订阅消息给所有参与抽奖的用户 对应这三步,该小程序有三个核心的云函数 (1)run,触发器,每个整点的1分开始执行,具体逻辑是根据当前时间和开奖时间进行比较,如果当前时间大于开奖时间,那么标记状态位为可开奖 (2)draw,触发器,每个整点的5分开始执行,具体抽奖的逻辑,也是本文具体分享的环节 (3)sendmore,触发器,每个整点的10分开始执行,进行推送订阅消息 f 本文的重点是在上面的第二步 在具体实现抽奖的逻辑,本文分享两个,所谓抽奖无非就是根据奖项设置的奖品个数随机从参与用户那里选取两个用户,这里注意一个关键词,是随机 随机就代表公平,这是该小程序的核心 方法1、云函数的sample 这个是云开发里面提供的随机检索的函数,小程序云开发支持 方法2、第三方库的suffle http://underscorejs.org/ [图片] [图片] 本文总结本文通过分享抽奖类小程序核心逻辑场景,然后给出具体抽奖环节的解决方案以及具体代码
2021-01-10 - 教大家用20行js代码,开发好小程序订阅消息
微信小程序官方决定在2020-1-10全面线下小程序模板消息,要去替换为订阅消息。那对开发者而言,又要一个一个地方去修改代码兼容.... 所以我替大家写了段代码,来快速解决问题。复制下面这段代码到app.js文件最上面即可解决问题。代码的主要功能是在每一个tap类型的点击事件中触发订阅弹窗,这样用户点几次界面,你就可以发几次消息。这也是让发送次数最大化,不可能比这个次数还多了。 预期结果是:用户点几次弹窗,就会注意到有一个不再提醒按钮,一旦选了它,那你就可以随便发订阅消息了! // 记录原Page方法 const originPage = Page; // 重写Page方法 Page = (page) => { Object.keys(page).forEach(function(key){ if(key !== 'data'){ let originMethod = page[key]; page[key] = function () { let e = arguments[0]; //给所有的点击事件增加订阅消息弹窗 if(!!e && !!e.type && e.type === 'tap'){ wx.requestSubscribeMessage({ tmplIds: ['3E66jPXafsnikZoQR5uk0OUzIUVASZE5scyAu5YCHPI'], ////////这里替换为自己的模板ID///// success (res) { // console.log(res) }, fail (res) { // console.log('订阅消息失败',res) } }) } return originMethod.call(this,...arguments) } } }); return originPage(page); };
2020-01-09 - 答题小程序(云开发版)
php+mysql版本 ->智汇答题Plus 智汇答题小程序,适用于考核,评测等场景,分为四大功能:练习,答题,错题集,排名; 练习功能分为答题模式和背题模式,答题模式可以在答完题目显示正确答案和帮助提示,可以进行跳题作答,背题模式可以直接查看正确答案和帮助提示,实时查看答题卡,统计答题情况,记忆功能,能够继续上次答题,也可以清除答题记录,重新答题。 答题功能主要包括倒计时功能,随机取题功能,可单独对每个分类设置取题数量,答题时间,可以进行跳题作答,交卷评分,倒计时自动提示交卷;记忆功能,保存可下次继续做题。题目,帮助解释支持文字,图片。 错题集功能主要是针对练习和答题两大模块的作答错题进行分类收集,可像练习功能一样进行答题模式和背题模式的作答方式,可移除错题,具有针对性的答题。排名功能可以对每套试卷的作答人员进行分数等排名; 答题记录可以随时查看用户的测试记录;针对部门考核,可以开启信息审核功能,只有通过审核的人员才可进入答题。支持开启和禁用练习功能。 本程序还搭配管理后台,管理用户,系统设置,后台审核用户信息,发送模板消息通知用户,可以随时增加套题,增加题目,编辑题目,删除题目,以及使用模板批量的导入题目,查看答题记录,查看反馈意见等,非常方便实用! 小程序二维码: [图片] 小程序展示图: [图片] [图片] [图片] [图片] [图片] 后台展示图: [图片]
2020-10-16 - 24小时内开发一款优质的在线答题小程序
本文背景本文将复盘24小时内如何开发一款答题小程序,目前该代码托管在码云 [图片] 本文内容本文主要介绍答题小程序,该小程序从7月11号9点开始开发,在12号凌晨1点已完成,目前未发现已知BUG 该小程序主要功能有 (1)用户注册,主要用于收集用户的姓名、部门等信息,跟openid进行绑定; (2)用户登录,我觉得这个登录时多余的,但是没有办法,需求存在这个功能点 (3)考试模块,可重复考试,考试成绩后面覆盖前面; (4)练习模块,可展示当前题目的正确率,每天可以参加三次答题; (5)排行榜模块,包括考试的排行榜和练习的排行榜; (6)文章列表、文章详情,主要用于提供企业内部制度学习用; (7)考试规则介绍模块,主要用于公布考试的时间信息; (8)错题排行榜,主要按照题库中错题的顺序进行排序展示,方便大家复习用。 技术架构本文采用小程序云开发,未采用任何第三方框架, 知识点播开发过程中用到以下新知识点 (1)富文本展示 (2)左滑动系统公告栏 (3)关于考试开始、结束日期的判断逻辑有了新的办法,相较于我以往的判断方法 (4) 界面截图f [图片] f [图片] f [图片] f [图片] f [图片] f [图片] f [图片] f [图片] f 本文总结目前该小程序已经通过加急送审的模式,审核通过,并且发布上线,在后续,我会监控该小程序的运行情况,有更新也会第一时间同步到社区。 [图片] f
2020-07-12 - 干货--02 余小浪
哈喽 我又来了 这是我第二次分享文章了 希望能够帮助大家 也希望大家喜欢~ 第一个 image组件中的 mode=“aspectFill” 属性 这个属性是等比例缩放 如果你的图片是这个属性的需要注意注意注意 图片渲染完成后 再等比例缩放 及 先渲染 再等比缩放 例子: 当你要获取这个图片距离顶部的距离是 需要使用 wx.createSelectorQuery来来找到这个标签并获取到这个标签的参数 一般会写在 onReady() 生命周期钩子函数里 但是 问题就在这个时候出 现了 我获取的标签数据 不是 实际的数据 而是 图片没有缩放的数据 解决这个问题的时候 我使用了 setTimeout 函数 把时间设置为500 即 半秒后 再获取图片的标签的 参数 这时候 获取到的数据就是正确的数据了 暂时没有测试不写等待时间 有兴趣大家可以试一下 第二个 前端绘制海报性能优化 绘制海报我们用到了canvas 绘制海报的前 提是 绘制的素材要下载到本地 如果我们在绘制的时候下载素材 这个时 候 绘制的进度就会变慢 优化的思想如下 B页面是绘制海报的 A页面 点击某个按钮 进入到 B页面 那么我们就在 渲染A页面的时候 就下载素材呢 等到了B页面 素材都已经有了 直接使用,绘制效果会非常好 甚至是 秒绘制完成 在B页面onUnload函数内 清除下载文件的缓存 避免缓存太多 第三 字符串10 减去 数字0 最后 变成了 数字 10 let string = “10” string - 0 此时 string 就是 数字 10 类型是number // JS的隐式转换 很常用的一种改变数据类型的方式 0 的 布尔值 是 false 第四 防止数据抖动的方法 数据抖动 说白了 就是 一个按钮有一个事件 然后用户在很短的事件内重复点击 类似的有 购买物品 提交完成按钮 这些 解决方法 先声明一个变量 值为true 当做锁 当执行函数的时候 把这个锁变成 false 那么这个函数就被锁死了 只有这个函数完成所有操作的时候 再把锁变成true 此刻用户才可以再次真正的点击 代码如下: [图片] [图片] 今天的分享就到这里了 如果喜欢请大家动动小手指 点个赞吧 欢迎各位大佬亲临指导 如果有问题请及时指出 我会第一时间修改的 嘻嘻
2019-05-21 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - 开源的抽奖助手小程序
发现开源小程序之美三-抽奖助手小程序 发现开源小程序之美一,个人博客小程序 https://developers.weixin.qq.com/community/develop/article/doc/000a40e13ec550274e2a9addd56413发现开源小程序之美二,微慕WordPress小程序 https://developers.weixin.qq.com/community/develop/article/doc/000c44945dc728ab9c2aff2a55b013发现开源小程序之美三,抽奖助手小程序 https://developers.weixin.qq.com/community/develop/article/doc/0002846854056847b66a2d13451013发现开源小程序之美四,在线答题小程序 https://developers.weixin.qq.com/community/develop/article/doc/00040af07005609a223acee0151413发现开源小程序之美五,营销组件库 https://developers.weixin.qq.com/community/develop/article/doc/000c4235c98740a1dc2a1a6045b013发现开源小程序之美六,酱茄小程序 https://developers.weixin.qq.com/community/develop/article/doc/00040ede6d0388082a3aeb49b57813发现开源小程序之美七,二手书商场 https://developers.weixin.qq.com/community/develop/article/doc/0006ceb61a87182a4b3a1b32a5bc13发现开源小程序之美八,我要戴口罩https://developers.weixin.qq.com/community/develop/article/doc/0006a047b0cee0d5713ad731f5b813发现开源小程序之美九,失物招领小程序 https://developers.weixin.qq.com/community/develop/article/doc/000ca6a3b28ce8857b5a8bb3351c13发现开源小程序之美十,旅游攻略方面的微信小程序 https://developers.weixin.qq.com/community/develop/article/doc/000cc694e9c790ce755aee41556013 这个小程序是身边小伙伴开发的,基于云开发的一个抽奖助手小程序,我今天clone下代码,花了不到10分钟就运行成功了, 值得推荐给大家 先上截图大家参观下 [图片] 1 [图片] 2 [图片] 2 码云地址 https://gitee.com/xiaofeiyang3369/wechatlottery 我在调试过程中做了略微的改动 部署步骤建议按照下面三步走 第一步:创建集合,并将集合权限设置为:所有人可读,仅创建者可读写 第二步:将data里面的lottery.json文件导入到lottery集合 第三步:部署云函数 如没有意外就可以正常运行了,部署过程中遇到任何问题,请评论席留言。 更新记录 2020-07-20 重写了核心逻辑 ①开奖逻辑 ②抽奖逻辑 开奖逻辑目前是按照时间维度,到了时间不管人数有没有凑够都会进行开奖,开奖五分钟后,进行抽奖,确定中奖名额。 具体规则: (1)每个整点的1分去检测,根据当前时间检测是否有需要开奖的 (2)每个整点的5分去检测,是否有开奖未抽奖的,如果有,确定中奖名额 ~~
2021-01-25 - 小程序生成海报函数drawImage
本文通过前端压缩图片的场景来熟悉小程序api、drawImage的参数 该文提取自小程序海报生成模块 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.drawImage.html 有三个版本的写法: drawImage(imageResource, dx, dy)drawImage(imageResource, dx, dy, dWidth, dHeight)drawImage(imageResource, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) 从 1.9.0 起支持 该api传递的参数信息 要想使用该api实现图片的压缩效果,原理其实很简单,核心API就是使用[代码]canvas[代码]的[代码]drawImage()[代码]方法。 [代码]canvas[代码]的[代码]drawImage()[代码]方法API如下: ctx.drawImage(img, dx, dy); ctx.drawImage(img, dx, dy, dWidth, dHeight); ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); 后面最复杂的语法虽然看上去有9大参数,但不用慌,实际上可以看出就3个参数: img 就是图片对象,可以是页面上获取的DOM对象,也可以是虚拟DOM中的图片对象。 dx, dy, dWidth, dHeight 表示在[代码]canvas[代码]画布上规划处一片区域用来放置图片,[代码]dx, dy[代码]为canvas元素的左上角坐标,[代码]dWidth, dHeight[代码]指canvas元素上用在显示图片的区域大小。如果没有指定[代码]sx,sy,sWidth,sHeight[代码]这4个参数,则图片会被拉伸或缩放在这片区域内。 sx,sy,swidth,sheight 这4个坐标是针对图片元素的,表示图片在[代码]canvas[代码]画布上显示的大小和位置。[代码]sx,sy[代码]表示图片上[代码]sx,sy[代码]这个坐标作为左上角,然后往右下角的[代码]swidth,sheight[代码]尺寸范围图片作为最终在canvas上显示的图片内容。 [代码]drawImage()[代码]方法有一个非常怪异的地方,大家一定要注意,那就是5参数和9参数里面参数位置是不一样的,这个和一般的API有所不同。一般API可选参数是放在后面。但是,这里的[代码]drawImage()[代码]9个参数时候,可选参数[代码]sx,sy,swidth,sheight[代码]是在前面的。如果不注意这一点,有些表现会让你无法理解。 下图为MDN上原理示意: [图片] 对于本文的图片压缩,需要用的是是5个参数语法。举个例子,一张图片(假设图片对象是[代码]img[代码])的原始尺寸是4000*3000,现在需要把尺寸限制为400*300大小,很简单,原理如下代码示意: const ctx = wx.createCanvasContext('myCanvas') wx.chooseImage({ success: function(res){ ctx.drawImage(res.tempFilePaths[0], 0, 0, 400, 300) ctx.draw() } }) 把一张大的图片,直接画在一张小小的画布上。
2020-05-29 - 地表最强富文本展示渲染库
首先接触这个是在一个开源的博客类小程序里面,后面自己参与民宿项目需要展示酒店的详情介绍,就拿来用了,效果非常给力 相比其他富文本解析库,这个库的优势是可以点击富文本里面的图片,放大展示,细微处彰显 具体如何接入请移步官网 微信小程序HTML、Markdown渲染库 https://github.com/sbfkcel/towxml 1 以下截图来自官网 [图片] 2 使用说明文档请移步 https://github.com/sbfkcel/towxml/wiki 3
2020-06-01 - 第一次发布开源的省市区筛选,大佬们勿喷。
https://github.com/ysh98/regionSelector 省市区使用,从父组件传给子组件已经选中的省市区数组的话,可以识别省市区文字,并且滚动到对应的地区,如果有什么问题,可以在评论区里写,我会及时修改bug,有可能做的不符合您的需求,您也可以拿一些对您有用的代码。
2020-06-01 - 小程序自定义导航栏的开发原理及编码思路实践 (附带可直接使用的自定义导航栏组件)
1.引言 在小程序应用开发过程中,导航栏作为页面标题性的存在,不但起到了用户指引路由的作用,更是在应用功能较为完善和复杂的情况下,起到了进一步的功能性作用,小程序的原生导航栏如下: [图片] 显然,原生的导航栏除了能够提供标题,返回键等功能外,其他功能到目前为止并没有浮出水面。而为了定制符合我们自己开发要求的导航栏,此时,我们就需要对导航栏进行自定义的设计开发。自定义开发过导航栏效果如图: [图片] 2.原理及思路 (1)原理 首先我们要知道几个概念:状态栏,导航栏,导航栏胶囊,导航栏胶囊在导航栏中的上下边距 状态栏:是指通常手机顶部显示手机运营商,手机信号,手机电量等等信息的最顶层一栏。 导航栏:是指在小程序中显示胶囊按钮,返回键,标题的一栏。 导航栏胶囊:是指小程序中导航栏右部出现的胶囊状按钮,小程序中会一直置顶显示,在不同的机型中,胶囊的高度是固定的,目前均为32px。 导航栏胶囊在导航栏中的上下边距:胶囊按钮在导航中不是上下撑满的,是上下居中布局的,所以存在上下相等的边距。 效果说明图如下: [图片] 在小程序开发中,我们可以获取以下数据: 胶囊的布局位置及尺寸信息:(详情点击查看https://developers.weixin.qq.com/miniprogram/dev/api/ui/menu/wx.getMenuButtonBoundingClientRect.html) wx.getMenuButtonBoundingClientRect() (2)思路 原理我们都已经介绍过了,现在需要我们想一想设计的思路。 最常规的思路就是来个和导航栏一样宽度,一样高度的页面容器绝对定位覆盖在导航栏上面就可以了。然后在这个覆盖容器里面进行自定义发挥就可以了,保准自定义的内容和胶囊对齐的准准的,不会有任何问题。 那么,该怎么获取导航栏的高度和定位信息呢(宽度肯定是满宽,100vw)。 从上面的效果演示图我们可以知道,导航栏的高度是等于 胶囊高度+胶囊上边距 * 2 的,这个应该不难理解,胶囊高度上文已经提过,是固定的:32px,所以,我们只要再求出胶囊的上边距就可以了。上边距我们可以通过由 wx.getMenuButtonBoundingClientRect() 获取的胶囊距顶部的距离减去状态栏高度获取。从而计算出状态栏的高速。而后就可以实现我们一开始的思路,将一高度与导航栏相同,绝对定位在距屏幕顶部一个状态栏高度的距离便可以实现完全的导航栏精确覆盖。 3.代码实现 //胶囊上边框距顶部距离 const capsuleTop = wx.getMenuButtonBoundingClientRect().top; //状态栏高度 const statusBarHeight = wx.getSystemInfoSync().statusBarHeight; //药囊上边距状态栏下边的距离,即药囊在导航内容栏中的上下边距 const capsuleGap = capsuleTop - statusBarHeight; //导航内容栏的高度动态计算 const navContentHeight = capsuleGap * 2 + this.data.capsuleHeight; 4.注意事项。 (1)在定位好的自定义导航栏里编写内容时,不要与胶囊重合,否则会被胶囊覆盖,此时需要计算可使用区域的宽度,在计算宽度时,为了达到各机型效果的一致,需要根据当前模拟机型的宽度进行px对rpx的换算,才能使自定义组件中的内容的宽度在各个机型中效果一致。 (2)本文所述自定义导航栏的创建方法必须基于该自定义导航栏针对页面fixed绝对定位的基础上才可以。 5.结语 在编写自定义导航栏之前也查了很多材料,感觉都不太准,自己借鉴着摸索了一套自己的方法,请大家评判一番,我已将该自定义导航栏封装成了微信小程序自定义组件,插之即用,以供大家提高生产效率: NPM安装:https://www.npmjs.com/package/russell-weapp github:https://github.com/RussellCao824/russell-weapp
2020-06-07 - 小程序日历组件推荐
小程序日历组件推荐 这几天在做民宿小程序,遇到一个需求就是 用户选择日期,来下单,由于民宿每天的价格都是变化的,所以要在日历上显示价格,找了很久,终于找到了 vant https://youzan.github.io/vant-weapp/#/calendar 1 [图片] 2 [图片] 3 具体使用方法如下所示 [图片] 4 具体小程序界面截图如下所示: 5 [图片] 6 7 现在说起来都是风轻云淡,但是找的过程又盲目,有着急,开发中,也遇到各种问题,好在,回过头去,vant calendar就是我要找的日历组件 8
2020-06-07 - 全网最全小程序自定义tabbar实现方案
业务需求,底部导航栏(tabbar)支持自定义,目前有个局限性,必须要在app.json里面声明list数组,而且数量是五个,导致扩展受限制。目前还没有解决办法,基本可以满足五个和小于五个自定义的已存在的tabbar的动态配置。 感谢网友的热心分享,以下链接地址都非常有参考价值:不过在当前时间点2020年6月5号,和对应的小程序版本号来说或多或少会遇到一些问题。 2022-08-30 补充: 首先很抱歉,就是图片看不到了.确实影响阅读体验. 然后这个方案是不成熟的, 大家可以巧用: wx.setTabBarItem 这个api,来做一些文章. 其实可以用wx.setTabBarItem https://developers.weixin.qq.com/miniprogram/dev/api/ui/navigation-bar/wx.setNavigationBarTitle.html 参考地址:见文章结尾 自定义 tabBar 官方api地址:https://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html?search-key=%E8%87%AA%E5%AE%9A%E4%B9%89tabbar 效果图先上: [图片] 小程序自定义tabbar实现.gif 下面开始进入正文: 第一步、先下载官方的demo,然后进行合并。说明:这里存放的目录地址要是项目下和pages同目录,不然无法识别自定义的tabbar组件[图片] image.png 说明2,这个用的是全局设置的,需要加一下,不然不能开启自定义组件模式 "usingComponents": {}, "window": { 第二步、在 app.json 中的 tabBar 项指定 custom 字段,设置为true,同时其余 tabBar 相关配置也补充完整。说明:如果遇到报错,说什么组件不存在,可以把 custom 字段去掉,本项目没有去掉能正常使用。[图片] image.png 第三步、修改custom-tab-bar/index.js的文件说明,由于这里我们是后端动态返回的,这里我不展开说具体的业务,这里需要注意的是list中的pagePath一定要写绝对路径/pages开头第四步、把官方给的使用方法放到tabbar跳转页的onShow方法里,selected根据list下标位置进行设置selected: 这里的参数是对应的底部tabbar的顺序的,含义是进入当前界面,并选中这个当前的tabbar if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ selected: 4 }) } 第五步,关于点击后会闪一下这个问题的说明,目前官方也没有解决的方案,所以,除非必要,建议还是用自带的tabbar写业务。[图片] image.png 最后补充一个兼容跳到tabbar的语法糖: wx.navigateTo({ url: '/pages/cart_new/index/index', success: function(res) {}, fail: function(res) { wx.switchTab({ url: '/pages/cart_new/index/index', //注意switchTab只能跳转到带有tab的页面,不能跳转到不带tab的页面 }) }, complete: function(res) {}, }) 参考地址: 微信小程序开发---自定义tabBar:https://segmentfault.com/a/1190000016283268 微信小程序自定义tabbar:https://www.jianshu.com/p/8b918e21cc6b 小程序自定义tabbar报Component is not found in path "custom-tab-bar/index":https://blog.csdn.net/qq_34672907/article/details/93624433 微信小程序自定义导航栏天大的坑,报错提示:component is not found in path "custom-tab-bar/index"...https://blog.csdn.net/dqzd12345/article/details/102756681 鉴于文中图片丢失:可以移步到我的博客看文中图片 https://www.jianshu.com/p/c48281e61907?u_atoken=b039edc7-97e0-4fa9-9d2d-4aca9a1faa87&u_asession=01VEYqQobFRt91aDKSjf6RMv2PFMk-NGAnJp1a80FUHt5puOXqVMQ49qq2C1XR8xKKX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K_wjOK6EbT2ki0dkwQctnAQnHmbkqVcEgdObpAroqY1_GBkFo3NEHBv0PZUm6pbxQU&u_asig=05ESJ0rAXmqHXPIepLzgwTZn_I1UfOjo4mnyB5w-1YuP98oKPx90Z-OI_FpFckab0IKcUl39blHNAEIyPVHalH7evI9N43IRLtrs4TIbEtbub6fJW-7KGDLiz20hzMsz99AoqUieIp1TcUCwzplBT-dwzjA8FeyDmEzHEoYBRMWeH9JS7q8ZD7Xtz2Ly-b0kmuyAKRFSVJkkdwVUnyHAIJzYGzTYSVKNX8WdtkFZgDm7lSpGaDCR7AutzhN5tKbTbm6xbSxAaWh9ph0bRUFW-6vO3h9VXwMyh6PgyDIVSG1W-u2m4aSV3j7RjNp-oJ3rx3fX5y-uYeLDQishV-vt1GW8_QLnW4q9Rmwoz8SvMcA9UV9IzhK00je8y5MzvIwdeImWspDxyAEEo4kbsryBKb9Q&u_aref=uxwZVbN3vBalaKU8R2iVPkmS7C4%3D
2022-08-30 - 小程序中日期格式化或获取指定日期格式数据
最近开发代码时发现,日期格式化官方没有给出一个很好地解决方案,虽然使用wxs文件在wxml中可以引入使用,但是无法获取实时数据。 由于小程序中的js跟es5的规则类似,为便于使用工具性方法获取实时日期信息或根据传递的日期获取指定格式的日期信息。 下面将代码贴出,使用时,请在指定js中引入创建的工具js文件即可。 示例: const util = require('util.js'); let date = new Date(); console.log(util.formatDate(date, 'yyyy-mm-dd hh:mi:ss')); 如果是使用云开发,请参考官方云开发数据库查询文档,进行日期格式化。 /** * date 为日期Date类型, * 日期格式化信息 matter 定义 * 年:yyyy/YYYY/yy/YY * 月:mm/MM (不足两位用0补全) * 日:dd/DD (不足两位用0补全) * 时:hh/HH (24小时制) * 分:mi/MI (不足两位用0补全) * 秒:ss/SS (不足两位用0补全) */ const formatDate = function (date, matter) { let year = date.getFullYear().toString(); let month = (date.getMonth() + 1).toString(); month = (month.length > 1) ? month : ('0' + month); let day = date.getDate().toString(); day = (day.length > 1) ? day : ('0' + day); let hours = date.getHours().toString(); hours = (hours.length > 1) ? hours : ('0' + hours); let minutes = date.getMinutes().toString(); minutes = (minutes.length > 1) ? minutes : ('0' + minutes); let seconds = date.getSeconds().toString(); seconds = (seconds.length > 1) ? seconds : ('0' + seconds); let retVal = matter; if (matter.indexOf('yyyy') >= 0) { retVal = retVal.replace('yyyy', year); } else if (matter.indexOf('YYYY') >= 0) { retVal = retVal.replace('YYYY', year); } else if (matter.indexOf('yy') >= 0) { retVal = retVal.replace('yy', year.substring(2)); } else if (matter.indexOf('YY') >= 0) { retVal = retVal.replace('YY', year.substring(2)); } if (matter.indexOf('mm') > 0) { retVal = retVal.replace('mm', month); } else if (matter.indexOf('MM') > 0) { retVal = retVal.replace('MM', month); } if (matter.indexOf('dd') > 0) { retVal = retVal.replace('dd', day); } else if (matter.indexOf('DD') > 0) { retVal = retVal.replace('DD', day); } if (matter.indexOf('hh') > 0) { retVal = retVal.replace('hh', hours); } else if (matter.indexOf('HH') > 0) { retVal = retVal.replace('HH', hours); } if (matter.indexOf('mi') > 0) { retVal = retVal.replace('mi', minutes); } else if (matter.indexOf('MI') > 0) { retVal = retVal.replace('MI', minutes); } if (matter.indexOf('ss') > 0) { retVal = retVal.replace('ss', seconds); } else if (matter.indexOf('SS') > 0) { retVal = retVal.replace('SS', seconds); } return retVal; } exports.formatDate = formatDate;
2020-06-09 - 「分享」在微信小程序中快速的创建一个滚动卡片
最近做项目遇到的一个功能要求,没在网上找到类似代码,所以自己写了一个,整理分享出来,有需要的拿走。 手指上下滑动,可以滚动这个卡片;点击卡片顶部可以快速的展开或收起卡片。 [图片] 目前许多手机应用都使用了这种设计,使用 ScrollCard 的代码可以在微信小程序中快速的创建一个类似交互的卡片。 ScrollCard 主要使用了微信小程序的 scroll-view 组件,然后结合 CSS 的位置控制,从而实现视觉上的效果,几乎没有用到 Javascript(如果不需要点击卡片顶部快速展开或收起卡片,那么就完全不需要用到 Javascript)。 源码:https://github.com/impony/miniprogram-scrollcard
2020-06-10 - 手把手带你使用云开发实现微信小程序支付功能(完整视频讲解)
首先看下效果图 [图片] 下面把完整的讲解视频,免费发给大家。 1~注册企业小程序 [视频] 2~微信支付商户号注册及注意事项 [视频] 3~小程序关联微信支付的商户号 [视频] 4~创建云开发项目 [视频] 5~云开发控制台配置微信支付商户号 [视频] 6~编写支付的云函数 [视频] 7~云开发实现微信支付 [视频] 8~动态修改商品名和价格 [视频] 9~编写商品页面 [视频] 10~单个商品的支付 [视频] 11~商品列表实现购买支付 [视频] 如果你是B站用户,也可以去石头哥B站观看完整的视频讲解: https://www.bilibili.com/video/BV1Lp4y1D7oY/
2020-06-11 - 垃圾分类小程序拍照识别代码解析
我在前几天写过 开源的智能垃圾分类小程序代码详解 https://developers.weixin.qq.com/community/develop/article/doc/00004c9ef3c3d8329e7adde8156013 我本来计划在这个文章具体分析代码,但是发现截图多了之后,一篇文章打开太慢了,索性,按照知识点来分析吧 今天分享拍照识别这个地方的逻辑 [图片] 1 [图片] 2 [图片] 3 [图片] 4 首先该识别接口调用的×度的服务, 第一步:进拍照页面后,调用接口获取×度的access_token, [图片] 5 [图片] 第二步、拍照,将拍照图片转成base64,调用识别接口 [图片] 12 [图片] 3 第三步:渲染识别后的物品名称 [图片] 4 附:获取access_token用到了两个云函数 云函数一:getBaiduAccessToken 主要用于生产access_token [图片] 5 云函数二:getBaiduToken 主要用于判断access_token是否有效,如果过了有效期要重新请求获取,也就是调用云函数一getBaiduAccessToken [图片] 6 7 8
2020-06-12 - 企业级小程序开发、测试、发布流程梳理
背景 昨天持续开发2个月的某企业级小程序发布上线,今天开个帖子具体讲下,企业级小程序,用单个小程序复用多套环境的形式讲述小程序从开发、到上测试,到发布的全流程分析 本文非技术贴,仅仅从小程序发布流程的角度讲述 1 正文 关于测试小程序和生产小程序到底用几个小程序,在论坛一直有讨论,每一种各有优缺点 单个小程序做测试和生产用 优点 减少小程序仓库的代码同步 缺点 小程序权限分配 开发时如果环境切换不好,会带来生产的脏数据问题 发布时,如果环境切换不正确会造成严重的生产事故 一个小程序做测试,一个小程序做生产 优点 权限分配管控更为清晰 开发、生产分离,保证数据的安全 缺点 多个小程序带来维护上的成本会增加 在每一种情况都不具有绝对优势的情况下,采取什么方案主要就有项目的技术负责人根据习惯来决定,在本文讨论的小程序中是以单个小程序为例,也就是用一个小程序完成开发、测试、生产用 权限 先从权限讲下,大家都知道具有运营者角色的微信用户具有设置体验版和送审以及发布的功能 但是在日常开发中,开发和发布是分别由不同的部门来完成的,比如开发在技术部门,发布在运营部门,但是由于日常测试需要,测试要设置体验版来测试,这个功能就需要把运营者角色给到某个测试涉及岗位,或者是产品或者是项目经理,但是他们从严格权限来讲是不应该具有这种运营者角色的 环境切换 用一个小程序来开发,环境切换是不得不谈的一个问题,一个小程序多套环境如何切换,怎么切换比较安全 其实微信小程序官方也提供了一些api用于检测小程序是开发版、体验版、发布版,对应我们开发环境、测试环境、生产环境 但是这些api是向下兼容的吗,就是说这些api能覆盖目前所有小程序基础库吗? 这个问题要好好衡量,如果不能做到100%覆盖 我想这种api在企业级小程序是不敢用的 如上所示,我们这次环境切换也是纯手工,在发布生产之前,手工将api切换到生产环境,点击上传,开始送审,发布。 如何开发 开发小程序时,联调用的后端环境是由后端同事本地提供 如何测试 开发完成测试时,需要将配置文件中的api信息改为测试环境,并且上传代码,具有运营者权限的同学可以设置体验版,并且生成体验版小程序二维码,分发给测试的同学 提交审核 跟测试一样,提交审核之前,需要由开发的同学将配置文件中有关api的信息切换到生产环境,然后上传代码,并通知具有运营者权角色的同学,提交审核。 发布流程 服务端先发布生产 小程序切换生产环境,上传代码(开发同学完成) 通过小程序助手提交审核(运营同学完成) 审核通过后,通过小程序助手发布(运营同学完成) 小程序功能验收(市场同学完成) 总结 由于企业级小程序上线,都要很多人员配合验证,为保证流程的连贯性,建议第一次小程序上线审核,走加急审核 欢迎大家一起参与讨论
2020-06-19 - 生成带参数的小程序码,里面参数如何核对
生成带参数的小程序码,里面参数如何核对 https://developers.weixin.qq.com/community/develop/doc/000e2e4cf98960e6648a3550452000 场景 前几天我做小程序,需要生成具体某个页面的小程序码,比如考试,我想生成某场考试的入口小程序,用户通过该小程序码可以直达该考试 问题: 带参数的小程序码生成了,但是我怎么检测里面的参数,总不能上线后再去验证,这有点野 解决方案 如上面帖子里面评论的,具体有待我去体验
2020-06-19 - 小程序宝箱抽奖组件
在上一篇中《小程序canvas开发水果老虎机》我们做了一个水果机的抽奖组件,为水果抽奖机新增一种新的抽奖模式,复用水果机抽奖组件,实现上基本没什么难度,跳开水果机算法就基本实现了,效果如下 [图片] 支持有序抽奖 支持无序抽奖 支持自定义每个奖品 支持自定义奖品个数(>=2) 支持中奖后的回调方法 安装方法 GITHUB上有详细说明。 配置 wxml [代码]<view wx:if="{{fruitConfig}}" class="fruit-container {{fruitConfig.containerClass||''}}" style="{{fruitConfig.containerStyle||''}}"> <ui-list list="{{fruitConfig}}" /> <canvas type='2d' disable-scroll="true" id="fruit-canvas" class='fruit-canvas'></canvas> </view> [代码] js [代码]const Pager = require('../../components/aotoo/core/index') const mkFruits = require('../../components/modules/fruit') Pager({ data: { fruitConfig: mkFruits({ id: 'fruitTable', ... ... }, // callback,动画结束后响应 function (param) { console.log(param); // 中奖数据 console.log(param.value); // 中奖值 }) } }) [代码] id 配置实例的Id containerClass 容器样式 containerStyle 容器内联样式 max 设置max值后,根据平方算法,自动计算果盘数量,max定义一行几个水果,默认九宫格,max为3 count 与max的区别在于果盘数量由用户指定,且果盘不再为老虎机的游戏模式,max数量仍为定义一行几个水果 设置[代码]count[代码]为有效数据后,将以常规抽奖盘代替老虎机抽奖盘 confuse 使果盘数据无序化,只有当count为有效数据时,confuse才能生效 fruitsData Object,指定对应位置的格子内容,支持文字/图片 回调方法 mkFruits(config, callback)水果盘动画结束后响应方法 如何设置 设置宝箱抽奖盘 常规抽奖盘不再以水果老虎机的游戏形式展现 [代码]mkFruits({ id: 'fruit', count: 3, // count默认为0,当count设定>0时,组件不再输出水果盘 max: 3, // count>0时,max表示每行显示个数 fruitsData: { 1: {title: '石头', value: '001'}, // value默认为下标叙述,也可以手动指定 2: {title: '剪刀', value: '002'}, 3: {title: '布', value: '003'} }, }) [代码] 设置果盘内容(图片) 通过[代码]fruitsData[代码]设置指定位置显示内容,支持文本/图片 [代码]mkFruits({ id: 'fruit', max: 4, fruitsData: { 1: {title: '一等奖', value: '001'}, 5: {img: {src: '/images/xxx.jpg'}, value: '003'}, }, }) [代码] 如何获取水果盘的实例 运行水果盘需要调用实例方法,首先需要通过getElementsById获取水果盘实例 [代码]const Pager = require('../../components/aotoo/core/index') const mkFruits = require('../../components/modules/fruit') Pager({ data: { fruitConfig: mkNavball({ id: 'fruit' }), }, onReady(){ // 获取水果盘实例 const instance = this.fruit // 或者 // const instance = this.getElementsById('fruit') } }) [代码] run 运行水果盘 [代码]onReady() { // 获取水果盘实例 const instance = this.getElementsById('fruit') instance.run() } [代码] 源码戳这里 下列小程序DEMO包含[代码]多形态日历,下拉菜单、筛选列表、索引列表、markdown(包含表格)、评分组件、水果老虎机、折叠面板、双栏分类导航(左右)、刮刮卡、日历[代码]等组件 [图片]
2020-06-24 - 小程序页面滚动穿透
小程序页面滚动穿透 csdn原文 一、场景 在项目当中,基础遇到这样的需求 有一个长列表,或者其他可滚动展示的页面, 在这个页面会弹出一个[代码]Modal[代码]层,如下: 贝壳找房的 的筛选栏 <img src=“https://forguo-1302175274.cos.ap-shanghai.myqcloud.com/blog/imgs/beike.jpg” alt=“贝壳找房的” width=“400” align=“bottom” /> 二、问题 如果这个弹框内容不可滚动,不会有太大问题; 但是当弹出内容是可以滚动的时候,就会有问题, 触摸没有滚动的区域会发现滚动可以穿透,会传递给下面的列表页面, 三、解决办法 程序员是面向Google编程的,找到了下面的解决办法: 监听弹框状态,如果弹框展示就给列表 添加对应样式 [代码] // isShowMask 弹框是否展示 <view class="dog-container {{isShowMask ? 'bottom-fixed' : ''}}"></view> [代码] [代码].bottom-fixed { position: fixed; left: 0; top: 0; overflow: hidden; } [代码] 给遮罩层添加 [代码]catchtouchmove[代码]的阻止 [代码]myCatchTouch: function () { return; } <view wx:if="{{alert}}" catchtouchmove="myCatchTouch"> <template is="alert" data="{{alertData}}" /> </view> [代码] 这样的话,底部的列表内容就不会出现溢出,也自然不会滚动, ::: warning 但是,这样的做法有一个弊端, 不会去记录我之前访问的位置,也就是每次点开弹框,列表位置会归零,体验终归是不好的。 ::: 四、升级方案 于是我去翻了一些开源小程序UI的Demo,去试试看这种弹框类型的交互, 最后发现在[代码]Taro UI[代码]中有一个组件,[代码]Float LayOu[代码],是没有出现穿透的,列表位置也没有发生改变, Taro UI 一套基于 Taro 框架开发的多端 UI 组件库 于是,我翻了源码,发现他是这样写的(有删减): [代码] // 重点A:阻止事件冒泡 handleTouchMove = (e) => { e.stopPropagation() } render() { return ( <View className='rootClass' onTouchMove={this.handleTouchMove}> <!-- 遮罩层 --> <View onClick={this.close} className='at-float-layout__overlay' /> <View className='at-float-layout__container layout'> <View className='layout-body'> <!-- 重点B: ScrollView(开启scrollY)--> <ScrollView scrollY scrollX={false} className='layout-body__content' > {this.props.children} </ScrollView> </View> </View> </View> ) } [代码] 思路解析: i、首先,需要在自定义弹框的根元素,添加 [代码]onTouchMove[代码] 监听,并阻止时间的冒泡 [代码] <View className='rootClass' onTouchMove={this.handleTouchMove}> [代码] ii、但是,里面的内容,就不能滚动了,那么,可以使用 [代码]ScrollView[代码]代替[代码]View[代码],并开启Y轴的滚动 [代码] <ScrollView scrollY scrollX={false} className='layout-body__content' > <!-- 内容区域--> <!-- 内容区域--> </ScrollView> [代码] 按照这样的思路,我在项目里面尝试了下,果然奏效,这样的方式更优雅体验也更好。 笔者使用了Taro,但原理都是一样的。 最终效果: <img src=“https://forguo-1302175274.cos.ap-shanghai.myqcloud.com/blog/imgs/touch-flow.gif” alt=“最终效果” width=“400” align=“bottom” /> 五、关于[代码]stopPropagation[代码] 简单来说: [代码]JavaScript[代码]中,冒泡和捕获是事件流的两种行为,使用event.stopPropagation()可以起到阻止捕获和冒泡阶段中当前事件的进一步传播。 而使用event.preventDefault()可以取消默认事件。 事件流 事件流描述的是从页面中接受事件的顺序,分为 IE的事件流是 事件冒泡流, 标准的浏览器事件流是 事件捕获流。 好了,希望对大家有用,对事件流有兴趣的可以自行Google 或者看下这片文章:JavaScript的事件流 <br /> Ending… <br /> <br /> csdn github 个人站点
2020-06-26 - 新拟物风小程序的第一次尝试(已开源)
小桃答题 | 新拟物 答题小程序 📑 介绍 新拟物风格 主要面向答题,考研,测试,排行 无须服务器, 无须域名 微信小程序 目前仅适配开放微信小程序, 后续适配其他平台小程序 扫描下面二维码可体验 👇 [图片] 💡 软件架构 后端: 知晓云(https://cloud.minapp.com/) 前端: uni-app(https://uniapp.dcloud.io/) 🕹 应用截图 功能 对应界面 首页 [图片] 选择目标 (真机流畅,gif 卡顿) [图片] 答题页面 [图片] 答题流程 [图片] 选择课程分类 [图片] 排行榜 [图片] 收藏列表为空 [图片] 🚀 安装教程 应用注册 先注册微信小程序, 将项目中 src/manifest.json 的 appid 更改自己的 appid(在微信后台开发/开发设置中) 在知晓云注册账号后, 按指引流程接入后获取 key, 替换项目 App.vue 中 wx.BaaS.init(“xxxxxxxxxxxxx”)位置!; 为什么没有用云开发, 单纯是因为知晓云的后台操作起来方便些, 当然个人觉得目前云开发功能在微信小程序上是比知晓云强大 导入 data/structure 内的 json 文件 生成数据结构(点击可以看操作图片哦!) (可选) 可导入 data/mock 内的 json 文件作为初始数据导入 mock 数据 安装环境 第一步: node 安装 用于编译 javascript 代码 node 安装地址 windows 选择 msi mac 选择 pkg 一键安装环境啥的最适合我了 第二步: 执行编译命令 在项目目录下执行 [代码]npm install -g @vue/cli & npm run dev:mp-weixin[代码] 🧰 懒人模式 这个就是让小桃帮你部署, 给她买条裙子 👗 的费用部署(199), 当然如果后面有新版本更新的话也有优先推送, 有少量需求也可以满足下. 下面是小桃的微信 👇 [图片] 源码地址 [https://gitee.com/github-31064550/uni_app-question] ❤️ 最后 如果这个项目对你学习, 工作或是其他需求有帮助的, 点个赞呗 💃💃💃
2020-07-02 - 字符串转日期大坑请绕行
问题来源是这样的,由于最近开发的党建答题小程序的用时数据不对,我发了个版本来修复整个问题,但是发布上去之后,用我的苹果手机答题,用时竟然报NAN的的现象,但是我观察版本发布后,其他用户的答题用时数据却是正常的,具体看下面截图,用时这个地方,我们不难发现问题 [图片] 问题描述在苹果手机里面字符串转成日期的时候,会有问题,具体看下面截图 在下面截图里面date1、date2分别是通过time1、time2转换来的,但是都不是预期的效果, [图片] 参考文章小程序开发中,IOS与Android的坑,你踩过几个?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000aee0c6c8f004bb41a77dfa5b413 微信小程序中的日期格式在Android和iOS真机下兼容性问题的坑? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000e2e82d14cd80838c9cb8b552013 微信小程序中的日期格式在Android和iOS真机下兼容性问题的坑? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000e2e82d14cd80838c9cb8b552013 不同手机对于字符串转换成日期处理不一样? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000a84689607b8cfe8474423f51c00 https://blog.csdn.net/xinyi_jin/article/details/88047165 解决方案将字符串里面的-进行替换,具体看下面代码 [图片] 总结真的没有想到,在测试都很到位的情况下,遇到了苹果手机的兼容性问题,而且在本地开发环境还不好发现。 所幸的是这次党建答题活动的用户并不多,具体到苹果用户那就更少了,我手工修复的还能凑合,目前修复版本已经提交了,希望快快审核通过吧 [图片][图片] 备注目前版本已发布,生产运行良好
2020-06-29 - 3行代码实现小程序直播,带美颜优惠券抽奖功能
最近准备给自己的小程序做个直播功能,看下直播所需要的一些资质,瞬间被吓止步。后面发现小程序官方出了直播插件,这就为小程序接入直播提供的诸多便利。仅仅需要一些简单的配置,就可以轻松实现直播功能了。下面带大家来一步步给自己的小程序添加直播功能吧。 老规矩,先看效果图 [图片] 一,首先要给你的小程序开通直播插件功能 登录我们的小程序后台,可以看到如下图所示的直播 [图片] 点击一下,就可以进入小程序直播开通页面 [图片] 注意我们上图红色框里的一些要求。必须要满足这些条件,才可以开通直播功能。更详细些的如下: [图片] 这就注定目前只能是通过认证的企业小程序才可以开通直播功能了。个人小程序目前是没法开通的。我刚开始还不信,用我的个人小程序试了试。结果就如下图,后面没办法就注册了一个企业小程序。 [图片] 并且小程序的服务类目也要符合官方要求 [图片] 到这里,才算真正开通了小程序直播功能。 [图片] 二,创建直播间 点击创建直播间 [图片] 选择手机直播 [图片] 这里需要用一个实名认证的微信做主播端。 [图片] 认证后如下: [图片] 这里设置直播的一些封面等信息 [图片] 直播间创建成功后如下 [图片] [图片] 这里的直播码,扫码后就可以直接开播了,还有这里的房间号一定要记牢,后面会用到。 [图片] 这里可以往直播间里添加商品,优惠券等 [图片] 下面就是根据官方文档来代码实现直播功能了 三,直播功能的代码实现 我们创建号直播间以后,接下来就要在小程序代码里实现直播功能了。 1,首先是要创建一个小程序项目 至于如何创建小程序项目我这里就不再教大家了,如果你还不知道如何创建小程序项目,建议你去翻下我的历史文章,或者看看我录的《10小时零基础入门小程序开发》 创建好的小程序项目如下 [图片] 2,在app.json里添加直播插件 其实官方的接入文档写的很清晰了。下面把官方文档贴出来给大家:https://developers.weixin.qq.com/miniprogram/dev/framework/liveplayer/live-player-plugin.html [图片] 我们只需要把上面红色框里的代码复制到app.json里就可以了。记得把注释去掉 [图片] 一定要记得,除了把注释去掉之外,其他的都不要做改动。 3,然后编写可以跳转到直播间的代码 代码很简单,就写一个button按钮,然后添加点击事件即可。 [图片] 点击事件如下 [图片] 其实官方文档里也有讲 [图片] 直播房间的房间id我们在创建直播间成功后其实可以拿的到的。 [图片] 到这里我们的直播功能就完整的实现了。下面我们来看看都有哪些直播状态 四,直播状态的显示 未开播状态,这里我们可以订阅开播提醒,等开播的时候,会有订阅消息提醒。 [图片] 如果你订阅开播提醒了,还会有开播提醒 [图片] 直播结束状态 [图片] 主播暂时离开 [图片] 主播端网络异常中断 [图片] 主播端可以设置美颜等功能 [图片] 并且我们的小程序直播间里可以设置优惠券,抽奖,添加商品。 [图片] 直播结束后,还有回放功能 [图片] 好,到这里就给大家把小程序直播功能完整的讲解完了。由于代码量太少,实现起来比较简单,所以就不给大家录讲解视频了。
2020-06-30 - 微信小程序性能优化实践
三个月前接手一个小程序项目,用户量大,交互复杂,性能一般。想法设法做了各种优化,取得了不错的效果,也在过程中收获了一些经验。昨天在公司内部做了一个技术分享,总结了一些优化经验,顺便整理为本文。 提高加载性能 典型的web类的应用,跑起来都需要3步: 第一步,加载运行环境; 第二步,下载代码; 第三步,执行代码,渲染页面。 小程序也如此。第一步加载运行环境,是微信实现的,开发者无需优化。 第二步下载代码,减少首次下载的包的大小,可提高速度。代码压缩、分包加载、预加载等操作,web应用需要借助webpack等打包工具才能实现,而小程序只需修改配置就行了,方便很多。 注意,subpackages配置路径外的目录将被打包到 app(主包)中。原项目在根目录下存了全局组件、样式、图片,导致主包非常大,一度逼近主包大小上线2M。这次把分包里的才会用到的组件,拆到分包所在的文件夹中;把图片资源放到CDN中。这样主包中只留下主包用拿到的代码和组件: [图片] 如果图片资源不多,可以考虑放在代码里,方便管理。但如果图片资源较大,建议使用CDN,这样图片就是在第三步(渲染页面)时才下载,而不是在第二步下载代码时一起下载,能够更快显示骨架屏,优化用户体验。 [图片] 第三步执行代码,优化项包括骨架屏、接口时序调整、懒加载等。其他优化方向还包括,使用数据预拉取,在下载代码阶段提前获取业务数据。这个跟业务高度相关,此处就不展开细讲。 控制setData数据量 setData是小程序开发中使用最频繁的接口,也最容易引发性能问题。每次setData都会触发页面的重新渲染,如果渲染量计算量过大,很容易造成页面卡顿,影响用户体验。原理可以参考官方文档,这里只说实践:不要频繁setData,每次setData数据量不要太大。下面举两个例子。 前端经常会遇到"超长的列表"的难题。因业务需求,后台返回的数据不做分页,一个列表几百甚至上千项,如果直接通过setData写进去,页面一下子就卡死了。优化方案是做前端的分页加载:收到数据后把全部数据存在一个变量里,只取第一页的数据setData更新页面。等用户下拉触发更新的时候,再继续setData下一页的数据。 长度实在太大的情况下,卡顿不再仅仅是setData的问题,而是节点过多导致占用内存过大,这时还可以试下循环列表。 另一个例子是两个列表的数据对比。先说下业务背景:先查商品列表,再查购物车列表,如果该改商品的在购物车列表中有数据,则要更新列表上的数据。之前的做法一直都是循环一遍对比列表,再整个列表数据进行setData,当购物车数量越来越多的时候,就开始卡死了。这次优化引入了Worker,每次购物车更新的时候,在Worker中进行diff运算,算出需要更新列表的哪几项,再进行setData,大大减少了setData的数据量,性能有了明显的改善。 其他细节优化 响应客户滑动事件的需求,使用WXS。之前是把页面touchmove事件绑到js中的逻辑,这样会导致页面的响应不够及时。这次优化把滑动相关的事件都做到wxs里,滑动的体验大大改善。原理涉及小程序的框架视图层(Webview)和逻辑层(App Service)通信机制,官方文档有详细说明。 适当选择同步或异步API。小程序里有很多同步的Api(结尾带Sync的都是),都是阻塞性的。举个例子,常用的wx.getStorageSync()至少消耗50ms运行时间,如果是游戏等性能敏感的项目,卡顿大约3帧。积少成多,用户体验的差异就很明显了。能用异步的地方,尽量不要偷懒用同步的API。 还有一些代码逻辑优化,算法优化等,此处不再展开细讲。推荐一本老书《高性能JavaScript编程》。 总结 以上优化思路,并不局限于微信小程序。小程序的原理和web高度相似,很多优化经验,都可以和web互相借鉴参考。个人总结的三大优化原则:1.使用合理的技术架构;2.养成良好的编码习惯;3.遇到瓶颈之前不要瞎优化。 这两年来小程序生态蓬勃发展,追加了许多实用的功能,开发配套也日益完善。跨页面数据通信可以用Omix,做多端同构用kbone,开发体验比以前有了很多的改善。现在越来越多的产品选择微信小程序平台,体验好、文档完善、兼容性不错、学习曲线平滑、容易找工作,非常推荐新手入坑。 [图片]
2020-07-01 - 【组件库】编写一套小程序商城UI组件库
小程序商城UI组件库 wx-mall-components 最近很久没有更新文章了,在开源一个商城UI组件库,将日常商城用到的一些组件整合打包整理出来,并且支持一键更换组件皮肤等商城常用的功能,供大家使用,组件库还在完善当中,后续会陆陆续续加上各种商城用到的酷炫功能,希望大家可以来一起维护。 项目地址 https://github.com/csonchen/wx-mall-components 技术 pug # 编写静态模板 stylus # 编写样式 gulp # 文件编译,复制操作等 启动 [代码]# 安装&开发阶段 npm install (cnpm install) npm run start # 小程序调试阶段 打开“小程序开发者工具”,选择“导入项目”,选中 dist 目录 [代码] 四大模块 目前根据实际情况拆成四大组件版块,分别如下: 基础组件 交互组件 表单组件 业务组件 目录结构 [代码] wx-mall-components 微信小程序商城组件库 ./dist # 编译后的小程序文件目录存放路径 ./src ├── app.js ├── app.json ├── app.styl ├── components # 组件存放路径 │ ├── card-swiper │ │ ├── card-swiper.js │ │ ├── card-swiper.json │ │ ├── card-swiper.pug │ │ └── card-swiper.styl │ ├── date-picker │ ├── dropdown-select │ ├── form-input │ ├── search │ └── toast ├── images ├── node_modules ├── package.json ├── pages # 页面存放路径 │ ├── cardSwiperPage │ │ ├── cardSwiperPage.js │ │ ├── cardSwiperPage.json │ │ ├── cardSwiperPage.pug │ │ ├── cardSwiperPage.styl │ │ └── doc.js # 组件说明文档 │ ├── datePickerPage │ ├── dropdownSelectPage │ ├── index │ ├── inputPage │ ├── richTextPage │ ├── searchPage │ └── toastPage ├── sitemap.json ├── styles # 公用样式 │ ├── define.styl # 定义变量 │ ├── flex.styl # flex布局 │ ├── framework.styl # 框架定义 │ ├── normal.styl # 字体,边距等 │ └── plugin.styl # 三角形,下三角等插件定义 └── templates # 公用模板文件 └── pageHead.pug [代码] 示例 [图片] 小程序码 [图片]
2020-07-06 - 基于小程序请求接口 wx.request 封装的类 axios 请求
Introduction wx.request 的配置、axios 的调用方式 ----------------源码戳我--------------- ---------------demo 戳我-------------- feature 支持 wx.request 所有配置项 支持 axios 调用方式 支持 自定义 baseUrl 支持 自定义响应状态码对应 resolve 或 reject 状态 支持 对响应(resolve/reject)分别做统一的额外处理 支持 转换请求数据和响应数据 支持 请求缓存(内存或本地缓存),可设置缓存标记、过期时间 use app.js @onLaunch [代码] import axios form 'axios' axios.creat({ header: { content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }, baseUrl: 'https://api.baseurl.com', ... }); [代码] page.js [代码]axios .post("/url", { id: 123 }) .then((res) => { console.log(response); }) .catch((err) => { console.log(err); }); [代码] API [代码] axios(config) - 默认get axios(url[, config]) - 默认get axios.get(url[, config]) axios.post(url[, data[, config]]) axios.cache(url[, data[, config]]) - 缓存请求(内存) axios.cache.storage(url[, data[, config]]) - 缓存请求(内存 & local storage) axios.creat(config) - 初始化定制配置,覆盖默认配置 [代码] config 默认配置项说明 [代码]export default { // 请求接口地址 url: undefined, // 请求的参数 data: {}, // 请求的 header header: "application/json", // 超时时间,单位为毫秒 timeout: undefined, // HTTP 请求方法 method: "GET", // 返回的数据格式 dataType: "json", // 响应的数据类型 responseType: "text", // 开启 http2 enableHttp2: false, // 开启 quic enableQuic: false, // 开启 cache enableCache: false, /** 以上为wx.request的可配置项,参考 https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html */ /** 以下为wx.request没有的新增配置项 */ // {String} baseURL` 将自动加在 `url` 前面,可以通过设置一个 `baseURL` 便于传递相对 URL baseUrl: "", // {Function} (同axios的validateStatus)定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 reject validateStatus: undefined, // {Function} 请求参数包裹(类似axios的transformRequest),通过它可统一补充请求参数需要的额外信息(appInfo/pageInfo/场景值...),需return data transformRequest: undefined, // {Function} resolve状态下响应数据包裹(类似axios的transformResponse),通过它可统一处理响应数据,需return res transformResponse: undefined, // {Function} resolve状态包裹,通过它可做接口resolve状态的统一处理 resolveWrap: undefined, // {Function} reject状态包裹,通过它可做接口reject状态的统一处理 rejectWrap: undefined, // {Boolean} _config.useCache 是否开启缓存 useCache: false, // {String} _config.cacheName 缓存唯一key值,默认使用url&data生成 cacheName: undefined, // {Boolean} _config.cacheStorage 是否开启本地缓存 cacheStorage: false, // {Any} _config.cacheLabel 缓存标志,请求前会对比该标志是否变化来决定是否使用缓存,可用useCache替代 cacheLabel: undefined, // {Number} _config.cacheExpireTime 缓存时长,计算缓存过期时间,单位-秒 cacheExpireTime: undefined, }; [代码] 实现 axios.js [代码]import Axios from "./axios.class.js"; // 创建axios实例 const axiosInstance = new Axios(); // 获取基础请求axios const { axios } = axiosInstance; // 将实例的方法bind到基础请求axios上,达到支持请求别名的目的 axios.creat = axiosInstance.creat.bind(axiosInstance); axios.get = axiosInstance.get.bind(axiosInstance); axios.post = axiosInstance.post.bind(axiosInstance); axios.cache = axiosInstance.cache.bind(axiosInstance); axios.cache.storage = axiosInstance.storage.bind(axiosInstance); [代码] Axios class 初始化 defaultConfig 默认配置,即 defaults.js axios.creat 用户配置覆盖默认配置 注意配置初始化后 mergeConfig 不能被污染,config 需通过参数传递 [代码]constructor(config = defaults) { this.defaultConfig = config; } creat(_config = {}) { this.defaultConfig = mergeConfig(this.defaultConfig, _config); } [代码] 请求别名 axios 兼容 axios(config) 或 axios(url[, config]); 别名都只是 config 合并,最终都通过 axios.requst()发起请求; [代码] axios($1 = {}, $2 = {}) { let config = $1; // 兼容axios(url[, config])方式 if (typeof $1 === 'string') { config = $2; config.url = $1; } return this.request(config); } post(url, data = {}, _config = {}) { const config = { ..._config, url, data, method: 'POST', }; return this.request(config); } [代码] 请求方法 _request 请求配置预处理 实现 baseUrl 实现 transformRequest(转换请求数据) [代码] _request(_config = {}) { let config = mergeConfig(this.defaultConfig, _config); const { baseUrl, url, header, data = {}, transformRequest } = config; const computedConfig = { header: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', ...header, }, ...(baseUrl && { url: combineUrl(url, baseUrl), }), ...(transformRequest && typeof transformRequest === 'function' && { data: transformRequest(data), }), }; config = mergeConfig(config, computedConfig); return wxRequest(config); } [代码] wx.request 发起请求、处理响应 实现 validateStatus(状态码映射 resolve) 实现 transformResponse(转换响应数据) 实现 resolveWrap、rejectWrap(响应状态处理) [代码]export default function wxRequest(config) { return new Promise((resolve, reject) => { wx.request({ ...config, success(res) { const { resolveWrap, rejectWrap, transformResponse, validateStatus, } = config; if ((validateStatus && validateStatus(res)) || ifSuccess(res)) { const _resolve = resolveWrap ? resolveWrap(res) : res; return resolve( transformResponse ? transformResponse(_resolve) : _resolve ); } return reject(rejectWrap ? rejectWrap(res) : res); }, fail(res) { const { rejectWrap } = config; reject(rejectWrap ? rejectWrap(res) : res); }, }); }); } [代码] 请求缓存的实现 默认使用内存缓存,可配置使用 localStorage 封装了 Storage 与 Buffer 类,与 Map 接口一致:get/set/delete 支持缓存标记&过期时间 缓存唯一 key 值,默认使用 url&data 生成,无需指定 [代码] import Buffer from '../utils/cache/Buffer'; import Storage from '../utils/cache/Storage'; import StorageMap from '../utils/cache/StorageMap'; /** * 请求缓存api,缓存于本地缓存中 */ storage(url, data = {}, _config = {}) { const config = { ..._config, url, data, method: 'POST', cacheStorage: true, }; return this._cache(config); } /** * 请求缓存 * @param {Object} _config 配置 * @param {Boolean} _config.useCache 是否开启缓存 * @param {String} _config.cacheName 缓存唯一key值,默认使用url&data生成 * @param {Boolean} _config.cacheStorage 是否开启本地缓存 * @param {Any} _config.cacheLabel 缓存标志,请求前会对比该标志是否变化来决定是否使用缓存,可用useCache替代 * @param {Number} _config.cacheExpireTime 缓存时长,计算缓存过期时间,单位-秒 */ _cache(_config) { const { url = '', data = {}, useCache = true, cacheName: _cacheName, cacheStorage, cacheLabel, cacheExpireTime, } = _config; const computedCacheName = _cacheName || `${url}#${JSON.stringify(data)}`; const cacheName = StorageMap.getCacheName(computedCacheName); // return buffer if (useCache && Buffer.has(cacheName, cacheLabel)) { return Buffer.get(cacheName); } // return storage if (useCache && cacheStorage) { if (Storage.has(cacheName, cacheLabel)) { const data = Storage.get(cacheName); // storage => buffer Buffer.set( cacheName, Promise.resolve(data), cacheExpireTime, cacheLabel ); return Promise.resolve(data); } } const curPromise = new Promise((resolve, reject) => { const handleFunc = (res) => { // do storage if (useCache && cacheStorage) { Storage.set(cacheName, res, cacheExpireTime, cacheLabel); } return res; }; this._request(_config) .then((res) => { resolve(handleFunc(res)); }) .catch(reject); }); // do buffer Buffer.set(cacheName, curPromise, cacheExpireTime, cacheLabel); return curPromise; } [代码]
2020-07-03 - web-view 小程序跳转踩过的坑
一.小程序跳转到web-view网页在小程序中加载H5页面需要通过小程序提供的 <web-view> (官方文档:https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html) 主要是在src中绑定相应的H5页面url地址,如 [图片] 注意: 在 web-view 中加载的页面的域名,需要在微信公共平台中配置业务域名,否则会在加载页面时给出非法业务域的安全提示,个人和海外账号暂时不支持。(配置业务域名可参考这篇腾讯官方文档:https://kf.qq.com/touch/sappfaq/171102ue6viI171102jm63uy.html),在这篇文档的底部有几个注意事项需要关注,尤其时联合开发的时候:一个小程序最多配置20个安全业务域名,每个域名最多绑定20个小程序,一年内修改域名的次数不能超过50次(次数这个限制要注意了,所以输入域名的时候一定要谨慎一点); 二.web-view 跳转到 小程序从 web-view 的网页跳转到小程序,坑就比较多啦 官方文档提供了相应的接口,但最重要的是需要引入JSSDK,jssdk官方文档请看这里(https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115) [图片] 注意: 但是请注意这一点!很重要,虽然下图中提示不进行wx.config无法使用jssdk,但是!实际情况,使用上图中 wx.miniProgram 的一系列方法只需要引入jssdk就好了,并不需要进行复杂的注册(跳转调用的是navigateTo方法,不需要注册,不需要注册,不需要注册),如果你需要使用jssdk的其他接口方法,请务必按api进行注册。 [图片] 重点注意: 这里要说的就是我遇到的坑啦。 ①如果你是用HTML写的H5网页,那么你只需要在要跳转的页面通过<script>引入即可 <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script> ②如果你是VUE开发的H5网页,那就需要用npm安装导入(文档看这里 https://www.npmjs.com/package/weixin-js-sdk) 安装: npm install weixin-js-sdk 引入: import wx from "weixin-js-sdk" ; 使用: var wx = require('weixin-js-sdk') 三.小程序跳转到小程序小程序跳转小程序,官方文档看这里(https://developers.weixin.qq.com/miniprogram/dev/api/wx.navigateToMiniProgram.html) 一开始没有仔细看文档......以为简单的在判断逻辑里面加跳转方法就好了,结果!我的基础库是2.3.0以上的,当然没有生效了,原因是这一句 [图片] 所以,需要多做一步,我的解决方法是在逻辑判断里面添加了modal确认跳转提示框,在确认事件里引入了跳转,多了一步确认,哈哈, wx.showModal({ title: '提示', content: '您已xxx,将跳转至xxxx', confirmColor: "#1aad19",//设置确认按钮为绿色 showCancel: false,//不显示取消按钮 success: function (sm) { wx.navigateToMiniProgram({ appId: "需要跳转的小程序的appId", path: '跳转页面的路径如path/index/index', extraData: {//传递的参数 id: id }, envVersion: "develop",//线上版固定为release,开发为develop,体验版为trial success(res) { // 打开成功 console.log("跳转成功"); } }); } }) 注意: 跳转还需要在app.json中配置可跳转的小程序的id集合,上图最后也有提示到。配置文档在这里(https://developers.weixin.qq.com/miniprogram/dev/framework/config.html#%E5%85%A8%E5%B1%80%E9%85%8D%E7%BD%AE),如图添加 [图片] 这个如果跳转成功的话,在开发者工具中就可以得到验证,2.3.0以上会提示将打开xx小程序,是否同意,成功~
2020-07-03 - 微信小程序图表Demo(附上源码)
开源微信小程序图表Demo github地址 https://github.com/kesixin项目:WX-Graphic 线性图[图片] 滚动线性图[图片] 柱状图[图片] 扇形图[图片] 环形图[图片] 区域图[图片] 雷达图[图片] <!--index.wxml--> <view class="container"> <view bindtap="gotoPage" data-page="line" class="mt20 list-item">线性图</view> <view bindtap="gotoPage" data-page="scrollline" class="mt20 list-item">滚动线性图</view> <view bindtap="gotoPage" data-page="column" class="mt20 list-item">柱状图</view> <view bindtap="gotoPage" data-page="pie" class="mt20 list-item">扇形图</view> <view bindtap="gotoPage" data-page="ring" class="mt20 list-item">环形图</view> <view bindtap="gotoPage" data-page="area" class="mt20 list-item">区域图</view> <view bindtap="gotoPage" data-page="radar" class="mt20 list-item">雷达图</view> </view>
2020-07-03 - 云开发如何接入微信支付
本文背景答题小程序付费版,需要用户付费才能刷题,基于这个场景,今晚做了云开发的支付功能 本文内容本文主要接前文如何接入微信支付,具体会上代码,这样更实际一些, 云开发如何接入微信支付实践? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000ecc7c48034043169ab04f456813 初始代码为官方示例代码,具体改动四个地方 1、outTradeNo 2、subMchId 3、envId 4、functionName 参考文章 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/wechatpay.html https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/open/pay/CloudPay.unifiedOrder.html 微信支付云调用拼夕夕版尝鲜踩坑教程 [拎包哥]? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000cc2d036c690ec576acce8656813 云开发怎么在原生微信支付回调函数传参?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/0002aab17f8ae88db95a38cf451800 新能力|云调用支持微信支付啦!? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000ae40c258c58219d5a48afc56813 [图片] 2[图片] 3 [图片] [图片] 界面截图 [图片] [图片] 调试信息[图片] [图片] [图片] 本文总结这次接入微信支付,从开始敲代码,到调通花了不到一个小时,其间遇到几个问题,但是都是自己的疏忽造成的, 微信支付云调用能力,对于接入微信支付来说太容易了,小白福音来了 参考视频 bilibili李东教学 https://www.bilibili.com/video/BV1uz411B7Kb
2020-07-06 - 小程序状态管理方案
详细的文档 [图片] [图片] redux 小程序适配方案。在小程序开发中使用 [代码]redux[代码] 管理全局状态。 尽管小程序入门门槛非常之低,但是在项目不停的迭代过程中,不可避免的项目代码复杂度也会越来越高,从前我们可以将跨页面数据管理在Storage或者SessionStorage中,利用一定的代码规范来管理不同页面不同开发者的数据,但随着时间的推移这种方式会造成代码、数据过于分散,且容易出错覆盖。再者每个页面间都需要手动的去 [代码]storage[代码] 读取数据,略显繁琐。 当项目开始变得复杂,我们想要统一的管理起状态数据,自动的同步、分发数据到需要的页面、组件([代码]Reactive[代码])。 安装 [代码]# 使用npm安装 npm i -S @wxa/redux # 使用yarn安装 yarn add @wxa/redux [代码] 基本用法 挂载插件 在[代码]app.js[代码]/[代码]app.wxa[代码]中挂载插件 [代码]// app.js or app.wxa import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers} from '@wxa/redux' import promiseMiddleware from 'redux-promise'; // 注册插件 wxa.use(wxaRedux, { reducers: combineReducers(...your reducer), middlewares: [promiseMiddleware] }) @App export default class Main {}; [代码] 注册完 redux 插件之后,将会自动的调用 [代码]redux.createStore[代码] 创建一个用于存储全局状态数据 [代码]store[代码],并且插件会在自动的挂载 [代码]store[代码] 到 App、Component、Page 实例中 [代码]$store[代码] 。 通过 [代码]this.$store.getState()[代码]可以获得所有全局状态。 通过 [代码]this.$store.dispatch()[代码]可以提交一个状态修改的 action。 更详细的 store api 获取全局状态 在页面/组件类中定义 [代码]mapState[代码] 对象,指定关联的全局状态(在[代码]react[代码]中叫[代码]connect[代码])。 [代码]import {Page} from '@wxa/core'; @Page export default class Index { mapState = { todolist$ : (state)=>state.todo, userInfo$ : (state)=>state.userInfo } add() { // dispatch change state. // todo list will auto add one. this.$store.dispatch({type: 'Add_todo_list', payload: 'coding today'}); } } [代码] 然后再[代码]template[代码]中就可以直接使用映射的数据了。 [代码]<view>{{userInfo$.name}}</view> <view wx:for="{{todolist$}}">{{key+1}}{{item}}</view> [代码] 得益于 [代码]@wxa/core[代码] 的 diff方法,redux在同步数据的时候只会增量的修改数据,而不是全量覆盖 😁 在任意位置获取全局状态数据 编写一些通用的基础函数提供给页面调用的时候,可能会需要从 [代码]store[代码] 中读取相应数据做处理。 例如在我们需要在所有请求的 postdata 中统一的加上用户的基本信息,可以这么实现: [代码]// 任意 api.js import {fetch} from '@wxa/core'; import {getStore} from '@wxa/redux'; export default const customFetch = (...args) => { let {idNo, name} = getStore().getState().UserModel; // 每个请求自动添加用户 args[1] = { idNo, name ...args[1], }; return fetch(...args); } [代码] 个性化页面数据 有时我们可能需要临时改写一下数据用于展示,实现类似 [代码]vue[代码] [代码]computed[代码] 的效果,此时我们可以相应的改造 mapState。 [代码]export default class A { mapState = { userInfo$(state){ let model = state.UserModel; // 自动掩码用户的身份证、姓名 // diff 数据并自动调用 setData this.$diff({ idNoCover: model.idNo.replace(/([\d]{4})(\d{10})([\dxX]{4})/, '$1***$3') }) return model } } } [代码] 分包用法 当小程序应用开始使用分包技术的时候,redux 方案也需要相应的做出优化,分包有以下特点: 引用原则 packageA 无法 require packageB JS 文件,但可以 require app、自己 package 内的 JS 文件 packageA 无法 import packageB 的 template,但可以 require app、自己 package 内的 template packageA 无法使用 packageB 的资源,但可以使用 app、自己 package 内的资源 即当分包 A 定义了自己业务逻辑的数据 model 之后,且该 model 无法被其他分包复用,则我们完全可以把对应 model 放到分包的页面中,懒加载对应 [代码]redux.reducer[代码],以此减少主包体积。 为了做到懒加载对应的 reducer,我们需要在改造一下我们的代码。 挂载插件 在[代码]app.js[代码]/[代码]app.wxa[代码]中,改造 [代码]reducer[代码] 的注册方式。 [代码]// app.js or app.wxa import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers} from '@wxa/redux' import promiseMiddleware from 'redux-promise'; // 注册插件 wxa.use(wxaRedux, { // reducers: combineReducers(...your reducer), reducers: { UserModel: userReducer, AppModel: appReducer, ...your reducer }, middlewares: [promiseMiddleware] }) @App export default class Main {}; [代码] 动态添加分包 Reducer 假设我们在分包 A 中定义了专门用于订单处理的 [代码]reducer[代码],分包入口页面为 [代码]subpages/A/pages/board[代码]。 在分包页面被使用之前我们需要动态的注册一个新的 [代码]reducer[代码]。 [代码]// subpages/A/pages/board import {reducerRegistry} from '@wxa/redux'; import AOrderModel from '/subpages/A/models/order.model.js'; // 注册对应的数据 model reducerRegistry.register('AOrderModel', AOrderModel); [代码] 注册完毕之后,后续所有分包 A 的页面都可以正常的使用 [代码]mapState[代码] 中映射页面需要使用的状态数据。 调试 Redux [代码]@wxa/redux[代码] 提供了小程序 [代码]redux-remote-devtools[代码] 的适配代码。稍微改造一下我们的挂载插件部分的代码即可使用: [代码]import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers, applyMiddleware} from '@wxa/redux' import { composeWithDevTools } from '@wxa/redux/libs/remote-redux-devtools.js'; import promiseMiddleware from 'redux-promise'; const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 }); // 注册插件 wxa.use(wxaRedux, { // reducers: combineReducers(...your reducer), reducers: { UserModel: userReducer, AppModel: appReducer, ...your reducer }, middlewares: composeEnhancers(applyMiddleware(promiseMiddleware)) }) [代码] 打开开发者工具不校验合法域名开关,就可以正常使用 [代码]redux-devtools[代码] 了。 由于 [代码]devtools[代码] 仅用于开发阶段,我们可以利用 [代码]wxa[代码] 提供的依赖分析能力,按需引入。 改写上续配置如下: [代码]import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers, applyMiddleware} from '@wxa/redux' import promiseMiddleware from 'redux-promise'; let composeEnhancers = (m) => m; if (process.env.NODE_ENV === 'production') { let composeWithDevTools = require('@wxa/redux/libs/remote-redux-devtools.js').composeWithDevTools; composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 }); } // 注册插件 wxa.use(wxaRedux, { // reducers: combineReducers(...your reducer), reducers: { UserModel: userReducer, AppModel: appReducer, ...your reducer }, middlewares: composeEnhancers(applyMiddleware(promiseMiddleware)) }) [代码] 如上配置,当 [代码]process.env.NODE_ENV[代码] 设置为生产环境的时候,[代码]@wxa/redux/libs/remote-redux-devtools.js[代码] 将不会被打包进 [代码]dist[代码] 持久化数据 某些场景,为了用户体验,我们需要将对应数据缓存下来,方便下次用户可以直接看到对应页面,此时我们需要将 [代码]store[代码] 的数据缓存下来,这里我们使用 [代码]redux-persist[代码] 用于持久化数据。 示例如下: [代码]import {wxa, App} from '@wxa/core'; import wxaRedux from '@wxa/redux'; import wxPersistStorage from '@wxa/redux/libs/wx.storage.min.js'; import {persistStore, persistReducer} from 'redux-persist'; import orderModel from './order.model.js'; let persistOrderModel = persistReducer({ key: 'orderModel', storage: wxPersistStorage, timeout: null, // 超时时间,设置为 null }, orderModel); wxa.use(wxaRedux, { reducers: { orderModel: persistOrderModel } }) @App export default class { onLaunch() { // 冷启动开始就加载缓存数据 persistStore(this.$store, {}, ()=>this.$storeReady=true); } } [代码] 实时日志 我们可以结合小程序实时日志和 [代码]redux-logger[代码] 一起使用。 [代码]import {wxa, App} from '@wxa/core'; import {createLogger} from 'redux-logger'; import wxaRedux from '@wxa/redux'; let log = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : console; let logger = createLogger({ logger: log }); wxa.use(wxaRedux, { reducers: {...your reducers}, middlewares: [logger] }); [代码] 配置完毕之后,项目中所有的 [代码]Action[代码] 日志都将上报到微信的实时日志后台,开发者可以登录 mp.weixin.qq.com 查看用户所有操作记录。 配置 reducers 类型: Function [代码]combineReducers(...reducer)[代码]的返回 Object [代码]reducer[代码] 列表,用于动态注册场景 middlewares 类型: Array redux 中间件列表 Function [代码]applyMiddleware(...middlewares)[代码]的返回 initialState 类型: any reducer 初始状态,参考 [代码]redux 文档[代码] debug 类型: Boolean [代码]false[代码] 是否打印插件日志 技术细节 [代码]wxa/redux[代码]根据不同的实例类型有不同的任务,在App层,我们需要创建一个[代码]store[代码]并挂载到app中,在[代码]Page[代码]和[代码]Component[代码]层,我们做了更多细节处理。 App Level 创建[代码]store[代码],应用redux的中间件,挂载[代码]store[代码]到App实例。 Page Level 在不同的生命周期函数,有不同的处理。 [代码]onLoad[代码] 根据[代码]mapState[代码]订阅[代码]store[代码]的数据,同时挂载一个[代码]unsubscribe[代码]方法到实例。 [代码]onShow[代码] 标记页面实例[代码]$$isCurrentPage[代码]为[代码]true[代码], 同时做一次状态同步。因为有可能状态在其他页面做了改变。 [代码]onHide[代码] 重置[代码]$$isCurrentPage[代码],这样子页面数据就不会自动刷新了。 [代码]onUnload[代码] 调用[代码]$unsubscribe[代码]取消订阅状态 Component Level 针对组件生命周期做一些单独处理 [代码]created[代码] 挂载[代码]store[代码] [代码]attached[代码] 订阅状态,并同步状态到组件。 [代码]detached[代码] 取消订阅
2020-07-05 - 开源小程序-头像加口罩
来吧,请不要吝啬你的star [图片] 1、我不是作者,但是作者同意我在社区发帖,想联系作者的私聊我吧 2、开发环境:基于uniapp使用VUE快速实现 3、这个小程序从起名字到运营,基本走的是我的运营思路,2月份的时候我说过这个小程序,目前衍生比较完整,每月差不多4位数收益,累计用户10W+ [图片] 4、有图片安全检测,可放心使用 5、部分功能预览 [图片] [图片] https://github.com/infinityu/mina-wear-mask 不要吝啬你的STAR
2020-07-05 - 使用钩子,解耦应用生命周期上触发的业务逻辑
/* Pubsub.js 定义钩子基础 on监听 emit触发 off脱离监听 */ function Pubsub(){ //存放事件和对应的处理方法 this.handles = {}; } Pubsub.prototype={ //传入事件类型type和事件处理handle on: function (type, handle) { if(!this.handles[type]){ this.handles[type] = []; } this.handles[type].push(handle); }, emit: function () { //通过传入参数获取事件类型 let type = Array.prototype.shift.call(arguments); if(!this.handles[type]){ return false; } for (let i = 0; i < this.handles[type].length; i++) { let handle = this.handles[type][i]; //执行事件 handle.apply(this, arguments); } }, off: function (type, handle) { handles = this.handles[type]; if(handles){ if(!handle){ handles.length = 0;//清空数组 }else{ for (let i = 0; i < handles.length; i++) { let _handle = handles[i]; if(_handle === handle){ handles.splice(i,1); } } } } }, /* //数据格式处理 dataHandle: function (data) { if(!data) { return } Object.keys(data).forEach(item => { // 性别格式化 if (item.indexOf('_sex') > -1 && data[item] !== null && data[item] !== undefined) { if (data[item] === '0' || data[item] === 'female' || data[item] === 0) { data[item] = '女' } else if (data[item] === '1' || data[item] === 'male' || data[item] === 1) { data[item] = '男' } } }) return data },*/ } export default Pubsub /* businessHook.js*/ import baseHook from "@/base" const businessDataHook = new baseHook() businessDataHook.on('selectPage', (data) => { console.log('do something') // dos something when emit the event 'selectPage' }) export default peopleDataHook /* businessPage.js */ import businessHook from "@/businessHook" // 业务触发‘selectPage’将执行businessHook中的on selectPage。执行打印 :do something businessHook.emit('selectPage', 'selectSomething') 如果要在小程序生命周期中,需要触发很多的不相干的业务逻辑,且多处要触发同样的事件,可用以上方式解耦。 目前我用的业务逻辑是,当用户登录/用户注册/小程序显示都需要往缓存更新数据,同时向用户行为手收集平台上传数据。
2020-07-06 - 动手打造更强更好用的微信开发者工具-编辑器扩展篇
1. 写在前面 1.1 微信开发者工具现状 具备一些基本的通用IDE功能,但是第三方的支持扩展需要加强。 1.2 开发者工具自带的编辑器扩展功能 可能很多老铁没用过官方的微信开发者工具的编辑器扩展(我一般称为编辑器插件)。官方把这块功能也隐藏得很深,也没有相关文档介绍,但是预留了相关的入口。合理利用第三方编辑器插件,可以极大的提升开发效率。下面先来看看官方预留的编辑器插件入口: [图片] (图一) 2. 几个不错插件安装效果 2.1 标签高亮插件-vincaslt.highlight-matching-tag [图片] 功能:可以把当前行对应的标签开头和结尾高亮起来,让开发者一目了然 2.2 小程序开发助手插件-overtrue.miniapp-helper [图片] 功能:必须要说的这个是纯国产的插件,里面的代码片段功能很全,具体介绍:小程序开发助手 - Visual Studio Marketplace https://marketplace.visualstudio.com/items?itemName=overtrue.miniapp-helper 2.3 minapp插件-qiu8310.minapp-vscode [图片] 功能:这个是今天的明星插件,里面的跳转功能很强,可以在wxml里CMD+点击对应变量/方法和CSS样式名称直接跳转到对应的js/wxss文件对应的地方。具体的下面是官方介绍: 标签名与属性自动补全 根据组件已有的属性,自动筛选出对应支持的属性集合 属性值自动补全 点击模板文件中的函数或属性跳转到 js/ts 定义的地方(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 样式名自动补全(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 在 vue 模板文件中也能自动补全,同时支持 pug 语言 支持 link(纯 wxml 或 pug 文件才支持,vue 文件不支持) 自定义组件自动补全(纯 wxml 文件才支持,vue 或 pug 文件不支持) 模板文件中 js 变量高亮(纯 wxml 或 pug 文件才支持,vue 文件不支持) 内置 snippets 支持 emmet 写法 wxml 格式化 3. DIY添加适合自己的插件 3.1 添加插件功能简介 仔细研究过微信开发者工具的人可能知道或者了解,其实微信开发者工具编辑器跟微软的开源编辑器vsCode「颇有渊源」。再深入研究发现,vsCode的插件完全可以无缝移植到微信开发者工具编辑器里来,所以今天的内容就是移植vsCode的插件到微信开发者工具。咱们先看看微信开发者工具自带的「管理编辑器扩展」功能(图1标注为2的地方) [图片](图二) 3.2 插件添加具体步骤 3.2.1 安装插件,获取插件文件 安装vsCode并安装你需要移植的插件,必须要说的是vsCode的插件非常多,好的插件也很多。相关安装,搜索插件教程建议大家百度相关教程。或者直接下载vsCode亲自体验,插件安装过程还是非常简单的。 3.2.2 复制插件文件夹 找到vsCode相关插件的安装文件夹: 操作系统 安装路径 windows %USERPROFILE%.vscode\extensions macOS ~/.vscode/extensions Linux ~/.vscode/extensions 复制对应插件文件夹到微信开发者工具的「打开编辑器扩展目录」(图一标注为1的地方) 3.2.3 添加插件配置文件 新版开发者工具直接进入图形设置,扩展设置里勾选对应插件即可。如下图: [图片] 旧版操作方法:进入微信开发者工具的「管理编辑器扩展」功能页面,在尾端加入对应添加的插件名称。以以上3个介绍的插件为例,在原来的尾端加入: “vincaslt.highlight-matching-tag”, “overtrue.miniapp-helper”, “qiu8310.minapp-vscode” 3.2.4 见证奇迹 重启微信开发者工具,见证插件带来的编码便利吧! 4 需要注意的 vsCode的插件很多,小程序相关的也越来越多了,但是插件质量参差不齐,所以安装时建议选择「标星」star比较多的插件。
2020-05-02 - 微信小程序云开发数据库脚手架
介绍 1、封装、简化、模型化数据库操作 2、支持查看执行的sql语句 基础库 大于2.8.1 代码片段DEMO 小程序demo: https://developers.weixin.qq.com/s/oXiGFFmI7mia 说明文档: https://www.yuque.com/docs/share/80cbef90-f262-4d2a-b245-079bc462d5e3 如何使用 一、小程序端 1、下载wx-cloud-db-falsework npm i wx-cloud-db-falsework 2、将wx-cloud-db-falsework.min.js拷贝到小程序项目中,如小程序根目录下的lib文件夹内 [图片] 3、在app.js中引入wx-cloud-db-falsework.min.js,使用重写过的Page,和原生Page一致,只是增加了use属性 [代码]// 根目录/app.js import { page } from './lib/wx-cloud-db-falsework.min' Page = page App({ onLaunch: function () {} } [代码] 4、创建模型,如: Order,根目录/model/order.js,集合名,默认为model名,首字母会强制小写 [代码]// 根目录/model/order.js import { Model, Controller } from '../lib/wx-cloud-db-falsework.min' class Order extends Model { constructor(o) { let t = super(o) return t } } // collectionName 集合名,默认为model名,如Order默认为order,首字母会小写 // collectionKey 集合主键,默认_id const Odr = Order.init({ env: '环境ID', cloud: wx.cloud [, collectionName = '集合名称', , collectionKey = '主键字段名'] }) export { Odr as Order, Controller } [代码] 5、创建控制器,如: Order的controller,根目录/controller/order.js [代码]// 根目录/controller/order.js import { Order, Controller as Base } from '../model/order' class Controller extends Base { constructor(o){ super(o) } // 在控制器里可以自己封装一些业务功能方法 getById(id){ return Order.find(id) } } let controller = new Controller(Order) export { controller } [代码] 6、在页面中通过use配置需要使用的控制器,控制器实例会挂载在页面实例上,直接通过 this.控制器实例 即可访问,页面: 根目录/pages/index/index.js [代码]// 根目录/pages/index/index.js import { DB } from '../../lib/wx-cloud-db-falsework.min' Page({ use:{ Order: require('../../controller/order.js') }, data:{}, onLoad: async function() { // 直接通过 this.控制器实例 即可访问控制器的封装的方法 await this.Order.getById(1).then(res=>{ console.log(res) // res 可为 DB.DbError实例 或者 DB.DbRes实例 // 为DB.DbError实例,表示数据库操作异常 // 为DB.DbRes实例,表示数据库操作正常,而DB.DbRes又包括了DB.DbResultJson和DB.DbResultArray子实例 // DB.DbResultJson实例说明结果为JSON对象,DB.DbResultArray实例则说明结果为Array对象 }) // 页面中也可以通过 this.(控制器实例名+'Md')访问模型的方法 let row = await this.OrderMd.find('xxxx') // 页面中也可以通过 this.(控制器实例名+'Dd')访问原生db对象,进行原生操作 let doc = await this.OrderDb.collection('xxx').doc(xx).get() } } [代码] 二、云函数端 1、配置云函数package.json,添加wx-cloud-db-falsework [代码]"dependencies": { "wx-cloud-db-falsework": "latest", "wx-server-sdk": "^2.1.2" } [代码] 2、配置云函数package.json,添加wx-cloud-db-falsework [代码]const cloud = require('wx-server-sdk') let { Model, DB } = require('wx-cloud-db-falsework') # order模型 class Order extends Model { constructor(o) { let t = super(o) return t } } exports.main = async (event, context) => { const Odr = Order.init({ env: '环境ID', cloud }) return await Odr.add({ _id: (new Date().getTime())+'add'}) } [代码] 数据库操作 添加记录 以上述Order模型为例,在页面中添加记录 [代码]// 使用model.add(data)添加记录, data为数据json对象或array对象 // 添加一条记录 let res = await this.OrderMd.add({ a:1, b:2 }) // 返回DbResultJson对象 {_id:'xxxx'} // 添加多条记录 res = await this.OrderMd.add([{ a:1, b:2 }, { a:3, b:4 }]) // 返回DbResultArray对象 [{_id:'xxxx'},{_id:'xxxx'}] // 支持原生参数 res = await this.OrderMd.add({ data:{ a:1, b:2 } }) res = await this.OrderMd.add({ data: [{ a:1, b:2 }, { a:3, b:4 }] }) // 支持回调函数 this.OrderMd.add({ data:{ a:1, b:2 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.add({ data:[{ a:1, b:2 }, { a:3, b:4 }], success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 删除记录 以上述Order模型为例,在页面中删除记录 [代码]// 使用model.remove(where)删除记录, where为删除添加,可为空 // 根据条件删除记录 let res = await this.OrderMd.where({ _id: 'xxx', abc: 123 }).remove() res = await this.OrderMd.remove({ _id: 'xxx', abc: 123 }) // 根据主键删除 res = await this.OrderMd.doc('xxx').remove() // 返回DB.DbError对象或DB.DbRes对象 {removed:Number} // 支持回调函数 this.OrderMd.where({ _id: 'xxx', abc: 123 }).remove({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.remove({ where:{ _id: 'xxx', abc: 123 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 查询记录 查询一条记录 以上述Order模型为例,在页面中查询记录 [代码]// 使用model.find()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ _id: 'xxx', abc: this.OrderMd._.eq(123) }).find() res = await this.OrderMd.find({ _id: 'xxx', abc: this.OrderMd._.eq(123) }) // 根据主键查询记录 res = await this.OrderMd.doc('xxx').find() res = await this.OrderMd.find('xxx') // 返回DB.DbError对象或DB.DbRes对象 {_id:'xxx', ....} // 使用model.where().get()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ _id: 'xxx' }).get() // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}] // 根据主键查询记录 res = await this.OrderMd.doc('xxx').get() // 返回DB.DbError对象或DB.DbRes对象 {_id:'xxx', ....} // 支持回调函数 this.OrderMd.find({ where:{ _id: 'xxx', abc: 123 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ _id: 'xxx', abc: 123 }).find({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ _id: 'xxx' }).get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.doc('xxx').get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 特殊支持: [代码]// json字符串条件 let res = await this.OrderMd.find(`{ _id: 'xxx' }`) res = await this.OrderMd.where(`{ _id: 'xxx' }`).find() // 使用command时需要加this res = await this.OrderMd.find(`{ _id: this._.eq('xxx') }`) res = await this.OrderMd.where(`{ _id: this._.eq('xxx') }`).find() [代码] 查询多条记录 [代码]// 使用model.findAll()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ abc: 'xxx', abc: this.OrderMd._.eq(123) }).findAll() res = await this.OrderMd.findAll({ abc: 'xxx', abc: this.OrderMd._.eq(123) }) // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}] // 使用model.where().get()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ _id: 'xxx' }).get() // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}] // 支持回调函数 this.OrderMd.findAll({ where:{ abc: 'xxx' }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ abc: 'xxx' }).findAll({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ _id: 'xxx' }).get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.doc('xxx').get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 特殊支持: [代码]// json字符串条件 let res = await this.OrderMd.findAll(`{ abc: 'xxx' }`) res = await this.OrderMd.where(`{ abc: 'xxx' }`).findAll() // 使用command时需要加this res = await this.OrderMd.findAll(`{ abc: this._.eq('xxx') }`) res = await this.OrderMd.where(`{ abc: this._.eq('xxx') }`).findAll() [代码] 更新记录 以上述Order模型为例,在页面中查询记录 [代码]// 使用model.update()更新记录 // 根据条件更新记录 let res = await this.OrderMd.where({ _id: 'xxx', abc: this.OrderMd._.eq(123) }).update({ cde: 456 }) res = await this.OrderMd.update({ cde: 456 }, { _id: 'xxx', abc: this.OrderMd._.eq(123) }) // 根据主键更新记录 res = await this.OrderMd.doc('xxx').update({ cde: 456 }) // 返回DB.DbError对象或DB.DbRes对象 {updated:Number} // 支持回调函数 this.OrderMd.where({ _id: 'xxx', abc: this.OrderMd._.eq(123) }).update({ data:{ cde: 456 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 聚合操作 以上述Order模型为例,在页面中查询记录 [代码]// 使用model.aggregate()开始聚合操作 let res = await this.OrderMd.aggregate() .addFields({ test: 1 }) .match({ test: 1 }) ... // 其他聚合操作 .end() // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}, ...] [代码] 数据监听 以上述Order模型为例,在页面中监听数据 [代码]// 使用model.wcthis(ops)监听记录, let res = await this.OrderMd.wcthis({ onChange(res){ // 监听操作 }, onError(err){ // 监听出错 } }).where({ _id: 'xxx' }).update({ cde: 456 }) // 使用原生watch方法监听记录, await this.OrderMd.where({ _id: 'xxx' }).watch({ onChange(res){ // 监听操作 }, onError(err){ // 监听出错 } }) let res = this.OrderMd.where({ _id: 'xxx' }).update({ cde: 456 }) [代码] 数据库操作结果 数据库操作结果返回 DB.DbError实例 或者 DB.DbRes实例 add结果 res = model.add(…) 添加一条数据返回 { _id: ‘xxx’ } ,添加多条时返回 [{ _id: ‘xxx’ }, { _id: ‘xxx’ }] 返回的结果可以直接调用update,remove方法,如: [代码]// 添加一条记录 let res = await this.OrderMd.add({ a:1, b:2 }) // 返回DbResultJson对象 {_id:'xxxx'} if(res instanceof DB.DbRes){ // 更新操作,更新刚新增的这条记录 let back = res.update({c: 555}) // 删除操作,删除刚新增的这条记录 back = res.remove() } // 添加多条记录 res = await this.OrderMd.add([{ a:1, b:2 }, { a:3, b:4 }]) // 返回DbResultArray对象 [{_id:'xxxx'},{_id:'xxxx'}] if(res instanceof DB.DbRes){ // 更新操作,更新刚新增的这几条记录 let back = res.update({c: 555}) // 删除操作,删除刚新增的这几条记录 back = res.remove() } [代码] remove结果 res = model.remove() 删除记录返回 { removed:Number } [代码]// 删除记录 let res = await this.OrderMd.remove({ a:1, b:2 }) // 返回DbResultJson对象 {removed:Number} if(res instanceof DB.DbRes){ if(res.removed){ // 删除成功 }else{ // 删除失败 } }else{ consloe.errr(res) } [代码] find/findAll/get结果 res = model.find() 返回 { _id: ‘xxx’, … } res = model.findAll/get() 返回 [{ _id: ‘xxx’, … }, { _id: ‘xxx’, … }, …] 返回的结果可以直接调用update,remove方法,如: [代码]// 添加一条记录 let res = await this.OrderMd.doc('xxx').find() // 返回DbResultJson对象 {_id:'xxxx'} if(res instanceof DB.DbRes){ // 更新操作,更新这条记录 // let back = res.update({c: 555}) // 删除操作,删除这条记录 let back = res.remove() } // 添加多条记录 res = await this.OrderMd.where({abc:123}).findAll() res = await this.OrderMd.where({abc:123}).get() // 返回DbResultArray对象 [{ _id: 'xxx', ... }, { _id: 'xxx', ... }, ...] if(res instanceof DB.DbRes){ // 更新操作,更新这几条记录 // let back = res.update({c: 555}) // 删除操作,删除这几条记录 let back = res.remove() } [代码] update结果 res = model.update() 更新数据返回 { updated: Number } [代码]// 更新记录 let res = await this.OrderMd.where({ a:1, b:2 }).update({c:555}) res = await this.OrderMd.doc('xxx').update({c:555}) // 返回DbResultJson对象 {update:Number} if(res instanceof DB.DbRes){ if(res.update){ // 更新成功 }else{ // 更新失败 } }else{ consloe.errr(res) } [代码] 查看SQL语句 以上述Order模型为例,在页面中查询记录 数据库操作结果返回 DB.DbRes 对象时,可查看执行的SQL语句 [代码]var res = await this.OrderMd.add({ a:1, b:2 }) console.log(res.sql) var res = await this.OrderMd.remove({ a:1, b:2 }) console.log(res.sql) var res = await this.OrderMd.doc('xxx').find() console.log(res.sql) var res = await this.OrderMd.where({ a:1, b:2 }).update({c:555}) console.log(res.sql) var res = await this.OrderMd.aggregate() .addFields({ test: 1 }) .match({ test: 1 }) .end() console.log(res.sql) [代码] ps: 可以拿去练练手,不保证无BUG
2020-07-18