轮播
小程序内部有现成的 swiper 组件可以用:
<swiper class="banner" indicator-dots="{{BannerIndicatorDots}}" autoplay="{{BannerAutoPlay}}" interval="{{BannerInterval}}" duration="{{BannerDuration}}" circular="{{BannerCircular}}">
<block wx:for="{{SwiperImageUrls}}" wx:key="*this">
<swiper-item>
<image src="{{item}}" mode="widthFix" style="width:100%"></image>
</swiper-item>
</block>
</swiper>
indicator-dots 取值可以是 true 或 false,表示是否显示轮播图下方的用来代表第几张图片的圆点;autoplay 自动播放;interval 是切换的间隔时间,以毫秒为单位;duration 是切换的持续时间,也是以毫秒为单位;circular 是否循环播放。
这里的
swiper-item 是轮播图的一个单位,此处代表一张图片,我在里面放了一个 标签,然后用数据绑定的方式把后台的图片链接绑定到这里,从而实现数据和前端分离便于后期运维。
选项卡导航栏
导航栏需要用
<view class="tab {{TabFixed? \'tab-position-fixed\' : \'\'}}" id=\'tab\'>
<view wx:for="{{TabItems}}" wx:key="id" class="tab-item {{CurrentTab == item.id ? \'tab-item-active\' : \'\'}}" data-current="{{item.id}}" bindtap="clickTab">{{item.title}}</view>
</view>
这里给导航栏设置了一个吸顶效果,根据 data 字段中 TabFixed 标志的取值来给导航栏施加一个吸顶的类 tab-position-fixed,这里我采用的吸顶方法是监听页面滑动,如果滑动距离超过了初始时导航栏距离顶部的距离,就把 TabFixed 标志设置为 true,也就吸顶了。js 代码如下:
// 获取初始状态时导航栏距离页面顶部的距离
getInitTab2Top: function() {
wx.createSelectorQuery().select(\'#tab\').boundingClientRect((res) => {
this.setData({
InitTab2Top: res.top
})
}).exec()
},
// 监听页面滑动事件
onPageScroll: function(e) {
if (this.data.TabFixed != (e.scrollTop > this.data.InitTab2Top - 1)) {
this.setData({
TabFixed: (e.scrollTop > this.data.InitTab2Top)
})
}
},
先创建一个请求绑定前台的 id 为 tab 的元素,然后调用 boundingClientRect().exec() 方法得到此元素距离用户手机显示屏顶部的距离,存到 InitTab2Top 中,之后监听页面滑动即可,如果滑动的位置距离顶部的距离大于 InitTab2Top,说明需要吸顶,否则就不需要吸顶。但这种做法存在一些问题,我用一个巧妙的招数解决了,但不知道实际环境中的解决方案。此时我们虽然能实现导航栏的吸顶,但导航栏下方的内容会在导航栏吸顶后顶上去,占据原来属于导航栏的内容,这样就会导致在滑动页面时不连贯,会看到内容突然平移了一下,用户体验不好。为了解决这个问题,我设置导航栏下面的内容根据导航栏是否固定进行平移 translate,这样暂时抵消掉了导航栏吸顶带来的 bug,可以在后面的代码中看到这里的操作。
导航栏内部的每一项都是一个 view,需要根据当前选项卡是否处于激活状态来设置其样式,所以要对每一个选项卡都进行编号,再设置一个 data-current 来传递当前选项卡的编号是啥,在后台用一个变量 CurrentTab 来表示当前显示的选项卡的编号,再绑定点击事件 bindtap 从而实现前台更改编号传递数据给后台,后台更改变量值控制前台显示内容。后台的控制导航栏点击事件的 js 代码如下:
// 单击选项卡切换
clickTab: function(e) {
if (this.data.CurrentTab == e.target.dataset.current) {
return false
} else {
this.setData({
CurrentTab: e.target.dataset.current
})
}
},
e.target.dataset.current 是我在前台设置的 data-current 传递过来的参数,这样就拿到了当前选项卡的编号,再把它赋值给 this.data.CurrentTab 即可实现选项卡的切换。此处暂时还没到切换选项卡内容的部分,仅仅是到达了根据 CurrentTab 控制哪个选项卡处于激活状态而已,真正的切换在下面一节。
选项卡内容
选项卡内容其实就是 swiper 加 swiper-item 的嵌套,我在其中增加了一个 scroll-view 来实现新闻栏目的内部滚动,每一栏都是用几个 view 实现的,循环产出即可:
<swiper style="height: {{ClientHeight? ClientHeight + \'px\' : \'auto\'}}" class="{{TabFixed? \'swiper-translate\' : \'\'}}" current="{{CurrentTab}}" duration="{{TabSwiperDuration}}" bindchange="swiperTab">
<swiper-item wx:for="{{TabItems}}" wx:key="id">
<scroll-view id="scroll" scroll-y="true" style="height: {{ClientHeight? ClientHeight + \'px\' : \'auto\'}}" bindscrolltolower="loadMore">
<view class="swiper-item-container">
<block wx:for="{{item.content}}" wx:key="index">
<view class="content" bindtap="contentTapped" data-id="{{item._id}}">
<view class="content-text">
<view class="title"><text>{{item.title}}</text></view>
<view class="subtitle"><text>{{item.subtitle}}</text></view>
<view class="date"><text>{{item.date}}</text></view>
</view>
</view>
</block>
</view>
</scroll-view>
</swiper-item>
</swiper>
由于 swiper 默认不能像 view 一样进行自动延申,所以首先需要对它进行高度自适应的设置,这里我给它一个内嵌的 style,设置高度根据 ClientHeight 变量的取值而改变,后台这里的 ClientHeight 是根据用户手机屏幕高度自动变化的,并减去了轮播图占据的高度,代码如下:
// 设置swiper自适应高度
onReady: function() {
wx.getSystemInfo({
success: (res) => {
this.setData({
ClientHeight: res.windowHeight - this.data.InitTab2Top
})
},
})
},
之后根据导航栏是否固定在顶部,对选项卡内容进行高度补偿,修复了上文提到的 bug。
swiper 的 current 属性表示当前选项卡的编号,从 0 开始,我把它绑定到 CurrentTab 上,从而实现后台控制前台选项卡的切换。同时我给它设置了 bindchange 事件,后台处理的 js 代码 如下:
// 滑动切换选项卡
swiperTab: function(e) {
this.setData({
CurrentTab: e.detail.current
})
wx.pageScrollTo({
duration: 500,
scrollTop: this.data.InitTab2Top
})
},
这样就实现了切换选项卡的所有功能,还进行了一点视觉优化,切换时自动滑动到导航栏吸顶的位置,跳过轮播图,给一个 500ms 的切换动画时长,实测效果不错。
swiper-item 也是批量生成的,我一共设置了三大块内容,所以就是三个 swiper-item,内部嵌套 scroll-view 进行滚动,触底事件 bindscrolltolower 绑定好,作为加载更多文章的入口。高度也需要和 swiper 一样做成自适应的,因为小程序的 scroll-view 暂时还不支持自动扩展高度,有点难顶。注意这里 scroll-y 一定要设置为 true,否则滚不动。
再往里就没什么说的了,绑定点击事件并传递文章的 id 以供阅读,再根据标题、副标题和日期区分一下字体即可。下面介绍一下加载文章部分,触底加载 loadMore 放在下一节介绍。
我设置在页面 onload 时加载一次文章,数据库用的是小程序自带的免费云开发数据库(穷冒烟了),所以一次最多只能加载 20 个记录,也就是我首次加载的文章个数,其他文章在触底时会自动加载,直到加载完毕。加载文章的 js 代码:
// 从云数据库中获取每个选项卡的内容
getTabItemContent: function(tabindex, loadnum) {
let CollectionName = "Index_TabItem" + tabindex.toString() + "_Content"
let ContentTarget = "TabItems[" + tabindex.toString() + "].content"
let BottomTarget = "TabItems[" + tabindex.toString() + "].reachBottom"
const db = wx.cloud.database()
db.collection(CollectionName).count().then(res => {
let TotalArticles = res.total
// 需要加载的文章数 >= 已有的文章总数,必是初次加载,且加载后没有更多文章
if (loadnum >= TotalArticles) {
db.collection(CollectionName).orderBy(\'date\', \'desc\').get().then(res => { // 最新文章放在前面
for (let i = 0; i < res.data.length; i++) {
res.data[i].date = util.formatTime(res.data[i].date) // 格式化时间
}
this.setData({
[ContentTarget]: res.data,
[BottomTarget]: true
})
})
// 需要加载的文章数 < 已有的文章总数
} else {
let curArticleNum = this.data.TabItems[tabindex].content.length
if (!curArticleNum) { // 初次加载(无需skip)
db.collection(CollectionName).orderBy(\'date\', \'desc\').get().then(res => {
for (let i = 0; i < res.data.length; i++) {
res.data[i].date = util.formatTime(res.data[i].date)
}
this.setData({
[ContentTarget]: res.data
})
})
} else { // 非初次加载
db.collection(CollectionName).orderBy(\'date\', \'desc\').skip(curArticleNum).get().then(res => {
let originContent = this.data.TabItems[tabindex].content
for (let i = 0; i < res.data.length; i++) {
res.data[i].date = util.formatTime(res.data[i].date)
originContent.push(res.data[i])
}
this.setData({
[ContentTarget]: originContent
})
if (res.data.length + curArticleNum >= TotalArticles) { // 没有更多的文章了
this.setData({
[BottomTarget]: true
})
}
})
}
}
})
},
我把数据库名写死在里面了,确实,并不是一个明智的做法,懒了。调用云数据库需要先 wx.cloud.init(),这里我在 onload 方法中已经 init 过了,然后创建一个数据库对象 db,根据 collection 名和 doc 名获取指定的数据库中的记录即可,一次最多20个记录,取文章的时候需要用 orderBy 根据时间进行排序,还需要考虑用 skip 方法跳过已经取出来的内容。这里我根据是否是初次加载来区分后续有无更多文章,如果文章数小于等于 20 的话,说明加载完了就没有更多文章了,设置 BottomTarget 标志位为 true;否则文章数大于 20,需要根据当前文章列表中有无文章来判断是不是初次加载,从而确定后续是否还有文章。
这里网上还有一种思路,我觉得应该比我这种方法要好。他们先把所有文章按照时间排序取出来放到 data 中的数组中,然后每次 loadMore 的时候就从中取出一部分给当前显示出来的数组,这样省得老是调数据库麻烦,下次再做的话我会使用这种方法试试。
下拉加载提示框
触底时需要给一些提示,这里我采用的是底部弹出的提示框:
<block wx:if="{{TabItems[CurrentTab].scrolltolower}}">
<view class="load-more-container" bindtap="loadMoreExit" style="{{TabItems[CurrentTab].closePopupTextBox? \'display:none;\' : \'\'}}">
<block wx:if="{{TabItems[CurrentTab].reachBottom}}">
<view class="load-more-text">没有更多文章了</view>
</block>
<block wx:else>
<view class="load-more-text">滑动屏幕浏览更多</view>
</block>
</view>
</block>
根据当前选项卡是否触底的标志位来决定是否显示此提示框,如果触底就显示出来,单击提示框可以关闭,提示框内部文字根据是否触底显示不同的内容。后台 js 代码如下:
// 监听触底事件
loadMore: function() {
let scrolltolowerTarget = "TabItems[" + this.data.CurrentTab.toString() + "].scrolltolower"
let closePopupTextBoxTarget = "TabItems[" + this.data.CurrentTab.toString() + "].closePopupTextBox"
this.setData({
[scrolltolowerTarget] : true,
[closePopupTextBoxTarget] : false
})
if (!this.data.TabItems[this.data.CurrentTab].reachBottom) {
this.getTabItemContent(this.data.CurrentTab, this.data.LoadArticleNum)
}
},
// 监听消息提示框被点击事件
loadMoreExit: function() {
let closePopupTextBoxTarget = "TabItems[" + this.data.CurrentTab.toString() + "].closePopupTextBox"
this.setData({
[closePopupTextBoxTarget] : true
})
}
这里就基本没有要解释的部分了,都是一些我设置的标志位,如 scrolltolower 表示当前选项卡是否触底,closePopupTextBox 表示是否关闭提示框。
最后还要再提一下页面跳转 wx.navigateTo,即点击了新闻栏目之后应该跳转的操作,js:
// 点击列表中一个栏目触发事件
contentTapped: function(e) {
let curTab = this.data.CurrentTab
wx.navigateTo({
url: \'../index_details/index_details\',
success: function(res) {
res.eventChannel.emit(\'acceptDataFromOpenerPage\', [curTab, e.currentTarget.dataset.id])
}
})
},
由于我在数据库中设置的就是选项卡 id + 文章 id = 文章唯一标识,所以这里要把选项卡也传递进来,用 emit 方法将参数传递到新的页面即可。在新的页面用 eventChannel.on() 方法实现参数的接收,新页面 js 代码如下:
// 接收从index传递过来的参数
getParamFromOpenerPage: function() {
let that = this
const eventChannel = this.getOpenerEventChannel()
eventChannel.on(\'acceptDataFromOpenerPage\', function(param) {
that.setData({
tabid: param[0],
articleid: param[1]
})
that.getDetails(param[0], param[1])
})
},
that.getDetails() 方法是我后面写的自定义函数,从云数据库拿数据的。
请发表评论