- 食品经营许可证备案凭证可以申请了
食品许可证备案凭证可以申请了,销售预包装食品又不能申请食品许可证的伙伴,现在可以申请食品许可证备案凭证,直接网上申请。 下面按广州市的为例:4月18号就出来了,申请链接→http://scjgj.gz.gov.cn/zwfw/index.html (其他地区的请自查) 1、选择食品药品行政审批网上办理 [图片] 2、选择仅销售预包装食品备案 [图片] 3、申请之后可以查询到备案 [图片]
2022-04-21 - 微信小程序前端生成图片用于分享朋友圈最终解决方案
前段时间一直在做微信小程序的,遇到了许多的坑,其中遇到了需要前端合成图片保存到相册用于分享到朋友圈。借简书记录一下最终解决方案,先看一下最终效果 [图片] 该文章的所有演示代码托管与github,代码地址,微信调试工具中访问请[代码]关闭合法域名检查[代码],[代码]开启es6转换[代码],真机调试请打开调试[代码]vconsole[代码] 该文章解决的问题如下: 微信小程序生成图片,并保存到相册 微信小程序生成图片实现响应式 微信小程序canvas原生组件如何给画布添加css动画 保存高清分享图方案 微信小程序生成图片实现单屏适应 微信小程序生成图片,并保存到相册 首先,我们希望能实现如下功能,点击用户头像,从底部弹出一个分享弹窗,可以保存合成图片到相册,可以关闭弹层 我们将该功能封装成一个Component自定义组件 定义wxml基本结构 [代码]<view class="share {{visible ? 'show' : ''}}"> <view class="content"> <canvas class="canvas" canvas-id="share" /> <view class="footer"> <view class="save">保存到相册</view> <view class="close">关闭</view> </view> </view> </view> [代码] 定义wxss样式 [代码].share { position: fixed; top: 0; left: 0; min-height: 100vh; width: 100%; background: rgba(61, 61, 61, 0.5); visibility: hidden; opacity: 0; transition: opacity 0.2s ease-in-out; z-index: 99999; } .share.show { visibility: visible; opacity: 1; } .share .content { display: flex; flex-direction: column; justify-content: center; align-items: center; } .share .content .footer { width: 562rpx; height: 100rpx; background: #fff; border-top: 2rpx solid #e9e9e9; display: flex; flex-direction: row; justify-content: center; align-items: center; font-size: 28rpx; } .share .content .footer .close { width: 100rpx; height: 100rpx; line-height: 100rpx; flex-grow: 0; flex-shrink: 0; text-align: center; border-left: 2rpx solid #e9e9e9; } .share .content .footer .save { height: 100rpx; line-height: 100rpx; flex-grow: 1; flex-shrink: 1; text-align: center; } .share.show .content .canvas { display: inline-block; } .share .content .canvas { display: inline-block; background: #fff; margin: 60rpx 0 0 0; width: 562rpx; height: 1000rpx; } [代码] 定义json [代码]{ "component": true } [代码] 定义组件构造器 [代码]Component({ properties: { visible: { type: Boolean, value: false }, // 由于需要绘制用户信息,由页面传入 userInfo: { type: Object, value: false } }, methods: { draw() { // 实际绘制函数,后续绘制代码放于此处 } } }) [代码] 基本结构和样式定义完成,接下来开始可一开始我们绘制之旅了,合成图片需要用到微信小程序wx.getImageInfo函数,我们先对它进行Promise化方便后期调用 [代码]function getImageInfo(url) { return new Promise((resolve, reject) => { wx.getImageInfo({ src: url, success: resolve, fail: reject, }) }) } [代码] 前期的准备工作建立完成,我们开始定义绘制方法draw [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = 281 const canvasHeight = 500 // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = 60 const avatarHeight = 60 const avatarTop = 40 // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight ) // 绘制用户名 ctx.setFontSize(20) ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + 50, ) ctx.stroke() // 完成作画 ctx.draw() }) [代码] 接下来,我们需要监测visible属性的变化,决定是否开始绘制 [代码]Component({ properties: { visible: { type: Boolean, value: false, observer(visible) { // 当开始显示分享弹窗时开始绘制 if (visible) { this.draw() } } }, }, ....省略其他代码 }) [代码] 此时,前端的绘制已基本成型,运行小程序变可看见合成图,由于我们的绘制尺寸是基于iphone6s进行绘制的,在iphone6s及部分相同分辨率查看,尺寸完全吻合,没有任何问题,然而当我们用iphone6s plus或者其他不同分辨率的手机打开时却变成了下面这个样子 [图片] 绘制的图像没有完全占满画布了,为什么呢?这个是遇到的第二个问题 微信小程序生成图片实现响应式 其实我们的画布宽高单位都是基于rpx单位,因此在不同分辨率的手机上,实际的尺寸也就不同,然而我们绘制图片的尺寸都是以px为单位,自然无法实现响应式,因此我们需要一个js方法用于转换rpx值为px值 解读微信官方文档我们定义如下一个简单的转换方法 [代码]function createRpx2px() { const { windowWidth } = wx.getSystemInfoSync() return function(rpx) { return windowWidth / 750 * rpx } } const rpx2px = createRpx2px() [代码] 定义好了单位转换函数,我们只需转换相关值即可 [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = rpx2px(281 * 2) const canvasHeight = rpx2px(500 * 2) // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = rpx2px(60 * 2) const avatarHeight = rpx2px(60 * 2) const avatarTop = rpx2px(40 * 2) // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight ) // 绘制用户名 ctx.setFontSize(rpx2px(20 * 2)) ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + rpx2px(50 * 2), ) ctx.stroke() // 完成作画 ctx.draw() [代码] 此时不管在什么分辨率下的手机都能正常显示了 微信小程序canvas原生组件如何给画布添加css动画 我们都知道微信小程序的canvas是原生组件,对于原生组件有许多的限制,比如不可以使用css动画,官方文档如下: [图片] 首先我们试着给canvas父层标签View.content标签添加弹出动画,修改样式如下: [代码].share .content { display: flex; flex-direction: column; justify-content: center; align-items: center; // 新增动画控制 transform: translate3d(0, 100%, 0); transition: transform 0.2s ease-in-out; } // 新增动画控制 .share.show .content { transform: translate3d(0, 0, 0); } [代码] 在调试器中使用,一切都很美好,完全按着预期由底部弹出,然后淡隐,不过当你用真机调试,canvas部分的效果变得不那么顺畅,流畅,没有弹出动画,没有淡隐效果,一切都变得那么的僵硬,那我们该怎么办呢? 解决办法的思路如下: 提供一个canvas标签,不可以做隐藏(隐藏会导致绘制失效),通过css tansform属性移除屏幕让其不可见 用image标签代替canvas标签显示给用户查看 当画布绘制完成后,我们保存绘制的图像到临时目录中,并获取图片地址 将地址提供给image标签用于展示 基于以上思路,首先改造我们的文档结构 [代码]<view class="share {{ visible ? 'show' : '' }}"> <canvas class="canvas-hide" canvas-id="share" /> <view class="content"> <image class="canvas" src="{{imageFile}}" /> <view class="footer"> <view class="save">保存到相册</view> <view class="close" bindtap="handleClose">关闭</view> </view> </view> </view> [代码] 新增样式 [代码].share .canvas-hide { position: fixed; top: 0; left: 0; transform: translateX(-100%); width: 562rpx; height: 1000rpx; } [代码] 想要保存canvas绘制的图像到临时目录,我们需要利用微信小程序的一个api接口wx.canvasToTempFilePath,因此首先我们还是对其进行Promise化 [代码]function canvasToTempFilePath(option, context) { return new Promise((resolve, reject) => { wx.canvasToTempFilePath({ ...option, success: resolve, fail: reject, }, context) }) } [代码] 在组件的data属性中新增imageFile [代码]// 仅列出新增部分,省略之前的代码 Component({ data: { imageFile: '' } }) [代码] 修改我们的绘制方法 [代码]// 仅列出新增部分,省略之前的代码 // 修改画布的draw函数如下 ctx.draw(false, () => { canvasToTempFilePath({ canvasId: 'share', }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath })) }) [代码] 此时在真机上运行调试,可以看到完美的满足我们的需求(沾沾自喜) 保存高清分享图方案 接下来我们需要实现保存到相册中,用于分享给朋友圈或者其他微博 保存图片到相册需要调用微信小程序api,wx.saveImageToPhotosAlbum,依照惯例进行Promise化 [代码]function saveImageToPhotosAlbum(option) { return new Promise((resolve, reject) => { wx.saveImageToPhotosAlbum({ ...option, success: resolve, fail: reject, }) }) } [代码] 我们为保存相册新增点击事件 [代码]<view class="save" bindtap="handleSave">保存到相册</view> [代码] 最后定义我们的保存方法 [代码]// 仅列出新增部分,省略之前的代码 Component({ methods: { handleSave() { const { imageFile } = this.data if (imageFile) { saveImageToPhotosAlbum({ filePath: imageFile, }).then(() => { wx.showToast({ icon: 'none', title: '分享图片已保存至相册', duration: 2000, }) }) } } } }) [代码] 至此保存到相册功能完成了,但是有点瑕疵,原本我们用于绘制的图片非常的高清,可以绘制后保存的图片变得模糊了,没那么高清,这是过不了UED小姐姐那关的 那如何保证保存的图片不会失真呢,我们可以考虑把canvas大小放大到3倍,绘制3倍的图 修改样式 [代码].share .content .canvas { display: inline-block; background: #fff; margin: 60rpx 0 0 0; width: 1686rpx; // 修改为之前的3倍 height: 3000rpx; // 修改为之前的3倍 } [代码] 修改绘制函数,增长绘制大小为3倍 [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = rpx2px(281 * 2 * 3) // 扩大3倍 const canvasHeight = rpx2px(500 * 2 * 3) // 扩大3倍 // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = rpx2px(60 * 2 * 3) // 扩大3倍 const avatarHeight = rpx2px(60 * 2 * 3) // 扩大3倍 const avatarTop = rpx2px(40 * 2 * 3) // 扩大3倍 // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight ) // 绘制用户名 ctx.setFontSize(rpx2px(20 * 2 * 3)) // 扩大3倍 ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + rpx2px(50 * 2 * 3), // 扩大3倍 ) ctx.stroke() // 完成作画 ctx.draw(false, () => { canvasToTempFilePath({ canvasId: 'share', }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath })) }) [代码] 我们重新保存图片,发现图片变得高清了,hu~~~ 最后我们可以兴高采烈的把成果交给小测试了,一切看起来都很顺利,可惜终究过不了各种机型分辨率的测试,由于我们的设计基于iphone6s尺寸设计,在部分宽高比不同的机型,高度会超出屏幕高度,变成下面这个样子 [图片] 按钮被挡住了,这下无奈了 微信小程序生成图片实现单屏适应 我们希望分享弹窗内容能在一个屏幕下显示完全,那可以根据当前手机宽高比与设计稿尺寸宽高比求出一个缩放比例对整体内容进行缩放即可 定义缩放比例计算 [代码]// 仅列出新增部分,省略之前的代码 Component({ data: { responsiveScale: 1, // 缩放比例默认为1 }, lifetimes: { ready() { const designWidth = 375 const designHeight = 603 // 这是在顶部位置定义,底部无tabbar情况下的设计稿高度 // 以iphone6为设计稿,计算相应的缩放比例 const { windowWidth, windowHeight } = wx.getSystemInfoSync() const responsiveScale = windowHeight / ((windowWidth / designWidth) * designHeight) if (responsiveScale < 1) { this.setData({ responsiveScale, }) } }, }, }) [代码] 修改wxml文档 [代码]<view class="share {{ visible ? 'show' : '' }}"> <canvas class="canvas-hide" canvas-id="share" /> <view class="content" style="transform:scale({{responsiveScale}});-webkit-transform:scale({{responsiveScale}});"> <image class="canvas" src="{{imageFile}}" /> <view class="footer"> <view class="save" bindtap="handleSave">保存到相册</view> <view class="close" bindtap="handleClose">关闭</view> </view> </view> </view> [代码] 修改wxss样式表 [代码].share .content { // 省略其他定义 // 新增缩放中心控制为顶部中心 transform-origin: 50% 0; } [代码] 整体分享遇到的坑都得到了解决,代码较多,所有的代码都托管到了github,欢迎访问运行代码地址,只有亲力亲为才能真正的掌握知识
2019-01-14 - 小程序顶部自定义导航组件实现原理及坑分享
为什么使用自定义导航 对比默认导航栏,我们会更需要: 统一Android、IOS手机对于页面title的展示样式及位置 更丰富的导航栏定制效果,如添加home图标等 左上角返回事件的监听处理 统一实现双击返回顶部功能 自定义导航组件实现思路 自定义导航组件实现的核心是需要计算导航栏的真实高度 这里以官方文档->扩展能力中的Navigation组件为例分析实现思路。当使用"navigationStyle": "custom"时,默认导航被移除,页面的开始位置变成了屏幕顶部,这时我们需要实现的导航栏是在状态栏下面。 导航栏的真实高度=状态栏高度+导航栏内容。 [图片] 使用wx.getSystemInfo获取到statusBarHeight便是导航栏的高度,但是导航栏内容高度呢? 有人可能觉得导航栏内容高度顾名思义就是导航栏内容高度啊,内容撑起还用管嘛!要,必须要! 因为右上角胶囊按钮是原生加载的,我们的导航栏内容需要正好贴在胶囊的下方且垂直居中。 导航栏内容高度=(胶囊按钮的顶部距离 - 状态高度)*2 + 胶囊高度 [图片] 如何计算胶囊的数据呢?幸运的是我们有 wx.getMenuButtonBoundingClientRect() 获取胶囊按钮的布局位置信息,那么动态计算导航栏的内容高度就很方便啦。 好了,以上就是动态计算的核心思路,我们再来看官方Navigation组件高度是怎么实现的 [代码]page{--height:44px;--right:190rpx;} .weui-navigation-bar .android{--height:48px;--right:222rpx} .weui-navigation-bar__inner{ position:fixed;top:0;left:0;z-index:5001;display:flex;align-items:center; height:var(--height);padding-right:var(--right);width:calc(100% - var(--right)) } [代码] 导航栏内容的高度是通过- -height这个css变量提前声明好的,安卓机型会重新覆盖为新的css变量值,目前没发现有适配问题。 官方就是官方啊,具体尺寸都知道,那就不用一番计算周折啦,直接拿来主义即可。 导航的布局位置已经搞定啦,剩下就是写具体的内容,不同业务实现需求不同这里就不一一赘述了。 完善官方顶部导航组件 本着拿来主义,直接使用官方Navigation组件,但在实际业务开发中还是遇到不少需要自定义的需求,就比如: loadding样式没实现 标题内容超出没有出现省略号 和原生顶部的样式不兼容,导致单个页面引入时跳转有明显差异出现 没有双击返回顶部功能开关功能 引入页面需要获取导航栏的高度,来控制其他元素距离顶部的位置, 不能根据页面栈数据动态显示隐藏back按钮, 针对以上需求,我们对官方的组件进行二次完善开发,满足常规的自定义需求绰绰有余,直接引入开箱即用。 源码使用示例 https://github.com/YuniorZen/minicode-debug/tree/master/minicode02 [图片] 使用说明 [代码]/*自定义头部导航组件,基于官方组件Navigation开发。*/ <navigation-bar title="会员中心" bindgetBarInfo="getBarInfo"></navigation-bar> [代码] 组件属性列表 属性 类型 描述 bindgetBarInfo function 组件实例载入页面时触发此事件,首参为event对象,event.detail携带当前导航栏信息,如导航栏高度 event.detail.topBarHeight bindback function 点击back按钮触发此事件响应函数 backImage string back按钮的图标地址 homeImage string home按钮的图标地址 ext-class string 添加在组件内部结构的class,可用于修改组件内部的样式 title string 导航标题,如果不提供为空 background string 导航背景色,默认#ffffff color string 导航字体颜色 dbclickBackTop boolean 是否开启双击返回顶部功能,默认true border boolean 是否显示顶部导航下边框分割线,默认false loading boolean 是否显示标题左侧的loading,默认false show boolean 显示隐藏导航,隐藏的时候navigation的高度占位还在,默认true left boolean 左侧区域是否使用slot内容,默认false center boolean 中间区域是否使用slot内容,默认false Slot name 描述 left 左侧slot,在back按钮位置显示,当left属性为true的时候有效 center 标题slot,在标题位置显示,当center属性为true的时候有效 自定义顶部导航目前存在的坑 弹窗的背景蒙层无法覆盖原生胶囊按钮 页面下拉刷新的圆点会被自定义导航遮盖 如果要自定义顶部导航,以上问题避免不了,只能忍着接受。 目前还没遇到完美的解决方案,针对下拉刷新圆点被遮挡的问题微信官方还在需求开发中,如果你有好的想法欢迎留言反馈,一起学习交流。
2019-10-31 - 使用云函数+云调用,四步实现微信支付
微信支付是云开发原生支持的微信生态能力之一,开发者只需要简单调用相应的函数即可完成整套支付流程,安全又高效。部分优势包括: 无需关心证书、签名,支付流程简化;基于微信私有协议和私有链路,更加安全、高效;免运维,高可用性;按需扩容,弹性伸缩,按量计费,成本缩减;支持通过云函数接受支付回调,无需自建回调服务。流程对比:传统流程 vs 云开发[图片] 代码示例第 1 步:小程序调用云函数C 端用户发起支付流程后,小程序端调用云函数(此处假设云函数名为 [代码]makeOrder[代码]): // 小程序代码 wx.cloud.callFunction({ name: "makeOrder", data: { /* 开发者自定义参数 */ } }); 第 2 步:云函数生成订单,返回订单信息云函数 makeOrder 收到调用之后,使用微信服务端 SDK 提供的 API,无需证书和签名,可直接生成订单。 生成订单之后,利用 CloudPay.unifiedOrder() 统一下单接口,将订单信息返回给小程序。 CloudPay.unifiedOrder() 接口文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/open/pay/CloudPay.unifiedOrder.html // 云函数 makeOrder const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); exports.main = async (event, context) => { const res = await cloud.cloudPay.unifiedOrder({ body: "小秋TIT店-超市", outTradeNo: "1217752501201407033233368018", spbillCreateIp: "127.0.0.1", subMchId: "1900009231", totalFee: 1, envId: "test-f0b102", functionName: "payCallback" // 支付回调的函数名 }); return res; }; 第 3 步:小程序端发起支付小程序端收到云函数返回的订单信息后,发起支付: // 小程序代码 wx.cloud.callFunction({ name: "makeOrder", data: { /* 开发者自定义参数 */ }, success: (res) => { // 取得云函数返回的订单信息 const payment = res.result.payment; // 调起微信客户端支付 wx.requestPayment({ ...payment, success(res) { /* 成功回调 */ }, fail(res) { /* 失败回调 */ } }); } }); 第 4 步:使用云函数接收支付回调,完成支付流程用户完成付款之后,微信后台将会调用指定的云函数(此处假设名为 payCallback),传入的参数中会带有订单信息。 开发者可以在此云函数中,实现自己的发货、完成订单的逻辑。 // 云函数 payCallback exports.main = async (event, context) => { const { return_code, // 状态码 appid, // 小程序 AppID mch_id, // 微信支付的商户号 device_info, // 微信支付分配的终端设备号 openid, // 用户在商户appid下的唯一标识 trade_type, // 交易类型:JSAPI、NATIVE、APP bank_type // 银行类型 // ...... // 更多参数请参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8 } = event; /* 开发者自己的逻辑 */ // 向微信后台返回成功,否则微信后台将会重复调用此函数 return { errcode: 0 }; }; 相关文档:云函数文档: https://docs.cloudbase.net/cloud-function/introduce.html 云调用文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/openapi/openapi.html wx-server-sdk 文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/Cloud.html CloudPay.unifiedOrder() 接口文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/open/pay/CloudPay.unifiedOrder.html
2021-07-08