- 1000%增长!我仅用一个小时搞定!AI智能体+AI小程序=MVP王炸组合!
前言[图片] 在万圣节的前一晚上10月30日,一位运营朋友跟我说了个点子万圣节头像生成器,然后大概给我分析了下整体思路,于是我用扣子Coze平台(coze.cn)搭建了一个AI智能体整个过程花了一个小时就搞定了!我一键部署到了我的AI小程序上,第二天随便发了下小程序访问页面数据直接增长1000%,接下来我来拆解下这个全过程。 不了解扣子Coze平台可以这篇《教你 5 分钟搭建 AI 应用(无需编码)》 [图片] 拆解10月30日的晚上收到了朋友发来的消息 [图片] 然后我整体体验了一下需要收集用户的头像、性别、绘画风格、万圣节元素,然后生成万圣节头像。 当我体验后,感觉效果挺不错,并且这位朋友还给我分析了整个实现的思路以及AI绘画的提示词。 [图片] 我基于这个思路做了一些交互上的优化,比如她的案例是收集用户头像,然后让用户去选择性别和绘画风格以及元素,这种形式属于表单收集的方式,但是AI智能体是对话的形式,如果在对话形式中去收集这么多信息,对于用户来说非常麻烦,所以我改为只需要用户发送一个照片,性别从头像里面提取,然后绘画风格和元素进行随机。 这样做有两个好处: 用户使用阻力更小,降低门槛生成结果变化更多,增加趣味[图片] 外层逻辑确认后就开始搭建整个工作流,工作流非常简单,只有5个节点。 [图片] 第一个消息节点 输出随机风格和元素+加载动画,让用户有期待让用户更容易等下去。 [图片] 第二个「图片理解」插件节点 提取用户上传的照片特征为了后续生成图片提示词,使用的是「阶跃星辰」的视觉理解大模型,识别图片速度很快和准确度很高。 视觉理解大模型:https://platform.stepfun.com/docs/guide/image_chat [图片] promptText:图片是一个人的照片,请你描述ta的肤色、头发、眼睛、鼻子、嘴巴、脸型、面部特征、国籍或种族特征、身高、体型、年龄、表情、妆容、眼镜、耳饰、动作、服饰特征、配饰。 输出内容参考案例格式:性别:女,脸型:瓜子脸,体型:微胖,年龄:40岁左右,面貌:亚洲女性,肤色:浅色 外貌:一头长长的黑发,戴着一顶黑色的报童帽 表情:很平静,嘴角微微上扬 妆容:化了淡妆 动作:伸出手轻轻抚摸马的鼻子 服饰:穿黄色长袖衬衫、深色长裤,斜挎黑色Champion品牌挎包 第三个「文本处理」节点 主要是组装AI绘画生成图片的提示词,结合随机的风格+元素+图片理解后的人物特征。 [图片] 第四个「图片生成」插件节点 将上个组装好的提示词给到AI绘画进行图片生成,这里使用「阶跃星辰」的文生图大模型,生成图片速度很快,而且质量挺不错的。 文生图模型:https://platform.stepfun.com/docs/guide/image_generate [图片] 第五个消息节点 输出生成图片的结果显示,最后提示用户每次生成不一样,引导用户再次生成。 [图片] 这个AI智能体搭建完成后,我们团队搭建了一套接入扣子API的小程序,只需要在后台通过BotId就能发布智能体到我们的AI小程序里面,无需开发和发版本。 扣子API:https://www.coze.cn/open 总结这次追热点让我想到了最近看的一本书《上瘾》里面提到的公式集合这个案例来看: 触发(节日热点引发)+行动(上传头像生成)+多变的酬赏(随机的组合结果)+投入(想再看看别的组合)=上瘾 还有一点,正如《我能写什么内容?》文中提到过一个观点: 我认为当你想做一个产品的时候,很多功能不一定要通过写代码的方式去解决,可以用智能体搭建平台的插件和工作流去解决。用代码开发功能可能都需要一天时间才能完成,而在平台智能体搭建一个小时就搞定了,然后再暴露API出来就可以很快的完成这个产品了。我认为AI智能体+AI小程序=MVP(最小可行性产品)王炸组合,它可以有效降低成本的试错,当你没有赚钱的时候省钱才是硬道理
11-12 - 解锁小程序中使用SVG新姿势
SVG 的优势 清晰度: 可以进行放大,而不失真 更小的文件体积 可扩展性,可以动态颜色 动效 可以添加动效 在小程序中使用 目前小程序 的image标签已经支持了 svg 的显示 [代码] <image src="./xx.svg"/> [代码] 如何动态的改变 svg 属性呢? 大体思路:把svg转成 base64 然后通过 image标签 src设置图片,再动态赋值svg颜色 把svg转成base64 如下一个svg 代码文件 [代码]<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="24 24 48 48"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" from="0" to="360" dur="1400ms"></animateTransform><circle cx="48" cy="48" r="20" fill="none" stroke="#eeeeee" stroke-width="2" transform="translate\(0,0\)"><animate attributeName="stroke-dasharray" values="1px, 200px;100px, 200px;100px, 200px" dur="1400ms" repeatCount="indefinite"></animate><animate attributeName="stroke-dashoffset" values="0px;-15px;-125px" dur="1400ms" repeatCount="indefinite"></animate></circle></svg> [代码] 转成base64,其实就是 对这个svg进行 encodeURIComponent 得到 如下代码 [代码]%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20viewBox%3D%2224%2024%2048%2048%22%3E%3CanimateTransform%20attributeName%3D%22transform%22%20type%3D%22rotate%22%20repeatCount%3D%22indefinite%22%20from%3D%220%22%20to%3D%22360%22%20dur%3D%221400ms%22%3E%3C%2FanimateTransform%3E%3Ccircle%20cx%3D%2248%22%20cy%3D%2248%22%20r%3D%2220%22%20fill%3D%22none%22%20stroke%3D%22%23eeeeee%22%20stroke-width%3D%222%22%20transform%3D%22translate%5C(0%2C0%5C)%22%3E%3Canimate%20attributeName%3D%22stroke-dasharray%22%20values%3D%221px%2C%20200px%3B100px%2C%20200px%3B100px%2C%20200px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3Canimate%20attributeName%3D%22stroke-dashoffset%22%20values%3D%220px%3B-15px%3B-125px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3C%2Fcircle%3E%3C%2Fsvg%3E [代码] 拼接base64 [代码] data:image/svg+xml;charset=utf-8,encodeURIComponent后的代码 [代码] 在对应svg属性上动态设置颜色,比如这里用到的是填充颜色 在js文件 data中定义 color 状态 在wxml中动态渲染 [代码] <image src="data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20viewBox%3D%2224%2024%2048%2048%22%3E%3CanimateTransform%20attributeName%3D%22transform%22%20type%3D%22rotate%22%20repeatCount%3D%22indefinite%22%20from%3D%220%22%20to%3D%22360%22%20dur%3D%221400ms%22%3E%3C%2FanimateTransform%3E%3Ccircle%20cx%3D%2248%22%20cy%3D%2248%22%20r%3D%2220%22%20fill%3D%22none%22%20stroke%3D%22%23{{color}}%22%20stroke-width%3D%222%22%20transform%3D%22translate%5C(0%2C0%5C)%22%3E%3Canimate%20attributeName%3D%22stroke-dasharray%22%20values%3D%221px%2C%20200px%3B100px%2C%20200px%3B100px%2C%20200px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3Canimate%20attributeName%3D%22stroke-dashoffset%22%20values%3D%220px%3B-15px%3B-125px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3C%2Fcircle%3E%3C%2Fsvg%3E" /> [代码] [代码]注意:这里的颜色 由于是已经被编码了,所以# 已经被转义了 %23, 直接写颜色数字即可[代码] 当然你也可以 去掉%23 自己实现一个内部方法 [代码] if (color && color.startsWith('#')) { return `%23${color.slice(1)}`; } [代码] 这样其实就实现了 svg的动态渲染,可是这种写法,写在wxml中 不是特别的优雅,那么如何重构下让我们的代码看起来更优雅呢? 把 svg 单独存放 支持动态返回 动态赋值 image src 属性 svg 动态函数 loading.svg.js 文件 [代码]export const loadingSvg = (color='#ddd') =>{ const svgXml = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="24 24 48 48"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" from="0" to="360" dur="1400ms"></animateTransform><circle cx="48" cy="48" r="20" fill="none" stroke="${color}" stroke-width="2" transform="translate\(0,0\)"><animate attributeName="stroke-dasharray" values="1px, 200px;100px, 200px;100px, 200px" dur="1400ms" repeatCount="indefinite"></animate><animate attributeName="stroke-dashoffset" values="0px;-15px;-125px" dur="1400ms" repeatCount="indefinite"></animate></circle></svg>` return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}` } [代码] 逻辑层引入,setData [代码] onLoad(){ const { loadingSvg } = require('./loading.svg.js') const svgImg = loadingSvg('#eee') this.setData({svgImg}) }, [代码] 渲染层使用 [代码] <image src="{{svgImg}}"/> [代码] github 使用案例 demoFormpSvg
2022-04-30 - 微信小程序深度合成类目如何通过?
前言 在8月15日当天我分享了一篇《微信小程序深度合成类目资质如何准备?》,发完文章后有很多朋友私信我,发现有很人和我遇到了同样的问题,这个问题我昨天已经解决了,接下来分享下具体流程和材料。 [图片] 准备材料 最开始需要《安全评估报告》现在已经不需要了,最新的只有自己的AI大模型算法备案或者合作的AI达模型算法备案+合作协议。 第一种方式 在《微信小程序深度合成类目资质如何准备?》这篇有人留言(我没有询问过待办机构,我用的是合作的方式)。 [图片] 不建议这种方式,费用虽然不多,但是时间太久了,等办下来可能产品竞争力就没了。 第二种方式 算法备案证明截图 找市面上已经有AI模型算法备案的公司合作,比如:通义千问(阿里巴巴)、文心一言(百度)、讯飞星火(科大讯飞)等,那么怎么知道公司有没有算法备案?直接在「互联网信息服务算法备案系统」通过公司名称查找。 如:百度 [图片] 我们可以看到这里面就可以查看算法为「生成合成类」的算法备案信息为「正常」状态。 合作协议 确定有算法备案后找相关产品的商户去谈合作,看具体条件和签合同流程(在这里我不做任何公司的推荐,大家根据自身业务需求去体验下已备案的产品,然后再做判断) 在这里要注意合同中要包含算法备案证明截图中的【算法名称】双方公司都需要盖公章。 最后 当然具体你要用什么方式根据自己的实际情况决定,我就是用的第二种方式提供这两个材料就申请通过,最后祝大家都申请通过,产品顺利上线!
2023-08-31 - 最佳实践 | 使用微信云开发,轻松对接微信支付
微信支付作为国内最大的支付服务之一,多年以来一直是各类APP、小程序、Web应用必须对接的公共服务之一。当然,对接微信支付也并非那么简单,除了官方文档、代码示例、SDK以外,依然会有一些较为复杂、繁重的开发工作量,例如: 部署和维护微信支付证书实现微信支付的签名、验签、回调、对账流程搭建微信支付的后台管理系统,实现退款审核、查账单等功能现在,微信云开发推出了全新的 「可视化工作流」 和 「云开发管理系统」 能力,帮助开发者更加快速、简便地完成微信支付场景的业务开发。 [图片] 可视化工作流:提供可视化的逻辑编排能力,支持多种类型的节点,帮助快速对接外部API,帮助开发者更清晰、灵活、高效地组织和管理业务逻辑。 云开发管理系统:提供了一套基础的业务运营管理系统+低代码开发工具,涵盖小程序、公众号、微信支付等场景。开通后的管理系统支持从 Web 网页登录,支持分配运营人员账号和权限管理。开发者可基于低代码工具开发运营系统内的应用,拖拽组件生成前端 UI,从而定制各类管理端应用。 接下来我们对这两个能力进行简单地介绍。 一、使用「可视化工作流」对接微信支付根据微信支付的官方最佳实践,想要实现一个完备的下单、付款、落库流程,通常有以下四件事情需要开发者实现: 客户端(小程序、Web、原生App)根据服务端生成的预付单信息,调用客户端接口,完成支付服务端接收微信支付回调,更新订单状态定时调用查单接口校验订单,避免没收到回调导致订单状态异常(可选)每日or每周固定时间调用对账单接口进行对账我们可以使用云开发可视化工作流以极高的效率实现以上需求。 [图片] 使用云开发工作流,五分钟快速对接微信支付云开发工作流提供了开箱即用的多种微信支付模板,仅需要少量配置即可完成微信支付下单、回调流程的对接。 第 1 步:部署工作流微信支付模板打开微信开发者工具 -> 云开发控制台 -> 云函数 -> 新建 -> 工作流 [图片] 选择工作流「微信支付下单」模板,并安装模板: [图片] 第 2 步:完成「下单」工作流的配置模板部署完成后,进入工作流编辑器内完成配置。 首先我们选择触发节点,在配置栏内,找到触发工作流的 URL 配置(这个 URL 将会在后续的云函数内使用到)。 [图片] 在支付下单节点内,完成 API 的配置。 [图片] PS:首次使用的用户如果没有可用的微信支付 API 实例,可以新建凭证+API实例。 [图片] 支付通知回调的配置,可以配置成另一个工作流,或者已有 HTTP 服务的 URL: [图片] 第 3 步:完成「支付成功回调」工作流的配置如果在第 2 步中,把支付通知回调配置为了另一条工作流,那么我们同样需要完成「支付成功回调」工作流的配置工作。 [图片] 例如,我们可以使用自定义代码的节点,在支付回调中,修改云数据库内的订单状态: const cloudbase = require("@cloudbase/node-sdk"); cloud.init({ env: "<环境ID>", }); const db = cloud.database(); // 示例:向 orders 集合中插入一条记录 const result = await db.collection("orders").add(wxpayTrigger.output.resource); 第 4 步:完成客户端与云函数代码工作流配置完成后,接下来我们通过云函数来调用工作流,完成全套场景。 首先是云函数的示例代码,我们通过 HTTP 请求,调用「支付下单」工作流: const axios = require("axios"); const cloud = require('wx-server-sdk'); // 工作流的触发 URL,在第 1 步中获取 const FLOW_URL = "https://xxxx.ap-shanghai.tcbautomation.cn/Automation/....."; exports.main = async (event, context) => { const res = await axios.post( FLOW_URL, { description: "云开发工作流-微信支付测试", out_trade_no: "xxxxxxxxxxxxx", // 业务订单号,自定义指定 amount: { total: 1, // 订单金额,单位为分 }, payer: { // 用户在小程序中的 OpenID,云函数中可直接获取 openid: cloud.getWXContext().OPENID, }, } ); const { timeStamp, packageVal, paySign, nonceStr } = res; return { timeStamp, packageVal, paySign, nonceStr, }; }; 然后在小程序中,调用上述云函数获取预付单信息,使用 [代码]wx.requestPayment[代码] 调起客户端支付控件: // 小程序端内 // 首先我们调用 makePayment 云函数 wx.cloud .callFunction({ name: "makePayment", data: { // 业务自定义参数 }, }) .then((res) => { // 获取到预付单信息 const { timeStamp, packageVal, paySign, nonceStr } = res.result; // 唤起微信支付组件,完成支付 wx.requestPayment({ timestamp: timeStamp, nonceStr: nonceStr, package: packageVal, signType: "RSA", paySign: paySign, success(res) { // 支付成功回调 // 实现自定义的业务逻辑 }, }); }); 可见,全程只有约 50 行代码,即可完成生成预付单、完成付款、接收微信支付回调三大块逻辑,非常方便快捷。 二、使用「云后台」管理微信支付云开发的云后台功能提供了一套基础的业务运营管理系统+低代码开发工具,解决小程序、公众号、微信支付等场景的管理后台搭建问题。 开通后的云后台支持从 Web 网页登录,支持分配运营人员账号和权限管理。 [图片] 在云后台中,开发者可以选用「微信支付管理」的模板,完成商家号的证书信息等配置之后,即可直接使用支付管理模板。 [图片] 目前「微信支付管理」模板提供以下能力: 订单查询 使用商户号或订单号查询订单信息退款管理 退款订单查询退款申请交易账单 通过时间、账单类型查询账单支持下载账单资金账单 通过时间、账单类型查询账单支持下载账单 如何开通使用「微信支付管理系统」?第 1 步:进入云后台控制台,安装模板进入云开发控制台,点击「云后台」 [图片] 选择「小程序支付管理」模板,并安装模板 [图片] 安装完成后,点击「编辑」,进入低代码编辑器 第 2 步:完成微信支付相关配置在低码编辑器的侧边栏中,点击「数据源」 进入「APIs」-「微信支付-小程序」-「基本信息」,点击编辑按钮,配置微信支付的基础信息: 您的小程序 AppID支付商户号商户证书序列号商户 API 私钥API V3 秘钥[图片] 配置完成后,点击保存按钮。 第 3 步:发布应用回到页面编辑器,点击右上角发布,将会自动触发依赖项检查点击发布数据源发布数据源完毕后,再次点击发布应用[图片] 第 4 步:访问管理系统发布成功后,您可以直接在编辑器内跳转至管理系统,或者在应用详情页中获得管理系统的地址和初始账号密码: [图片] 您也可以在下方的用户管理中,自行创建运营者账号,详情可参考 用户管理文档。 三、总结云开发提供了 可视化工作流 和 云后台 两个强大的能力,帮助开发者更加简单、高效、安全地对接微信支付,并且对支付相关业务进行后台管理。 [图片] 后续我们将会进一步深化功能,支持微信支付的分账、转账到零钱、代金券等运营场景,欢迎在评论区留言,您的反馈将会决定后续产品功能的上线优先级。 除了微信支付,您还有哪些场景期望通过工作流快速对接API、可视化构建后台管理系统?(例如:公众号、企业微信、视频号?) 也欢迎在评论区给我们留言。 本文相关文档: 云开发云后台:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/extensions/manage-system/introduction.html 云开发工作流:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/wechatpay/workflow.html
01-15 - 适配隐私保护协议接口
1、相关api说明 适配过程中主要是用到了以下几个接口 1. wx.onNeedPrivacyAuthorization:用于监听隐私接口需要用户授权事件,只有当隐私协议需要用户授权时才会由平台触发此事件,然后开发者需要弹窗显示隐私协议说明。如果用户拒绝授权,则隐私接口调用失败,在下次调用到隐私接口时还会继续弹。 2. wx.requirePrivacyAuthorize:用于模拟隐私接口调用,并触发隐私弹窗逻辑,也就是会触发wx.onNeedPrivacyAuthorization,但如果用户之前已经同意过隐私授权,会立即返回success回调,不会触发 wx.onNeedPrivacyAuthorization 。这个api的使用场景目前我是用在获取用户昵称时,在下一篇文章会进行说明。 3. wx.openPrivacyContract:用于打开隐私协议页面。 其他api大家可以根据具体情况选择使用。 2、弹窗思路 其实就是写一个自定义组件,然后在有调用到隐私接口的页面引入,利用自定义组件的attached方法,把各个页面的隐私弹窗组件的显示、隐藏方法保存到自定义组件的全局变量中,当用户点击隐私协议弹窗的同意、拒绝按钮时调用resolve方法,将对应的参数通知给平台。 3、注意点 隐私接口在隐私保护指引中有声明才能调用基础库版本2.32.3及以上开始支持2023.9.15号之前,在 app.json 中配置 __usePrivacyCheck__: true 后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。2023.9.15号之后,不论 app.json 中是否有配置 __usePrivacyCheck__,隐私相关功能都会启用wx.onNeedPrivacyAuthorization 是覆盖式注册监听,若重复注册监听,则只有最后一次注册会生效同意授权后如果想取消授权,在开发工具上 清缓存->清除模拟器缓存->清除授权数据,在手机上删除小程序即可隐私弹窗的z-index要设置成最大,避免被其他遮罩挡住导致无法点击,另外如果有调到官方的api例如wx.showLoading,也要注意是否设置了mask,避免在loading的时候弹出隐私弹窗,导致弹窗无法点击,而loading又要等弹窗关闭才会消失的尴尬情况4、代码如下 4.1、app.json "usingComponents": { //全局自定义组件 "privacyPopup": "/components/privacy/privacyPopup" }, //开启隐私相关功能 "__usePrivacyCheck__": true 4.2、自定义组件privacyPopup 4.2.1、privacyPopup.wxml <view class="container" wx:if="{{show}}"> <view class="cover {{showCoverAnimation?'cover-fade-in':''}}" catch:touchmove="return"></view> <view class="privacy-box {{showBoxAnimation?'slade-in':''}} {{device.isPhoneX? 'phx_68':''}}" catch:touchmove="return"> <view class="title flex-start-horizontal"> <view class="logo" wx:if="{{privacyConfig.icon}}"> <image class="icon" src="{{privacyConfig.icon}}"></image> </view> <view class="mini-name">{{privacyConfig.name || '小程序'}}</view> </view> <view class="tips"> <view class="privacy-content"> <view class="start">{{privacyConfig.content.start}}</view> <view class="link" bindtap="openPrivacyContract"> {{privacyConfig.content.mid}} </view> <view class="end">{{privacyConfig.content.end}}</view> </view> </view> <view class="buttons flex-center"> <button class="cancel reset-btn" bindtap="disagree">拒绝</button> <button class="save reset-btn" id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="agree">同意</button> </view> </view> </view> 4.2.2、privacyPopup.wxss @import "/app.wxss"; .cover{ background-color: #111; opacity: 0; position: fixed; left: 0; top: 0; right: 0; bottom: 0; z-index: 50000; transition: opacity .3s; } .privacy-box{ position: fixed; left: 0rpx; right: 0rpx; bottom: 0rpx; z-index: 51000; background-color: #fff; box-sizing: border-box; padding-top: 60rpx; padding-left: 50rpx; padding-right: 50rpx; border-radius: 30rpx 30rpx 0 0; transform: translateY(100%); transition: transform .3s, bottom .3s; } .cover-fade-in{ opacity: 0.7; } .slade-in{ transform: translateY(0); bottom: 0rpx; } .privacy-box .title{ font-size: 32rpx; font-weight: 500; text-align: center; margin-bottom: 40rpx; color: #161616; } .privacy-box .title .icon{ width: 46rpx; height: 46rpx; margin-right: 8rpx; vertical-align: bottom; border-radius: 50%; } .privacy-box .title .mini-name{ /*margin-bottom: 2rpx;*/ } .privacy-box .tips{ margin-bottom: 20rpx; } .privacy-box .tips .privacy-content{ color: #606266; font-size: 30rpx; margin-bottom: 20rpx; line-height: 1.6; } .privacy-box .tips .privacy-content .start, .privacy-box .tips .privacy-content .link, .privacy-box .tips .privacy-content .end { display: inline; } .privacy-box .tips .privacy-content .link{ color: #1890FF; } .privacy-box .buttons{ margin-bottom: 40rpx; margin-top: 50rpx; text-align: center; font-size: 34rpx; font-weight: 550; } .privacy-box .buttons .save{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #fff; /*background:#fff linear-gradient(90deg,rgba(246, 120, 121,.9) 10%, rgb(246, 120, 121));*/ background-color: #07c160; margin-left: -50rpx; } .privacy-box .buttons .cancel{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #07c160; background-color: #F2F2F2; } 4.2.3、privacyPopup.js //获取应用实例 const tabbar = require("../../utils/tabbar.js"); const app = getApp(); //用来保存各个页面注册的隐私协议回调事件 let privacyHooks = {}; if (wx.onNeedPrivacyAuthorization) { console.warn("当前基础库支持api wx.onNeedPrivacyAuthorization"); wx.onNeedPrivacyAuthorization(resolve => { console.warn("需要隐私协议弹窗:", resolve); const pages = getCurrentPages(); const route = pages[pages.length - 1].route; const hook = privacyHooks[route]; if (hook) { hook.resolve = resolve; hook.show(resolve); } else { console.error("当前页面没有注册隐藏协议弹窗回调:", route); } }) } else { console.warn("当前基础库不支持api wx.onNeedPrivacyAuthorization"); } Component({ data: { show: false, showCoverAnimation: false,//显示类别选择窗口动画 showBoxAnimation: false,//显示类别选择窗口动画 }, lifetimes: { //在组件实例进入页面节点树时执行 attached: function () { console.warn("privacyPopup attached"); const pages = getCurrentPages(); privacyHooks[pages[pages.length - 1].route] = { show: resolve => { this.show(resolve); }, close: () => { this.hide(); } } }, //在组件实例被从页面节点树移除时执行 detached: function () { console.warn("privacyPopup detached"); } }, methods: { /** * 同意隐私协议 * @param e */ agree(e) { Object.values(privacyHooks).forEach(hook => { hook.close(); hook.resolve && hook.resolve({ event: "agree", buttonId: "agree-btn" }); }); }, /** * 不同意隐私协议 * @param e */ disagree(e) { Object.values(privacyHooks).forEach(hook => { hook.close(); hook.resolve && hook.resolve({ event: "disagree" }); }); }, /** * 显示隐私协议授权弹窗 */ show(resolve) { app.fillConfig(this, ["privacyConfig"], data => { console.log("显示隐私协议弹窗"); const device = app.getSystemInfo(); this.setData({ show: true, device, }, () => { this.setData({ showCoverAnimation: true, showBoxAnimation: true }); //自定义隐私弹窗曝光告知平台 resolve({event: "exposureAuthorization"}); }); tabbar.hideTabByPrivacy(this); }); }, /** * 关闭隐私协议授权弹窗 */ hide() { console.log("隐藏隐私协议弹窗"); this.setData({ showCoverAnimation: false, showBoxAnimation: false }, () => { const that = this; setTimeout(function () { that.setData({ show: false }) }, 300) }) tabbar.showTabByPrivacy(this); }, /** * 打开隐私协议 */ openPrivacyContract() { wx.openPrivacyContract({ success: res => { console.log("openPrivacyContract success") }, fail: res => { console.error("openPrivacyContract fail", res) } }) } } }) 5、弹窗效果 [图片]
2023-09-13 - Skyline|长列表也可以丝滑~
[图片] [图片] 对于长列表出现的白屏、卡顿、界面跳动等问题,小程序提供了新 scroll-view 来解决这一系列问题。我们先来看看效果~ 快速滚动效果对比我们通过一组长列表来展示新旧 scroll-view 在快速滚动下的效果对比。 当长列表快速滚动时,旧 scroll-view 容易出现白屏的情况,新 scroll-view 则不会出现白屏。 左:旧 scroll-view、右:新 scroll-view [视频] 在安卓机器快速滚动过程中,旧 scroll-view 反应卡顿,容易出现手指离开操作时,滚动动画还在进行。 而新 scroll-view 则可以保持界面滚动效果跟随手指,停止滚动则缓慢结束动画效果。 左:旧 scroll-view、右:新 scroll-view ,测试机型:Xiaomi MIX 3 [视频] 反向滚动效果对比在对话等场景下,反向滚动是常见的功能,旧 scroll-view 并没有提供反向滚动的能力,我们来看看旧 scroll-view 下是怎么完成反向滚动的~ 在对话数据在加载的时候,对话列表需要在更新完列表数据之后,再使用 scroll-into-view 或者 scroll-top 来保持当前滚动位置,因为设置滚动位置会有延迟,所以容易出现 界面跳动 的情况。 // .js // scroll-view 滚动到顶部时触发 bindscrolltoupper() { // 先更新列表数据 this.setData({ recycleList: getnewList() }, () => { // 更新完数据后再设置滚动位置 this.setData({ scrollintoview: scrollintoview }) }) } 为了解决界面跳动的问题,社区上也有通过翻转的方法来解决,将 scroll-view 与 scroll-view 的子元素进行翻转。 // .wxss .reserve { transform: rotateX(180deg); } // .wxml 然而进行翻转之后,会遇到手指滚动方向与列表滚动方向相反、scroll-into-view 属性无效等问题。 为了帮开发者们解决反向滚动类列表的一系列问题,新 scroll-view 直接提供了 reverse 属性支持反向滚动的能力,滚动效果更加顺滑。 左:旧 scroll-view、右:新 scroll-view(图片加载期间,GIF 渲染较慢) [视频] 怎么接入新 scroll-view ?新的 scroll-view 使用起来很简单,主要有以下两个步骤: 修改小程序配置scroll-view 增加 type="list"// app.json // "renderer": "skyline" 开启之后所有页面会变成自定义导航,可参考 https://developers.weixin.qq.com/s/Y5Y8rrm37qEY 实现自定义导航 // 也可在 page.json 中配置 "renderer": "skyline" 逐个页面开启 { ... "lazyCodeLoading": "requiredComponents", "renderer": "skyline" } // page.json { ... "disableScroll": true, "navigationStyle": "custom" } // page.wxml ... // 反向滚动 新的 scroll-view 从安卓 8.0.28 / iOS 8.0.30 开始支持,如需兼容低版本需要进行兼容处理。 wx.getSkylineInfo({ success(res) { if (res.isSupported) { // 使用新版 scroll-view } else { // 使用旧版 scroll-view } } }) 如需体验长列表效果,可在微信开发者工具导入该代码片段即可体验:https://developers.weixin.qq.com/s/Y5Y8rrm37qEY 更多接入详情请参考文档 怎么做到的?大家肯定好奇为什么新 scroll-view 可以解决这个头疼的问题呢? 我们来对比一下新旧 scroll-view 有什么区别就可以知道答案啦~ 旧 scroll-view 逻辑层与渲染层的通信需要通过 JSBridge 进行转换,需要一定的时间开销渲染采用异步分块光栅化,当渲染赶不上滚动的速度,来不及渲染则会出现白屏渲染大量节点内存占用高,需要开发者自行优化只渲染在屏节点,开发成本高新 scroll-view 逻辑层与渲染层的通信无需通过 JSBridge 进行转换,减少了大量通信时间开销渲染采用同步光栅化,滚动与渲染在同一线程,不会出现白屏针对长列表进行优化,只渲染在屏节点,内存占用低,减少了一些渲染耗时,且开发接入成本低[图片] 除此之外,新 scroll-view 后续将提供 type="custom" 支持 sticky 吸顶效果、网格布局、瀑布流布局等能力便于开发者接入使用~
2023-08-03 - Skyline|探秘下拉二楼,打造更丰富的内容展示
下拉二楼是一种常见的交互设计,可以为应用中的内容展示提供更多的可能性。 通过下拉操作,开发者可以在二楼展示更丰富、更多样化的内容,从而增加用户的点击量和留存率,例如宣传视频、精选商品、走心故事等等。 在小程序中,下拉二楼一直是一种难以实现的交互设计,即使部分小程序实现了,但效果和性能都很差。 为了丰富小程序的内容展示,提高用户的使用体验,小程序官方近期推出了下拉二楼的能力,方便小程序开发者使用。 效果展示 让我们来看看小程序 scroll-view 实现下拉效果的效果~ [图片] 实现步骤 接下来,我们来看下如何使用 scroll-view 实现下拉二楼 1、配置下拉相关属性 scroll-view 新增了以下接口供开发者配置下拉二楼的能力,开发者可以根据业务需要配置相关的属性 属性 说明 refresher-two-level-enabled 开启下拉二级能力,配置开启需同时配置 refresher-two-level-triggered 设置打开/关闭二级 refresher-two-level-threshold 下拉二级阈值 refresher-two-level-close-threshold 滑动返回时关闭二级的阈值 refresher-two-level-scroll-enabled 处于二级状态时是否可滑动 refresher-ballistic-refresh-enabled 惯性滚动是否触发下拉刷新 refresher-two-level-pinned 即将打开二级时否定住 [代码]<scroll-view type="list" scroll-y // 开启下拉刷新(下拉二级必须开启下拉刷新) refresher-enabled="{{true}}" // 开启下拉二级能力 refresher-two-level-enabled="{{true}}" // 处于二级状态是否可滑动 refresher-two-level-scroll-enabled="{{true}}" > ... </scroll-view> [代码] 2、实现二楼内容 配置完下拉二楼属性之后,接着就是将我们的二楼实现在 scroll-view 中。 在 scroll-view 放置一个子节点,声明 slot=“refresher”,该节点中的内容即为下拉二楼的内容。 [代码]<scroll-view ... > <view slot="refresher"> 这里是二楼的内容 </view> </scroll-view> [代码] 3、根据下拉状态回调进行个性化处理 接着我们需要根据业务小程序自身的诉求,根据下拉状态的回调进行个性化的处理,例如:下来完成跳转页面等。 在 scroll-view 绑定 bind:refresherstatuschange 监听下拉状态,下拉状态有以下几种 属性 说明 Idle 空闲 CanRefresh 超过下拉刷新阈值 Refreshing 下拉刷新 Completed 下拉刷新完成 Failed 下拉刷新失败 CanTwoLevel 超过下拉二级阈值 TwoLevelOpening 开始打开二级 TwoLeveling 打开二级 TwoLevelClosing 开始关闭二级 [代码]<scroll-view bind:refresherstatuschange="onStatusChange" ... > <view slot="refresher"></view> ... </scroll-view> // .js onStatusChange(e) { const status: RefreshStatus = e.detail.status if (status === RefreshStatus.TwoLeveling) { const that = this // 当打开二级之后,跳转到新的页面 wx.navigateTo({ url: '../goods/index', events: { nextPageRouteDone: function(data) { // 新页面打开之后,关闭下拉二楼 that.scrollContext.closeTwoLevel({ duration: 1 }) } } }) } } [代码] 我们来演示一下松手立即跳转(图左)、完全打开二楼后跳转(图右) [图片] 丰富小程序展示内容和形式,欢迎大家使用小程序下拉二楼,为小程序的内容展示提供更多的可能性和创意发挥的空间。 通过下拉二楼,可以展示更丰富、更多样化的内容,也为小程序的发展带来了更多的机会和挑战~ 赶紧 mark 下这个 代码片段 来接入使用吧~
2023-08-03 - 省钱有道之 云开发环境共享小结
#前言 最近为了节省一点小程序的运营成本,一些没啥流量的小程序如果每个月也要19块略微有些肉疼(主要还是穷),研究了一下云环境共享,在这里简单做一下总结。 [图片] 这里有官方的小程序环境共享文档需提前了解一下,具体共享步骤按官方文档操作即可。 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/introduce.html #注意点 共享环境有几个注意点大致如下: 1、必须是相同主体 2、开通了云开发环境的小程序可以共享给同主体的小程序、公众号,被共享方无需开通云开发环境 3、一个云开发环境最多可以共享给10个小程序/公众号 4、共享后双发均可主动解除 5、按官方文档要求,资源方需有云函数cloudbase_auth,测试时发现没有这个云函数其实也能正常运行,可能我验证的场景还不够多 6、云能力初始化的方式不同,资源方按传统的云环境初始化方式即可,也就是 wx.cloud.init({ env: env.activeEnv, traceUser: true }); 而调用方的初始化方式有所不同 const cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); 后续调用资源方的云函数就用这个cloud就行了:cloud.callFunction({...}); 7、调用方有操作到云存储文件的api也需要用6步骤中的cloud 8、云存储fileId需要用cloud.getTempFileURL转换成临时/永久链接,否则在调用方无法展示 9、一些api的云调用方式也有变化,需指明具体的appid。比如A小程序授权给了B小程序,想给B小程序推送客服消息需要写成 await cloud.openapi({appid:B小程序appid}).customerServiceMessage.send({...}); 10、获取调用方的appid/openid/unionid也有所不同 // 跨账号调用时,由此拿到来源方小程序/公众号 AppID console.log(wxContext.FROM_APPID) // 跨账号调用时,由此拿到来源方小程序/公众号的用户 OpenID console.log(wxContext.FROM_OPENID) // 跨账号调用、且满足 unionid 获取条件时,由此拿到同主体下的用户 UnionID console.log(wxContext.FROM_UNIONID) #适配 基于以上注意点,开始进行适配,由于我是一套代码部署N个小程序,然后一个云环境共享给其他小程序,希望通过配置决定哪个小程序作为资源方,哪些作为调用方 首先是云开发环境的初始化: 1、env.js 环境配置: //云开发环境 const cloudBase = { //使用共享云环境资源,资源方=false,调用方=true useShareResource: false, //资源方AppID resourceAppid: "wx9d2xxxxxxxx0088", //资源方环境ID resourceEnv: "prod-9gxqvi3qb3c257ef", //云环境ID prod: "prod-9gxqvi3qb3c257ef" } 2、api.js 操作模块 const env = require('../env.js'); let cloud; /** * 初始化云能力 * @returns {Promise} */ const wxCloudInit = async function () { const {cloudBase} = env; if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else if (cloudBase.useShareResource) { const {resourceAppid, resourceEnv} = cloudBase; // 声明新的 cloud 实例 cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); console.log("初始化云能力完毕:", initRes, "资源方appid:", resourceAppid, "资源方环境ID:", resourceEnv); } else { wx.cloud.init({ env: env.activeEnv, traceUser: true }); console.log("初始化云能力完毕,当前环境:", env.activeEnv); cloud = wx.cloud; } this.cloud = cloud; } /** * 云函数调用 * @param name * @param data * @param success * @param fail * @param complete */ const callCloudFunction = function (name, data, success, fail, complete) { //执行云函数 cloud.callFunction({ // 云函数名称 name: name, // 传给云函数的参数 data: Object.assign({}, data, {env: env.activeEnv}) }).then(res => { typeof success == 'function' && success(res); }).catch(res => { typeof fail == 'function' && fail(res); }).then(res => { typeof complete == 'function' && complete(res); }); }; 3、在app.js中初始化云环境,后续有用到wx.cloud的都需要改成api.cloud const api = require('utils/api.js'); App({ onLaunch: async function (options) { await api.wxCloudInit(); } }); 其次是资源方的获取用户信息调整 每次都要判断wxContext.FROM_OPENID是否为空,不为空则是调用方的用户信息,为空则是资源方的用户信息,略微繁琐,干脆封装了一个npm包wx-server-inherit-sdk,改造了一下getWxContext函数,源码如下,引入这个包后也就可以不用引入官方的wx-server-sdk const cloud = require('wx-server-sdk'); // 保存原始getWXContext方法到另一个变量 const originalGetWXContext = cloud.getWXContext; cloud.getWXContext = function () { //调用原始getWXContext方法 const wxContext = originalGetWXContext.call(this); const {FROM_APPID, FROM_OPENID} = wxContext; //云开发环境共享时获取到的APPID会替换成源方APPID if (FROM_APPID) { Object.assign(wxContext, {APPID: FROM_APPID}); } //云开发环境共享时获取到的OPENID会替换成源方OPENID if (FROM_OPENID) { Object.assign(wxContext, {OPENID: FROM_OPENID}); } return wxContext; } module.exports = cloud; 到此也就大功告成。为了省钱也是够折腾的[哭笑]
2023-08-28 - 公众号运营:如何文章内跳转公众号主页(不是历史文章页面)
近期社区中运营公众号的朋友咨询,如何文章内跳转到公众号主页 [图片] 不是历史文章页面 [图片] 这点是可以通过文章内的图片实现的。 接下来教大家如何实现图片跳转公众号主页: 1、首先获取历史文章链接 打开该公众号任意一篇内容,复制链接,在浏览器打开 [图片] 右击鼠标查看源代码 [图片] 快捷键ctrl+F开启全局搜索“ __biz”然后把该公众号卡版ID复制,记得后面两个“==”号必须都复制上。 [图片] 2、这个功能要通过SVG排版来实现。官方编辑器是不支持的,所以在这里我们使用某排版中的SVG编辑器。 [图片] 打开某排版SVG编辑器后,在组件这里搜索公众号或10214 [图片] 找到公众号——图片(免费的) 组件 [图片] 然后添加图片,粘贴公众号卡片ID [图片] 设置好后,先保存,后同步到微信后台 [图片] 这时候就可以去公众号预览了。 当然要注意的是,这种SVG排版同步到公众号后是不能修改的。所以建议在某排版编辑好所有内容之后,再同步到微信公众号后台。 而且这种排版可以跳转任何公众号主页。 还有一点这种跳转没有任何提示,直接跳到公众号主页,大家快去试试吧。 当然,还有朋友不知道如何跳转历史文章,可以参考:公众号运营:公众号菜单如何跳转历史文章? https://developers.weixin.qq.com/community/develop/article/doc/000e2e5dca09589f9f6fc985456013 我是立十,非官方人员💍公众号💍运营资深忠实粉丝,专注回答社区中关于公众号的问题。
2023-06-21 - canvas绘制毛玻璃背景分享海报
最近重新设计了分享海报,用毛玻璃作为背景,使整体更有质感,如果没有用到canvas,毛玻璃效果其实很好实现,给元素添加一个滤镜即可(比如:filter: blur(32px)),但是实践的过程中发现,canvas在IOS端一直没有效果,查了一个文档发现IOS端不支持filter。。。有点想骂人。。(PS:微信官方有关CanvasRenderingContext2D的文档还比较简略,更加详细的文档大家可以移步至https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D) [图片] 没办法,实在是喜欢毛玻璃的效果,决定想想办法迈过这道坎,继续网上查阅资料,查到大概的方法就是 1、用canvas上下文先画一个image,image的src就是要模糊处理的图片路径, 2、用canvas上下文把步骤1的image提取imageData,然后进行高斯模糊,高斯模糊的算法网上有挺多,但是拿来执行运算时间要7,8s,比较不理想,最后我是找到了一个stackblur-canvas的插件,这个插件也适用于H5,这里只需要调用里面高斯模糊算法的方法(stackBlur.imageDataRGBA)即可,执行下来几百毫秒就能出来,跟网上的算法差距还是很明显啊。。 3、最后把已经模糊处理的imageData绘制到位图中即可。 canvas滤镜的问题搞定了,还有一个小问题是开发者工具上面canvas的层级始终会比其他元素高,但在IOS真机上元素是可以布局在canvas上。由于不知道在其他客户端会不会也会像开发者工具那样有层级显示问题。所以保险起见我干脆就把canvas放在在页面可视区域底部,在可视区域用其他页面元素做了一个分享效果图出来。 底下两张图,左边是预览,右边是最终画布生成图片。可以看出wxss的滤镜效果还是会比通过第三方高斯算法处理后的模糊效果来的更自然舒服,不过整体效果也还在可接受范围内。 [图片] [图片] 附上核心代码,感谢阅读 组件shareList.wxml <view class="share-box" wx:if="{{visibility}}"> <!-- style="display:none;"--> <view class="share-view" style="opacity:{{shareView?1:0}}"> <view class="cover-image"> <view class="cover"></view> <image class="image" src="{{shareInfo.imgSrc}}"></image> </view> <view class="content-box"> <view class="detail"> <view class="up"> <view class="expand"> <view class="time" wx:if="{{shareInfo.date}}">{{shareInfo.date}}</view> <view class="place" wx:if="{{shareInfo.place}}">{{shareInfo.place}}</view> </view> <image mode="widthFix" class="image" src="{{shareInfo.imgSrc}}" bind:load="imageLoaded"></image> </view> <view class="middle flex-end-vertical"> <image class="header-img" src="{{shareInfo.avatarUrl}}"></image> <image class="joiner-header-img" wx:if="{{shareInfo.joinerAvatarUrl}}" src="{{shareInfo.joinerAvatarUrl}}"></image> <!--<view class="nickname flex-full">showms</view>--> </view> <view class="down"> <view class="desc"><view class="title" wx:if="{{shareInfo.title}}">#{{shareInfo.title}}#</view>{{shareInfo.content}}</view> </view> </view> </view> </view> <view class="save-tool-bar {{device.isPhoneX?'phx_68':''}} flex-center" style="transform: translateY({{shareView?0:100}}%)"> <view class="op_btn flex-full"> <view class="icon-view" bindtap="close"> <image class="icon" src="/images/share/close.png"></image> </view> <text class="text" bindtap="close">关闭</text> </view> <view class="op_btn flex-full {{!allowSave?'disable':''}}"> <view class="icon-view" bindtap="save"> <image class="icon" src="/images/share/save.png"></image> </view> <text class="text" bindtap="save">保存到相册</text> </view> </view> <!--transform: translateY(100%);--> <canvas class="share-canvas" type="2d" id="myCanvas" style="position:absolute;top:0%;width:100%;height:100%;z-index:1000;transform: translateY(100%);"></canvas> </view> <!--toast--> <toast id="toast"></toast> 组件shareList.wxss @import "/app.wxss"; .share-box{ position: absolute; left: 0; right: 0; top: 0; bottom: 0; } .share-cover{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 1200; background-color: #111; opacity: 0.8; } .share-view{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 300; opacity: 0; transition: opacity .3s; } .share-view .cover-image{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; } .share-view .cover-image .image{ width: 100%; height: 100%; transform: scale(3); filter: blur(32px); } .share-view .cover-image .cover{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 120; background-color: rgba(255, 255, 255, .5); } .share-view .content-box{ /*padding: 300rpx 40rpx 40rpx; position: relative;*/ position: fixed; left: 40rpx; right: 40rpx; top: 50%; transform: translateY(-50%); margin-top: -80rpx; z-index: 500; box-sizing: border-box; } .share-view .content-box .detail{ background-color: #fff; box-sizing: border-box; /*padding: 70rpx 30rpx 30rpx;*/ border-radius: 36rpx; overflow: hidden; } .share-view .content-box .detail .up{ box-sizing: border-box; position: relative; } .share-view .content-box .detail .up .image{ width: 100%; /*height: auto;*/ display: block; } .share-view .content-box .detail .up .expand{ position: absolute; right: 20rpx; bottom: 20rpx; z-index: 10; text-align: right; font-size: 22rpx; font-weight: 500; color: #fff; text-shadow: 0px 0px 10rpx rgba(158, 163, 175, 1); } .share-view .content-box .detail .middle{ position: relative; } .share-view .content-box .detail .middle .header-img, .share-view .content-box .detail .middle .joiner-header-img{ width: 100rpx; height: 100rpx; border-radius: 50%; border: 6rpx solid #fff; box-sizing: border-box; margin-left: 24rpx; margin-right: 10rpx; margin-top: -64rpx; position: relative; z-index: 80; display: block; } .share-view .content-box .detail .middle .joiner-header-img{ z-index: 60; margin-left: -60rpx; } .share-view .content-box .detail .middle .nickname{ font-size: 30rpx; font-weight: 500; color: #434343; padding-bottom: 10rpx; display: none; } .share-view .content-box .detail .down{ padding: 10rpx 30rpx 70rpx; } .share-view .content-box .detail .down .title{ font-size: 28rpx; font-weight: 500; color: #303133; margin-bottom: 10rpx; margin-right: 10rpx; display: inline; } .share-view .content-box .detail .down .desc{ /*font-size: 28rpx; font-weight: 500; color: #303133; display: inline;*/ font-size: 28rpx; font-weight: 500; color: #303133; line-clamp: 2; box-orient: vertical; text-overflow: ellipsis; overflow: hidden; /*将对象作为弹性伸缩盒子模型显示*/ display: -webkit-box; /*从上到下垂直排列子元素(设置伸缩盒子的子元素排列方式)*/ -webkit-box-orient: vertical; /*这个属性不是css的规范属性,需要组合上面两个属性,表示显示的行数*/ -webkit-line-clamp: 2; height: 76rpx; } .share-canvas{ } .save-tool-bar{ position: absolute; left: 0; right: 0; bottom: 0; z-index: 1600; border-radius: 40rpx 40rpx 0 0; text-align: center; background-color: #f0f0f0; transform: translateY(100%); transition: transform .3s; } .save-tool-bar .op_btn{ text-align: center; padding: 50rpx 0 20rpx; transition: opacity .3s; } .save-tool-bar .op_btn .icon-view{ padding: 26rpx; background-color: #fff; border-radius: 50%; display: inline-block; margin-bottom: 10rpx; } .save-tool-bar .op_btn .icon{ display: block; width: 48rpx; height: 48rpx; } .save-tool-bar .op_btn .text{ display: block; font-size: 20rpx; font-weight: 400; } 组件shareList.js //获取应用实例 const app = getApp(); const tabbar = require('../../utils/tabbar.js'); const canvasHelper = require('../../utils/canvasHelper'); const fonts = require("../../utils/fonts.js"); let ctx, canvas; Component({ /** * 组件的属性列表 */ properties: {}, /** * 组件的初始数据 */ /*{left: 'rgba(26, 152, 252, 0.8)', right: 'rgba(26, 152, 252, 1)'}*/ data: { visibility: false, paddingLeft: 34, letterSpace: 2, width: 300, height: 380, shareView: false, shareInfo: { /*imgSrc: "cloud://ydw-49d951.7964-ydw-49d951-1259010930/love/images/default/dangao.jpg", avatarUrl: "cloud://test-wjaep.7465-test-wjaep-1259010930/images/ozzW05Gch7jMMhsn1r_SWLGdGtF0/avatarUrl_1668440942555.webp", joinerAvatarUrl: " https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83erg8t0El6jTaZY87icvjR71ww52VMibg8fONBgggRtYHTnR2tXibB0IRQ45dCVgNCX5BRhY0KibjfxjGA/132 ", title: "一起去旅游", content: "这里是内容啊,怎么没写内容呢这里是内容啊,怎么没写内容呢,我特知道当时的回复对方法国发过快速减肥", date: "2022-12-28", place: "四川成都", shareQrcode: "cloud://ydw-49d951.7964-ydw-49d951-1259010930/qrcode/mini-qrcode.jpg"*/ }, allowSave: false, }, ready() { const device = app.getSystemInfo(); this.setData({ device, }); this.toast = this.selectComponent('#toast'); }, /** * 组件的方法列表 */ methods: { save() { const {device} = this.data; let that = this; that.toast.showLoadingToast({text: "保存中", mask: false}); canvasHelper.saveImage( this, canvas, 0, 0, device.screenWidth, device.screenHeight, device.screenWidth * 5, device.screenHeight * 5 ).then(res => { that.toast.hideLoadingToast(); that.toast.showToast({text: "保存成功"}); that.close(); console.log(res); }).catch(res => { console.error("保存失败:", JSON.stringify(res)); that.toast.showToast({text: "保存失败"}); that.toast.hideLoadingToast(); }); }, imageLoaded: function (e) { console.log("图片加载完毕:", e); this.setData({shareView: true}); }, show(shareInfo) { console.log("开始显示画布:", shareInfo); this.setData({ shareInfo }); tabbar.hideTab(this); this.setData({visibility: true}, () => { canvasHelper.init(app, this, "#myCanvas").then(async (res) => { ctx = res.ctx; canvas = res.canvas; this.toast.showLoadingToast({text: "生成中", mask: false}); const {device} = this.data; //加大尺寸 const largerSize = 100; console.log("1.绘制毛玻璃背景图片:", { width: device.screenHeight + largerSize, height: device.screenHeight + largerSize, }); /*await canvasHelper.drawImage( canvas, ctx, shareInfo.imgSrc, -(device.screenHeight - device.screenWidth) / 2.0, -largerSize / 2, device.screenHeight + largerSize, device.screenHeight + largerSize, 190);*/ await canvasHelper.drawBlurImage( canvas, ctx, shareInfo.imgSrc, -(device.screenHeight - device.screenWidth) / 2.0, 0, device.screenHeight, device.screenHeight, 180); console.log("2.绘制毛玻璃覆盖层灰色背景"); canvasHelper.drawRoundRect(ctx, 0, 0, device.screenWidth, device.screenHeight, 0, 'rgba(255, 255, 255, .5)'); console.log("3.绘制内容承载区域"); const leftPadding = 20,//边距20 headerImgHeight = 50,//头像尺寸 descHeight = 40,//内容区域高度 descPaddingTop = 0,//内容区域paddingTop descPaddingBottom = 25,//内容区域paddingBottom adjustHeight = 40;//调节高度,人为设定 const contentWidth = device.screenWidth - leftPadding * 2; const contentHeight = contentWidth + headerImgHeight + descHeight + descPaddingTop + descPaddingBottom; canvasHelper.drawRoundRect( ctx, (device.screenWidth - contentWidth) / 2.0, (device.screenHeight - contentHeight) / 2.0 - adjustHeight, contentWidth, contentHeight, 18, 'rgba(255, 255, 255, 1)' ); console.log("4.绘制内容区域图片"); ctx.clip();//裁剪后父元素的圆角才会显示 await canvasHelper.drawImage( canvas, ctx, shareInfo.imgSrc, (device.screenWidth - contentWidth) / 2.0, (device.screenHeight - contentHeight) / 2.0 - adjustHeight, contentWidth, contentWidth, 0, ); ctx.restore(); console.log("5.绘制头像边框"); const headerSize = 50, borderWidth = 3, headerMarginLeft = 12; if (shareInfo.joinerAvatarUrl) { console.log("5.1.绘制共享对象头像"); await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.joinerAvatarUrl, leftPadding + headerMarginLeft + 30, (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2, headerSize, borderWidth, "#fff", ); console.log("5.2.绘制当前用户头像"); await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.avatarUrl, leftPadding + headerMarginLeft, (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2, headerSize, borderWidth, "#fff", ); } else { console.log("5.1.绘制当前用户头像"); await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.avatarUrl, leftPadding + headerMarginLeft, (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2, headerSize, borderWidth, "#fff", ); } console.log("6.绘制日期和地点"); let textPositionY = (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2 + 14; if (shareInfo.place) { console.log("6.1.绘制地点"); canvasHelper.drawText( ctx, shareInfo.place, leftPadding + contentWidth - 10, textPositionY, "right", 11, 400, fonts.list["SFRounded-Regular"].name, "#fff", "rgba(158, 163, 175, 1)", 0, 0, 5); textPositionY = textPositionY - 16 } if (shareInfo.date) { console.log("6.2.绘制日期"); canvasHelper.drawText( ctx, shareInfo.date, leftPadding + contentWidth - 10, textPositionY, "right", 11, 400, fonts.list["SFRounded-Regular"].name, "#fff", "rgba(158, 163, 175, 1)", 0, 0, 5); } if (shareInfo.title || shareInfo.desc) { console.log("7.绘制标题和内容", contentWidth); let leftContent = (shareInfo.title ? "#" + shareInfo.title + "# " : "") + shareInfo.content; //显示区域宽度 const displayWidth = contentWidth - 40; textPositionY = (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth + headerSize; const contentFontSize = 14, contentFontWeight = 600; for (let i = 1; i <= 2; i++) { let dynamicText = ""; for (let j = 0; j < leftContent.length;) { ctx.font = contentFontWeight + " " + contentFontSize + "px " + fonts.list["SFRounded-Semibold"].name; let metrics = ctx.measureText(dynamicText + leftContent[j]); if (metrics.width >= displayWidth) { //最后一行,最后一个字替换成省略号 if (i === 2) { dynamicText = dynamicText.substring(0, dynamicText.length - 1); dynamicText += "…" } break; } dynamicText += leftContent[j]; leftContent = leftContent.slice(1); } //console.log("文本内容:", dynamicText); canvasHelper.drawText( ctx, dynamicText, leftPadding + 20, textPositionY, "left", contentFontSize, contentFontWeight, fonts.list["SFRounded-Semibold"].name, "#303133", "", 0, 0, 0 ); textPositionY = textPositionY + 20; } } console.log("8.绘制二维码"); const qrcodeSize = 66, qrcodeHolderSize = 70; canvasHelper.drawRoundRect( ctx, (device.screenWidth - qrcodeHolderSize) / 2.0, (device.screenHeight - contentHeight) / 2.0 + contentHeight, qrcodeHolderSize, qrcodeHolderSize, 10, 'rgba(255, 255, 255, 1)' ); ctx.clip(); await canvasHelper.drawImage( canvas, ctx, shareInfo.shareQrcode, (device.screenWidth - qrcodeSize) / 2.0, (device.screenHeight - contentHeight) / 2.0 + contentHeight + 2, qrcodeSize, qrcodeSize, 0, ); ctx.restore(); /** await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.shareQrcode, (device.screenWidth - qrcodeSize) / 2.0, (device.screenHeight - contentHeight) / 2.0 + contentHeight, qrcodeSize, 0 );*/ ctx.fill(); this.setData({allowSave: true}); this.toast.hideLoadingToast(); }); }); }, /** * 下载头像 * @param avatarUrl * @returns {Promise} * @private */ _downloadHeaderImg(avatarUrl) { return new Promise(((resolve, reject) => { wx.downloadFile({ url: avatarUrl, //下载头像 success(res) { console.log("下载结束:", res); if (res.statusCode === 200) { //res.tempFilePath resolve(res); } }, fail(res) { reject(res); } }); })); }, /** * 关闭分享页面 */ close() { this.toast.hideLoadingToast(); this.setData({visibility: false, shareView: false, allowSave: false}); tabbar.showTab(this); }, } }) ; 工具类canvasHelper const stackBlur = require("stackblur-canvas"); /** * 初始化画布 * @param app * @param base * @param canvasId * @returns {Promise} */ const init = (app, base, canvasId) => { return new Promise((resolve, reject) => { const device = app.getSystemInfo(); const query = base.createSelectorQuery(); query.select(canvasId) .fields({node: true, size: true}) .exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = device.pixelRatio; canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); console.log("画布初始化完毕,画布宽:", canvas.width, "画布高:", canvas.height, "设备像素比:", dpr); resolve({ctx, canvas}); }); }); } /** * 绘制圆角矩形 * @param ctx * @param {number} x 圆角矩形选区的左上角 x坐标 * @param {number} y 圆角矩形选区的左上角 y坐标 * @param {number} w 圆角矩形选区的宽度 * @param {number} h 圆角矩形选区的高度 * @param {number} r 圆角的半径 * @param {string} f 填充颜色 */ const drawRoundRect = (ctx, x, y, w, h, r, f) => { ctx.save(); // 开始绘制 ctx.beginPath(); // 因为边缘描边存在锯齿,最好指定使用 transparent 填充 // 这里是使用 fill 还是 stroke都可以,二选一即可 ctx.fillStyle = f; // ctx.setStrokeStyle('transparent') // 左上角 ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5); // border-top ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.lineTo(x + w, y + r); // 右上角 ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2); // border-right ctx.lineTo(x + w, y + h - r); ctx.lineTo(x + w - r, y + h); // 右下角 ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5); // border-bottom ctx.lineTo(x + r, y + h); ctx.lineTo(x, y + h - r); // 左下角 ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI); // border-left ctx.lineTo(x, y + r); ctx.lineTo(x + r, y); // 这里是使用 fill 还是 stroke都可以,二选一即可,但是需要与上面对应 ctx.fill(); // ctx.stroke() ctx.closePath(); // 剪切 //ctx.clip(); }; /** * 绘制圆形 * @param ctx * @param circlePointX * @param circlePointY * @param radius * @param backgroundColor * @param save */ const drawCircle = (ctx, circlePointX, circlePointY, radius, backgroundColor, save = true) => { if (save) ctx.save(); //ctx.save(); ctx.beginPath(); ctx.arc(circlePointX, circlePointY, radius, 0, 2 * Math.PI, false); ctx.fillStyle = backgroundColor; ctx.fill(); ctx.clip(); //ctx.restore(); if (save) ctx.restore(); }; /** * 绘制圆形白边图像 * @param canvas * @param ctx 图像 * @param imageUrl 图像 * @param startPositionX x坐标 * @param startPositionY y坐标 * @param size 图像大小,除以2就是圆半径 * @param borderWidth 边框宽度 * @param borderColor 边框颜色 * @returns {Promise<*>} * @private */ const drawCircleImage = (canvas, ctx, imageUrl, startPositionX, startPositionY, size, borderWidth, borderColor) => { return new Promise((resolve, reject) => { //叠加圆形的绘制,需要先保存一下原始环境,然后绘制完第一个圆形后恢复绘制环境,即ctx.restore(); ctx.save(); drawCircle( ctx, startPositionX + size / 2, startPositionY + size / 2, size / 2, borderColor, false ); if (borderWidth) { drawCircle( ctx, startPositionX + size / 2, startPositionY + size / 2, size / 2 - borderWidth, borderColor, false ); } drawImage( canvas, ctx, imageUrl, startPositionX, startPositionY, size, size, 0, ).then(res => { ctx.restore(); resolve(res); }).catch(res => { reject(res); }); }); }; /** * 绘制高斯模糊效果的图像 * @param canvas * @param ctx * @param imageUrl * @param startPositionX * @param startPositionY * @param width * @param height * @param blur * @returns {Promise} * @private */ const drawBlurImage = (canvas, ctx, imageUrl, startPositionX, startPositionY, width, height, blur) => { return new Promise((resolve, reject) => { wx.getImageInfo({ src: imageUrl,//服务器返回的图片地址 success: function (res) { console.log("=>", res); let imgObj = canvas.createImage(); imgObj.src = res.path; imgObj.onload = async function (e) { console.log("=========>", imgObj.width, imgObj.height) ctx.save(); ctx.beginPath(); ctx.drawImage(imgObj, startPositionX, startPositionY, width, height); //提取图片信息 let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); //进行高斯模糊 let gd = stackBlur.imageDataRGBA(imageData, 0, 0, canvas.width, canvas.height, blur); //绘制模糊图像 ctx.putImageData(gd, 0, 0) ctx.restore(); console.log("图片加载完毕:", e); resolve(); }; }, fail: function (res) { console.log(res); reject(res); } }); }) }; /** * 绘制文本 * @param ctx * @param text * @param startPositionX * @param startPositionY * @param textAlign * @param fontSize * @param fontWeight * @param color * @param shadowColor 阴影颜色 * @param shadowOffsetX 阴影水平偏移距离 * @param shadowOffsetY 阴影垂直偏移距离 * @param shadowBlur 模糊程度 * @param letterSpace 间隔 * @returns {number} * @private */ const drawText = (ctx, text, startPositionX, startPositionY, textAlign, fontSize, fontWeight, fontFamily, color, shadowColor, shadowOffsetX, shadowOffsetY, shadowBlur, letterSpace = 0) => { if (!text) { return 0; } let textWidth = 0; ctx.save(); ctx.beginPath(); ctx.fillStyle = color; ctx.textAlign = textAlign; ctx.font = fontWeight + " " + fontSize + "px " + fontFamily; if (shadowColor) { console.log("设置阴影:", shadowColor); // 设置阴影 ctx.shadowColor = shadowColor; //阴影颜色 ctx.shadowOffsetX = shadowOffsetX; //偏移 ctx.shadowOffsetY = shadowOffsetY; ctx.shadowBlur = shadowBlur; //模糊程度 } if (!letterSpace) { let metrics = ctx.measureText(text); console.log("文字[" + text + "]宽度:", metrics.width); ctx.fillText( text, startPositionX, startPositionY ); textWidth = metrics.width; } else { //对齐方式调整为left ctx.textAlign = "left"; let positionXArr = [];//坐标集合 textWidth = ctx.measureText(text).width + (text.length - 1) * letterSpace;//含letterSpace的文字总宽度 for (let i = 0; i < text.length; i++) { if (i === 0) { switch (textAlign) { case "left": positionXArr.push(startPositionX); break; case "center": positionXArr.push(startPositionX - textWidth / 2); break; case "right": positionXArr.push(startPositionX - textWidth); break; default: console.warn("暂不支持的textAlign:", textAlign); break; } } else { let metrics = ctx.measureText(text[i - 1]); positionXArr.push(positionXArr[i - 1] + metrics.width + letterSpace); } } for (let i = 0; i < text.length; i++) { ctx.fillText( text[i], positionXArr[i], startPositionY ); } } ctx.restore(); return textWidth; }; /** * 绘制图图像 * @param canvas * @param ctx * @param startPositionX * @param startPositionY * @param width * @param height * @param blur * @param imageUrl * @returns {Promise} * @private */ const drawImage = (canvas, ctx, imageUrl, startPositionX, startPositionY, width, height, blur) => { return new Promise((resolve, reject) => { wx.getImageInfo({ src: imageUrl,//服务器返回的图片地址 success: function (res) { console.log("=>", res); let imgObj = canvas.createImage(); imgObj.src = res.path; imgObj.onload = function (e) { ctx.save(); ctx.beginPath(); ctx.filter = 'blur(' + blur + 'px)'; // ctx.globalAlpha = 0.6 ctx.drawImage(imgObj, startPositionX, startPositionY, width, height); ctx.restore(); console.log("图片加载完毕:", e); resolve(res); }; }, fail: function (res) { console.log(res); reject(res); } }); }) }; /** * 将画布内容保存为图片 * @param base * @param canvas * @param x * @param y * @param width * @param height * @param destWidth * @param destHeight * @param quality * @param fileType * @returns {Promise} */ const saveImage = (base, canvas, x, y, width, height, destWidth, destHeight, quality = 1, fileType = "png") => { return new Promise((resolve, reject) => { wx.canvasToTempFilePath({ x, y, width, height, destWidth, destHeight, quality, fileType, canvas, success(res) { console.log(res.tempFilePath); wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: (res) => { resolve(res); }, fail: (err) => { console.error(err) reject(err); } }) }, fail(res) { console.error("保存失败:", JSON.stringify(res)); reject(res); } }, base) }); } module.exports = { init, drawCircle, drawCircleImage, drawImage, drawBlurImage, drawText, drawRoundRect, saveImage }
2023-05-30 - 一个云函数五行代码搞定云调用openapi
云调用接口如下: https://developers.weixin.qq.com/miniprogram/dev/api-backend/ 1、该文档中的几十个接口,全部可由下面5行代码实现: 2、同时支持共享环境下的云调用 云函数名:openapi index.js代码: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async event => { let appid = cloud.getWXContext().FROM_APPID || cloud.getWXContext().APPID return await cloud.openapi({appid})[event.action](event.body) } 小程序端调用代码: onOpenapi: function () { wx.cloud.callFunction({ name: 'openapi', data: { action: 'urlscheme.generate', body: {} } }).then(res => { console.log(res) }) }, 将云调用相关的云函数合并成一个。 而且,极简。。。
2022-09-29 - 免费ICP备案攻略。不花1分钱拥有一台云服务器并顺利ICP备案。
写在前面: 大家不要将ICP证和ICP备案搞混了。 ICP证指的是【电信增值业务经营许可证】,这个资质需要企业主体至少100万注金,去工信部办理,比较难办理;社交-交友需要ICP证。 而ICP备案,【非经营性互联网信息服务备案核准】仅仅是指企业主体的域名备案,可以简单的按以下步骤免费办理成功,其他社交类目如社区、论坛、笔记等,只需要ICP备案即可。 1、在腾讯云注册一个账号并认证企业主体(不吹不黑,开发小程序当然首选腾讯云,好用)。http://www.qcloud.com/ 如果你是个人主体,就不要往下看了,没必要折腾了。 2、找到腾讯云免费活动页:https://cloud.tencent.com/act/free?from=10107 3、选择一款云服务器,180天免费试用。 云服务器申请成功后,它的使命就完成了,没用了,让它自生自灭吧。 在整个备案过程中,也不需要部署网站(域名都没有备案,哪来的网站?)。 [图片] 云服务器180天到期后,可以自己决定是否续费,每个月也才99元,促销期甚至更低,完全可以接受吧。 备案成功后,该服务器就没什么作用了,让它180天后自然欠费销毁得了。 服务器销毁后会有什么影响?答:没有任何影响。 但是。。。。。 你备案的域名最后还得指向一个网站,因为腾讯云会应工信部的要求定期检查网站是否合规,所以你还是要建一个简单的网站,(备案期间,可以暂时不管网站的事,等将来需要的时候再管理)。 至于有多简单,答,多简单都行。此时你可以在七牛、腾讯云、阿里云租点免费的对象存储空间,做个简单的网站。 4、在进行ICP备案之前,你需要在腾讯云注册你的域名地址,如果你已有域名,但不在腾讯云,建议先将要域名过户到腾讯云的账号上。 5、进入控制台,开始ICP备案,这个流程就不介绍了,因为完全一看就懂。而且现在使用备案小程序后,不需要幕布或现场拍照了,极其方便,大家跟着流程走就一点问题没有,有人脸识别和在线拍一段小视频。另外,大家可以随便作,随便填,填错或者填得不合适也不用怕,会有专门的备案客服打电话告诉你哪哪要改,还会告诉你应该怎么填才更容易通过工信部的审核,客服的态度好得发指。 仅说一点其中的几个小坑: a、人脸识别的时候,白色背景、白色背景、白色背景,笔者在人脸识别的时候,满世界找白墙,结果还被打回来重拍了3次。 b、网站用途一律写:公司官网,好通过工信部审核。 6、腾讯云提交资料到工信部审核。这是一个漫长的让人无语的等待,20-30天。笔者最近两次都是20天才过审;不要幻想会有可能提前完成审核,这是政府部门在审核,提前完成说明某政府人员的工作安排有问题,会犯错误的。 7、备案成功后,会有短信通知你,但是,你需要去工信部网站查询结果,并将结果切屏拷贝下来,因为小程序类目审核需要上传这张图片。http://beian.miit.gov.cn/publish/query/indexFirst.action [图片] 把上面这张图片保存好,小程序类目审核的时候需要上传。收到通知后,如果在这里查不到结果,也别急,据说需要24小时。 8、接下来是小程序上线审核。 因为ICP备案的小程序内容肯定涉及到社交,最后小程序上线时还要提交到工信部审核,还需要7天左右的时间,加上前面ICP备案的时间,加起来怎么也得30-40天。大家估计时间,别影响小程序上线。这7天也是政府部门在审核,不要幻想会提前。 9、计算一下时间: 腾讯云注册账号和认证:1-3天; 域名备案:腾讯云环节:1-3天; 域名备案:工信部环节:20-30天; 小程序添加服务类目:社交类目审核:1-3天; 小程序上线审核:腾讯环节:1-2天; 小程序上线审核:工信部环节:7+天; 总天数:30-40天; 10、节省时间的一些建议: 在开发小程序之前,就开始备案工作,小程序可以同时开发,相互不影响; 在开发完成之前一、两星期之内,先发布一版小程序,别管功能是不是完整,能通过审核就行,这样会有7天的等待类目审核的时间,这个时间里,小程序可以照常开发,不影响进度; 只要是社交类,基本需要有文字和图片安全检查功能,别忘了加上,别到时审核通过不了。 11、结束。 [图片]
2021-01-19 - Skyline|原生级卡片转场,小程序轻松实现
在上一篇文章《在小程序中实现原生相册》中,我们学习了自定义路由搭配共享元素实现的原生相册效果,共享元素可以让用户在体验小程序时视觉关联性更强。 除了相册实现之外,常见的卡片转场也非常适合。 [图片] ⬆️ 演示效果:默认动画 vs 卡片转场动画 👇 下面我们来看看卡片转场中通过 共享元素 + 自定义路由 来实现无痕跳转。 [图片] 这里的转场稍微有点复杂,涉及到以下 3 个点 旧卡片:图片放大、内容渐隐新页面:按比例放大、页面渐显手势搭配1、旧卡片:图片放大、内容渐隐 在本示例中,列表页采用的是 scroll-view 瀑布流布局的实现。 [图片] 这里我们的共享元素是卡片,即 grid-view 中的内容 card,卡片包括 图片、内容描述。 [图片] 默认情况下,共享元素是整个节点进行飞跃的,由于前后页面的图片元素一致但文本内容不一致, 导致在第一帧或者最后一帧会有跳动的效果。 为了让转场动画更加自然,我们需要在飞跃的过程中渐隐旧卡片的内容描述。 [图片] 在这里,我们需要先用 this.applyAnimatedStyle 来给对应的节点绑定 worklet 驱动动画。 .card_wrap 节点:整个卡片按比例放大.card_desc 节点:内容描述渐隐[图片] 关于动画执行的时机,我们可以通过配置项修改。 immediate:设置是否立即执行驱动动画flush:shareValue 更新时,applyAnimatedStyle 的 updater 函数刷新时机在本例中,需要保证共享元素的图片与目标页面图片位置重叠,所以 flush 设置 sync 在当前时间片刷新。 [图片] 绑定完驱动动画之后,我们需要给共享元素绑定帧回调事件,根据当前动画进度改变共享变量的值来驱动共享动画 [图片] 2、新页面:按比例放大、页面渐显 新页面在路由中的动画,需要在自定义路由中进行配置。关于自定义路由的更多介绍,可参考《小程序页面转场动画》 在路由动画过程中,我们将上一步的共享元素帧回调拿到 begin、end 的值,然后结合动画进度 t 计算得出新页面的位置、缩放比例。 还有根据动画进度,设置页面渐显,与前面的卡片渐隐承接。 [图片] 3、手势搭配 学习过我们前面的文章的同学都知道,自定义路由经常需要结合页面手势,来实现手势返回,关于手势的基础知识可参考《小程序页面转场动画》 [图片] 这里我们希望手势缩小整个当前页面,所以这里手势返回时只在当前页面做手势动画即可。 在页面详情页的最外层,嵌套一个手势组件 pan-gesture-handler,当手势拖动时根据手势的位置改变整个页面(通过 #fake-host 控制)的位置和大小来达到拖动的效果。 [图片] 同样绑定页面驱动动画,通过 applyAnimatedStyle 给 #fake-host 绑定驱动动画,当共享变量 transX、transY 等变化时则自动改变 transform 来驱动 #fake-host 缩小。 [图片] 接着绑定手势事件,根据手势拖动时拿到位置信息改变共享变量 transX、transY 的值。 [图片] 最后我们需要设置背景颜色透明,来达到类似把卡片拖回列表的视觉效果,更好的减少页面切换感~ [图片] 一个自定义路由的页面会有 3 层可以设置到背景色,要做到透明的效果需要将 3 个背景色都设置为透明。更多自定义路由背景色的详情参考官方文档。 [图片] 想要试试卡片转场的无恒效果~扫描 ⬇️ 下方小程序码即可体验。 如果你也想在小程序中实现卡片转场动画,mark 下这个 源码 直接接到到你的小程序吧~ [图片]
2023-08-03 - Skyline|小程序手势:让半屏弹窗更顺滑
在小程序页面开发中,我们经常用半屏弹窗来进来内容展示,例如:微信开放社区切换主页、加入购物车的选项页、文章留言区等等。 [图片] 常见的半屏弹窗展示逻辑是这样的: 打开弹窗:点击 “打开弹窗” 按钮展示弹窗关闭弹窗:点击“关闭按钮” or 遮罩层 关闭弹窗当我们想在半屏弹窗加一些交互动画时,可以监听节点的 touch 事件来做一些手势判断,进而处理拖拽事件。但是这种方式实现的滚动动画容易卡顿,出现延迟的情况,效果并不理想。 为了丰富小程序的交互体验,我们内置了一批手势组件,可以帮助开发者更好的实现交互动画的效果。 下图演示使用手势的半屏弹窗下拉效果与普通半屏下拉的对比。 当内部评论列表往下拉到顶部时,变为半屏的下拉,可直接下拉关闭弹窗。 [图片] 我们来看下这种操作是怎么实现的 在上面评论列表的半屏弹窗中会有一个 scroll-view 滚动组件,在 scroll-view 中会有滚动事件,当滚动到顶部时,我们希望有整个半屏的下拉事件。 所以我们需要在半屏的最外层放置一个拖动手势组件 pan-gesture-handler 由于拖动组件内部的 scroll-view 也是可以滚动的,所以这里需要进行一个手势协商的处理,就是什么条件下由哪个组件来响应手势。 当手势往下 ⬇️ 滚动时,此时判断内部 scroll-view 滚动条的位置 滚动条处于顶部:外层 pan-gesture-handler 响应滚动,此时半屏往下拖动至关闭半屏滚动条不处于顶部:内层 scroll-view 响应滚动,此时内部列表往上滚[图片] 当手势往上 ⬆️ 滚动时,此时判断半屏的位置 半屏不完全打开时:外层 pan-gesture-handler 响应滚动,此时半屏往上拖动至完全打开半屏半屏完全打开时:内层 scroll-view 响应滚动,此时内部列表往下滚[图片] 我们来看一下代码的实现,这里用到的手势组件 pan-gesture-handler(拖动时触发)和 vertical-drag-gesture-handler(纵向滑动时触发),手势组件有以下属性 on-gesture-event:手势回调事件should-response-on-move:是否响应当前手势的 move 阶段simultaneous-handlers:指定需要协商的手势是哪几个,下面演示表示 pan 和 scroll 协同触发。native-view:代理的原生节点,这里 scroll-view(scroll-y) 内有个 vertical-drag 手势,scroll-view 自身无法处理,需要被代理出来 ... 接着,我们看看在页面 js 中怎么处理手势。 在手势处理的回调中因为会改变半屏的状态值,所以这里的回调函数采用 worklet 函数,worklet 函数运行在 UI 线程,使得小程序可以做到类原生动画般的体验。 // page.js // shared 创建的变量为共享变量,可在 UI 线程和 JS 线程间同步 this.transY = wx.worklet.shared(1000) this.scrollTop = wx.worklet.shared(0) this.startPan = wx.worklet.shared(true) // shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商 shouldPanResponse() { 'worklet' return this.startPan.value }, shouldScrollViewResponse(pointerEvent) { 'worklet' // transY > 0 说明 pan 手势在移动半屏,此时 scroll-view 滚动不应生效 if (this.transY.value > 0) return false const scrollTop = this.scrollTop.value const { deltaY } = pointerEvent // deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,scroll-view 滚动不生效 const result = scrollTop <= 0 && deltaY > 0 this.startPan.value = result return !result }, // pan 手势处理 handlePan(gestureEvent) { 'worklet' if (gestureEvent.state === GestureState.ACTIVE) { const curPosition = this.transY.value const destination = Math.max(0, curPosition + gestureEvent.deltaY) // 改变半屏的位置 this.transY.value = destination } // 其他手势状态的处理,如滚动结束时计算半屏处于打开还是关闭的状态 } 目前,同程旅行 已经上线了手势结合半屏的效果 体验路径:酒店查询 - 选择酒店 - 选择入住人 - 新增入住人 [图片] 普通半屏结合手势代码片段:https://developers.weixin.qq.com/s/lx0RH1mD7rGj 手势除了在普通半屏的应用之外,也可以实现分段式半屏。下面演示的分段式半屏比普通半屏的判断条件更多一些。 判断条件同普通半屏类似,根据手势方向 和 分段式半屏当前的位置来判断是响应分段式半屏还是内部列表,响应分段式半屏是改变到哪一个位置。 [图片] 这里与普通半屏不同的是我们还改变了地图的缩放级别(scale) 因为 worklet 函数是在 UI 线程运行的,当要改变 data 值时,需要通过 wx.worklet.runOnJS 调回 JS 线程。 // page.js // 设置 map scale // 运行在 JS 线程 setMapScale(scale) { this.setData({ scale }) }, // worklet 函数,运行在 UI 线程 scrollTo(toValue) { 'worklet' let scale = 18 if (toValue > screenHeight / 2) { scale = 16 } // 从 UI 线程调回 JS 线程 wx.worklet.runOnJS(this.setMapScale.bind(this))(scale) this.transY.value = timing(toValue, { duration: 200 }) }, // 处理拖动半屏的手势 handlePan(gestureEvent) { 'worklet' // 滚动半屏的位置 if (gestureEvent.state === GestureState.ACTIVE) { // deltaY < 0,往上滑动 this.upward.value = gestureEvent.deltaY < 0 // 当前半屏位置 const curPosition = this.transY.value // 只能在 [statusBarHeight, screenHeight] 之间移动 const destination = clamp(curPosition + gestureEvent.deltaY, statusBarHeight, screenHeight) if (curPosition === destination) return // 改变 transY,来改变半屏的位置 this.transY.value = destination } if (gestureEvent.state === GestureState.END || gestureEvent.state === GestureState.CANCELLED) { if (this.transY.value <= screenHeight / 2) { // 在上面的位置 if (this.upward.value) { this.scrollTo(statusBarHeight) } else { this.scrollTo(screenHeight / 2) } } else if (this.transY.value > screenHeight / 2 && this.transY.value <= this.initTransY.value) { // 在中间位置的时候 if (this.upward.value) { this.scrollTo(screenHeight / 2) } else { this.scrollTo(this.initTransY.value) } } else { // 在最下面的位置 this.scrollTo(this.initTransY.value) } } }, 分段式页面代码片段:https://developers.weixin.qq.com/s/fw0U31mI7bGf 半屏的交互除了在页面内实现,也能跨页面实现,如常见的下沉式半屏交互。其中,半屏效果与上述实现类似,而前一页面的下沉实现需要结合自定义路由 后面的文章中我们会介绍自定义路由结合手势怎么去实现下沉式半屏效果,不仅如此,还有很多类原生的页面切换效果都能通过自定义路由实现 [图片]
2023-08-03 - 微搭低代码xChatGPT,五步搭建AI聊天机器人小程序
本文将向您展示如何使用低代码工具,在30分钟左右搭建一个基于 ChatGPT 的聊天机器人小程序。让拥有OpenAI账号的朋友能随时随地体验ChatGPT,也希望低代码xAI技术普惠更多人。 [图片] ChatGPT最近大火,让原本已经沉寂许久的AI领域再次被唤醒狂欢。但是还是有很多朋友受限于OpenAI对国内用户的限制,无法愉快地体验这项革命性的AIGC技术。 众所周知,ChatGPT 是一个基于 GPT-3 的聊天机器人模型,能够通过分析提问内容生成流畅的自然语言结果,我们除了可以在 OpenAI 的ChatGPT官网上体验,也可以通过调用官方API来获取 ChatGPT 机器人模型进行训练和体验。本文将向您展示如何使用低代码工具,在30分钟左右搭建一个基于 ChatGPT 的聊天机器人小程序。一方面能让拥有OpenAI账号的朋友能随时随地体验ChatGPT;另一方面,也希望通过教程学习搭建出AI聊天小程序,去分享给更多人,把前沿的AI技术普惠到更广泛的群体,一起体验AIGC技术所带来的便利。 我们这次就以腾讯云微搭低代码作为搭建平台,来演示一下应该如何快速开发一个基于ChatGPT的聊天机器人应用,即便是新手开发者也可以试试哦 一、准备工作在开始搭建聊天机器人之前,您需要做以下准备: 微信小程序账号:如果您还没有微信小程序账号,可以在微信公众平台注册(如果没有小程序,也可以发布为移动端H5应用)开通腾讯云微搭低代码:微搭低代码是腾讯云官方推出的一款快速搭建应用的低代码开发工具,可以直接访问腾讯云微搭官网免费开通注册OpenAI账号:OpenAI账号注册也是免费的,不过OpenAI有地域限制,这里网上教程关键词搜索一大把,就不做赘述了。注册成功后,可以登录OpenAI的个人中心来获取[代码]API KEY[代码]本教程适用人群和应用类型: 适用人群:初级开发者(操作门槛较低,有一定技术背景的非开发者也可以体验)应用类型:小程序 或 H5应用(基于微搭一码多端特性,也可以直接发布为Web应用,点击文末链接可体验作者微搭搭建的Web版GPT聊天机器人)二、搭建聊天机器人首先,一个常见的聊天对话机器人应用界面效果,如下图所示: [图片] 通过应用界面可以看到,它主要由如下几个部分组成: 一个对话聊天界面一个API数据查询接口界面UI与后端数据的联动渲染那现在,我们就参照上面的几个模块,正式开始通过微搭低代码工具,分5个步骤来依次拆解搭建: 1.对应用界面进行样式配置[图片] 我们先拖入一个滚动容器和一个普通容器,一个用来展示聊天的上下文对话,一个用来展示输入框和发送按钮。然后依次拖入图中大纲树所示的组件,组件相应的层级关系可以参考上图中的大纲树结构。 接下来针对上述的组件分别进行样式的配置,我们默认使用样式面板的弹性(Flex)布局,包含接收消息和发送消息两个普通容器,可以分别选择样式面板中的弹性布局中的左对齐,如下图所示: [图片] 接着可以分别配置图片和文本两个组件的高度和宽度大小以及内外间距,以达到想要的视觉效果。 完成聊天上下文对话框的样式配置之后,可以进行底部多行输入框和按钮这个普通容器的样式配置,样式配置方式与上面的发送消息容器一样使用弹性布局并选择“平分”的方式布局,如下图所示: [图片] 完成布局配置之后,由于底部输入框按钮等是固定位置的,故需要额外配置一下定位属性,选中底部的“普通容器”,在样式面板底部,做如下配置即可: [图片] 以上,通过进一步微调一些样式细节如组件背景色以及间距等后,即可达到上文提到的应用界面效果了。 可以看到整个页面的配置过程是完全可视化操作的,不需要写一行代码。当然,如果样式配置不是很熟悉,或者有疑问的朋友,也可以等我们的视频教程,手把手教你用微搭低代码来配置AI聊天应用。 2. 配置数据变量和数据源API第2步,开始进行数据的绑定和数据源的配置: a. 新建一个数组对象变量chatList,用于存储聊天记录 [图片] 首先配置一个变量,如命名为chatLlist聊天记录这么一个变量,一个对象数组,默认值为如下所示,当然大家也可以基于这个结构任意修改。 [ { "res": "你好,欢迎体验ChatGPT聊天机器人,你可以直接输入你感兴趣的任何问题向我提问", "req": "红孩儿是牛魔王的亲儿子吗?", "index": 1 }, { "res": "不是,红孩儿是牛魔王的养子。据西游记中的记载,牛魔王是一个孤独的怪物,他在深山里住了很久,没有子女,却有一个养子——红孩儿,红孩儿的父母去世时,牛魔王便收养了他。", "req": "那谁教他的三味真火", "index": 2 } ] 接着把这个数组变量的初始值跟我们的这个页面的内容分别进行绑定。首先我们选择一个父级的普通容器,在属性配置的循环展示绑定为刚刚新建的数组变量。然后在里面的子节点中,如文本组件,分别绑定这个数组中的成员变量,他们的配置如下图所示: [图片] [图片] 这一步数据绑定完成之后,接下来就可以去配置请求远程数据的数据源API了。 b. 配置一个数据源APIs(用于请求Chat GPT接口) API的配置相对比较简单,主要参考OPENAI的官方文档,文档中可以看到文本对话接口对应的请求参数信息如下: curl https://api.openai.com/v1/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{"model": "text-davinci-003", "prompt": "Say this is a test", "temperature": 0, "max_tokens": 7}' 分别把上方的CURL请求头信息对应到HTTP的请求中即可,配置项参考如下: [图片] 我们通过数据源中的【方法测试】,得到API的返回结果如下,点击【出参映射】即可完成出参结构的配置: { "id": "cmpl-GERzeJQ4lvqPk8SkZu4XMIuR", "object": "text_completion", "created": 1586839808, "model": "text-davinci:003", "choices": [ { "text": "\n\nThis is indeed a test", "index": 0, "logprobs": null, "finish_reason": "length" } ], "usage": { "prompt_tokens": 5, "completion_tokens": 7, "total_tokens": 12 } } 其中的[代码]API_KEY[代码]是在完成OPENAI账号注册之后,在其个人中心中获取即可,至于OPENAI的账号注册方式,大家动动手搜索一下,网上教程很多就不啰嗦了。 3. 给发送按钮绑定请求事件我们在第1步中已经在页面中放置了输入框、按钮和文本展示等组件。接下来,我们需要给输入框配置相关的事件响应逻辑,来获取用户输入的消息内容,参考的关键配置如下: [图片] 然后,给按钮绑定事件来处理输入框中用户发送的消息,选择按钮组件,在右侧事件面板中配置如下逻辑,即 点击按钮 时触发API请求,并将获取到的API返回结果渲染在页面中。 [图片] 4. 将API返回数据 与 在页面中进行渲染展示第4步,将返回值用“变量赋值”方法加入到chatList数组中 [图片] 这里我们需要在数据中增加一条新的消息,采用表达式绑定的方式进行原有的[代码]ChatList[代码]变量进行解构后再赋值,表达式参考如下: [ ...$page.dataset.state.chatList, { req: $page.dataset.state.text, res: "" } ] 5. 完成开发,进行应用发布前端界面和后端数据逻辑都配置开发完成后,可在应用编辑器的右上角点击“发布”按钮,我们可以选择发布到 已绑定的小程序,也可以直接发布移动端的H5应用,如下所示: [图片] 至此,一个基础的AI聊天机器人应用搭建就完成了。 三、进一步完善基于上述步骤搭建完聊天机器人小程序后,你还可以进一步完善它的功能。 例如,您可以在小程序中添加聊天记录功能,让用户可以查看过往的聊天记录。您也可以使用其他自然语言处理技术;例如语音识别和文本分类,来使聊天机器人更加智能。 如需要存储聊天历史记录的话,可以在数据源中配置一个“聊天历史记录”数据模型,参考模型配置如下: [图片] 总之,使用微搭低代码搭建聊天机器人小程序,对于熟悉低代码或者喜欢钻研能力的朋友来说,确实是一件非常简单而有趣的事情。当然如果确实对界面样式配置不是很熟悉,或者其他有疑问的朋友,也可以关注漫话开发者公众号后续视频教程,手把手教你用微搭低代码来配置AI聊天机器人应用。 通过本教程的介绍,你已经基本熟悉了如何使用微搭低代码快速搭建基于 ChatGPT 的聊天机器人了,有任何疑问也欢迎关注漫话开发者公众号留言。 四、附录Q/A腾讯云微搭低代码平台,连接微信和企微用户,帮助企业快速定制和构建移动协同办公应用,让信息和流程流转更高效。3分钟可视化搭建和发布小程序、H5、Web等多端应用。快速搭建企业专属的业务管理平台,表单流程等办公和管理类应用,提供企业级账号和权限管控等能力。在搭建聊天机器人应用过程中,你可能会遇到一些问题,下面是常见问题的解决方法:i. 机器人无法回复:这可能是因为 ChatGPT 机器人模型无法理解用户的问题。可以尝试使用更加具体的问题,或者尝试使用不同的自然语言处理模型。ii. 机器人回复不流畅:这可能是因为 ChatGPT 机器人模型生成的回复不够流畅或在服务器在境外所致。可以尝试调整模型的「[代码]temperature[代码]」参数,使生成的回复更加流畅。iii. 机器人回复内容不准确:这可能是因为 ChatGPT 机器人模型无法理解用户的问题,或者因为模型没有学习到足够的知识。可以尝试使用更加具体的问题,或者尝试使用不同的自然语言处理模型。iv. 如果遇到其他低代码配置问题,可以尝试在微搭社区或通过网上搜索中寻求帮助。
2023-02-10 - 微搭低代码xChatGPT,五步搭建AI聊天机器人小程序
[图片] ChatGPT最近大火,让原本已经沉寂许久的AI领域再次被唤醒狂欢。但是还是有很多朋友受限于OpenAI对国内用户的限制,无法愉快地体验这项革命性的AIGC技术。 众所周知,ChatGPT 是一个基于 GPT-3 的聊天机器人模型,能够通过分析提问内容生成流畅的自然语言结果,我们除了可以在 OpenAI 的ChatGPT官网上体验,也可以通过调用官方API来获取 ChatGPT 机器人模型进行训练和体验。 本文将向您展示如何使用低代码工具,在30分钟左右搭建一个基于 ChatGPT 的聊天机器人小程序。一方面能让拥有OpenAI账号的朋友能随时随地体验ChatGPT;另一方面,也希望通过教程学习搭建出AI聊天小程序,去分享给更多人,把前沿的AI技术普惠到更广泛的群体,一起体验AIGC技术所带来的便利。 我们这次就以腾讯云微搭低代码作为搭建平台,来演示一下应该如何快速开发一个基于ChatGPT的聊天机器人应用,即便是新手开发者也可以试试哦 一、准备工作 在开始搭建聊天机器人之前,您需要做以下准备: 微信小程序账号:如果您还没有微信小程序账号,可以在微信公众平台注册(如果没有小程序,也可以发布为移动端H5应用) 开通腾讯云微搭低代码:微搭低代码是腾讯云官方推出的一款快速搭建应用的低代码开发工具,可以直接访问腾讯云微搭官网免费开通注册 OpenAI账号:OpenAI账号注册也是免费的,不过OpenAI有地域限制,这里网上教程关键词搜索一大把,就不做赘述了。注册成功后,可以登录OpenAI的个人中心来获取[代码]API KEY[代码] 本教程适用人群和应用类型: 适用人群:初级开发者(操作门槛较低,有一定技术背景的非开发者也可以体验) 应用类型:小程序 或 H5应用(基于微搭一码多端特性,也可以直接发布为Web应用,点击文末链接可体验作者微搭搭建的Web版GPT聊天机器人) 二、搭建聊天机器人 首先,一个常见的聊天对话机器人应用界面效果,如下图所示: [图片] 通过应用界面可以看到,它主要由如下几个部分组成: 一个对话聊天界面 一个API数据查询接口 界面UI与后端数据的联动渲染 那现在,我们就参照上面的几个模块,正式开始通过微搭低代码工具,分5个步骤来依次拆解搭建: 1.对应用界面进行样式配置 [图片] 我们先拖入一个滚动容器和一个普通容器,一个用来展示聊天的上下文对话,一个用来展示输入框和发送按钮。然后依次拖入图中大纲树所示的组件,组件相应的层级关系可以参考上图中的大纲树结构。 接下来针对上述的组件分别进行样式的配置,我们默认使用样式面板的弹性(Flex)布局,包含接收消息和发送消息两个普通容器,可以分别选择样式面板中的弹性布局中的左对齐,如下图所示: [图片] 接着可以分别配置图片和文本两个组件的高度和宽度大小以及内外间距,以达到想要的视觉效果。 完成聊天上下文对话框的样式配置之后,可以进行底部多行输入框和按钮这个普通容器的样式配置,样式配置方式与上面的发送消息容器一样使用弹性布局并选择“平分”的方式布局,如下图所示: [图片] 完成布局配置之后,由于底部输入框按钮等是固定位置的,故需要额外配置一下定位属性,选中底部的“普通容器”,在样式面板底部,做如下配置即可: [图片] 以上,通过进一步微调一些样式细节如组件背景色以及间距等后,即可达到上文提到的应用界面效果了。 可以看到整个页面的配置过程是完全可视化操作的,不需要写一行代码。当然,如果样式配置不是很熟悉,或者有疑问的朋友,也可以等我们的视频教程,手把手教你用微搭低代码来配置AI聊天应用。 2. 配置数据变量和数据源API 第2步,开始进行数据的绑定和数据源的配置: a. 新建一个数组对象变量chatList,用于存储聊天记录 [图片] 首先配置一个变量,如命名为chatLlist聊天记录这么一个变量,一个对象数组,默认值为如下所示,当然大家也可以基于这个结构任意修改。 [代码][ { "res": "你好,欢迎体验ChatGPT聊天机器人,你可以直接输入你感兴趣的任何问题向我提问", "req": "红孩儿是牛魔王的亲儿子吗?", "index": 1 }, { "res": "不是,红孩儿是牛魔王的养子。据西游记中的记载,牛魔王是一个孤独的怪物,他在深山里住了很久,没有子女,却有一个养子——红孩儿,红孩儿的父母去世时,牛魔王便收养了他。", "req": "那谁教他的三味真火", "index": 2 } ] [代码] 接着把这个数组变量的初始值跟我们的这个页面的内容分别进行绑定。首先我们选择一个父级的普通容器,在属性配置的循环展示绑定为刚刚新建的数组变量。然后在里面的子节点中,如文本组件,分别绑定这个数组中的成员变量,他们的配置如下图所示: [图片] [图片] 这一步数据绑定完成之后,接下来就可以去配置请求远程数据的数据源API了。 b. 配置一个数据源APIs(用于请求Chat GPT接口) API的配置相对比较简单,主要参考OPENAI的官方文档,文档中可以看到文本对话接口对应的请求参数信息如下: [代码]curl https://api.openai.com/v1/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{"model": "text-davinci-003", "prompt": "Say this is a test", "temperature": 0, "max_tokens": 7}' [代码] 分别把上方的CURL请求头信息对应到HTTP的请求中即可,配置项参考如下: [图片] 我们通过数据源中的【方法测试】,得到API的返回结果如下,点击【出参映射】即可完成出参结构的配置: [代码]{ "id": "cmpl-GERzeJQ4lvqPk8SkZu4XMIuR", "object": "text_completion", "created": 1586839808, "model": "text-davinci:003", "choices": [ { "text": "\n\nThis is indeed a test", "index": 0, "logprobs": null, "finish_reason": "length" } ], "usage": { "prompt_tokens": 5, "completion_tokens": 7, "total_tokens": 12 } } [代码] 其中的[代码]API_KEY[代码]是在完成OPENAI账号注册之后,在其个人中心中获取即可,至于OPENAI的账号注册方式,大家动动手搜索一下,网上教程很多就不啰嗦了。 3. 给发送按钮绑定请求事件 我们在第1步中已经在页面中放置了输入框、按钮和文本展示等组件。接下来,我们需要给输入框配置相关的事件响应逻辑,来获取用户输入的消息内容,参考的关键配置如下: [图片] 然后,给按钮绑定事件来处理输入框中用户发送的消息,选择按钮组件,在右侧事件面板中配置如下逻辑,即 点击按钮 时触发API请求,并将获取到的API返回结果渲染在页面中。 [图片] 4. 将API返回数据 与 在页面中进行渲染展示 第4步,将返回值用“变量赋值”方法加入到chatList数组中 [图片] 这里我们需要在数据中增加一条新的消息,采用表达式绑定的方式进行原有的[代码]ChatList[代码]变量进行解构后再赋值,表达式参考如下: [代码][ ...$page.dataset.state.chatList, { req: $page.dataset.state.text, res: "" } ] [代码] 5. 完成开发,进行应用发布 前端界面和后端数据逻辑都配置开发完成后,可在应用编辑器的右上角点击“发布”按钮,我们可以选择发布到 已绑定的小程序,也可以直接发布移动端的H5应用,如下所示: [图片] 至此,一个基础的AI聊天机器人应用搭建就完成了。 三、进一步完善 基于上述步骤搭建完聊天机器人小程序后,你还可以进一步完善它的功能。 例如,您可以在小程序中添加聊天记录功能,让用户可以查看过往的聊天记录。您也可以使用其他自然语言处理技术;例如语音识别和文本分类,来使聊天机器人更加智能。 如需要存储聊天历史记录的话,可以在数据源中配置一个“聊天历史记录”数据模型,参考模型配置如下: [图片] 总之,使用微搭低代码搭建聊天机器人小程序,对于熟悉低代码或者喜欢钻研能力的朋友来说,确实是一件非常简单而有趣的事情。当然如果确实对界面样式配置不是很熟悉,或者其他有疑问的朋友,也可以关注漫话开发者公众号后续视频教程,手把手教你用微搭低代码来配置AI聊天机器人应用。 通过本教程的介绍,你已经基本熟悉了如何使用微搭低代码快速搭建基于 ChatGPT 的聊天机器人了,有任何疑问也欢迎关注漫话开发者公众号留言。点击右侧链接,可以立即体验作者搭建的Web版GPT聊天机器人。 也欢迎关注「漫话开发者」低代码系列文章: 微信支付x低代码,快速构建支付类小程序实操教程 小程序消息推送x微搭低代码,微信消息推送快速上手实操教程 四、附录Q/A 腾讯云微搭低代码平台,连接微信和企微用户,帮助企业快速定制和构建移动协同办公应用,让信息和流程流转更高效。3分钟可视化搭建和发布小程序、H5、Web等多端应用。快速搭建企业专属的业务管理平台,表单流程等办公和管理类应用,提供企业级账号和权限管控等能力。 在搭建聊天机器人应用过程中,你可能会遇到一些问题,下面是常见问题的解决方法: i. 机器人无法回复:这可能是因为 ChatGPT 机器人模型无法理解用户的问题。可以尝试使用更加具体的问题,或者尝试使用不同的自然语言处理模型。 ii. 机器人回复不流畅:这可能是因为 ChatGPT 机器人模型生成的回复不够流畅或在服务器在境外所致。可以尝试调整模型的「[代码]temperature[代码]」参数,使生成的回复更加流畅。 iii. 机器人回复内容不准确:这可能是因为 ChatGPT 机器人模型无法理解用户的问题,或者因为模型没有学习到足够的知识。可以尝试使用更加具体的问题,或者尝试使用不同的自然语言处理模型。 iv. 如果遇到其他低代码配置问题,可以尝试在微搭社区或通过网上搜索中寻求帮助。 点击右侧链接,可以立即体验作者搭建的Web版聊天机器人 最后,上述教程文本都通过ChatGPT校验,enjoy~
2023-02-09 - 小程序地理位置接口提审注意事项
通过这段时间发现,有很多小伙伴卡在“获取当前的地理位置、速度(wx.getLocation)”接口的审核上,一直审核不通过,在微信官方论坛上这也是频繁发生被吐槽的问题,现在领健小助手总结经验如下,望帮助大家通过提审 一、选择正确的类目 建议选择两个以上符合要求的类目: https://developers.weixin.qq.com/miniprogram/dev/api/location/wx.getLocation.html 2.申请理由 申请理由要站在用户角度写,例如:帮助用户XXXX 更便捷的XXX。。。。 历史通过的文案: “我的业务涉及到线下门店服务,该接口可帮助我的用户快速找到离他最近的门店,方便用户进行附近门店查看; 业务涉及附近门店地址导航,用于实时定位用户位置,精准计算附近门店距离和导航信息,帮助用户到店时的地图导航; 业务涉及线下发货,帮助用户更好的收到物品” (以上文案需根据自身情况及类目要求调整文案,若相同文案提交过多也会导致审核不通过的概率) 3.图片和视频一定都要填写 建议附上【弹出地理位置截图】、【门店地址】、【提交订单-快递配送】、【付款界面截图】、【预约门店选择】等涉及到地里位置的截图及视频 如果审核仍然不通过,我们可以帮您分析一下,不通过的原因一般包含这几种,可做对应的操作: 1.你提供的申请原因/辅助图片/网页/视频内容无法确认申请接口使用场景 问题:导致此种原因一般是审核人员不知道你这个接口用在哪里 解决:你只需要进入你的小程序,找到用到自动定位的页面,截图或者录个视频上传审核即可。 2.你所描述的小程序接口使用场景,目前未符合接入wx.getLocation...接口的开放范围 分析:导致此种原因一般是小程序的服务类目选择不对。 解决:建议选择两个以上符合要求的类目:https://developers.weixin.qq.com/miniprogram/dev/api/location/wx.getLocation.html 3.暂不支持测试 分析:小程序页面存在测试的内容 解决:修改小程序页面内容,包裹文字,图片等..确保打开小程序看到的是一个生产环境下的小程序,连自己看了都像是测试的小程序,那肯定是过不了。 4.小程序流程涉及账户登录或者有测试环境配置要求,暂时无法确认接口使用场景 分析:审核人员无法到达接口运用场景进行实际体验 解决方法:修提交审核的时候备注好登录的账户密码,方便审核人员登录进去审核,还可以提供视频或者图片引导审核人员到达接口调用页面。
2023-03-29 - 图片安全检测data exceed max size解决方案
最近在重构小程序恋爱小清单,在用云函数做图片的安全检测时报了一个错:cloud.callFunction:fail Error: data exceed max size 也就是图片超过了大小限制。 早期的版本是通过画布将图片缩小(wx.canvasToTempFilePath),接着读取文件流(wx.getFileSystemManager().readFile),然后再提交云函数检测,过程感觉有些繁琐复杂 最近发现其实有更简单的方法,可以借助临时的CDN,传递大数据,最终在云函数端会收到一个CDN地址,接着通过request-promise读取文件流,然后再做安全检测,相比旧版的方法个人感觉简单清爽不少。 参考官方文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/utils/Cloud.CDN.html 代码如下: 小程序端: const api = require("api.js"); /** * 图片安全检测 * 借助临时CDN传递大数据 * @param filePath 图片的临时文件路径 (本地路径) * @returns {Promise<unknown>} */ const imgSecCheckViaCDN = (filePath) => { return new Promise(function (resolve, reject) { api.callCloudFunction("securityCheck", { type: "imgSecCheckViaCDN", imgData: wx.cloud.CDN({ type: "filePath", filePath, }) }, res => { console.log("图片安全检测结果:", JSON.stringify(res)); const result = res.result; if (result.success) { resolve(result); } else { reject(result); } }, reject); }); } api.js /** * 云函数调用 * @param name * @param data * @param success * @param fail * @param complete */ const callCloudFunction = function (name, data, success, fail, complete) { //执行云函数 wx.cloud.callFunction({ // 云函数名称 name: name, // 传给云函数的参数 data: Object.assign({}, data, {env: env.activeEnv}) }).then(res => { typeof success == 'function' && success(res); }).catch(res => { typeof fail == 'function' && fail(res); }).then(res => { typeof complete == 'function' && complete(res); }); }; module.exports = {callCloudFunction} 云函数端: // 云函数入口文件 const cloud = require('wx-server-sdk'); const responce = require('easy-responce'); const requestHelper = require('./utils/requestHelper'); const headers = { encoding: null, headers: { "content-type": "application/octet-stream", // "content-type": "video/mpeg4", }, }; // 云函数入口函数 exports.main = async (event, context) => { cloud.init({ env: event.env }); let result = {}; try { const {type, content, imgData} = event; let {buffer} = event; console.log("检测类型:", type, "文本内容:", content, "图片内容:", imgData); switch (type) { case "imgSecCheckViaCDN": const imageResponse = await requestHelper.request(imgData, headers, {}); buffer = imageResponse.body; case "imgSecCheck": result = await cloud.openapi.security.imgSecCheck({ media: { contentType: 'image/png', // value: Buffer.from(imgBase64, "base64") value: Buffer.from(buffer) } }); break; case "msgSecCheck": result = await cloud.openapi.security.msgSecCheck({content}); break; default: console.log("不支持的检测类型:", type); break; } } catch (e) { console.error(e); result = e; } console.log("检测结果:", result); const {errCode, errMsg} = result; return errCode !== 87014 ? responce.success({errCode}) : responce.fail(errMsg); }; requestHelper.js const rp = require('request-promise'); /** * http请求 * @param url * @param options * @param data * @param autoFollowRedirect * @returns {Promise<unknown>} */ const request = function (url, options, data, autoFollowRedirect = true) { return new Promise(function (resolve, reject) { const p = Object.assign({ json: true, resolveWithFullResponse: true, followRedirect: autoFollowRedirect }, options, data, {url}); console.log("请求参数:", JSON.stringify(p)); return rp(p) .then(async function (repos) { //console.log("获取到最终内容,执行回调函数:", repos); return resolve(repos); }) .catch(async function (err) { if (err && (err.statusCode === 301 || err.statusCode === 302)) { // console.log("停止重定向,重定向信息:", err); console.log("停止重定向"); return resolve(err); } console.error("重定向失败:", err); return reject(err); }); }); } module.exports = {request }
2022-10-21 - 时间方块「自律,才能更自由」
纪念日、日程待办功能应该说是安卓手机里面必备的一个功能,而且作为系统应用,这些功能的设计和体验也都还可以。但是作为一名苹果用户,对于他的日历功能我一直是不太习惯,不喜欢他的体验,丑丑的UI。。加上自己经常丢三落四的毛病,又想好好规划一下自己的每一天,感觉还是有必要开发一款日程待办相关的小程序。 时间方块还是基于云开发,我自己独立开发的一个小程序,主要提供了纪念日、日程待办、超值优惠券(美团饿了么瑞幸滴滴等优惠券)、特价电影票四大功能模块。 1、纪念日 [图片] 纪念日主要分为倒数、累计日两个类型。倒数可以是某某的生日,或者某个即将来临的待办(这点跟第二个功能日程待办有些重合,之所以单独拎出来,主要是考虑一些功能是纪念日重度使用同时又是日程待办轻度使用用户);累计日可以是来到地球的天数。纪念日功能当然是有提供功能,也可以生成图片分享朋友圈或好友。 2、日历(日程待办) [图片] 日程待办是基于日历(日历会显示二十四节气、法定节假日、调休、农历等信息)。这里对于常见的日程待办做了一点优化是可以指定特定的日程(事件)类型,系统会自动帮你设定重复类型以及提醒事件。举个栗子,比如日程(事件)类型是花呗还款,那这种日程就是每个月重复,当天提醒。 操作tips:日历可以上下滑动切换显示。日程左滑可以删除,右划可以完成,再次右划就可以取消完成。 [图片] 3、超值优惠券 汇集了饿了么、美团、瑞幸、滴滴等超值优惠券。每天都可领取。 4、特价电影票 对比了猫眼电影票,这里的价格会便宜个3-10块钱。感觉挺实惠。 简单介绍就是这些了,欢迎围观,有什么不足的地方还希望路过的大佬们指点指点。
2021-09-21 - #小程序云开发挑战赛#-恋爱小清单-404
应用场景恋爱小清单的设计主旨是用照片和文字来记录美好瞬间,记录那些在多年以后再看到时,内心还会为之感动的点点滴滴。是本人独立开发 目标用户恋爱中和已步入幸福婚姻的小伙伴 当然单身的小伙伴也可以使用,可以提前记录一些未来想要跟另一半一起做的清单 实现思路本小程序采用基于云开发的原生开发,用到了云数据库存储数据,使用云函数获取当前用户信息、读写操作云数据库,云存储保存图片,云调用内容安全接口检查提交信息、图片是否存在违法违规内容等 主要功能如下: 1、记录用户自定义清单,对于用户提交的文本、图片都会调用内容安全检查接口检测是否违法违规 2、支持和另一半共享清单,一起完成清单。共享清单以后,彼此对清单的修改会互相同步 3、利用画布绘图实现卡片清单分享功能 4、恋人圈:开发这个功能主要是想让大家可以分享感动,分享幸福点滴。 5、纪念日:支持记录农历、公历纪念日,对于倒数类型的纪念日后台会通过云函数定时器实现纪念日提醒功能 6、消息推送:该功能会引导用户关注恋人小清单公众号,关注公众号后另一半如果有修改或者新增清单都会推送消息提醒(这个功能需要公众号做微信认证) 7、礼物说:旨在未情侣推荐一些比较有纪念意义的小礼物。后续会考虑接入微信小店。 等等 架构图[图片] 效果截图[图片][图片][图片][图片][图片] 功能演示视频https://v.qq.com/x/page/k315115ijid.html 作品体验二维码[图片] 团队介绍独立开发者 微信:Yunfay
2021-09-09 - 恋爱小清单
款小程序其实是送给女票的一份生日礼物,比较程序猿的一种表达方式吧。这款小程序参考了app store里面的一个app【恋人清单】,这款小程序列出了情侣可以一起做的一些清单。可以记录一些情侣之间做过的事情,可以上传照片,记录文字。可以和你的那个TA一起共享清单,一起记录美好回忆,也可以记录在一起的时间,还可以把卡片生成图片分享朋友圈,也可以保存到手机里,后面拿去打印成相册(个人觉得这点还是挺不错的)等等,后续还会继续添加一些新功能来完善它。 我自己也不是做前端开发,之前有接触过小程序,但也只是皮毛,这次是下班以后一边看文档一边写代码,UI也是自己慢慢调出来,LOGO自己画了一个,一个人包办所有,能赶在女票生日前一天发布出来还蛮开心的^_^ 路过的大佬们,可以留下你们的建议哦,要是感觉还不错,不要吝啬你们的赞哦^_^ 来几张图片 [图片] [图片][图片] [图片]
2023-06-13