- 小程序粘性布局组件实现
一、前言 开发中,我们经常会遇需要让组件在屏幕范围内时,按照正常布局排列,而组件滚出屏幕范围时,让其始终固定在屏幕顶部的情况,也就是常说的粘性布局。今天我们就一起用小程序来实现一个适用于不同场景下的粘性布局组件。 二、demo演示 如图,实现的组件主要适用于以下几种场景: 吸顶页面最上方; 吸顶与页面有固定距离的位置; 在指定容器内吸顶; 嵌套在scroll-view中吸顶。 [图片] 三、代码演示 其中,粘性组件通过<weimob-sticky></weimob-sticky>调用,参数信息用法如下: 参数 说明 类型 默认值 offset-top 吸顶时与顶部的距离,单位px number 0 z-index 吸顶时的 z-index number 99 container 一个函数,返回容器对应的 NodesRef 节点 function - scroll-top 当前滚动区域的滚动位置,非 null 时会禁用页面滚动事件的监听 number - 滚动时触发scroll函数,其中isFixed为是否吸顶,scrollTop为距离顶部的位置。详细代码如下。 3.1 页面代码 3.1.1 基础用法 [代码]<view class="weimob-block"> <view class="weimob-title">基础用法</view> <view class="weimob-body"> <weimob-sticky> <!-- 需要粘性的部分 --> <button class="margin-left-base" size="mini"> 基础用法 </button> </weimob-sticky> </view> </view> [代码] 3.1.2 吸顶距离 [代码]<view class="weimob-block"> <view class="weimob-title">吸顶距离</view> <view class="weimob-body"> <!-- 吸顶时与顶部的距离,单位px --> <weimob-sticky offset-top="{{ 50 }}"> <!-- 需要粘性的部分 --> <button class="margin-left-top" type="primary" size="mini"> 吸顶距离 </button> </weimob-sticky> </view> </view> [代码] 3.1.3 指定容器 [代码]<view class="weimob-block"> <view class="weimob-title">指定容器</view> <view class="weimob-body"> <!-- 这里需要固定高度 --> <view id="container" style="height: 300rpx;background-color: #fff"> <weimob-sticky container="{{ container }}"> <button size="mini" class="margin-left-special"> 指定容器 </button> </weimob-sticky> </view> </view> </view> [代码] 3.1.4 嵌套在scroll-view使用 [代码]<view class="weimob-block"> <view class="weimob-title">嵌套在 scroll-view 内使用</view> <!-- 这里需要固定高度,scroll-view里的元素高度需要大于其高度 --> <scroll-view bind:scroll="onScroll" scroll-y id="scroller" style="height: 400rpx; background-color: #fff;margin-top: 40rpx;" > <view style="height: 800rpx"> <weimob-sticky scroll-top="{{ scrollTop }}" offset-top="{{ offsetTop }}" > <button size="mini" class="margin-left-scoll"> 嵌套在 scroll-view 内 </button> </weimob-sticky> </view> </scroll-view> </view> [代码] 页面js [代码]Page({ data: { container: null, //一个函数,返回容器对应的 NodesRef 节点 scrollTop: 60, // 当前滚动区域的滚动位置,非null时会禁用页面滚动事件的监听 offsetTop: 0 // 吸顶时与顶部的距离,单位px }, onReady() { // 页面渲染完,获取节点信息 this.setData({ container: () => wx.createSelectorQuery().select('#container'), }); }, onScroll(event) { // 容器滚动时获取节点信息 wx.createSelectorQuery() .select('#scroller') .boundingClientRect((res) => { this.setData({ scrollTop: event.detail.scrollTop, offsetTop: res.top, }); }) .exec(); } }); [代码] 3.2 组件代码 组件wxml [代码]<wxs src="./index.wxs" module="computed" /> <view class="weimob-sticky" style="{{ computed.containerStyle({ fixed, height, zIndex }) }}" > <view class="{{ fixed ? 'weimob-sticky-wrap--fixed' : ''}}" style="{{ computed.wrapStyle({ fixed, offsetTop, transform, zIndex }) }}" > <slot /> </view> </view> [代码] 组件wxs 这里使用使用小程序的wxs对吸顶元素的transform,top,height,z-index元素进行实时渲染,ios设备在滚动监听时性能会优于在js 2-20倍,androd设备效率暂无差异。 [代码]function wrapStyle(data) { var style = ""; if (data.transform) { style += 'transform: translate3d(0, ' + data.transform + 'px, 0);' } if (data.fixed) { style += 'top: ' + data.offsetTop + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } function containerStyle(data) { var style = ""; if (data.fixed) { style += 'height: ' + data.height + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } module.exports = { wrapStyle: wrapStyle, containerStyle: containerStyle } [代码] 组件js [代码]import pageScrollMixin from "./page-scroll"; const ROOT_ELEMENT = ".weimob-sticky"; Component({ options: { multipleSlots: true }, properties: { zIndex: { type: Number, value: 99 }, offsetTop: { type: Number, value: 0, observer: "onScroll" }, disabled: { type: Boolean, observer: "onScroll" }, container: { type: null, observer: "onScroll" }, scrollTop: { type: null, observer(val) { this.onScroll({ scrollTop: val }); } } }, data: { height: 0, fixed: false, transform: 0 }, behaviors: [pageScrollMixin(function pageScrollMixinCallback(event) { // 非null时会禁用页面滚动事件的监听 if (this.data.scrollTop != null) { return; } this.onScroll(event); })], lifetimes: { attached() { this.onScroll(); } }, methods: { onScroll({ scrollTop } = {}) { const { container, offsetTop, disabled } = this.data; if (disabled) { this.setDataAfterDiff({ fixed: false, transform: 0 }); return; } this.scrollTop = scrollTop || this.scrollTop; if (typeof container === "function") { // 情况一:指定容器下时,吸顶距离+吸顶元素高度>容器高度+容器距顶部距离,随页面滚动; // 情况二:指定容器下时,吸顶距离>吸顶元素高度,元素固定; // 情况三:元素初始化。 // this.getRect获取节点ROOT_ELEMENT相对于显示区域的top,height等信息,通过root获取 // this.getContainerRect获取父容器相对于显示区域的top,height等信息,通过container获取 Promise.all([this.getRect(ROOT_ELEMENT), this.getContainerRect()]).then( ([root, container]) => { if (offsetTop + root.height > container.height + container.top) { this.setDataAfterDiff({ fixed: false, transform: container.height - root.height }); } else if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height, transform: 0 }); } else { this.setDataAfterDiff({ fixed: false, transform: 0 }); } }); return; }else{ this.getRect(ROOT_ELEMENT).then(root => { // 吸顶时与顶部的距离小于可视区域的top距离时,随着滚动条滚动,否则吸顶 if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height }); this.transform = 0; } else { this.setDataAfterDiff({ fixed: false }); } return Promise.resolve(); }); } }, setDataAfterDiff(data) { // 比较数据是否与上次相同,不同则触发父组件scroll事件更新isFixed,scrollTop。 wx.nextTick(() => { const diff = Object.keys(data).reduce((prev, key) => { const prevCopy = prev; if (data[key] !== this.data[key]) { prevCopy[key] = data[key]; } return prevCopy; }, {}); this.setData(diff); this.triggerEvent("scroll", { scrollTop: this.scrollTop, isFixed: data.fixed || this.data.fixed }); }); }, getContainerRect() { const nodesRef = this.data.container(); return new Promise(resolve => nodesRef.boundingClientRect(resolve).exec()); }, getRect(selector) { return new Promise(resolve => { wx.createSelectorQuery().in(this).select(selector).boundingClientRect(rect => { resolve(rect); }).exec(); }); } } }); [代码] page-scroll.js 滚动事件在页面进入和离开时共享的pageScrollMixin函数。 [代码]function getCurrentPage() { const pages = getCurrentPages(); return pages[pages.length - 1] || {}; } function onPageScroll(event) { const { weimobPageScroller = [] } = getCurrentPage(); weimobPageScroller.forEach(scroller => { if (typeof scroller === "function" && event) { // @ts-ignore scroller(event); } }); } const pageScrollMixin = scroller => Behavior({ attached() { const page = getCurrentPage(); if (Array.isArray(page.weimobPageScroller)) { page.weimobPageScroller.push(scroller.bind(this)); } else { page.weimobPageScroller = typeof page.onPageScroll === "function" ? [page.onPageScroll.bind(page), scroller.bind(this)] : [scroller.bind(this)]; } page.onPageScroll = onPageScroll; }, detached() { const page = getCurrentPage(); page.weimobPageScroller = (page.weimobPageScroller || []).filter(item => item !== scroller); } }); export default pageScrollMixin; [代码] 总结 最后,我将上述代码放在了代码片段中供大家使用了解,https://developers.weixin.qq.com/s/qiym3wmr7znx ,希望能够帮到小伙伴们,欢迎评论区建议或指教哦~
2021-01-26 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 新能力 | 云开发CMS内容管理系统,5分钟搞定小程序管理后台
小程序·云开发的云调用能力,让用户可以免鉴权快速调用微信的开放能力,极大节约了开发成本。现在,大家期待已久的云开发 CMS 内容管理系统,终于上线啦!顺便提示,接下来还可以二次开发哦! 云开发 CMS 管理系统是什么? 云开发 CMS 内容管理系统是云开发提供的一个扩展程序,可以在云开发控制台一键安装在自己的云开发环境中,方便开发人员和内容运营者随时随地管理小程序 / Web 等多端云开发内容数据。不用编写代码就可以使用,还提供了 PC /移动端浏览器访问支持,支持文本、富文本、图片、文件、关联类型等多种类型的可视化编辑。 [图片] 先来看看云开发CMS的"庐山真面目" 首先我们通过几张截图来直观感受一下 CMS 内容管理系统扩展: 图1 云开发控制台的安装界面截图 [图片] 图2 安装并配置好内容的 CMS 内容管理系统界面演示 [图片] 图3 CMS 内容管理系统界面的移动端演示 [图片] 云开发 CMS 内容管理系统有哪些功能特性 ? 特性 介绍 免开发 基于后台建模配置生成内容管理界面,无须编写代码 多端适配 支持 PC/移动端访问和管理内容 功能丰富 支持文本、富文本、图片、文件 等多种类型内容的可视化编辑,并且支持内容关联 权限控制 系统基于管理员/运营者两种身份角色的访问控制 外部系统集成 支持 Webhook 接口,可以用于在运营修改修改内容后通知外部系统,如自动构建静态网站、发送通知等 数据源兼容 支持管理小程序/ Web / 移动端的云开发数据,支持管理已有数据集合,也可以在 CMS 后台创建新的内容和数据集合 部署简单 可在云开发控制台扩展管理界面一键部署和升级 什么场景下适合使用 CMS ? 1. 适用于需要为小程序应用增加一个运营管理后台的业务 小程序应用有偏运营方面的文章编辑和发布、运营活动配置、素材管理等数据管理需求,使用 CMS 扩展之后,不用手动线上修改 db 数据,也不用投入人力物力开发管理后台,可以随时随地使用自己环境下部署的 CMS 内容管理系统来管理,同时还支持区分管理员和运营者的身份权限。 2. 适用于快速开发内容型的网站应用、小程序应用等场景 CMS 内容管理系统还可以帮助开发者提升开发网站应用、小程序应用的效率,省去一部分后端开发工作。例如安装了CMS 扩展之后,解决了内容和数据的管理和生产问题,直接可以结合前端应用框架读取 db 数据进行渲染。例如基于 CMS 可以快速开发博客、企业官网等小程序/网站应用,最后悄悄透露一下,云开发的官网 (http://cloudbase.net/) 就是基于 CMS 扩展 + Next.js + 云开发静态托管搭建和部署的。 如何安装和使用 CMS ? 第一步:切换为按量付费 由于 CMS 扩展需要用到静态网站托管资源,必须在按量计费的环境下才可以部署,因此首先要切换计费方式为按量付费。 1. 微信小程序开发者 登录微信开发者工具-云开发控制台 在【云开发控制台】-【设置】-【环境设置】-【支付方式】中点击切换【按量付费】即可。 注意:这里需要先保证腾讯云账户中是有充值金额的哦~ [图片] 2. 腾讯云开发者 登录腾讯云云开发控制台 在【云开发 CloudBase 控制台】-【环境】-【资源购买】-【计费模式】中点击【切换按量付费】即可。 [图片] 第二步:在腾讯云控制台安装扩展 登录腾讯云控制台 微信小程序开发者需要使用微信公众号登录! [图片] 在【云开发 CloudBase 控制台】-【扩展能力】-【扩展管理】中找到 CMS内容管理系统 扩展进行安装 安装时需要进行资源的授权和扩展程序的配置,比如管理员和运营者的账号密码配置等,同时需要提供自定义登录的密钥,可以点击自定义登录密钥旁边的小图标了解如何填写。 [图片] 第三步:使用 CMS 内容管理系统 完成【CMS内容管理系统】的安装以后,然后访问该扩展的管理页,可以在【扩展运行方式】Tab 查看使用指引,依照文档完成 CMS 的使用,下面简单介绍一下快速上手的步骤,更多细节可以参考运行方式。 [图片] 访问 CMS 系统 CMS 扩展已经部署在当前环境下的静态网站托管中,访问路径为“静态托管的默认域名+安装设置的部署路径” 访问地址的格式如下: [代码]云开发静态托管默认域名/部署路径[代码],例如 [代码]https://xxxx.tcloudbaseapp.com/tcb-cms/[代码] 账号登录 打开 CMS 系统后首先会提示需要登录,我们首先使用使用安装扩展时设置的管理员账号和密码进行登录 内容建模 登录成功后,首先需要进行内容的建模设置,例如我们想为自己的博客应用(小程序/网站)来生成管理界面。 假设当前已有一个管理 文章的数据库集合 [代码]articles[代码],我们可以在 CMS 管理后台新建一个 “文章” 内容(如果新建内容的时候指定的集合名不存在,CMS 扩展会自动新建集合)来生成“文章”类型的内容管理界面。 假设数据库集合 [代码]articles[代码] 的结构如下: 字段名 类型 描述 _id ID 文章唯一 id name String 文章标题 cover String 封面图,这里存放云开发的存储的文件的 cloudID content String 文章内容,采用 markdown 格式 author ID 作者的用户 id createTime DateTime 创建时间 updateTime DateTime 更新时间 tag String[] 标签,例如 [代码]["serverless","cms"][代码] category String[] 分类,例如 [代码]["前端","开发"][代码] 我们在“内容设置”中点击“新建”来创建“文章”类型时,可以对照上面的集合数据把字段类型和字段的限制进行配置,例如封面图可以直接选择 “图片”字段类型,文章内容可以直接选择 “Markdown” 类型,这样在生成的管理界面里可以直接上传图片和通过编辑器编写文章,保存在数据库集合的时候,依然会保存为数据库支持的类型,图片会存储为云存储的 CloudID, 内容会存储为字符串等。 [图片] 创建并保存之后会自动刷新生成”文章“的运营界面 管理内容 接下来就可以进行运营管理内容操作了,可以使用运营者身份登录,对新创建的“文章”进行操作,我们可以新建一篇文章。 [图片] 文章发布成功后,即可在文章列表中看到这篇文章 [图片] 使用内容数据 采用 CMS 管理的内容,依然可以通过云开发各端 SDK 进行访问(需要注意的是在前端访问时,需要正确设置数据库的安全规则设置,例如设置为所有用户可读,仅创建者可写)。 例如,在上面的例子里,我们需要在云函数中获取文章的标签是 [代码]CloudBase[代码] 的最新 10 条文章,可以采用以下代码来获取数据: [代码]db.collection("articles") .where({ tag: "CloudBase" }) .orderBy("createTime", "desc") .limit(10) .get(); [代码] 获取到内容数据就可以在各种场景使用了,比如在小程序/ Web 中构建应用和网站,具体的CMS + 应用开发的实践可以关注后期我们的实践教程。 [图片] 后续,云开发CMS内容管理系统将支持二次开发,用户可以自由定制自己的管理后台。云开发将始终坚持,为开发者提供一站式云服务! [图片] 最后,小编赠上《5分钟部署云开发CMS系统》教程,帮助大家快快上车! 视频链接: https://v.qq.com/x/page/f09687on1qv.html 文档链接 :(CMS 内容管理系统链接) https://cloud.tencent.com/document/product/876/44547
2020-09-14