fix(code): 修正代码错误及不严谨的地方

This commit is contained in:
2025-12-23 16:59:14 +08:00
parent 68ecdd6e3c
commit 5588165f2c
91 changed files with 8364 additions and 8996 deletions

View File

@@ -1,52 +1,52 @@
<!-- 下拉刷新区域 -->
<template>
<view v-if="mOption.use" class="mescroll-downwarp">
<view class="downwarp-content">
<view class="downwarp-progress" :class="{ 'mescroll-rotate': isDownLoading }" :style="{ transform: downRotate }"></view>
<view class="downwarp-tip">{{ downText }}</view>
</view>
</view>
</template>
<script>
export default {
props: {
option: Object, // down的配置项
type: Number, // 下拉状态inOffset1 outOffset2 showLoading3 endDownScroll4
rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {};
},
// 是否在加载中
isDownLoading() {
return this.type === 3;
},
// 旋转的角度
downRotate() {
return 'rotate(' + 360 * this.rate + 'deg)';
},
// 文本提示
downText() {
switch (this.type) {
case 1:
return this.mOption.textInOffset;
case 2:
return this.mOption.textOutOffset;
case 3:
return this.mOption.textLoading;
case 4:
return this.mOption.textLoading;
default:
return this.mOption.textInOffset;
}
}
}
};
</script>
<style>
@import './mescroll-down.css';
</style>
<!-- 下拉刷新区域 -->
<template>
<view v-if="mOption.use" class="mescroll-downwarp">
<view class="downwarp-content">
<view class="downwarp-progress" :class="{ 'mescroll-rotate': isDownLoading }" :style="{ transform: downRotate }"></view>
<view class="downwarp-tip">{{ downText }}</view>
</view>
</view>
</template>
<script>
export default {
props: {
option: Object, // down的配置项
type: Number, // 下拉状态inOffset1 outOffset2 showLoading3 endDownScroll4
rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {};
},
// 是否在加载中
isDownLoading() {
return this.type === 3;
},
// 旋转的角度
downRotate() {
return 'rotate(' + 360 * this.rate + 'deg)';
},
// 文本提示
downText() {
switch (this.type) {
case 1:
return this.mOption.textInOffset;
case 2:
return this.mOption.textOutOffset;
case 3:
return this.mOption.textLoading;
case 4:
return this.mOption.textLoading;
default:
return this.mOption.textInOffset;
}
}
}
};
</script>
<style lang="scss">
@import './mescroll-down.css';
</style>

View File

@@ -1,90 +1,90 @@
<!--空布局
可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
-->
<template>
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
<image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" />
<view v-if="tip" class="empty-tip">{{ tip }}</view>
<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
</view>
</template>
<script>
// 引入全局配置
import GlobalOption from './../mescroll-uni-option.js';
export default {
props: {
// empty的配置项: 默认为GlobalOption.up.empty
option: {
type: Object,
default() {
return {};
}
}
},
// 使用computed获取配置,用于支持option的动态配置
computed: {
// 图标
icon() {
return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
},
// 文本提示
tip() {
return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
}
},
methods: {
// 点击按钮
emptyClick() {
this.$emit('emptyclick');
}
}
};
</script>
<style>
/* 无任何数据的空布局 */
.mescroll-empty {
box-sizing: border-box;
width: 100%;
padding: 100rpx 50rpx;
text-align: center;
}
.mescroll-empty.empty-fixed {
z-index: 99;
position: absolute; /*transform会使fixed失效,最终会降级为absolute */
top: 100rpx;
left: 0;
}
.mescroll-empty .empty-icon {
width: 280rpx;
height: 280rpx;
}
.mescroll-empty .empty-tip {
margin-top: 20rpx;
font-size: $font-size-tag;
color: gray;
}
.mescroll-empty .empty-btn {
display: inline-block;
margin-top: 40rpx;
min-width: 200rpx;
padding: 18rpx;
font-size: $font-size-base;
border: 1rpx solid #e04b28;
border-radius: 60rpx;
color: #e04b28;
}
.mescroll-empty .empty-btn:active {
opacity: 0.75;
}
</style>
<!--空布局
可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
-->
<template>
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
<image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" />
<view v-if="tip" class="empty-tip">{{ tip }}</view>
<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
</view>
</template>
<script>
// 引入全局配置
import GlobalOption from './../mescroll-uni-option.js';
export default {
props: {
// empty的配置项: 默认为GlobalOption.up.empty
option: {
type: Object,
default() {
return {};
}
}
},
// 使用computed获取配置,用于支持option的动态配置
computed: {
// 图标
icon() {
return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
},
// 文本提示
tip() {
return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
}
},
methods: {
// 点击按钮
emptyClick() {
this.$emit('emptyclick');
}
}
};
</script>
<style lang="scss">
/* 无任何数据的空布局 */
.mescroll-empty {
box-sizing: border-box;
width: 100%;
padding: 100rpx 50rpx;
text-align: center;
}
.mescroll-empty.empty-fixed {
z-index: 99;
position: absolute; /*transform会使fixed失效,最终会降级为absolute */
top: 100rpx;
left: 0;
}
.mescroll-empty .empty-icon {
width: 280rpx;
height: 280rpx;
}
.mescroll-empty .empty-tip {
margin-top: 20rpx;
font-size: $font-size-tag;
color: gray;
}
.mescroll-empty .empty-btn {
display: inline-block;
margin-top: 40rpx;
min-width: 200rpx;
padding: 18rpx;
font-size: $font-size-base;
border: 1rpx solid #e04b28;
border-radius: 60rpx;
color: #e04b28;
}
.mescroll-empty .empty-btn:active {
opacity: 0.75;
}
</style>

View File

@@ -1,81 +1,81 @@
<!-- 回到顶部的按钮 -->
<template>
<image
v-if="mOption.src"
class="mescroll-totop"
:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', { 'mescroll-safe-bottom': mOption.safearea }]"
:style="{ 'z-index': mOption.zIndex, left: left, right: right, bottom: addUnit(mOption.bottom), width: addUnit(mOption.width), 'border-radius': addUnit(mOption.radius) }"
:src="mOption.src"
mode="widthFix"
@click="toTopClick"
/>
</template>
<script>
export default {
props: {
// up.toTop的配置项
option: Object,
// 是否显示
value: false
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {};
},
// 优先显示左边
left() {
return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
},
// 右边距离 (优先显示左边)
right() {
return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
}
},
methods: {
addUnit(num) {
if (!num) return 0;
if (typeof num === 'number') return num + 'rpx';
return num;
},
toTopClick() {
this.$emit('input', false); // 使v-model生效
this.$emit('click'); // 派发点击事件
}
}
};
</script>
<style>
/* 回到顶部的按钮 */
.mescroll-totop {
z-index: 99;
position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
right: 46rpx !important;
bottom: 272rpx !important;
width: 72rpx;
height: auto;
border-radius: 50%;
opacity: 0;
transition: opacity 0.5s; /* 过渡 */
margin-bottom: var(--window-bottom); /* css变量 */
}
/* 适配 iPhoneX */
.mescroll-safe-bottom {
margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
}
/* 显示 -- 淡入 */
.mescroll-totop-in {
opacity: 1;
}
/* 隐藏 -- 淡出且不接收事件*/
.mescroll-totop-out {
opacity: 0;
pointer-events: none;
}
</style>
<!-- 回到顶部的按钮 -->
<template>
<image
v-if="mOption.src"
class="mescroll-totop"
:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', { 'mescroll-safe-bottom': mOption.safearea }]"
:style="{ 'z-index': mOption.zIndex, left: left, right: right, bottom: addUnit(mOption.bottom), width: addUnit(mOption.width), 'border-radius': addUnit(mOption.radius) }"
:src="mOption.src"
mode="widthFix"
@click="toTopClick"
/>
</template>
<script>
export default {
props: {
// up.toTop的配置项
option: Object,
// 是否显示
value: false
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {};
},
// 优先显示左边
left() {
return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
},
// 右边距离 (优先显示左边)
right() {
return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
}
},
methods: {
addUnit(num) {
if (!num) return 0;
if (typeof num === 'number') return num + 'rpx';
return num;
},
toTopClick() {
this.$emit('input', false); // 使v-model生效
this.$emit('click'); // 派发点击事件
}
}
};
</script>
<style lang="scss">
/* 回到顶部的按钮 */
.mescroll-totop {
z-index: 99;
position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
right: 46rpx !important;
bottom: 272rpx !important;
width: 72rpx;
height: auto;
border-radius: 50%;
opacity: 0;
transition: opacity 0.5s; /* 过渡 */
margin-bottom: var(--window-bottom); /* css变量 */
}
/* 适配 iPhoneX */
.mescroll-safe-bottom {
margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
}
/* 显示 -- 淡入 */
.mescroll-totop-in {
opacity: 1;
}
/* 隐藏 -- 淡出且不接收事件*/
.mescroll-totop-out {
opacity: 0;
pointer-events: none;
}
</style>

View File

@@ -1,39 +1,39 @@
<!-- 上拉加载区域 -->
<template>
<view class="mescroll-upwarp">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="isUpLoading">
<view class="upwarp-progress mescroll-rotate"></view>
<view class="upwarp-tip">{{ mOption.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
</view>
</template>
<script>
export default {
props: {
option: Object, // up的配置项
type: Number // 上拉加载的状态0loading前1loading中2没有更多了
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {};
},
// 加载中
isUpLoading() {
return this.type === 1;
},
// 没有更多了
isUpNoMore() {
return this.type === 2;
}
}
};
</script>
<style>
@import './mescroll-up.css';
</style>
<!-- 上拉加载区域 -->
<template>
<view class="mescroll-upwarp">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="isUpLoading">
<view class="upwarp-progress mescroll-rotate"></view>
<view class="upwarp-tip">{{ mOption.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
</view>
</template>
<script>
export default {
props: {
option: Object, // up的配置项
type: Number // 上拉加载的状态0loading前1loading中2没有更多了
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {};
},
// 加载中
isUpLoading() {
return this.type === 1;
},
// 没有更多了
isUpNoMore() {
return this.type === 2;
}
}
};
</script>
<style lang="scss">
@import './mescroll-up.css';
</style>

View File

@@ -1,298 +1,298 @@
<template>
<view
class="mescroll-body"
:style="{ minHeight: minHeight, 'padding-top': padTop, 'padding-bottom': padBottom, 'padding-bottom': padBottomConstant, 'padding-bottom': padBottomEnv }"
@touchstart="touchstartEvent"
@touchmove="touchmoveEvent"
@touchend="touchendEvent"
@touchcancel="touchendEvent"
>
<view class="mescroll-body-content mescroll-touch" :style="{ transform: translateY, transition: transition }">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp">
<view class="downwarp-content">
<view class="downwarp-progress" :class="{ 'mescroll-rotate': isDownLoading }" :style="{ transform: downRotate }"></view>
<view class="downwarp-tip">{{ downText }}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<!-- <mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> -->
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading" class="mescroll-upwarp">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType === 1">
<view class="upwarp-progress mescroll-rotate"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType === 2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick" v-if="showTop"></mescroll-top>
</view>
</template>
<script>
// 引入mescroll-uni.js,处理核心逻辑
import MeScroll from './mescroll-uni.js';
// 引入全局配置
import GlobalOption from './mescroll-uni-option.js';
// 引入空布局组件
import MescrollEmpty from './components/mescroll-empty.vue';
// 引入回到顶部组件
import MescrollTop from './components/mescroll-top.vue';
export default {
components: {
MescrollEmpty,
MescrollTop
},
data() {
return {
mescroll: { optDown: {}, optUp: {} }, // mescroll实例
downHight: 0, //下拉刷新: 容器高度
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
downLoadType: 4, // 下拉刷新状态 inOffset1 outOffset2 showLoading3 endDownScroll4
upLoadType: 0, // 上拉加载状态0loading前1loading中2没有更多了
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
windowHeight: 0, // 可使用窗口的高度
statusBarHeight: 0 // 状态栏高度
};
},
props: {
down: Object, // 下拉刷新的参数配置
up: Object, // 上拉加载的参数配置
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
topbar: Boolean, // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可自动加上状态栏高度的偏移量)
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
showTop: {
type: Boolean,
default: true
}
},
computed: {
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
minHeight() {
return this.toPx(this.height || '100%') + 'px';
},
// 下拉布局往下偏移的距离 (px)
numTop() {
return this.toPx(this.top) + (this.topbar ? this.statusBarHeight : 0);
},
padTop() {
return this.numTop + 'px';
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom);
},
padBottom() {
return this.numBottom + 'px';
},
padBottomConstant() {
return this.safearea ? 'calc(' + this.padBottom + ' + constant(safe-area-inset-bottom))' : this.padBottom;
},
padBottomEnv() {
return this.safearea ? 'calc(' + this.padBottom + ' + env(safe-area-inset-bottom))' : this.padBottom;
},
// 是否为重置下拉的状态
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4;
},
// 过渡
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
},
// 是否在加载中
isDownLoading() {
return this.downLoadType === 3;
},
// 旋转的角度
downRotate() {
return 'rotate(' + 360 * this.downRate + 'deg)';
},
// 文本提示
downText() {
switch (this.downLoadType) {
case 1:
return this.mescroll.optDown.textInOffset;
case 2:
return this.mescroll.optDown.textOutOffset;
case 3:
return this.mescroll.optDown.textLoading;
case 4:
return this.mescroll.optDown.textLoading;
default:
return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px的数值
toPx(num) {
if (typeof num === 'string') {
if (num.indexOf('px') !== -1) {
if (num.indexOf('rpx') !== -1) {
// "10rpx"
num = num.replace('rpx', '');
} else if (num.indexOf('upx') !== -1) {
// "10upx"
num = num.replace('upx', '');
} else {
// "10px"
return Number(num.replace('px', ''));
}
} else if (num.indexOf('%') !== -1) {
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
let rate = Number(num.replace('%', '')) / 100;
return this.windowHeight * rate;
}
}
return num ? uni.upx2px(Number(num)) : 0;
},
//注册列表touchstart事件,用于下拉刷新
touchstartEvent(e) {
this.mescroll.touchstartEvent(e);
},
//注册列表touchmove事件,用于下拉刷新
touchmoveEvent(e) {
this.mescroll.touchmoveEvent(e);
},
//注册列表touchend事件,用于下拉刷新
touchendEvent(e) {
this.mescroll.touchendEvent(e);
},
// 点击空布局的按钮回调
emptyClick() {
this.$emit('emptyclick', this.mescroll);
},
// 点击回到顶部的按钮回调
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
}
},
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
created() {
let vm = this;
let diyOption = {
// 下拉刷新的配置
down: {
inOffset(mescroll) {
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
},
outOffset(mescroll) {
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
},
onMoving(mescroll, rate, downHight) {
// 下拉过程中的回调,滑动过程一直在执行;
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
endDownScroll(mescroll) {
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
// 派发下拉刷新的回调
callback: function(mescroll) {
vm.$emit('down', mescroll);
}
},
// 上拉加载的配置
up: {
// 显示加载中的回调
showLoading() {
vm.upLoadType = 1;
},
// 显示无更多数据的回调
showNoMore() {
vm.upLoadType = 2;
},
// 隐藏上拉加载的回调
hideUpScroll() {
vm.upLoadType = 0;
},
// 空布局
empty: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowEmpty = isShow;
}
},
// 回到顶部
toTop: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowToTop = isShow;
}
},
// 派发上拉加载的回调
callback: function(mescroll) {
vm.$emit('up', mescroll);
}
}
};
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
let myOption = JSON.parse(
JSON.stringify({
down: vm.down,
up: vm.up
})
); // 深拷贝,避免对props的影响
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
// 初始化MeScroll对象
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
// init回调mescroll对象
vm.$emit('init', vm.mescroll);
// 设置高度
const sys = uni.getSystemInfoSync();
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使down的bottomOffset生效
vm.mescroll.setBodyHeight(sys.windowHeight);
// 因为使用的是page的scroll,这里需自定义scrollTo
vm.mescroll.resetScrollTo((y, t) => {
uni.pageScrollTo({
scrollTop: y,
duration: t
});
});
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {
} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
}
};
</script>
<style>
@import './mescroll-body.css';
@import './components/mescroll-down.css';
@import './components/mescroll-up.css';
</style>
<template>
<view
class="mescroll-body"
:style="{ minHeight: minHeight, 'padding-top': padTop, 'padding-bottom': padBottom, 'padding-bottom': padBottomConstant, 'padding-bottom': padBottomEnv }"
@touchstart="touchstartEvent"
@touchmove="touchmoveEvent"
@touchend="touchendEvent"
@touchcancel="touchendEvent"
>
<view class="mescroll-body-content mescroll-touch" :style="{ transform: translateY, transition: transition }">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp">
<view class="downwarp-content">
<view class="downwarp-progress" :class="{ 'mescroll-rotate': isDownLoading }" :style="{ transform: downRotate }"></view>
<view class="downwarp-tip">{{ downText }}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<!-- <mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> -->
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading" class="mescroll-upwarp">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType === 1">
<view class="upwarp-progress mescroll-rotate"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType === 2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick" v-if="showTop"></mescroll-top>
</view>
</template>
<script>
// 引入mescroll-uni.js,处理核心逻辑
import MeScroll from './mescroll-uni.js';
// 引入全局配置
import GlobalOption from './mescroll-uni-option.js';
// 引入空布局组件
import MescrollEmpty from './components/mescroll-empty.vue';
// 引入回到顶部组件
import MescrollTop from './components/mescroll-top.vue';
export default {
components: {
MescrollEmpty,
MescrollTop
},
data() {
return {
mescroll: { optDown: {}, optUp: {} }, // mescroll实例
downHight: 0, //下拉刷新: 容器高度
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
downLoadType: 4, // 下拉刷新状态 inOffset1 outOffset2 showLoading3 endDownScroll4
upLoadType: 0, // 上拉加载状态0loading前1loading中2没有更多了
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
windowHeight: 0, // 可使用窗口的高度
statusBarHeight: 0 // 状态栏高度
};
},
props: {
down: Object, // 下拉刷新的参数配置
up: Object, // 上拉加载的参数配置
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
topbar: Boolean, // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可自动加上状态栏高度的偏移量)
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
showTop: {
type: Boolean,
default: true
}
},
computed: {
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
minHeight() {
return this.toPx(this.height || '100%') + 'px';
},
// 下拉布局往下偏移的距离 (px)
numTop() {
return this.toPx(this.top) + (this.topbar ? this.statusBarHeight : 0);
},
padTop() {
return this.numTop + 'px';
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom);
},
padBottom() {
return this.numBottom + 'px';
},
padBottomConstant() {
return this.safearea ? 'calc(' + this.padBottom + ' + constant(safe-area-inset-bottom))' : this.padBottom;
},
padBottomEnv() {
return this.safearea ? 'calc(' + this.padBottom + ' + env(safe-area-inset-bottom))' : this.padBottom;
},
// 是否为重置下拉的状态
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4;
},
// 过渡
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
},
// 是否在加载中
isDownLoading() {
return this.downLoadType === 3;
},
// 旋转的角度
downRotate() {
return 'rotate(' + 360 * this.downRate + 'deg)';
},
// 文本提示
downText() {
switch (this.downLoadType) {
case 1:
return this.mescroll.optDown.textInOffset;
case 2:
return this.mescroll.optDown.textOutOffset;
case 3:
return this.mescroll.optDown.textLoading;
case 4:
return this.mescroll.optDown.textLoading;
default:
return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px的数值
toPx(num) {
if (typeof num === 'string') {
if (num.indexOf('px') !== -1) {
if (num.indexOf('rpx') !== -1) {
// "10rpx"
num = num.replace('rpx', '');
} else if (num.indexOf('upx') !== -1) {
// "10upx"
num = num.replace('upx', '');
} else {
// "10px"
return Number(num.replace('px', ''));
}
} else if (num.indexOf('%') !== -1) {
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
let rate = Number(num.replace('%', '')) / 100;
return this.windowHeight * rate;
}
}
return num ? uni.upx2px(Number(num)) : 0;
},
//注册列表touchstart事件,用于下拉刷新
touchstartEvent(e) {
this.mescroll.touchstartEvent(e);
},
//注册列表touchmove事件,用于下拉刷新
touchmoveEvent(e) {
this.mescroll.touchmoveEvent(e);
},
//注册列表touchend事件,用于下拉刷新
touchendEvent(e) {
this.mescroll.touchendEvent(e);
},
// 点击空布局的按钮回调
emptyClick() {
this.$emit('emptyclick', this.mescroll);
},
// 点击回到顶部的按钮回调
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
}
},
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
created() {
let vm = this;
let diyOption = {
// 下拉刷新的配置
down: {
inOffset(mescroll) {
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
},
outOffset(mescroll) {
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
},
onMoving(mescroll, rate, downHight) {
// 下拉过程中的回调,滑动过程一直在执行;
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
endDownScroll(mescroll) {
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
// 派发下拉刷新的回调
callback: function(mescroll) {
vm.$emit('down', mescroll);
}
},
// 上拉加载的配置
up: {
// 显示加载中的回调
showLoading() {
vm.upLoadType = 1;
},
// 显示无更多数据的回调
showNoMore() {
vm.upLoadType = 2;
},
// 隐藏上拉加载的回调
hideUpScroll() {
vm.upLoadType = 0;
},
// 空布局
empty: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowEmpty = isShow;
}
},
// 回到顶部
toTop: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowToTop = isShow;
}
},
// 派发上拉加载的回调
callback: function(mescroll) {
vm.$emit('up', mescroll);
}
}
};
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
let myOption = JSON.parse(
JSON.stringify({
down: vm.down,
up: vm.up
})
); // 深拷贝,避免对props的影响
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
// 初始化MeScroll对象
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
// init回调mescroll对象
vm.$emit('init', vm.mescroll);
// 设置高度
const sys = uni.getSystemInfoSync();
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使down的bottomOffset生效
vm.mescroll.setBodyHeight(sys.windowHeight);
// 因为使用的是page的scroll,这里需自定义scrollTo
vm.mescroll.resetScrollTo((y, t) => {
uni.pageScrollTo({
scrollTop: y,
duration: t
});
});
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {
} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
}
};
</script>
<style lang="scss">
@import './mescroll-body.css';
@import './components/mescroll-down.css';
@import './components/mescroll-up.css';
</style>

View File

@@ -1,429 +1,429 @@
<template>
<view class="mescroll-uni-warp">
<scroll-view :id="viewId" class="mescroll-uni" :class="{ 'mescroll-uni-fixed': isFixed }" :style="{
height: scrollHeight,
'padding-top': padTop,
'padding-bottom': padBottom,
'padding-bottom': padBottomConstant,
'padding-bottom': padBottomEnv,
top: fixedTop,
bottom: fixedBottom,
bottom: fixedBottomConstant,
bottom: fixedBottomEnv,
background: background,
'padding-left': paddingBoth,
'padding-right': paddingBoth
}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" @touchstart="touchstartEvent"
@touchmove="touchmoveEvent" @touchend="touchendEvent" @touchcancel="touchendEvent" :scroll-y="isDownReset"
:enable-back-to-top="true">
<view class="mescroll-uni-content" :style="{ transform: translateY, transition: transition }">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp">
<view class="downwarp-content">
<!-- <view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'transform': downRotate}"></view> -->
<view class="downwarp-tip">{{ downText }}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<!-- <mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> -->
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading" class="mescroll-upwarp">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType === 1">
<!-- <view class="upwarp-progress mescroll-rotate"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> -->
<ns-loading></ns-loading>
</view>
<!-- 无数据 -->
<view v-if="upLoadType === 2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
</scroll-view>
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
<mescroll-top v-if="showTop" v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
</view>
</template>
<script>
// 引入mescroll-uni.js,处理核心逻辑
import MeScroll from './mescroll-uni.js';
// 引入全局配置
import GlobalOption from './mescroll-uni-option.js';
// 引入空布局组件
import MescrollEmpty from './components/mescroll-empty.vue';
// 引入回到顶部组件
import MescrollTop from './components/mescroll-top.vue';
import nsLoading from '@/components/ns-loading/ns-loading.vue';
export default {
name: 'mescroll-uni',
components: {
MescrollEmpty,
MescrollTop
},
props: {
down: Object, // 下拉刷新的参数配置
up: Object, // 上拉加载的参数配置
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
topbar: Boolean, // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可自动加上状态栏高度的偏移量)
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
fixed: {
// 是否通过fixed固定mescroll的高度, 默认true
type: Boolean,
default () {
return true;
}
},
height: [String,
Number
], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
showTop: {
//是否需要展示返回顶部按钮
type: Boolean,
default () {
return true;
}
},
background: String,
paddingBoth: {
type: [String, Number],
default () {
return 0;
}
}
},
data() {
return {
mescroll: {
optDown: {},
optUp: {}
}, // mescroll实例
viewId: 'id_' +
Math.random()
.toString(36)
.substr(2), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
downHight: 0, //下拉刷新: 容器高度
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
downLoadType: 4, // 下拉刷新状态 inOffset1 outOffset2 showLoading3 endDownScroll4
upLoadType: 0, // 上拉加载状态0loading前1loading中2没有更多了
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
scrollTop: 0, // 滚动条的位置
scrollAnim: false, // 是否开启滚动动画
windowTop: 0, // 可使用窗口的顶部位置
windowBottom: 0, // 可使用窗口的底部位置
windowHeight: 0, // 可使用窗口的高度
statusBarHeight: 0 // 状态栏高度
};
},
computed: {
// 是否使用fixed定位 (当height有值,则不使用)
isFixed() {
return !this.height && this.fixed;
},
// mescroll的高度
scrollHeight() {
if (this.isFixed) {
return 'auto';
} else if (this.height) {
return this.toPx(this.height) + 'px';
} else {
return '100%';
}
},
// 下拉布局往下偏移的距离 (px)
numTop() {
// #ifdef H5
return this.toPx(this.top) + (this.topbar ? this.statusBarHeight : 0);
// #endif
// #ifdef MP
return this.toPx(this.top) + (this.topbar ? 44 + this.statusBarHeight : 0);
// #endif
},
fixedTop() {
return this.isFixed ? this.numTop + this.windowTop + 'px' : 0;
},
padTop() {
return !this.isFixed ? this.numTop + 'px' : 0;
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom);
},
fixedBottom() {
return this.isFixed ? this.numBottom + this.windowBottom + 'px' : 0;
},
fixedBottomConstant() {
return this.safearea ? 'calc(' + this.fixedBottom + ' + constant(safe-area-inset-bottom))' : this
.fixedBottom;
},
fixedBottomEnv() {
return this.safearea ? 'calc(' + this.fixedBottom + ' + env(safe-area-inset-bottom))' : this.fixedBottom;
},
padBottom() {
return !this.isFixed ? this.numBottom + 'px' : 0;
},
padBottomConstant() {
return this.safearea ? 'calc(' + this.padBottom + ' + constant(safe-area-inset-bottom))' : this.padBottom;
},
padBottomEnv() {
return this.safearea ? 'calc(' + this.padBottom + ' + env(safe-area-inset-bottom))' : this.padBottom;
},
// 是否为重置下拉的状态
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4;
},
// 过渡
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' :
''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
},
// 是否在加载中
isDownLoading() {
return this.downLoadType === 3;
},
// 旋转的角度
downRotate() {
return 'rotate(' + 360 * this.downRate + 'deg)';
},
// 文本提示
downText() {
switch (this.downLoadType) {
case 1:
return this.mescroll.optDown.textInOffset;
case 2:
return this.mescroll.optDown.textOutOffset;
case 3:
return this.mescroll.optDown.textLoading;
case 4:
return this.mescroll.optDown.textLoading;
default:
return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px的数值
toPx(num) {
if (typeof num === 'string') {
if (num.indexOf('px') !== -1) {
if (num.indexOf('rpx') !== -1) {
// "10rpx"
num = num.replace('rpx', '');
} else if (num.indexOf('upx') !== -1) {
// "10upx"
num = num.replace('upx', '');
} else {
// "10px"
return Number(num.replace('px', ''));
}
} else if (num.indexOf('%') !== -1) {
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
let rate = Number(num.replace('%', '')) / 100;
return this.windowHeight * rate;
}
}
return num ? uni.upx2px(Number(num)) : 0;
},
//注册列表滚动事件,用于下拉刷新和上拉加载
scroll(e) {
this.mescroll.scroll(e.detail, () => {
this.$emit('scroll', this
.mescroll); // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
});
},
//注册列表touchstart事件,用于下拉刷新
touchstartEvent(e) {
this.mescroll.touchstartEvent(e);
},
//注册列表touchmove事件,用于下拉刷新
touchmoveEvent(e) {
this.mescroll.touchmoveEvent(e);
},
//注册列表touchend事件,用于下拉刷新
touchendEvent(e) {
this.mescroll.touchendEvent(e);
},
// 点击空布局的按钮回调
emptyClick() {
this.$emit('emptyclick', this.mescroll);
},
// 点击回到顶部的按钮回调
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
},
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
setClientHeight() {
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
this.isExec = true; // 避免多次获取
this.$nextTick(() => {
// 确保dom已渲染
try {
let view = uni
.createSelectorQuery()
.in(this)
.select('#' + this.viewId);
view.boundingClientRect(data => {
this.isExec = false;
if (data) {
this.mescroll.setClientHeight(data.height);
} else if (this.clientNum != 3) {
// 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
setTimeout(() => {
this.setClientHeight();
}, this.clientNum * 100);
}
}).exec();
} catch (e) {}
});
}
}
},
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
created() {
let vm = this;
let diyOption = {
// 下拉刷新的配置
down: {
inOffset(mescroll) {
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
},
outOffset(mescroll) {
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
},
onMoving(mescroll, rate, downHight) {
// 下拉过程中的回调,滑动过程一直在执行;
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
endDownScroll(mescroll) {
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
// 派发下拉刷新的回调
callback: function(mescroll) {
vm.$emit('down', mescroll);
}
},
// 上拉加载的配置
up: {
// 显示加载中的回调
showLoading() {
vm.upLoadType = 1;
},
// 显示无更多数据的回调
showNoMore() {
vm.upLoadType = 2;
},
// 隐藏上拉加载的回调
hideUpScroll() {
vm.upLoadType = 0;
},
// 空布局
empty: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowEmpty = isShow;
}
},
// 回到顶部
toTop: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowToTop = isShow;
}
},
// 派发上拉加载的回调
callback: function(mescroll) {
vm.$emit('up', mescroll);
// 更新容器的高度 (多mescroll的情况)
vm.setClientHeight();
}
}
};
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
let myOption = JSON.parse(
JSON.stringify({
down: vm.down,
up: vm.up
})
); // 深拷贝,避免对props的影响
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
// 初始化MeScroll对象
vm.mescroll = new MeScroll(myOption);
vm.mescroll.viewId = vm.viewId; // 附带id
// init回调mescroll对象
vm.$emit('init', vm.mescroll);
// 设置高度
const sys = uni.getSystemInfoSync();
if (sys.windowTop) vm.windowTop = sys.windowTop;
if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使down的bottomOffset生效
vm.mescroll.setBodyHeight(sys.windowHeight);
// 因为使用的是scrollview,这里需自定义scrollTo
vm.mescroll.resetScrollTo((y, t) => {
let curY = vm.mescroll.getScrollTop();
vm.scrollAnim = t !== 0; // t为0,则不使用动画过渡
if (t === 0 || t === 300) {
// 当t使用默认配置的300时,则使用系统自带的动画过渡
vm.scrollTop = curY;
vm.$nextTick(function() {
vm.scrollTop = y;
});
} else {
vm.mescroll.getStep(
curY,
y,
step => {
// 此写法可支持配置t
vm.scrollTop = step;
},
t
);
}
});
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
},
mounted() {
// 设置容器的高度
this.setClientHeight();
}
};
</script>
<style>
@import './mescroll-uni.css';
@import './components/mescroll-down.css';
@import './components/mescroll-up.css';
<template>
<view class="mescroll-uni-warp">
<scroll-view :id="viewId" class="mescroll-uni" :class="{ 'mescroll-uni-fixed': isFixed }" :style="{
height: scrollHeight,
'padding-top': padTop,
'padding-bottom': padBottom,
'padding-bottom': padBottomConstant,
'padding-bottom': padBottomEnv,
top: fixedTop,
bottom: fixedBottom,
bottom: fixedBottomConstant,
bottom: fixedBottomEnv,
background: background,
'padding-left': paddingBoth,
'padding-right': paddingBoth
}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" @touchstart="touchstartEvent"
@touchmove="touchmoveEvent" @touchend="touchendEvent" @touchcancel="touchendEvent" :scroll-y="isDownReset"
:enable-back-to-top="true">
<view class="mescroll-uni-content" :style="{ transform: translateY, transition: transition }">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp">
<view class="downwarp-content">
<!-- <view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'transform': downRotate}"></view> -->
<view class="downwarp-tip">{{ downText }}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<!-- <mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> -->
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading" class="mescroll-upwarp">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType === 1">
<!-- <view class="upwarp-progress mescroll-rotate"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> -->
<ns-loading></ns-loading>
</view>
<!-- 无数据 -->
<view v-if="upLoadType === 2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
</scroll-view>
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
<mescroll-top v-if="showTop" v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
</view>
</template>
<script>
// 引入mescroll-uni.js,处理核心逻辑
import MeScroll from './mescroll-uni.js';
// 引入全局配置
import GlobalOption from './mescroll-uni-option.js';
// 引入空布局组件
import MescrollEmpty from './components/mescroll-empty.vue';
// 引入回到顶部组件
import MescrollTop from './components/mescroll-top.vue';
import nsLoading from '@/components/ns-loading/ns-loading.vue';
export default {
name: 'mescroll-uni',
components: {
MescrollEmpty,
MescrollTop
},
props: {
down: Object, // 下拉刷新的参数配置
up: Object, // 上拉加载的参数配置
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
topbar: Boolean, // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可自动加上状态栏高度的偏移量)
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
fixed: {
// 是否通过fixed固定mescroll的高度, 默认true
type: Boolean,
default () {
return true;
}
},
height: [String,
Number
], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
showTop: {
//是否需要展示返回顶部按钮
type: Boolean,
default () {
return true;
}
},
background: String,
paddingBoth: {
type: [String, Number],
default () {
return 0;
}
}
},
data() {
return {
mescroll: {
optDown: {},
optUp: {}
}, // mescroll实例
viewId: 'id_' +
Math.random()
.toString(36)
.substr(2), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
downHight: 0, //下拉刷新: 容器高度
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
downLoadType: 4, // 下拉刷新状态 inOffset1 outOffset2 showLoading3 endDownScroll4
upLoadType: 0, // 上拉加载状态0loading前1loading中2没有更多了
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
scrollTop: 0, // 滚动条的位置
scrollAnim: false, // 是否开启滚动动画
windowTop: 0, // 可使用窗口的顶部位置
windowBottom: 0, // 可使用窗口的底部位置
windowHeight: 0, // 可使用窗口的高度
statusBarHeight: 0 // 状态栏高度
};
},
computed: {
// 是否使用fixed定位 (当height有值,则不使用)
isFixed() {
return !this.height && this.fixed;
},
// mescroll的高度
scrollHeight() {
if (this.isFixed) {
return 'auto';
} else if (this.height) {
return this.toPx(this.height) + 'px';
} else {
return '100%';
}
},
// 下拉布局往下偏移的距离 (px)
numTop() {
// #ifdef H5
return this.toPx(this.top) + (this.topbar ? this.statusBarHeight : 0);
// #endif
// #ifdef MP
return this.toPx(this.top) + (this.topbar ? 44 + this.statusBarHeight : 0);
// #endif
},
fixedTop() {
return this.isFixed ? this.numTop + this.windowTop + 'px' : 0;
},
padTop() {
return !this.isFixed ? this.numTop + 'px' : 0;
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom);
},
fixedBottom() {
return this.isFixed ? this.numBottom + this.windowBottom + 'px' : 0;
},
fixedBottomConstant() {
return this.safearea ? 'calc(' + this.fixedBottom + ' + constant(safe-area-inset-bottom))' : this
.fixedBottom;
},
fixedBottomEnv() {
return this.safearea ? 'calc(' + this.fixedBottom + ' + env(safe-area-inset-bottom))' : this.fixedBottom;
},
padBottom() {
return !this.isFixed ? this.numBottom + 'px' : 0;
},
padBottomConstant() {
return this.safearea ? 'calc(' + this.padBottom + ' + constant(safe-area-inset-bottom))' : this.padBottom;
},
padBottomEnv() {
return this.safearea ? 'calc(' + this.padBottom + ' + env(safe-area-inset-bottom))' : this.padBottom;
},
// 是否为重置下拉的状态
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4;
},
// 过渡
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' :
''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
},
// 是否在加载中
isDownLoading() {
return this.downLoadType === 3;
},
// 旋转的角度
downRotate() {
return 'rotate(' + 360 * this.downRate + 'deg)';
},
// 文本提示
downText() {
switch (this.downLoadType) {
case 1:
return this.mescroll.optDown.textInOffset;
case 2:
return this.mescroll.optDown.textOutOffset;
case 3:
return this.mescroll.optDown.textLoading;
case 4:
return this.mescroll.optDown.textLoading;
default:
return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px的数值
toPx(num) {
if (typeof num === 'string') {
if (num.indexOf('px') !== -1) {
if (num.indexOf('rpx') !== -1) {
// "10rpx"
num = num.replace('rpx', '');
} else if (num.indexOf('upx') !== -1) {
// "10upx"
num = num.replace('upx', '');
} else {
// "10px"
return Number(num.replace('px', ''));
}
} else if (num.indexOf('%') !== -1) {
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
let rate = Number(num.replace('%', '')) / 100;
return this.windowHeight * rate;
}
}
return num ? uni.upx2px(Number(num)) : 0;
},
//注册列表滚动事件,用于下拉刷新和上拉加载
scroll(e) {
this.mescroll.scroll(e.detail, () => {
this.$emit('scroll', this
.mescroll); // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
});
},
//注册列表touchstart事件,用于下拉刷新
touchstartEvent(e) {
this.mescroll.touchstartEvent(e);
},
//注册列表touchmove事件,用于下拉刷新
touchmoveEvent(e) {
this.mescroll.touchmoveEvent(e);
},
//注册列表touchend事件,用于下拉刷新
touchendEvent(e) {
this.mescroll.touchendEvent(e);
},
// 点击空布局的按钮回调
emptyClick() {
this.$emit('emptyclick', this.mescroll);
},
// 点击回到顶部的按钮回调
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
},
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
setClientHeight() {
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
this.isExec = true; // 避免多次获取
this.$nextTick(() => {
// 确保dom已渲染
try {
let view = uni
.createSelectorQuery()
.in(this)
.select('#' + this.viewId);
view.boundingClientRect(data => {
this.isExec = false;
if (data) {
this.mescroll.setClientHeight(data.height);
} else if (this.clientNum != 3) {
// 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
setTimeout(() => {
this.setClientHeight();
}, this.clientNum * 100);
}
}).exec();
} catch (e) {}
});
}
}
},
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
created() {
let vm = this;
let diyOption = {
// 下拉刷新的配置
down: {
inOffset(mescroll) {
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
},
outOffset(mescroll) {
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
},
onMoving(mescroll, rate, downHight) {
// 下拉过程中的回调,滑动过程一直在执行;
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
endDownScroll(mescroll) {
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
// 派发下拉刷新的回调
callback: function(mescroll) {
vm.$emit('down', mescroll);
}
},
// 上拉加载的配置
up: {
// 显示加载中的回调
showLoading() {
vm.upLoadType = 1;
},
// 显示无更多数据的回调
showNoMore() {
vm.upLoadType = 2;
},
// 隐藏上拉加载的回调
hideUpScroll() {
vm.upLoadType = 0;
},
// 空布局
empty: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowEmpty = isShow;
}
},
// 回到顶部
toTop: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowToTop = isShow;
}
},
// 派发上拉加载的回调
callback: function(mescroll) {
vm.$emit('up', mescroll);
// 更新容器的高度 (多mescroll的情况)
vm.setClientHeight();
}
}
};
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
let myOption = JSON.parse(
JSON.stringify({
down: vm.down,
up: vm.up
})
); // 深拷贝,避免对props的影响
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
// 初始化MeScroll对象
vm.mescroll = new MeScroll(myOption);
vm.mescroll.viewId = vm.viewId; // 附带id
// init回调mescroll对象
vm.$emit('init', vm.mescroll);
// 设置高度
const sys = uni.getSystemInfoSync();
if (sys.windowTop) vm.windowTop = sys.windowTop;
if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使down的bottomOffset生效
vm.mescroll.setBodyHeight(sys.windowHeight);
// 因为使用的是scrollview,这里需自定义scrollTo
vm.mescroll.resetScrollTo((y, t) => {
let curY = vm.mescroll.getScrollTop();
vm.scrollAnim = t !== 0; // t为0,则不使用动画过渡
if (t === 0 || t === 300) {
// 当t使用默认配置的300时,则使用系统自带的动画过渡
vm.scrollTop = curY;
vm.$nextTick(function() {
vm.scrollTop = y;
});
} else {
vm.mescroll.getStep(
curY,
y,
step => {
// 此写法可支持配置t
vm.scrollTop = step;
},
t
);
}
});
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
},
mounted() {
// 设置容器的高度
this.setClientHeight();
}
};
</script>
<style lang="scss">
@import './mescroll-uni.css';
@import './components/mescroll-down.css';
@import './components/mescroll-up.css';
</style>