123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693 |
- <template>
- <view class="scroll-list-wrap" :style="[scrollListWrapStyle]">
- <scroll-view
- class="scroll-view"
- :class="[elClass]"
- :style="[listWrapStyle]"
- scroll-y
- scroll-anchoring
- enable-back-to-top
- :scroll-top="scrollTop"
- :lower-threshold="defaultOption.lowerThreshold"
- @scroll="handleScroll"
- @touchend="handleTouchEnd"
- @touchmove.prevent.stop="handleTouchMove"
- @touchstart="handleTouchStart"
- @scrolltolower="handleScrolltolower"
- >
- <view class="scroll-content" :style="[scrollContentStyle]">
- <view class="pull-down-wrap">
- <slot name="pulldown" v-if="$slots.pulldown"></slot>
- <view class="refresh-view" :style="[refreshViewStyle]" v-else>
- <view class="pull-down-animation" :class="{ refreshing: refreshing }" :style="[pullDownAnimationStyle]"></view>
- <text class="pull-down-text" :style="[pullDownTextStyle]">{{ refreshStateText }}</text>
- </view>
- </view>
- <view class="empty-wrap-me" v-if="showEmpty">
- <slot name="empty" v-if="$slots.empty"></slot>
- <view class="empty-view-me" v-else>
- <image class="empty-image" :src="defaultOption.emptyImage || images.empty" mode="aspectFit"></image>
- <text class="empty-text" :style="[emptyTextStyle]">{{ emptyText }}</text>
- </view>
- </view>
- <view class="list-content"><slot></slot></view>
- <view class="pull-up-wrap" v-if="showPullUp">
- <slot name="pullup" v-if="$slots.pullup"></slot>
- <view class="load-view" v-else>
- <view class="pull-up-animation" v-if="loading" :style="[pullUpAnimationStyle]"></view>
- <text class="pull-up-text" :style="[pullUpTextStyle]">{{ loadStateText }}</text>
- </view>
- </view>
- </view>
- </scroll-view>
- </view>
- </template>
- <script>
- import images from './images.js';
- export default {
- name: 'scroll-list',
- props: {
- // 配置信息
- option: {
- type: Object,
- default: () => ({})
- }
- },
- data() {
- return {
- defaultOption: {
- page: 1, // 分页
- size: 10, // 分页大小
- auto: true, // 自动加载
- height: null, // 组件高度
- disabled: false, // 禁用
- background: '', // 背景颜色属性
- emptyImage: '', // 空数据提示图片
- offsetBottom: 0, // 底部高度补偿
- pullDownSpeed: 0.5, // 下拉速率
- lowerThreshold: 40, // 距离底部上拉加载距离
- refresherThreshold: 80, // 距离顶部下拉刷新距离
- refreshDelayed: 800, // 刷新延迟
- refreshFinishDelayed: 800, // 刷新完成后的延迟
- safeArea: false, // 是否开启安全区域适配
- emptyTextColor: '#82848a', // 空提示文字颜色
- loadTextColor: '#82848a', // 上拉加载文字颜色
- loadIconColor: '#82848a', // 上拉加载图标颜色
- refresherTextColor: '#82848a', // 下拉刷新文字颜色
- refresherIconColor: '#82848a', // 下拉刷新图标颜色
- emptyText: '暂无列表~', // 空数据提示文字
- loadingText: '正在加载中~', // 加载中文字
- loadFailText: '加载失败啦~', // 加载失败文字
- noMoreText: '没有更多啦~', // 没有更多文字
- refreshingText: '正在刷新~', // 正在刷新文字
- refreshFailText: '刷新失败~', // 刷新失败文字
- refreshSuccessText: '刷新成功~', // 刷新成功文字
- pulldownText: '下拉刷新~', // 下拉中的文字
- pulldownFinishText: '松开刷新~' // 下拉完成的文字
- },
- images, // 内置图片
- elClass: '', // 组件动态class
- windowInfo: {}, // 窗口信息
- scrollTop: 0, // 距离顶部滚动高度
- scrollViewTop: -1, // 滚动视图顶部位置
- scrollViewHeight: 0, // 滚动视图高度
- currentPage: 1, // 当前分页页码
- currentSize: 15, // 当前分页大小
- currentScrollTop: 0, // 当前滚动高度
- emptyText: '暂无列表~',
- loadStateText: '正在加载中~', // 加载状态文字
- refreshStateText: '下拉刷新~', // 刷新状态文字
- loadDisabled: false, // 是否禁用上拉加载
- loading: false, // 是否加载中
- refreshing: false, // 是否刷新中
- refreshFinish: false, // 是否刷新完成
- pulldowning: false, // 是否正在下拉
- pullDownHeight: 0, // 下拉高度
- showEmpty: false, // 是否显示空数据提示
- showPullUp: false, // 是否显示上拉加载
- showPullDown: false // 是否显示下拉刷新
- };
- },
- methods: {
- // 组件初始化
- handleInit() {
- // 合并配置
- this.defaultOption = Object.assign(this.defaultOption, this.option);
- this.showEmpty = !this.defaultOption.auto;
- this.currentPage = this.defaultOption.page;
- this.currentSize = this.defaultOption.size;
- this.emptyText = this.defaultOption.emptyText;
- this.loadStateText = this.defaultOption.loadingText;
- this.refreshStateText = this.defaultOption.pulldownText;
- // 计算高度
- this.queryRect('.' + this.elClass).then(rect => {
- // 设置组件顶部位置
- this.scrollViewTop = rect.top;
- // 判断是否自动加载
- if (this.defaultOption.auto) this.load();
- });
- },
- /**
- * 加载数据
- *
- */
- load() {
- if (this.defaultOption.disabled || this.loading || this.loadDisabled) return;
- // 开启正在加载
- this.loading = true;
- // 设置正在加载状态文字
- this.loadStateText = this.defaultOption.loadingText;
- // 显示上拉加载
- this.showPullUp = true;
- // 分页参数
- let paging = { page: this.currentPage, size: this.currentSize };
- // 触发load事件
- this.$emit('load', paging);
- },
- // 加载成功
- loadSuccess(data = {}) {
- // 解构数据
- const { list, total } = data;
- // 判断列表是否是数组
- if (Array.isArray(list)) {
- // 判断列表长度
- if (list.length) {
- // 判断列表长度和列表总数是否相同
- if (list.length >= total) {
- // 设置禁用上拉加载
- this.loadDisabled = true;
- // 加载状态文字
- this.loadStateText = this.defaultOption.noMoreText;
- } else {
- // 关闭禁用上拉加载
- this.loadDisabled = false;
- // 设置分页参数
- this.currentPage++;
- // 加载状态为加载中
- this.loadStateText = this.defaultOption.loadingText;
- // 加载计算
- this.loadCompute();
- }
- // 显示上拉加载
- this.showPullUp = true;
- // 隐藏空数据提示
- this.showEmpty = false;
- } else {
- // 设置禁用上拉加载
- this.loadDisabled = true;
- // 隐藏上拉加载
- this.showPullUp = false;
- // 隐藏上拉加载
- this.showPullUp = false;
- // 显示空数据提示
- this.showEmpty = true;
- }
- // 关闭正在加载
- this.loading = false;
- // 触发加载成功事件
- this.$emit('loadSuccess', list);
- } else {
- // 不是数组类型当作加载失败处理
- this.loadFail();
- console.error('the list must be a array');
- }
- },
- // 加载失败
- loadFail() {
- // 关闭正在加载
- this.loading = false;
- // 关闭空数据提示
- this.showEmpty = false;
- // 显示上拉加载
- this.showPullUp = true;
- // 加载状态为加载失败
- this.loadStateText = this.defaultOption.loadFailText;
- // 触发加载失败事件
- this.$emit('loadFail');
- },
- // 刷新数据
- refresh() {
- // 如果是下拉刷新
- if (this.pullDownHeight == this.defaultOption.refresherThreshold) {
- // 关闭正在加载
- this.loading = false;
- // 隐藏上拉加载
- this.showPullUp = false;
- } else {
- // 开启正在加载
- this.loading = true;
- // 隐藏空数据提示
- this.showEmpty = false;
- // 显示上拉加载
- this.showPullUp = true;
- // 设置正在刷新状态文字
- this.loadStateText = this.defaultOption.refreshingText;
- }
- // 设置刷新未完成
- this.refreshFinish = false;
- // 开启正在刷新
- this.refreshing = true;
- // 设置正在刷新状态文字
- this.refreshStateText = this.defaultOption.refreshingText;
- // 设置分页参数
- this.currentPage = 1;
- this.currentSize = this.defaultOption.size;
- let paging = { page: this.currentPage, size: this.currentSize };
- // 触发refresh事件
- setTimeout(() => {
- this.$emit('refresh', paging);
- }, this.defaultOption.refreshDelayed);
- },
- // 刷新成功
- refreshSuccess(data) {
- // 解构数据
- const { list, total } = data;
- // 判断列表是否是数组
- if (Array.isArray(list)) {
- // 判断列表长度
- if (list.length) {
- // 判断列表长度和列表总数是否相同
- if (list.length >= total) {
- // 设置禁用上拉加载
- this.loadDisabled = true;
- // 设置没有更多状态文字
- this.loadStateText = this.defaultOption.noMoreText;
- } else {
- // 设置分页参数
- this.currentPage++;
- // 关闭禁用上拉加载
- this.loadDisabled = false;
- // 设置加载中状态文字
- this.loadStateText = this.defaultOption.loadingText;
- // 开启自动加载
- this.defaultOption.auto = true;
- // 加载计算
- this.loadCompute();
- }
- // 关闭空数据提示
- this.showEmpty = false;
- // 显示上拉加载
- this.showPullUp = true;
- } else {
- // 设置禁用上拉加载
- this.loadDisabled = true;
- // 隐藏上拉加载
- this.showPullUp = false;
- // 显示空数据提示
- this.showEmpty = true;
- // 设置没有更多状态文字
- this.loadStateText = this.defaultOption.noMoreText;
- }
- // 关闭正在加载
- this.loading = false;
- // 设置刷新成功状态文字
- this.refreshStateText = this.defaultOption.refreshSuccessText;
- // 关闭正在刷新
- this.refreshing = false;
- // 关闭正在下拉
- this.pulldowning = false;
- // 触发刷新成功事件
- this.$emit('refreshSuccess', list);
- setTimeout(() => {
- // 设置刷新完成
- this.refreshFinish = true;
- // 重置下拉高度
- this.pullDownHeight = 0;
- // 隐藏下拉刷新
- this.showPullDown = false;
- this.$emit('refreshSuccess');
- }, this.defaultOption.refreshFinishDelayed);
- } else {
- // 不是数组类型当作刷新失败处理
- this.refreshFail();
- console.error('the list must be a array');
- }
- },
- // 刷新失败
- refreshFail() {
- // 设置加载失败状态文字
- this.loadStateText = this.defaultOption.refreshFailText;
- // 设置刷新失败状态文字
- this.refreshStateText = this.defaultOption.refreshFailText;
- // 关闭正在加载
- this.loading = false;
- // 显示下拉加载
- this.showPullUp = true;
- // 关闭正在刷新
- this.refreshing = false;
- // 关闭正在下拉
- this.pulldowning = false;
- // 延迟执行刷新完成后状态
- setTimeout(() => {
- // 设置刷新完成
- this.refreshFinish = true;
- // 重置下拉高度
- this.pullDownHeight = 0;
- // 隐藏下拉刷新
- this.showPullDown = false;
- // 触发刷新失败事件
- this.$emit('refreshError');
- }, this.defaultOption.refreshFinishDelayed);
- },
- // 加载计算
- loadCompute() {
- // 判断是否自动加载
- if (this.defaultOption.auto) {
- // 延迟执行下否者可能会高度计算错误
- setTimeout(() => {
- this.$nextTick(() => {
- this.queryRect('.list-content').then(rect => {
- if (rect.height <= this.scrollViewHeight) {
- this.load();
- }
- });
- });
- }, 100);
- }
- },
- /**
- *
- * 上拉触底事件 滚动触底
- * @param {Object} e
- */
- handleScrolltolower(e) {
- if (this.loadDisabled) return;
- this.$emit('scrolltolower', e);
- //
- this.load();
- },
- /**
- * 滚动事件
- * @param {Object} event
- *
- */
- handleScroll(event) {
- this.currentScrollTop = event.detail.scrollTop;
- this.$emit('scroll', event.detail);
- },
- /**
- * 当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。
- * 触摸按下处理
- * @param {Object} event
- */
- handleTouchStart(event) {
- if (this.defaultOption.disabled) return;
- this.currentTouchStartY = event.touches[0].clientY;
- this.$emit('touchStart', event);
- },
- /**
- * 当手指在屏幕上滑动时连续地触发。在这个事件发生期间,调用preventDefault() 可以阻止滚动。
- * 触摸按下滑动处理
- * @param {Object} event
- */
- handleTouchMove(event) {
- if (this.defaultOption.disabled || this.currentScrollTop) return;
- if (event.touches[0].clientY >= this.currentTouchStartY) {
- this.pulldowning = true;
- this.showPullDown = true;
- let pullDownDistance = (event.touches[0].clientY - this.currentTouchStartY) * this.defaultOption.pullDownSpeed;
- this.pullDownHeight = pullDownDistance > this.defaultOption.refresherThreshold ? this.defaultOption.refresherThreshold : pullDownDistance;
- this.refreshStateText =
- this.pullDownHeight >= this.defaultOption.refresherThreshold ? this.defaultOption.pulldownFinishText : this.defaultOption.pulldownText;
- this.$emit('touchMove', event);
- }
- },
- /**
- * 移动端
- * 当手指从屏幕上移开时触发
- * 触摸松开处理
- * @param {Object} event
- */
- handleTouchEnd(event) {
- if (this.defaultOption.disabled) return;
- // 当下拉高度小于下拉阈值
- if (this.pullDownHeight < this.defaultOption.refresherThreshold) {
- // 关闭正在下拉
- this.pulldowning = false;
- // 重置下拉高度
- this.pullDownHeight = 0;
- // 隐藏下拉刷新
- this.showPullDown = false;
- // 触发下拉中断事件
- this.$emit('refreshStop');
- } else {
- this.refresh();
- }
- // 触发下拉触摸松开事件
- this.$emit('touchEnd', event);
- },
- // 更新组件
- updateScrollView() {
- if (this.defaultOption.height) {
- this.scrollViewHeight = uni.upx2px(this.defaultOption.height);
- } else {
- this.scrollViewHeight = this.windowInfo.windowHeight - this.scrollViewTop;
- }
- this.scrollViewObserve();
- },
- // 监听列表高度变化
- listContentObserve() {
- this.disconnectObserve('_listContentObserve');
- const listContentObserve = this.createIntersectionObserver({
- thresholds: [0, 0.5, 1]
- });
- listContentObserve.relativeToViewport({
- // #ifdef H5
- top: -(this.windowInfo.windowTop + rect.top),
- // #endif
- // #ifndef H5
- top: -rect.top
- // #endif
- });
- },
- // 监听组件位置变化
- scrollViewObserve() {
- this.disconnectObserve('_scrollViewObserve');
- this.$nextTick(() => {
- this.queryRect('.' + this.elClass).then(rect => {
- const scrollViewObserve = this.createIntersectionObserver({
- thresholds: [0, 0.5, 1]
- });
- scrollViewObserve.relativeToViewport({
- // #ifdef H5
- top: -(this.windowInfo.windowTop + rect.top),
- // #endif
- // #ifndef H5
- top: -rect.top
- // #endif
- });
- scrollViewObserve.observe('.' + this.elClass, position => {
- // #ifdef H5
- this.scrollViewTop = position.boundingClientRect.top - this.windowInfo.windowTop;
- // #endif
- // #ifndef H5
- this.scrollViewTop = position.boundingClientRect.top;
- // #endif
- });
- this._scrollViewObserve = scrollViewObserve;
- });
- });
- },
- // 断开监听组件
- disconnectObserve(observerName) {
- const observer = this[observerName];
- observer && observer.disconnect();
- },
- // 查询dom节点信息
- queryRect(selector, all) {
- return new Promise(resolve => {
- uni.createSelectorQuery()
- .in(this)
- [all ? 'selectAll' : 'select'](selector)
- .boundingClientRect(rect => {
- if (all && Array.isArray(rect) && rect.length) {
- resolve(rect);
- }
- if (!all && rect) {
- resolve(rect);
- }
- })
- .exec();
- });
- },
- // 16进制转RGB
- hexToRgb(hex) {
- const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
- hex = hex.replace(shorthandRegex, (m, r, g, b) => {
- return r + r + g + g + b + b;
- });
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
- return result
- ? {
- r: parseInt(result[1], 16),
- g: parseInt(result[2], 16),
- b: parseInt(result[3], 16)
- }
- : null;
- }
- },
- computed: {
- scrollListWrapStyle(){
- let style = {};
- style.background = this.defaultOption.background;
- return style;
- },
- // 组件容器样式
- listWrapStyle() {
- let style = {};
- const { offsetBottom } = this.defaultOption;
- style.height = this.scrollViewHeight - uni.upx2px(offsetBottom) + 'px';
- if (this.defaultOption.safeArea) style.paddingBottom = 'env(safe-area-inset-bottom) !important';
- return style;
- },
- // 滚动内容样式
- scrollContentStyle() {
- const style = {};
- const { pullDownHeight, pulldowning, showPullDown } = this;
- style.transform = showPullDown ? `translateY(${pullDownHeight}px)` : `translateY(0px)`;
- style.transition = pulldowning ? `transform 100ms ease-out` : `transform 200ms cubic-bezier(0.19,1.64,0.42,0.72)`;
- return style;
- },
- // 下拉刷新样式
- refreshViewStyle() {
- const style = {};
- const { showPullDown } = this;
- style.opacity = showPullDown ? 1 : 0;
- return style;
- },
- // 下拉中动画样式
- pullDownAnimationStyle() {
- const style = {};
- const { refresherIconColor, refresherThreshold } = this.defaultOption;
- const { refreshing, pullDownHeight } = this;
- const { r, g, b } = this.hexToRgb(refresherIconColor);
- const rate = pullDownHeight / refresherThreshold;
- style.borderColor = `rgba(${r},${g},${b},0.2)`;
- style.borderTopColor = refresherIconColor;
- if (!refreshing) {
- style.transform = `rotate(${360 * rate}deg)`;
- style.transition = 'transform 100ms linear';
- }
- return style;
- },
- pullDownTextStyle() {
- const style = {};
- const { refresherTextColor } = this.defaultOption;
- style.color = refresherTextColor;
- return style;
- },
- // 上拉中动画样式
- pullUpAnimationStyle() {
- const style = {};
- const { loadIconColor } = this.defaultOption;
- const { r, g, b } = this.hexToRgb(loadIconColor);
- style.borderColor = `rgba(${r},${g},${b},0.2)`;
- style.borderTopColor = loadIconColor;
- return style;
- },
- // 上拉中文字样式
- pullUpTextStyle() {
- const style = {};
- const { loadTextColor } = this.defaultOption;
- style.color = loadTextColor;
- return style;
- },
- // 空数据提示文字样式
- emptyTextStyle() {
- const style = {};
- const { emptyTextColor } = this.defaultOption;
- style.color = emptyTextColor;
- return style;
- }
- },
- watch: {
- scrollViewTop(val) {
- this.updateScrollView();
- }
- },
- created() {
- this.elClass = 'scroll-view-' + this._uid;
- this.windowInfo = uni.getSystemInfoSync();
- },
- mounted() {
- this.handleInit();
- }
- };
- </script>
- <style lang="scss" scoped>
- .scroll-list-wrap {
- box-sizing: border-box;
- .scroll-view {
- position: relative;
- .scroll-content {
- height: 100%;
- display: flex;
- will-change: transform;
- flex-direction: column;
- .pull-down-wrap {
- left: 0;
- width: 100%;
- display: flex;
- padding: 30rpx 0;
- position: absolute;
- align-items: flex-end;
- justify-content: center;
- transform: translateY(-100%);
- .refresh-view {
- display: flex;
- justify-content: center;
- .pull-down-animation {
- width: 32rpx;
- height: 32rpx;
- border-width: 4rpx;
- border-style: solid;
- border-radius: 50%;
- &.refreshing {
- animation: spin 0.5s linear infinite;
- }
- @keyframes spin {
- to {
- transform: rotate(360deg);
- }
- }
- }
- .pull-down-text {
- margin-left: 10rpx;
- }
- }
- }
- .empty-wrap-me {
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: flex;
- position: absolute;
- align-items: center;
- justify-content: center;
- flex-direction: column;
- .empty-view-me {
- margin: auto;
- display: flex !important;
- align-items: center;
- flex-direction: column;
- .empty-image {
- width: 200rpx;
- height: 200rpx;
- }
- .empty-text {
- color: #606266;
- margin-top: 20rpx;
- }
- }
- }
- .list-content {
- }
- .pull-up-wrap {
- display: flex;
- align-items: center;
- justify-content: center;
- .load-view {
- padding: 20rpx 0;
- display: flex;
- align-items: center;
- justify-content: center;
- .pull-up-animation {
- width: 32rpx;
- height: 32rpx;
- border-width: 4rpx;
- border-style: solid;
- border-radius: 50%;
- animation: spin 0.5s linear infinite;
- }
- .pull-up-text {
- margin-left: 10rpx;
- }
- }
- }
- }
- }
- }
- </style>
|