Files
mini-yu/uni_modules/tdesign-uniapp/components/pull-down-refresh/pull-down-refresh.vue
lingxiao865 c5af079d8c first commit
2026-02-10 08:05:03 +08:00

367 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<scroll-view
:style="tools._style([customStyle, 'max-height: calc(100vh - ' + distanceTop + 'px)'])"
:class="classPrefix + ' ' + tClass"
type="list"
:scroll-top="scrollTop"
scroll-y
:enable-back-to-top="enableBackToTop"
:enable-passive="enablePassive"
:lower-threshold="lowerThreshold"
:upper-threshold="upperThreshold"
:scroll-into-view="scrollIntoView"
:show-scrollbar="showScrollbar"
enhanced
scroll-with-animation
:bounces="false"
:throttle="false"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
@scroll="onScroll"
@scrolltoupper="onScrollToTop"
@scrolltolower="onScrollToBottom"
>
<slot name="header" />
<view
:class="classPrefix + '__track ' + (classPrefix + '__track--' + (loosing ? 'loosing' : ''))"
:style="barHeight > 0 ? 'transform: translate3d(0, ' + barHeight + 'px, 0);' : ''"
>
<view
:class="classPrefix + '__tips ' + (classPrefix + '__tips--' + (loosing ? 'loosing' : ''))"
:style="'height: ' + tipsHeight + 'px'"
aria-live="polite"
>
<t-loading
v-if="refreshStatus === REFRESH_STATUS_MAP.LOADING"
:delay="loadingProps.delay || 0"
:duration="loadingProps.duration || 800"
:indicator="loadingProps.indicator || true"
:layout="loadingProps.layout || 'horizontal'"
:loading="loadingProps.loading || true"
:pause="loadingProps.pause || false"
:progress="loadingProps.progress || 0"
:reverse="loadingProps.reverse || false"
:size="loadingProps.size || '50rpx'"
:text="loadingProps.text || dataLoadingTexts[refreshStatus]"
:theme="loadingProps.theme || 'circular'"
:t-class-indicator="tClassIndicator"
:t-class="tClassLoading"
/>
<view
v-else-if="refreshStatus > REFRESH_STATUS_MAP.INITIAL"
:class="classPrefix + '__text ' + tClassText"
>
{{ dataLoadingTexts[refreshStatus] }}
</view>
</view>
<slot />
</view>
</scroll-view>
</template>
<script>
import TLoading from '../loading/loading';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import { getRect, systemInfo, unitConvert } from '../common/utils';
import tools from '../common/utils.wxs';
import { getObserver } from '../common/wechat';
import { ParentMixin, RELATION_MAP } from '../common/relation';
const name = `${prefix}-pull-down-refresh`;
const defaultLoadingTexts = ['下拉刷新', '松手刷新', '正在刷新', '刷新完成'];
const REFRESH_STATUS_MAP = {
INITIAL: -1,
PULLING: 0,
LOSING: 1,
LOADING: 2,
SUCCESS: 3,
};
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
`${prefix}-class-loading`,
`${prefix}-class-text`,
`${prefix}-class-indicator`,
],
mixins: [
ParentMixin(RELATION_MAP.BackTop),
],
components: {
TLoading,
},
props: {
...props,
},
emits: [
'scrolltolower',
'scroll',
'change',
'refresh',
'dragstart',
'dragging',
'dragend',
'timeout',
],
data() {
return {
prefix,
classPrefix: name,
distanceTop: 0,
barHeight: 0,
tipsHeight: 0,
refreshStatus: REFRESH_STATUS_MAP.INITIAL,
loosing: false,
enableToRefresh: true,
scrollTop: 0,
_maxBarHeight: 0,
_loadingBarHeight: 0,
pixelRatio: 1,
startPoint: null,
isPulling: false,
maxRefreshAnimateTimeFlag: 0,
closingAnimateTimeFlag: 0,
refreshStatusTimer: null,
tools,
REFRESH_STATUS_MAP,
};
},
computed: {
touchEnable() {
return this.refreshStatus !== REFRESH_STATUS_MAP.LOADING
&& this.refreshStatus !== REFRESH_STATUS_MAP.SUCCESS
&& !this.disabled;
},
},
watch: {
value(val) {
if (!val) {
clearTimeout(this.maxRefreshAnimateTimeFlag);
if (this.refreshStatus > REFRESH_STATUS_MAP.PULLING) {
this.refreshStatus = REFRESH_STATUS_MAP.SUCCESS;
}
clearTimeout(this.successTimer);
this.successTimer = setTimeout(() => {
this.barHeight = 0;
this.refreshStatus = REFRESH_STATUS_MAP.INITIAL;
}, unitConvert(this.successDuration));
} else {
this.doRefresh();
}
},
barHeight(val) {
this.resetTimer();
if (val === 0 && this.refreshStatus !== REFRESH_STATUS_MAP.INITIAL) {
this.refreshStatusTimer = setTimeout(() => {
this.refreshStatus = REFRESH_STATUS_MAP.INITIAL;
}, 240);
}
this.tipsHeight = Math.min(val, this._loadingBarHeight);
},
maxBarHeight(v) {
this._maxBarHeight = unitConvert(v);
},
loadingBarHeight(v) {
this._loadingBarHeight = unitConvert(v);
},
},
mounted() {
const { screenWidth } = systemInfo;
const { loadingTexts, maxBarHeight, loadingBarHeight } = this;
const isCustomLoadingTexts = Array.isArray(loadingTexts) && loadingTexts.length >= 4;
this._maxBarHeight = unitConvert(maxBarHeight);
this._loadingBarHeight = unitConvert(loadingBarHeight);
this.dataLoadingTexts = isCustomLoadingTexts ? loadingTexts : defaultLoadingTexts;
this.pixelRatio = 750 / screenWidth;
this.updateDistanceTop();
},
beforeUnMount() {
clearTimeout(this.maxRefreshAnimateTimeFlag);
clearTimeout(this.closingAnimateTimeFlag);
this.resetTimer();
},
methods: {
updateDistanceTop() {
const update = (top) => {
this.distanceTop = top;
};
getRect(this, `.${name}`).then((rect) => {
if (rect.top) {
update(rect.top);
return;
}
getObserver(this, `.${name}`).then((res) => {
if (res.intersectionRatio > 0) {
update(res.boundingClientRect.top);
}
});
});
},
resetTimer() {
if (this.refreshStatusTimer) {
clearTimeout(this.refreshStatusTimer);
this.refreshStatusTimer = null;
}
},
onScrollToBottom() {
this.$emit('scrolltolower');
},
onScrollToTop() {
this.enableToRefresh = true;
},
onScroll(e) {
const { scrollTop } = e.detail;
this.enableToRefresh = scrollTop === 0;
this.$emit('scroll', { scrollTop });
},
onTouchStart(e) {
if (this.isPulling || !this.enableToRefresh || !this.touchEnable) return;
const { touches } = e;
this.$emit('dragstart', e);
if (touches.length !== 1) return;
const { pageX, pageY } = touches[0];
this.loosing = false;
this.startPoint = { pageX, pageY };
this.isPulling = true;
},
onTouchMove(e) {
if (!this.startPoint || !this.touchEnable) return;
const { touches } = e;
if (touches.length !== 1) return;
const { pageY } = touches[0];
const offset = pageY - this.startPoint.pageY;
if (offset > 0) {
this.setRefreshBarHeight(offset);
}
this.$emit('dragging', e);
},
onTouchEnd(e) {
if (!this.startPoint || this.disabled) return;
const { changedTouches } = e;
if (changedTouches.length !== 1) return;
const { pageY } = changedTouches[0];
const barHeight = pageY - this.startPoint.pageY;
this.startPoint = null; // 清掉起点之后将忽略touchMove、touchEnd事件
this.isPulling = false;
this.loosing = true;
// 松开时高度超过阈值则触发刷新
if (barHeight > this._loadingBarHeight) {
this._trigger('change', { value: true });
this.$emit('refresh');
} else {
this.barHeight = 0;
}
this.$emit('dragend', e);
},
doRefresh() {
if (this.disabled) return;
this.barHeight = this._loadingBarHeight;
this.refreshStatus = REFRESH_STATUS_MAP.LOADING;
this.loosing = true;
this.maxRefreshAnimateTimeFlag = setTimeout(() => {
this.maxRefreshAnimateTimeFlag = null;
if (this.refreshStatus === REFRESH_STATUS_MAP.LOADING) {
// 超时回调
this.$emit('timeout');
this._trigger('change', { value: false });
}
}, this.refreshTimeout);
},
setRefreshBarHeight(value) {
const barHeight = Math.min(value, this._maxBarHeight);
const data = { barHeight };
if (barHeight >= this._loadingBarHeight) {
data.refreshStatus = REFRESH_STATUS_MAP.LOSING;
} else {
data.refreshStatus = REFRESH_STATUS_MAP.PULLING;
}
return new Promise((resolve) => {
Object.keys(data).forEach((key) => {
this[key] = data[key];
});
setTimeout(() => {
resolve(barHeight);
}, 20);
});
},
setScrollTop(scrollTop) {
this.scrollTop = scrollTop;
},
scrollToTop() {
let parsed = false;
// #ifdef APP-PLUS || MP
this.scrollTop = 0;
setTimeout(() => {
this.scrollTop = 0.01;
});
parsed = true;
// #endif
// #ifdef H5
// https://yuanbao.tencent.com/chat/naQivTmsDa/c10ae37f-c66f-4489-ac4e-e72710a3f65a
this.scrollTop = this.scrollTop === 0 ? 0.01 : 0;
parsed = true;
// #endif
if (!parsed) {
this.setScrollTop(0);
}
},
},
});
</script>
<style scoped>
@import './pull-down-refresh.css';
</style>