1
This commit is contained in:
@@ -0,0 +1,799 @@
|
||||
<template>
|
||||
<!-- #ifdef APP-HARMONY -->
|
||||
<view class="xVirtualList" ref="xVirtualList">
|
||||
<view >
|
||||
<!--
|
||||
@slot sticky插槽
|
||||
-->
|
||||
<slot name="sticky"></slot>
|
||||
</view>
|
||||
<scroll-view
|
||||
:show-scrollbar="false"
|
||||
type="nested"
|
||||
style="flex:1"
|
||||
>
|
||||
|
||||
<nested-scroll-header>
|
||||
<view>
|
||||
<!--
|
||||
@slot 顶部插槽,仅APP有效,它是nested-scroll-header协商节点,会自动与内容衔接在头上然后自动上往上推。
|
||||
-->
|
||||
<slot name="header"></slot>
|
||||
</view>
|
||||
</nested-scroll-header>
|
||||
<nested-scroll-body>
|
||||
<view style="flex:1">
|
||||
<scroll-view
|
||||
:show-scrollbar="false"
|
||||
@scroll="onScroll"
|
||||
:refresher-enabled="_refresherEnabled"
|
||||
refresher-default-style="none"
|
||||
:refresher-triggered="refresherTriggered"
|
||||
:scroll-top="scrollTopTop"
|
||||
@refresherpulling="refresherpulling"
|
||||
@refresherrefresh="refresherrefresh"
|
||||
@refresherrestore="refresherrestore"
|
||||
@refresherabort="refresherabort"
|
||||
@scrolltoupper="scrolltoupper"
|
||||
@scrolltolower="scrolltolower"
|
||||
direction="vertical"
|
||||
associative-container="nested-scroll-view"
|
||||
>
|
||||
|
||||
<!-- 自定义下拉刷新元素 -->
|
||||
<!-- #ifndef APP-HARMONY -->
|
||||
<view v-if="_refresherEnabled" @click="refreshOnload" slot="refresher" class="refresh-box" :style="{height:refreshHeight+'px'}">
|
||||
<x-icon :color="refresState==2?'primary':''" v-if="refresState==0||refresState==1||refresState==2||refresState==3" :duration="800" :spin="refresState==2" name="loader-line" font-size="26"></x-icon>
|
||||
<x-icon v-if="refresState==5" color="success" name="checkbox-circle-line" font-size="26"></x-icon>
|
||||
<x-icon v-if="refresState==4" color="error" name="error-warning-fill" font-size="26"></x-icon>
|
||||
<view class="tip-text" >
|
||||
<x-text font-size="16">{{refresStateMap.get(refresState)}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-HARMONY -->
|
||||
<view v-if="(refresState==5||refresState==4) && _refresherEnabled" @click="refreshOnload" class="refresh-box" :style="{height:refreshHeight+'px'}">
|
||||
<x-icon v-if="refresState==5" color="success" name="checkbox-circle-line" font-size="26"></x-icon>
|
||||
<x-icon v-if="refresState==4" color="error" name="error-warning-fill" font-size="26"></x-icon>
|
||||
<view class="tip-text" >
|
||||
<x-text font-size="16">{{refresStateMap.get(refresState)}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- 虚拟占位容器,用于撑开滚动高度 -->
|
||||
<view class="xVirtualListContainer" :style="{ height: totalHeight + 'px', paddingTop: offsetY + 'px' }">
|
||||
<!--
|
||||
@slot 内容插槽你在这里渲染内容
|
||||
@prop {any[]} data - list数据
|
||||
@prop {number} startIndex - 数据切割的起始位置
|
||||
@prop {number} endIndex - 数据切割的结束位置
|
||||
-->
|
||||
<slot name="default" :data="renderList" :startIndex="startIndex" :endIndex="endIndex"></slot>
|
||||
</view>
|
||||
<!-- 自定义下拉刷新元素 -->
|
||||
<view @click="refreshBottomOnload" v-if="_refresherBottomEnabled&&refresBottomState!=0" class="refresh-box" :style="{height:refreshHeight+'px'}">
|
||||
<x-icon color="primary" v-if="refresBottomState==1" :spin="true" name="loader-line" font-size="26"></x-icon>
|
||||
<x-icon v-if="refresBottomState==2" color="error" name="error-warning-fill" font-size="26"></x-icon>
|
||||
<view class="tip-text" >
|
||||
<x-text font-size="16">{{bottomRefresStateMap.get(refresBottomState)}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
<!--
|
||||
@slot 底部插槽
|
||||
-->
|
||||
<slot name="bottom"></slot>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</nested-scroll-body>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef MP||WEB||APP-IOS||APP-ANDROID -->
|
||||
<view class="xVirtualList" ref="xVirtualList" >
|
||||
|
||||
<view >
|
||||
<!--
|
||||
@slot sticky插槽
|
||||
-->
|
||||
<slot name="sticky"></slot>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
:show-scrollbar="false"
|
||||
@scroll="onScroll"
|
||||
class="xVirtualListScrollBody"
|
||||
:refresher-enabled="_refresherEnabled"
|
||||
refresher-default-style="none"
|
||||
:scroll-y="true"
|
||||
:refresher-triggered="refresherTriggered"
|
||||
:scroll-top="scrollTopTop"
|
||||
:style="{ height: containerHeight + 'px' }"
|
||||
@refresherpulling="refresherpulling"
|
||||
@refresherrefresh="refresherrefresh"
|
||||
@refresherrestore="refresherrestore"
|
||||
@refresherabort="refresherabort"
|
||||
@scrolltoupper="scrolltoupper"
|
||||
@scrolltolower="scrolltolower"
|
||||
>
|
||||
|
||||
<!-- 自定义下拉刷新元素 -->
|
||||
<view @click="refreshOnload" slot="refresher" class="refresh-box" :style="{height:refreshHeight+'px'}">
|
||||
<x-icon :color="refresState==2?'primary':''" v-if="refresState==0||refresState==1||refresState==2||refresState==3" :duration="800" :spin="refresState==2" name="loader-line" font-size="26"></x-icon>
|
||||
<x-icon v-if="refresState==5" color="success" name="checkbox-circle-line" font-size="26"></x-icon>
|
||||
<x-icon v-if="refresState==4" color="error" name="error-warning-fill" font-size="26"></x-icon>
|
||||
<view class="tip-text" >
|
||||
<x-text font-size="16">{{refresStateMap.get(refresState)}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 虚拟占位容器,用于撑开滚动高度 -->
|
||||
<view class="xVirtualListContainer" :style="{ height: totalHeight + 'px', paddingTop: offsetY + 'px' }">
|
||||
|
||||
<!--
|
||||
@slot 内容插槽你在这里渲染内容
|
||||
@prop {any[]} data - list数据
|
||||
@prop {number} startIndex - 数据切割的起始位置
|
||||
@prop {number} endIndex - 数据切割的结束位置
|
||||
-->
|
||||
<slot name="default" :data="renderList" :startIndex="startIndex" :endIndex="endIndex"></slot>
|
||||
</view>
|
||||
<!-- 自定义下拉刷新元素 -->
|
||||
<view @click="refreshBottomOnload" v-if="_refresherBottomEnabled&&refresBottomState!=0" class="refresh-box" :style="{height:refreshHeight+'px'}">
|
||||
<x-icon color="primary" v-if="refresBottomState==1" :spin="true" name="loader-line" font-size="26"></x-icon>
|
||||
<x-icon v-if="refresBottomState==2" color="error" name="error-warning-fill" font-size="26"></x-icon>
|
||||
<view class="tip-text" >
|
||||
<x-text font-size="16">{{bottomRefresStateMap.get(refresBottomState)}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
<!--
|
||||
@slot 底部插槽
|
||||
-->
|
||||
<slot name="bottom"></slot>
|
||||
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- #endif -->
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCurrentInstance, ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
const i18n = xConfig.i18n;
|
||||
/**
|
||||
* 虚拟列表组件属性类型定义
|
||||
*/
|
||||
type xVirtualListType = {
|
||||
/** 项目高度,不可用auto,只能是数字(默认为px),带单位的rpx,px */
|
||||
itemHeight: string,
|
||||
/** 渲染显示多少个,它不是最终的渲染数量,是最少数量,因为内部还有个缓存数量 */
|
||||
viewCount: number,
|
||||
/** 数据列表 */
|
||||
list: any[],
|
||||
/** 缓冲区大小,在可视区域前后额外渲染的项目数量 */
|
||||
bufferSize: number,
|
||||
/** 是否启用虚拟滚动 */
|
||||
enableVirtual: boolean,
|
||||
/** 启用下拉刷新 */
|
||||
refresherEnabled: boolean,
|
||||
/** 需要自行管理下拉状态,默认为false,通过事件pull触发设置为true,结束设置为False */
|
||||
pullState:boolean,
|
||||
/** 启用触底刷新 */
|
||||
refresherBottomEnabled: boolean,
|
||||
/** 需要自行管理下拉状态,默认为false,通过事件bottom触发设置为true,结束设置为False */
|
||||
bottomState:boolean,
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 容器尺寸类型定义
|
||||
*/
|
||||
type Bounds = {
|
||||
width: number,
|
||||
height: number,
|
||||
top: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动容器尺寸类型定义
|
||||
*/
|
||||
type ScrollContainerBounds = {
|
||||
width: number,
|
||||
height: number,
|
||||
scrollHeight: number,
|
||||
scrollTop: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟列表渲染范围类型定义
|
||||
*/
|
||||
type RenderRange = {
|
||||
startIndex: number,
|
||||
endIndex: number,
|
||||
offsetY: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新状态
|
||||
* 0,'继续下拉刷新'
|
||||
* 1,'松开刷新'
|
||||
* 2,'数据刷新中'
|
||||
* 3,'取消刷新'
|
||||
* 4,'刷新超时,点击重试'
|
||||
*/
|
||||
type refresStateType = 0|1|2|3|4|5
|
||||
/**
|
||||
* 0初始值
|
||||
* 1刷新中
|
||||
* 2超时出错了
|
||||
*/
|
||||
type bottomStateType = 0|1|2
|
||||
|
||||
/**
|
||||
* @name 虚拟列表 xVirtualList
|
||||
* @description 这是一个超高虚拟列表,无论几万数据都能轻松应对是你列表处理数据的不二之选,集成下拉刷新,触底更新,指定索引位置等。
|
||||
* 未来将应于表格,picker等系列组件,作为其它组件的底层存在,极大提高性能。使用时请注意demo示例使用方法。
|
||||
* @page /pages/zhanshi/virtual-list
|
||||
* @category 展示组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xVirtualList"})
|
||||
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
const xVirtualList = ref<UniElement|null>(null)
|
||||
const nestHeader = ref<UniElement|null>(null)
|
||||
|
||||
|
||||
/**
|
||||
* 组件事件定义
|
||||
*/
|
||||
const emits = defineEmits<{
|
||||
/** 滚动事件 */
|
||||
scroll: [detail: UniScrollEventDetail],
|
||||
/** 滚动到顶时触发 */
|
||||
pull: [],
|
||||
/** 触底时触发 */
|
||||
bottom: [],
|
||||
/** 渲染范围变化时触发 */
|
||||
rangeChange: [range: RenderRange]
|
||||
}>()
|
||||
|
||||
/**
|
||||
* 组件属性定义
|
||||
*/
|
||||
const props = withDefaults(defineProps<xVirtualListType>(), {
|
||||
itemHeight: "50",
|
||||
viewCount: 5,
|
||||
bufferSize: 2,
|
||||
enableVirtual: true,
|
||||
list: () => [] as any[],
|
||||
refresherEnabled: false,
|
||||
refresherBottomEnabled: false,
|
||||
pullState:false,
|
||||
bottomState:false
|
||||
})
|
||||
|
||||
/** 当前滚动位置 */
|
||||
const scrollTop = ref(0)
|
||||
const scrollTopTop = ref(0)
|
||||
|
||||
/** 滚动容器总高度 */
|
||||
const scrollHeight = ref(0)
|
||||
/** 容器的尺寸 */
|
||||
const containerBounds = ref<Bounds>({ width: 0, height: 0,top:0 })
|
||||
/** 是否已初始化 */
|
||||
const isInitialized = ref(false)
|
||||
/** 防抖定时器 */
|
||||
let scrollTimer: number | null = null
|
||||
const disabledScroll = ref(false)
|
||||
const _refresherEnabled = computed((): boolean => props.refresherEnabled)
|
||||
const _refresherBottomEnabled = computed((): boolean => props.refresherBottomEnabled)
|
||||
const _isDark= computed((): boolean => xConfig.dark == 'dark')
|
||||
const refreshHeight = ref(45)
|
||||
const refresStateMap = ref<Map<number,string>>(new Map([
|
||||
[0,i18n.t("tmui4x.virtualList.status_0")],
|
||||
[1,i18n.t("tmui4x.virtualList.status_1")],
|
||||
[2,i18n.t("tmui4x.virtualList.status_2")],
|
||||
[3,i18n.t("tmui4x.virtualList.status_3")],
|
||||
[4,i18n.t("tmui4x.virtualList.status_4")],
|
||||
[5,i18n.t("tmui4x.virtualList.status_5")],
|
||||
]))
|
||||
const bottomRefresStateMap = ref<Map<number,string>>(new Map([
|
||||
[0,i18n.t("tmui4x.virtualList.status_0")],
|
||||
[1,i18n.t("tmui4x.virtualList.status_1")],
|
||||
[2,i18n.t("tmui4x.virtualList.status_2")]
|
||||
]))
|
||||
const refresState = ref<refresStateType>(0)
|
||||
const refresBottomState = ref<refresStateType>(0)
|
||||
|
||||
const refresherTriggered = ref(props.pullState)
|
||||
const bottomTriggered = ref(props.bottomState)
|
||||
let timeoutid = null as null|number
|
||||
let timeoutid2 = null as null|number
|
||||
|
||||
/**
|
||||
* 用户定义的项目高度(转换为px)
|
||||
*/
|
||||
const itemHeight = computed((): number => {
|
||||
const heightval = checkIsCssUnit(props.itemHeight, xConfig.unit);
|
||||
const unit = getUnit(heightval);
|
||||
let realHeight = parseInt(heightval);
|
||||
if (unit === 'rpx') {
|
||||
realHeight = uni.rpx2px(realHeight)
|
||||
}
|
||||
return realHeight;
|
||||
})
|
||||
|
||||
/**
|
||||
* 容器高度(转换为px)
|
||||
*/
|
||||
const containerHeight = computed((): number => {
|
||||
return containerBounds.value.height;
|
||||
})
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
const _list = computed((): any[] => props.list)
|
||||
|
||||
/**
|
||||
* 列表总高度
|
||||
*/
|
||||
const totalHeight = computed((): number => {
|
||||
return _list.value.length * itemHeight.value;
|
||||
})
|
||||
|
||||
/**
|
||||
* 可视区域能显示的项目数量
|
||||
*/
|
||||
const visibleCount = computed((): number => {
|
||||
if (containerHeight.value <= 0) return props.viewCount;
|
||||
return Math.ceil(containerHeight.value / itemHeight.value);
|
||||
})
|
||||
|
||||
/**
|
||||
* 渲染范围计算
|
||||
*/
|
||||
const renderRange = computed((): RenderRange => {
|
||||
if (!props.enableVirtual || _list.value.length == 0) {
|
||||
return {
|
||||
startIndex: 0,
|
||||
endIndex: _list.value.length - 1,
|
||||
offsetY: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 计算开始索引
|
||||
const startIndex = Math.floor(scrollTop.value / itemHeight.value);
|
||||
// 添加缓冲区
|
||||
const bufferedStartIndex = Math.max(0, startIndex - props.bufferSize);
|
||||
// 计算结束索引
|
||||
const endIndex = Math.min(
|
||||
_list.value.length - 1,
|
||||
startIndex + visibleCount.value + props.bufferSize * 2
|
||||
);
|
||||
|
||||
// 计算偏移量
|
||||
const offsetY = bufferedStartIndex * itemHeight.value;
|
||||
|
||||
return {
|
||||
startIndex: bufferedStartIndex,
|
||||
endIndex,
|
||||
offsetY
|
||||
};
|
||||
})
|
||||
|
||||
/**
|
||||
* 开始索引
|
||||
*/
|
||||
const startIndex = computed((): number => renderRange.value.startIndex)
|
||||
|
||||
/**
|
||||
* 结束索引
|
||||
*/
|
||||
const endIndex = computed((): number => renderRange.value.endIndex)
|
||||
|
||||
/**
|
||||
* Y轴偏移量
|
||||
*/
|
||||
const offsetY = computed((): number => renderRange.value.offsetY)
|
||||
|
||||
/**
|
||||
* 当前渲染的数据列表(通过可视计算切割得到的数据列表)
|
||||
*/
|
||||
const renderList = computed((): any[] => {
|
||||
if (!props.enableVirtual) {
|
||||
return _list.value;
|
||||
}
|
||||
return _list.value.slice(startIndex.value, endIndex.value + 1);
|
||||
})
|
||||
|
||||
/**
|
||||
* 读取容器宽和高
|
||||
*/
|
||||
function getContainerBounds(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ele = xVirtualList.value
|
||||
if (ele == null) {
|
||||
reject('容器元素不存在');
|
||||
console.error('容器元素不存在')
|
||||
return;
|
||||
}
|
||||
ele.getBoundingClientRectAsync()?.then((rect: DOMRect) => {
|
||||
containerBounds.value.width = rect.width;
|
||||
containerBounds.value.height = rect.height;
|
||||
containerBounds.value.top = rect.top;
|
||||
isInitialized.value = true;
|
||||
|
||||
resolve();
|
||||
}).catch((error) => {
|
||||
console.error("容器读取宽高失败:", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取非APP子协商头的宽和高。
|
||||
*/
|
||||
function getNestHeaderBounds():Promise<DOMRect>{
|
||||
return new Promise((resolve, reject) => {
|
||||
const ele = nestHeader.value
|
||||
if (ele == null) {
|
||||
reject('子协商头不存在');
|
||||
console.error('子协商头不存在')
|
||||
return;
|
||||
}
|
||||
ele.getBoundingClientRectAsync()?.then((rect: DOMRect) => {
|
||||
resolve(rect);
|
||||
}).catch((error) => {
|
||||
console.error("容器读取宽高失败:", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查滚动边界
|
||||
*/
|
||||
function checkScrollBoundary(detail: UniScrollEventDetail): void {
|
||||
// 检查是否滚动到顶部
|
||||
// if (detail.scrollTop <= 0) {
|
||||
// emits('pull');
|
||||
// }
|
||||
|
||||
// // 检查是否滚动到底部(允许5px的误差)
|
||||
// const isAtBottom = detail.scrollTop + containerHeight.value >= detail.scrollHeight - 5;
|
||||
// if (isAtBottom) {
|
||||
// emits('bottom');
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 滚动事件处理(带防抖优化)
|
||||
*/
|
||||
function onScroll(evt: UniScrollEvent): void {
|
||||
const detail = evt.detail
|
||||
// 触发滚动事件
|
||||
emits('scroll', detail);
|
||||
|
||||
// 优化scrollTop赋值,避免边界情况下的重复计算
|
||||
const newScrollTop = evt.detail.scrollTop;
|
||||
const maxScrollTop = totalHeight.value - containerHeight.value;
|
||||
|
||||
// 只有在有效范围内且值发生变化时才更新scrollTop
|
||||
if (newScrollTop >= 0 && newScrollTop <= maxScrollTop && newScrollTop != scrollTop.value) {
|
||||
scrollTop.value = newScrollTop;
|
||||
} else if (newScrollTop < 0 && scrollTop.value != 0) {
|
||||
// 到达顶部时,确保scrollTop为0
|
||||
scrollTop.value = 0;
|
||||
} else if (newScrollTop > maxScrollTop && scrollTop.value != maxScrollTop) {
|
||||
// 到达底部时,确保scrollTop为最大值
|
||||
scrollTop.value = Math.max(0, maxScrollTop);
|
||||
}
|
||||
|
||||
scrollHeight.value = evt.detail.scrollHeight;
|
||||
|
||||
// // 防抖处理边界检测
|
||||
// if (scrollTimer !=null) {
|
||||
// clearTimeout(scrollTimer!);
|
||||
// }
|
||||
// scrollTimer = setTimeout(() => {
|
||||
// checkScrollBoundary(detail);
|
||||
// }, 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到指定位置
|
||||
*/
|
||||
function scrollToIndex(index: number): void {
|
||||
if (index < 0 || index >= _list.value.length) {
|
||||
console.warn('滚动索引超出范围');
|
||||
return;
|
||||
}
|
||||
const targetScrollTop = index * itemHeight.value;
|
||||
scrollTop.value = targetScrollTop;
|
||||
scrollTopTop.value = targetScrollTop-1
|
||||
nextTick(()=>{
|
||||
scrollTopTop.value = targetScrollTop
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新容器尺寸
|
||||
*/
|
||||
function refreshSize(): void {
|
||||
nextTick(() => {
|
||||
getContainerBounds()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置滚动位置
|
||||
*/
|
||||
function resetScroll(): void {
|
||||
scrollTop.value = 0;
|
||||
scrollTopTop.value = 0
|
||||
}
|
||||
/**
|
||||
* 刷新加载数据
|
||||
*/
|
||||
function onpull(){
|
||||
emits('pull');
|
||||
if(timeoutid!=null){
|
||||
clearTimeout(timeoutid! as number)
|
||||
timeoutid = null;
|
||||
}
|
||||
/**
|
||||
* 是否超时
|
||||
*/
|
||||
timeoutid = setTimeout(function() {
|
||||
refresState.value = 4
|
||||
}, 6000);
|
||||
}
|
||||
/**
|
||||
* 刷新加载数据
|
||||
*/
|
||||
function onbottom(){
|
||||
emits('bottom');
|
||||
if(timeoutid2!=null){
|
||||
clearTimeout(timeoutid2! as number)
|
||||
timeoutid2 = null;
|
||||
}
|
||||
/**
|
||||
* 是否超时
|
||||
*/
|
||||
timeoutid2 = setTimeout(function() {
|
||||
refresBottomState.value = 2
|
||||
}, 6000);
|
||||
}
|
||||
function refreshOnload(){
|
||||
refresState.value = 2
|
||||
refresherTriggered.value = true;
|
||||
onpull()
|
||||
}
|
||||
/**
|
||||
* 被下拉。
|
||||
*/
|
||||
function refresherpulling(evt:UniRefresherEvent): void {
|
||||
let pullHeight = evt.detail.dy;
|
||||
// #ifdef APP-HARMONY
|
||||
if(refresState.value==2) return;
|
||||
// #endif
|
||||
if(pullHeight>=refreshHeight.value){
|
||||
refresState.value = 1
|
||||
|
||||
}else{
|
||||
refresState.value = 0
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 触发刷新
|
||||
*/
|
||||
function refresherrefresh(evt:UniRefresherEvent): void {
|
||||
refresState.value = 2
|
||||
refresherTriggered.value = true;
|
||||
onpull()
|
||||
}
|
||||
/**
|
||||
* 被复位
|
||||
*/
|
||||
function refresherrestore(evt:UniRefresherEvent): void {
|
||||
refresherTriggered.value = false;
|
||||
refresState.value = 3
|
||||
}
|
||||
/**
|
||||
* 刷新被中断
|
||||
*/
|
||||
function refresherabort(evt:UniRefresherEvent): void {
|
||||
refresState.value = 3
|
||||
refresherTriggered.value = false;
|
||||
}
|
||||
function scrolltoupper(evt:UniScrollToUpperEvent){
|
||||
if(refresState.value==3||refresState.value ==4){
|
||||
refresState.value =0
|
||||
refresherTriggered.value = false;
|
||||
}
|
||||
}
|
||||
function scrolltolower(evt:UniScrollToLowerEvent){
|
||||
if(refresBottomState.value==2||refresBottomState.value==1||!_refresherBottomEnabled.value) return;
|
||||
refresBottomState.value = 1;
|
||||
bottomTriggered.value = true
|
||||
onbottom()
|
||||
}
|
||||
function refreshBottomOnload(){
|
||||
refresBottomState.value = 1
|
||||
bottomTriggered.value = true;
|
||||
onbottom()
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听刷新外部设置的状态。
|
||||
*/
|
||||
watch(():boolean => props.pullState, (newPullState:boolean) => {
|
||||
if(!newPullState&&refresherTriggered.value&&refresState.value!=4){
|
||||
refresState.value = 5
|
||||
// nextTick(()=>{
|
||||
// refresherTriggered.value = newPullState;
|
||||
// })
|
||||
setTimeout(()=>{
|
||||
refresherTriggered.value = newPullState;
|
||||
},1000)
|
||||
}else if(refresState.value!=4){
|
||||
refresherTriggered.value = newPullState;
|
||||
}
|
||||
|
||||
}, { deep: true });
|
||||
|
||||
/**
|
||||
* 监听刷新外部设置的状态。
|
||||
*/
|
||||
watch(():boolean => props.bottomState, (newPullState:boolean) => {
|
||||
if(!newPullState&&bottomTriggered.value&&refresBottomState.value!=2){
|
||||
refresBottomState.value = 0
|
||||
bottomTriggered.value = false
|
||||
if(timeoutid2!=null){
|
||||
clearTimeout(timeoutid2! as number)
|
||||
timeoutid2 = null;
|
||||
}
|
||||
}else if(refresBottomState.value==2){
|
||||
bottomTriggered.value = false
|
||||
refresBottomState.value = 0
|
||||
}
|
||||
|
||||
}, { deep: true });
|
||||
|
||||
/**
|
||||
* 监听渲染范围变化
|
||||
*/
|
||||
watch(renderRange, (newRange:RenderRange) => {
|
||||
emits('rangeChange', newRange);
|
||||
}, { deep: true });
|
||||
|
||||
|
||||
/**
|
||||
* 监听列表数据变化
|
||||
*/
|
||||
watch(():any => props.list, (newList: any[], oldList: any[]) => {
|
||||
// 如果新列表为空,重置滚动位置
|
||||
// 如果是首次加载数据(从空到有数据)
|
||||
if ((oldList.length == 0 && newList.length > 0 )|| newList.length == 0) {
|
||||
resetScroll();
|
||||
refreshSize();
|
||||
return;
|
||||
}
|
||||
|
||||
// 数据长度变化的处理
|
||||
if (oldList.length != newList.length) {
|
||||
const currentIndex = Math.floor(scrollTop.value / itemHeight.value);
|
||||
|
||||
// 如果数据减少
|
||||
if (newList.length < oldList.length) {
|
||||
// 如果当前位置超出了新数据范围,调整到最后一项
|
||||
if (currentIndex >= newList.length) {
|
||||
const maxIndex = Math.max(0, newList.length - 1);
|
||||
scrollToIndex(maxIndex);
|
||||
}
|
||||
// 否则保持当前位置
|
||||
} else {
|
||||
// 数据增加时,保持当前滚动位置不变
|
||||
// 只需要刷新容器尺寸以适应新的总高度
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新容器尺寸以适应数据变化
|
||||
refreshSize();
|
||||
}, { deep: true });
|
||||
|
||||
/**
|
||||
* 组件挂载时初始化
|
||||
*/
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// #ifdef APP-IOS
|
||||
resetScroll();
|
||||
refreshSize();
|
||||
// #endif
|
||||
// #ifndef APP-IOS
|
||||
getContainerBounds()
|
||||
|
||||
// #endif
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 组件卸载时清理
|
||||
*/
|
||||
onBeforeUnmount(() => {
|
||||
if (scrollTimer!=null) {
|
||||
clearTimeout(scrollTimer! as number);
|
||||
scrollTimer = null;
|
||||
}
|
||||
if(timeoutid!=null){
|
||||
clearTimeout(timeoutid! as number)
|
||||
timeoutid = null;
|
||||
}
|
||||
if(timeoutid2!=null){
|
||||
clearTimeout(timeoutid2! as number)
|
||||
timeoutid2 = null;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
defineExpose({
|
||||
/** 滚动到指定索引 */
|
||||
scrollToIndex,
|
||||
/** 刷新容器尺寸 */
|
||||
refreshSize,
|
||||
/** 重置滚动位置 */
|
||||
resetScroll,
|
||||
/** 获取当前渲染范围 */
|
||||
getRenderRange: ():RenderRange => renderRange.value,
|
||||
/** 获取容器尺寸 */
|
||||
getContainerBounds: ():Bounds => containerBounds.value
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" >
|
||||
|
||||
.refresh-box{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
// #ifdef MP
|
||||
width:100%;
|
||||
// #endif
|
||||
}
|
||||
.tip-text{
|
||||
margin-left: 5px;
|
||||
}
|
||||
.xVirtualListWrap{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.xVirtualList {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xVirtualListContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user