You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
high-mini/components/dataList/dataList.vue

553 lines
15 KiB

<template>
<view
class="data-list-components safe-area-inset-bottom"
:class="{ 'safe-area-inset-bottom': safeAreaInsetBottom }"
:style="[dataListStyle]"
@touchmove.stop.prevent
>
<scroll-view
class="scroll-view"
:scroll-top="scrollTop"
:scroll-y="scrollingEnabled"
:lower-threshold="lowerThreshold"
:enable-back-to-top="enableBackToTop"
@scroll="handleScroll"
@scrolltolower="handleScrolltolower"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<view class="scroll-content" :style="[scrollContentStyle]">
<!-- 下拉 -->
<view class="pull-down-view">
<slot name="refresh" v-if="$slots.refresh"></slot>
<view class="refresh-view" v-else>
<!-- <view class="loading-animation"></view> -->
<!-- <text class="pull-down-text">{{ refreshText[refreshStatus] }}</text> -->
<text :class="' pull-down-text icon-ymt '+(refreshStatus == 'soon'?' to-blod-down ':refreshStatus == 'sure'?' to-blod-up ':' ') " >{{ refreshText[refreshStatus] }}</text>
</view>
</view>
<view class="scroll-list"><slot></slot></view>
<view class="loading-view" v-if="loadingMask">
<view class="loading-animation"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- -->
<view class="empty-view" v-if="showEmpty">
<slot name="empty" v-if="$slots.empty"></slot>
<view class="empty-content" v-else>
<image class="empty-image" :src="emptyImage || defaultEmptyImage" mode=""></image>
<text class="empty-text">{{ emptyText }}</text>
</view>
</view>
<!-- 底部上滑 -->
<view class="pull-up-view" v-else>
<slot name="load" v-if="$slots.load"></slot>
<view class="load-view" v-else>
<view class="loading-animation" v-if="loadStatus == 'loading'"></view>
<text :class="' loading-text icon-ymt '+(loadStatus == 'soon'?' to-blod-up ':loadStatus == 'sure'?' to-blod-down ':' ') " >{{ loadText[loadStatus] }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
/**
* data-list 数据列表
* @description 数据列表展示,上拉加载更多数据,下拉刷新数据
* @tips 如果父组件有设置padding,可能造成组件高度不正确的问题
* @property {Number, String} page 默认分页
* @property {Number, String} size 默认分页大小
* @property {Boolean} loading 是否显示首次加载时的loading遮罩层
* @property {Boolean} safeAreaInsetBottom 是否开启底部安全区域适配
* @property {String} emptyImage 空数据提示图片
* @property {String} emptyText 空数据提示文字
* @property {Boolean} enableBackToTop iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部
* @property {Number, String} refresherThreshold 下拉刷新阈值(单位px),触发 refresherrefresh 事件
* @property {Number, String} lowerThreshold 距底部多远时(单位px),触发 scrolltolower 事件
* @methods {Function} loadData 加载数据方法
* @methods {Function} refreshData 刷新数据方法
* @methods {Function} computedHeight 重新计算高度方法(有时候页面某些地方布局被改变了,会造成组件高度不能撑满的问题,就调用此方法)
* @event {Function} load 加载 事件(参数一:分页请求参数,参数二:回调方法,需要将列表和数据总数回调回去计算 {list:数据列表对象,total:后端数据总数})
* @event {Function} refresh 刷新 事件(参数一:分页请求参数,参数二:回调方法,需要将列表和数据总数回调回去计算 {list:数据列表对象,total:后端数据总数})
* @event {Function} scroll 滚动 事件
* @event {Function} scrolltolower 上拉触底 事件
* @event {Function} refresherpulling 正在下拉 事件
* @event {Function} refresherrestore 下拉刷新被复位 事件
* @event {Function} refresherabort 下拉刷新被终止 事件
* @event {Function} refresherrefresh 下拉刷新被触发 事件
* @example <data-list class="data-list" ref="list" @load="handleLoad" @refresh="handleRefresh" @scroll="handleScroll"></data-list>
*/
import emptyImages from './emptyImage.js';
export default {
name: 'data-list',
props: {
page: {
type: [Number, String],
default: 1
},
size: {
type: [Number, String],
default: 15
},
loading: {
type: Boolean,
default: false
},
safeAreaInsetBottom: {
type: Boolean,
default: true
},
emptyImage: {
type: String
},
emptyText: {
type: String,
default: '暂无数据~'
},
enableBackToTop: {
type: Boolean,
default: false
},
refresherThreshold: {
type: [Number, String],
default: 50
},
lowerThreshold: {
type: [Number, String],
default: 50
},
selectObject:{
type:Object,
default:{
selectIndex:0,
length:0
}
}
},
data() {
return {
list: [], // 数据列表
total: -1, // 数据总数
currentPage: 1, // 当前分页页码
currentSize: 10, // 当前分页大小
showEmpty: false, // 是否显示空数据
waiting: false, // 是否等待加载中
loadingMask: this.loading, // 是否显示加载中遮罩
autoLoad: false, // 是否自动加载
scrollTop: 0, // 设置竖向滚动条位置
currentScrollTop: 0, // 当前竖向滚动条位置
scrollingEnabled: true, // 是否启用竖向滚动
pullingDown: false, // 是否正在下拉
pullDownHeight: 0, // 下拉高度
pullDownTimeStamp: 0, // 下拉时的时间戳
pullingUp: false, // 是否正在上拉
pullUpHeight: 0, // 上拉高度
pullUpTimeStamp: 0, // 上拉时的时间戳
maxDistance:100,
touchDirection:'top',
loadStatus: 'loading',
loadText: {
loading: '加载中~',
nomore: '没有更多啦~',
soon: '上滑查看下一分类',
sure: '释放查看下一分类',
},
refreshStatus: 'soon',
refreshText: {
soon: '下滑查看上一分类',
sure: '释放查看上一分类',
nomore: '没有更多啦~',
refreshing: '正在刷新中~'
},
defaultEmptyImage: emptyImages.data,
componentsHeight: 'auto' // 组件高度属性
};
},
watch:{
selectObject:{
handler(n, o) {
if(n.selectIndex == n.length-1){
this.loadStatus = "nomore"
}else{
this.loadStatus = "soon"
}
if(n.selectIndex == 0){
this.refreshStatus = "nomore"
}else{
this.refreshStatus = "soon"
}
},
deep:true, // 深度监听
immediate:false, // 初始化监听
},
},
computed: {
dataListStyle() {
const { componentsHeight } = this;
return {
height: componentsHeight
};
},
scrollContentStyle() {
const style = {};
const { pullDownHeight, pullingDown,pullUpHeight, pullingUp } = this;
// if(this.touchDirection == 'top'){
style.transform = pullingDown ? `translateY(${pullDownHeight}px)` : `translateY(0px)`;
style.transition = pullingDown ? `transform .1s linear` : `transform 0.3s cubic-bezier(0.19,1.64,0.42,0.72)`;
// }else if(this.touchDirection == 'bottom'){
// style.transform = pullingUp ? `translateY(0px)` : `translateY(0px)`;
// style.transition = pullingUp ? `transform .1s linear` : `transform 0.3s cubic-bezier(0.19,1.64,0.42,0.72)`;
// }
return style;
}
},
mounted() {
this.handleInit();
},
methods: {
// 下滑触底后 数据加载
loadData() {
this.waiting = true;
this.$emit('load', { page: this.currentPage, size: this.currentSize }, res => {
this.waiting = false;
// this.loadingMask = false;
this.list = res.list;
this.total = res.total;
this.currentPage++;
this.handleComputeData();
this.computedHeight();
});
},
// 计算高度
computedHeight() {
this.getRect('.data-list-components').then(res => {
// #ifdef H5
this.componentsHeight = `calc(100vh - ${res.top}px - var(--window-top))`;
// #endif
// #ifndef H5
this.componentsHeight = `calc(100vh - ${res.top}px)`;
// #endif
});
},
// 计算数据
handleComputeData() {
const len = this.list.length;
const total = this.total;
if (len == 0 || total == 0) {
this.autoLoad = false;
this.showEmpty = true;
} else if (len >= this.total) {
this.autoLoad = false;
this.showEmpty = false;
this.loadStatus = 'soon';
} else if (len < this.total) {
this.showEmpty = false;
this.loadStatus = 'loading';
// console.log(len,this.total,this.loadStatus)
}
},
// 下滑触底事件
handleScrolltolower(e) {
this.$emit('scrolltolower', e);
if (this.loadStatus == 'nomore' || this.waiting) return;
this.loadData();
},
// 滚动事件
handleScroll(e) {
this.currentScrollTop = e.detail.scrollTop;
if (e.detail.scrollTop <= 20) {
this.touchDirection = 'top';
} else {
this.touchDirection = 'bottom';
}
this.$emit('scroll', e);
},
// 触摸按下处理
handleTouchStart(e) {
if(this.touchDirection=='top'){
this.pullingDown = true;
}else if(this.touchDirection=='bottom'){
this.pullingUp =true;
}
this.currentTouchStartY = e.touches[0].clientY;
},
// 触摸按下滑动处理
handleTouchMove(e) {
// this.$emit('refresherpulling', e);
// 顶部下滑
if (e.touches[0].clientY >= this.currentTouchStartY){
// const shakeInterval = 1000 / 60;
// const currentTimeStamp = new Date().getTime();
// if (this.pullDownTimeStamp && currentTimeStamp - this.pullDownTimeStamp <= shakeInterval) return;
// this.pullDownTimeStamp = currentTimeStamp;
const currentTouchMoveY = e.touches[0].clientY;
let movingDistance = (currentTouchMoveY - this.currentTouchStartY) ;
if(this.selectObject.selectIndex == 0){
this.refreshStatus = 'nomore'
}
else if(movingDistance > this.refresherThreshold){
this.refreshStatus = 'sure';// 释放
}else{
// movingDistance = 0;
this.refreshStatus = 'soon';// 划
}
let computeDistance = movingDistance;
if(computeDistance > this.maxDistance){
computeDistance = this.maxDistance;
}
this.pullDownHeight = computeDistance ;
}else{
// 底部上滑
const currentTouchMoveY = e.touches[0].clientY;
let movingDistance = (this.currentTouchStartY - currentTouchMoveY ) ;
if(this.selectObject.selectIndex == this.selectObject.length-1){
this.loadStatus = 'nomore'
}else if (movingDistance >= this.lowerThreshold) {
this.loadStatus = 'sure';//释放
} else {
this.loadStatus = 'soon';//划
}
let computeDistance = movingDistance;
if(computeDistance > this.maxDistance){
computeDistance = this.maxDistance;
}
this.pullUpHeight = computeDistance;
}
},
// 触摸松开处理
handleTouchEnd(e) {
// 顶部下滑
if(this.touchDirection == 'top'){
if(this.pullDownHeight>=this.refresherThreshold&&this.selectObject.selectIndex > 0){
this.$emit('refresherrefresh', "prev");
}
this.pullingDown = false;
this.pullDownHeight = 0;
// 底部上滑
}else if(this.touchDirection == 'bottom'){
if(this.pullUpHeight>=this.lowerThreshold&&this.selectObject.selectIndex < (this.selectObject.length-1)){
this.$emit('refresherrefresh', "next");
}
this.pullingUp = false;
this.pullUpHeight = 0;
}
},
// 初始化
handleInit() {
this.currentPage = this.page;
this.currentSize = this.size;
this.computedHeight();
},
// 查询dom节点信息
getRect(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();
});
}
}
};
</script>
<style scoped lang="scss">
.data-list-components {
height: 100%;
.scroll-view {
height: 100%;
.scroll-content {
width: 100%;
height: 100%;
position: relative;
.empty-view {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.empty-content {
margin: auto;
display: flex;
align-items: center;
flex-direction: column;
.empty-image {
width: 400rpx;
height: 400rpx;
}
.empty-text {
color: #606266;
margin-top: 20rpx;
}
}
}
.pull-down-view {
top: 0;
left: 0;
width: 100%;
padding: 30rpx 0;
display: flex;
position: absolute;
align-items: center;
flex-direction: column;
justify-content: center;
transform: translateY(-100%);
.refresh-view {
display: flex;
align-items: center;
flex-direction: column;
.loading-animation {
width: 50rpx;
height: 50rpx;
}
.pull-down-text {
color: #606266;
font-size: 24rpx;
margin-top: 10rpx;
}
}
}
.scroll-list {
display: flex;
flex-direction: column;
}
.loading-view {
width: 100%;
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
.loading-animation {
width: 40rpx;
height: 40rpx;
}
.loading-text {
color: #606266;
font-size: 24rpx;
margin-top: 10rpx;
}
}
.pull-up-view {
width: 100%;
padding: 15rpx 0;
display: flex;
align-items: center;
justify-content: center;
.load-view {
display: flex;
align-items: center;
.loading-animation {
width: 30rpx;
height: 30rpx;
}
.loading-text {
color: #606266;
font-size: 24rpx;
margin-left: 20rpx;
}
}
}
.loading-animation {
position: relative;
&::after,
&::before {
left: 0;
width: 100%;
position: absolute;
border: 0 solid currentColor;
background-color: #b7b7b7;
}
&::after {
content: '';
top: -25%;
z-index: 1;
height: 100%;
border-radius: 10%;
animation: spin 0.6s -0.1s linear infinite;
}
&::before {
content: '';
bottom: -9%;
height: 10%;
background: #000;
border-radius: 50%;
opacity: 0.2;
animation: shadow 0.6s -0.1s linear infinite;
}
@keyframes spin {
17% {
border-bottom-right-radius: 10%;
}
25% {
transform: translateY(25%) rotate(22.5deg);
}
50% {
border-bottom-right-radius: 100%;
transform: translateY(50%) scale(1, 0.9) rotate(45deg);
}
75% {
transform: translateY(25%) rotate(67.5deg);
}
100% {
transform: translateY(0) rotate(90deg);
}
}
@keyframes shadow {
50% {
transform: scale(1.25, 1);
}
}
}
}
}
}
</style>