小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
TIP 👉 着意栽花花不发,等闲插柳柳成阴。元·关汉卿《包待制智斩鲁斋郎》
前言
在我们日常项目开发中,我们在会涉及到滚动列表的功能,所以封装了这个滚动列表组件。滚动列表组件
属性
pullDownRefresh - 下拉刷新
- 值类型:Boolean | Object
- 默认值:
{
threshold: flexible.rem2px(100 / 75), // 触发刷新的下拉距离
stop: flexible.rem2px(90 / 75) // 回弹悬停的距离
}
pullUpLoad - 上拉加载
- 值类型:Boolean | Object
- 默认值:true
- Object类型值示例:
{
threshold: 0 // 触发上拉事件的阈值
}
scrollConfig - 滚动条组件配置
- 值类型:Object
- 默认值:
{
wrapperBgColor: '#F5F5F5', // 滚动条包裹器的背景色
bounce: true, // 显示回弹动画
bounceTime: 800, // 回弹动画的动画时长(单位:毫秒)
useTransition: false, // 使用 requestAnimationFrame 做动画
observeDOM: true // 开启对DOM改变的探测
}
具体配置项参考scroll组件
事件
1. refresh - 刷新数据(组件创建成功后或者列表顶部下拉后触发此事件)
参数:
- callback:刷新数据成功后的回调方法
- 方法参数: status 数据状态,success(成功)、no-data(没有数据)、no-more(没有更多数据)、fail(失败)
【注意】:
- 如果有分页此事件的监听应该查询第一页的的数据,查询到的数据应当替换当前数据数组而不是追加到数据数组后
- 组件创建成功后会自动触发一次 refresh 事件
2. load - 加载数据(滚动到列表底部上拉时触发此事件)
参数:
- callback:刷新数据成功后的回调方法
- 方法参数: status 数据状态,success(成功)、no-more(没有更多数据)、fail(失败)
【注意】:
- 如果有分页此事件的监听应该查询下一页的的数据,查询到的数据追加到数据数组后
示例
<template>
<ScrollList @refresh="handleRefresh" @load="handleLoad">
<ul class="data-list" v-if="dataList.length > 0">
<li class="item" v-for="item in dataList" :key="item.id">
<div class="title">{{item.title}}</div>
<div class="date">{{item.articleDate}}</div>
</li>
</ul>
</ScrollList>
</template>
<script>
import ScrollList from '@/components/m/scrollList'
export default {
components: {
ScrollList
},
data () {
return {
pageSize: 10, // 每页大小
pageNo: 0, // 页码
count: 0, // 数据总数
dataList: [], // 列表数据
isLoading: false // 是否正在加载中
}
},
methods: {
// 获取列表数据
getList (pageNo) {
if (this.isLoading) {
return
}
this.isLoading = true
pageNo = pageNo || this.pageNo + 1
return this.$api.post({
url: '/api/article/list',
data: { pageSize: this.pageSize, pageNo }
}, this).then(data => {
this.pageNo = data.page.pageNo
this.count = data.page.count
if (this.pageNo > 1) {
this.dataList = [...this.dataList, ...data.page.list]
} else {
this.dataList = data.page.list
}
this.isLoading = false
let status = 'success' // 查询成功
if (data.page.count === 0) {
status = 'no-data' // 暂无数据
} else if (data.page.count <= data.page.pageNo * this.pageSize) {
status = 'no-more' // 没有更过数据
}
return status
}, e => {
this.isLoading = false
return 'fail' // 查询失败
})
},
// 刷新操作(页面初始化时会自动触发一次刷新事件)
handleRefresh (callback) {
this.getList(1).then((status) => {
callback(status)
})
},
// 加载操作
handleLoad (callback) {
this.getList().then((status) => {
callback(status)
})
}
}
}
</script>
Scroll.vue
<template>
<Scroll v-if="config" ref="scroll" v-bind="config" @created="init">
<div v-if="inited" class="pulldown-wrapper">
<div v-show="beforePullDown" class="pulldown-tips">
<span>松开即可刷新</span>
</div>
<div v-show="!beforePullDown">
<div v-show="isPullingDown" class="loading">
<BaseSpinner spinner="bubbles" size="s"></BaseSpinner>
<span class="loading-txt">加载中...</span>
</div>
<div v-show="!isPullingDown" class="refresh-result">
<span v-if="isFail">刷新失败</span>
<span v-else>刷新成功</span>
</div>
</div>
</div>
<div v-else-if="isIniting" class="init-tips">
<div class="loading">
<BaseSpinner spinner="bubbles" size="s"></BaseSpinner>
<span class="loading-txt">加载中...</span>
</div>
</div>
<div v-else-if="isFail" class="error-tips">
<div v-if="!isPullingDown" class="error-content" @click="emitRefresh">
<Icon name="cry" class="error-icon"></Icon>
<span>查询失败,点击重试</span>
</div>
</div>
<slot ref="list"></slot>
<template v-if="inited">
<div v-if="status === 'no-data'" class="no-data-tips">
<div v-if="!isPullingDown" class="no-data-content">
<Icon name="no-data" class="no-data-icon"></Icon>
<span>暂无数据</span>
</div>
</div>
<div v-else class="pullup-tips">
<div v-if="status === 'no-more'" class="no-more-tips">
<span>没有更多数据了</span>
</div>
<div v-else-if="!isPullUpLoad">
<span v-if="isFail">加载失败,请上拉重试</span>
<span v-else>上拉加载更多</span>
</div>
<div v-else class="loading">
<BaseSpinner spinner="bubbles" size="s"></BaseSpinner>
<span class="loading-txt">加载中...</span>
</div>
</div>
</template>
</Scroll>
</template>
<script>
import Scroll from '@/components/base/scroll'
import BaseSpinner from '@/components/base/spinner'
const defaultConfig = {
wrapperBgColor: '#F5F5F5', // 滚动条包裹器的背景色
bounce: true, // 显示回弹动画
bounceTime: 800, // 回弹动画的动画时长(单位:毫秒)
useTransition: false, // 使用 requestAnimationFrame 做动画
observeDOM: true // 开启对DOM改变的探测
}
export default {
name: 'ScrollList',
components: {
Scroll,
BaseSpinner
},
props: {
// 下拉刷新
pullDownRefresh: {
type: [Boolean, Object],
default: () => {
let threshold = 100
let stop = 90
if (window.lib && window.lib.flexible) {
let flexible = window.lib.flexible
threshold = flexible.rem2px(threshold / 75)
stop = flexible.rem2px(stop / 75)
}
return {
threshold: threshold, // 触发刷新的下拉距离
stop: stop // 回弹悬停的距离
}
}
},
// 上拉加载
/* 示例对象
{
threshold: 0 // 触发上拉事件的阈值
}
*/
pullUpLoad: {
type: [Boolean, Object],
default: true
},
// 滚动条组件配置
scrollConfig: {
type: Object,
default: () => { return {} }
}
},
data () {
return {
config: null,
inited: false, // 是否已初始化
isIniting: false, // 是否正在初始化
isFail: false, // 是否失败
status: '', // 当前状态:success、no-data、no-more
beforePullDown: true, // 是否下拉松手前
isPullingDown: false, // 是否正在下拉刷新
isPullUpLoad: false // 是否正在上拉加载
}
},
watch: {
scrollConfig (val) {
this.config = this.getConfig()
},
pullDownRefresh (val) {
this.config = this.getConfig()
},
pullUpLoad (val) {
this.config = this.getConfig()
}
},
created () {
this.config = this.getConfig()
// 触发刷新
this.emitRefresh()
},
mounted () {
// 设置内容区域的最小高度比包裹器大1像素,保证能够滚动
this.setContentMinHeight()
},
methods: {
// 初始化
init (bs) {
bs.on('pullingDown', this.pullingDownHandler)
bs.on('pullingUp', this.pullingUpHandler)
// bs.on('scroll', e => { console.log('scroll') })
// bs.on('scrollEnd', e => { console.log('scrollEnd') })
},
getConfig () {
const config = {
...defaultConfig,
...this.scrollConfig
}
if (this.pullDownRefresh) {
config.pullDownRefresh = this.pullDownRefresh
}
if (this.pullUpLoad) {
config.pullUpLoad = this.pullUpLoad
}
return config
},
// 触发刷新
emitRefresh () {
if (this.isIniting) {
return
}
this.isIniting = true
this.$emit('refresh', (status) => {
this.status = status
this.isIniting = false
if (status !== 'fail') {
this.inited = true
this.isFail = false
} else {
this.isFail = true
}
})
},
// 下拉操作
pullingDownHandler () {
if (!this.inited || this.isPullingDown || this.isPullUpLoad) {
if (!this.isPullingDown) {
const bs = this.$refs.scroll && this.$refs.scroll.bs // BetterScroll 实例
bs.finishPullDown()
}
return
}
this.beforePullDown = false
this.isPullingDown = true
this.$emit('refresh', (status) => {
if (status !== 'fail') {
this.status = status
this.isFail = false
} else {
this.isFail = true
}
this.isPullingDown = false
const bs = this.$refs.scroll && this.$refs.scroll.bs // BetterScroll 实例
if (bs) {
bs.finishPullDown()
setTimeout(() => {
this.beforePullDown = true
bs.refresh()
if (this.pullUpLoad) {
bs.openPullUp(this.pullUpLoad)
}
}, this.config.bounceTime + 100)
}
})
},
// 上拉操作
pullingUpHandler () {
if (!this.inited || this.isPullingDown || this.isPullUpLoad ||
this.status === 'no-data' || this.status === 'no-more') {
const bs = this.$refs.scroll && this.$refs.scroll.bs // BetterScroll 实例
if (bs) {
bs.finishPullUp()
}
return
}
this.isPullUpLoad = true
this.$emit('load', (status) => {
if (status !== 'fail') {
this.status = status
this.isFail = false
} else {
this.isFail = true
}
this.isPullUpLoad = false
const bs = this.$refs.scroll && this.$refs.scroll.bs // BetterScroll 实例
if (bs) {
bs.finishPullUp()
bs.refresh()
}
})
},
// 设置内容区域的最小高度比包裹器大1像素,保证能够滚动
setContentMinHeight () {
const scroll = this.$refs.scroll
const wrapperHeight = scroll.$refs.wrapper.getBoundingClientRect().height
scroll.$refs.content.style.minHeight = (wrapperHeight + 1) + 'px'
}
}
}
</script>
<style lang="scss" scoped>
.pulldown-wrapper{
position: absolute;
width: 100%;
padding: 20px;
box-sizing: border-box;
transform: translateY(-100%) translateZ(0);
text-align: center;
color: #999;
}
.pulldown-tips {
line-height: 110px;
}
.init-tips {
padding-top: 20px;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
color: #999;
.loading-txt {
margin-left: 12px;
}
}
.refresh-result {}
.no-data-tips {
padding-top: 50px;
text-align: center;
color: #999;
.no-data-content {
display: flex;
flex-direction: column;
.no-data-icon {
font-size: 120px;
margin-bottom: 20px;
}
}
}
.error-tips {
padding-top: 40px;
text-align: center;
color: #999;
.error-content {
display: flex;
flex-direction: column;
.error-icon {
font-size: 120px;
margin-bottom: 10px;
}
}
}
.pullup-tips {
padding: 20px;
text-align: center;
color: #999;
.no-more-tips {
padding-bottom: 30px;
}
}
</style>
index.js
/**
* 滚动列表组件
* @see https://github.com/ustbhuangyi/better-scroll
* @see https://better-scroll.github.io/docs/zh-CN/
*/
import ScrollList from './ScrollList.vue'
export default ScrollList
「欢迎在评论区讨论」