Kaynağa Gözat

添加列表刷新组件

icssoa 7 ay önce
ebeveyn
işleme
4f2d437ef8

+ 5 - 0
App.uvue

@@ -48,4 +48,9 @@ export default {
 		margin-top: 0;
 	}
 }
+
+.uni-toast {
+	border-radius: 32rpx;
+	background-color: rgba(0, 0, 0, 0.8) !important;
+}
 </style>

+ 1 - 0
cool/hooks/index.ts

@@ -1,5 +1,6 @@
 export * from "./refs";
 export * from "./page";
+export * from "./pager";
 export * from "./long-press";
 export * from "./cache";
 export * from "./parent";

+ 94 - 0
cool/hooks/pager.ts

@@ -0,0 +1,94 @@
+import { ref } from "vue";
+import { assign, parse } from "../utils";
+import type { Response } from "../service";
+
+type Pagination = {
+	page: number;
+	size: number;
+	total: number;
+};
+
+type PagerResponse = {
+	list: UTSJSONObject[];
+	pagination: Pagination;
+};
+
+type PagerCallback = (params: UTSJSONObject) => Promise<UTSJSONObject>;
+
+export class Pager {
+	public page = 1;
+	public size = 20;
+	public total = 0;
+	public list = ref<UTSJSONObject[]>([]);
+	public loading = ref(false);
+	public refreshing = ref(false);
+	public finished = ref(false);
+	public params = {} as UTSJSONObject;
+	public cb: PagerCallback | null = null;
+
+	constructor(cb: PagerCallback) {
+		this.cb = cb;
+	}
+
+	done() {
+		this.loading.value = false;
+	}
+
+	clear() {
+		this.list.value = [];
+		this.finished.value = false;
+		this.refreshing.value = false;
+		this.loading.value = false;
+	}
+
+	public refresh = async (params: UTSJSONObject): Promise<UTSJSONObject> => {
+		return new Promise((resolve, reject) => {
+			assign(this.params, params);
+
+			const data = {
+				page: this.page,
+				size: this.size,
+				...this.params
+			};
+
+			this.loading.value = true;
+
+			this.cb!(data)
+				.then((res) => {
+					const { list, pagination } = parse<PagerResponse>(res)!;
+
+					this.page = pagination.page;
+					this.size = pagination.size;
+					this.total = pagination.total;
+					this.finished.value = this.list.value.length >= this.total;
+
+					if (data.page == 1) {
+						this.list.value = list;
+					} else {
+						this.list.value.push(...list);
+					}
+
+					resolve(res);
+				})
+				.catch((err) => {
+					reject(err);
+				})
+				.finally(() => {
+					this.loading.value = false;
+				});
+		});
+	};
+
+	public loadMore = () => {
+		if (this.loading.value || this.finished.value) {
+			return;
+		}
+
+		this.page += 1;
+		this.refresh({});
+	};
+}
+
+export function usePager(cb: PagerCallback) {
+	return new Pager(cb);
+}

+ 1 - 1
cool/store/user.ts

@@ -57,7 +57,7 @@ export class User {
 					}
 				})
 				.catch(() => {
-					this.logout();
+					// this.logout();
 				});
 		}
 	}

+ 11 - 8
pages/demo/components/goods-item.uvue

@@ -1,13 +1,15 @@
 <template>
-	<view class="w-full p-3 bg-white rounded-xl mb-3 dark:bg-surface-800">
-		<cl-image :src="item.image" mode="widthFix" width="100%" height="250rpx"></cl-image>
-		<cl-text :pt="{ className: 'mt-2' }">{{ item.title }}</cl-text>
+	<view class="p-3 pb-0">
+		<view class="w-full p-3 bg-white rounded-xl dark:bg-surface-800">
+			<cl-image :src="item?.image" mode="aspectFill" width="100%" height="280rpx"></cl-image>
+			<cl-text :pt="{ className: 'mt-2' }">{{ item?.title }}</cl-text>
+		</view>
 	</view>
 </template>
 
 <script lang="ts" setup>
-import { computed, type PropType } from "vue";
-import { useParent } from "@/cool";
+import { computed } from "vue";
+import { parse } from "@/cool";
 
 defineOptions({
 	name: "goods-item"
@@ -15,15 +17,16 @@ defineOptions({
 
 type GoodsItem = {
 	id: number;
-	likeCount: number;
 	title: string;
 	image: string;
 };
 
 const props = defineProps({
-	item: {
-		type: Object as PropType<GoodsItem>,
+	value: {
+		type: Object,
 		default: () => ({})
 	}
 });
+
+const item = computed(() => parse<GoodsItem>(props.value));
 </script>

+ 106 - 66
pages/demo/data/list-view-refresh.uvue

@@ -1,77 +1,117 @@
 <template>
 	<cl-page>
-		<view class="p-3">
-			<cl-list-view :data="data" :virtual="false">
-				<template #item="{ value }">
-					<goods-item :item="value"></goods-item>
-				</template>
-			</cl-list-view>
-		</view>
+		<cl-list-view
+			ref="listViewRef"
+			:data="data"
+			:virtual="false"
+			:pt="{
+				refresher: {
+					className: 'pt-3'
+				}
+			}"
+			:refresher-enabled="true"
+			@pull="onPull"
+			@bottom="loadMore"
+		>
+			<template #item="{ value }">
+				<goods-item :value="value"></goods-item>
+			</template>
+
+			<template #bottom>
+				<view class="py-3">
+					<cl-loadmore :loading="loading" v-if="list.length > 0"></cl-loadmore>
+				</view>
+			</template>
+		</cl-list-view>
 	</cl-page>
 </template>
 
 <script lang="ts" setup>
-import { useListView } from "@/uni_modules/cool-ui";
-import { ref } from "vue";
-import { random } from "@/cool";
+import { useListView, useUi, type ClListViewItem } from "@/uni_modules/cool-ui";
+import { computed, ref } from "vue";
+import { usePager } from "@/cool";
 import GoodsItem from "../components/goods-item.uvue";
 
+const ui = useUi();
+
+const listViewRef = ref<ClListViewComponentPublicInstance | null>(null);
+
 let id = 0;
 
-const data = useListView([
-	{
-		id: id++,
-		likeCount: random(100, 1000),
-		title: "春日樱花盛开时节,粉色花瓣如诗如画般飘洒",
-		image: "https://unix.cool-js.com/images/demo/1.jpg"
-	},
-	{
-		id: id++,
-		likeCount: random(100, 1000),
-		title: "夕阳西下的海滩边,金色阳光温柔地洒在波光粼粼的海面上,构成令人心旷神怡的日落美景",
-		image: "https://unix.cool-js.com/images/demo/2.jpg"
-	},
-	{
-		id: id++,
-		likeCount: random(100, 1000),
-		title: "寒冬腊月时分,洁白雪花纷纷扬扬地覆盖着整个世界,感受冬日的宁静与美好",
-		image: "https://unix.cool-js.com/images/demo/3.jpg"
-	},
-	{
-		id: id++,
-		likeCount: random(100, 1000),
-		title: "都市夜景霓虹闪烁,五彩斑斓光芒照亮城市营造梦幻般景象",
-		image: "https://unix.cool-js.com/images/demo/5.jpg"
-	},
-	{
-		id: id++,
-		likeCount: random(100, 1000),
-		title: "云雾缭绕的山间风光如诗如画让人心旷神怡,微风轻抚树梢带来阵阵清香,鸟儿在林间自由歌唱",
-		image: "https://unix.cool-js.com/images/demo/6.jpg"
-	},
-	{
-		id: id++,
-		likeCount: random(100, 1000),
-		title: "古老建筑与现代摩天大楼交相辉映,传统与现代完美融合创造独特城市景观",
-		image: "https://unix.cool-js.com/images/demo/7.jpg"
-	},
-	{
-		id: id++,
-		likeCount: random(100, 1000),
-		title: "广袤田野绿意盎然风光无限,金黄麦浪在微风中轻柔摇曳,农家炊烟袅袅升起",
-		image: "https://unix.cool-js.com/images/demo/8.jpg"
-	},
-	{
-		id: id++,
-		likeCount: random(100, 1000),
-		title: "璀璨星空下银河横跨天际,繁星闪烁神秘光芒营造浪漫夜空美景",
-		image: "https://unix.cool-js.com/images/demo/9.jpg"
-	},
-	{
-		id: id++,
-		likeCount: random(100, 1000),
-		title: "雄伟瀑布从高耸悬崖飞流直下激起千层浪花,彩虹在水雾中若隐若现如梦如幻",
-		image: "https://unix.cool-js.com/images/demo/10.jpg"
-	}
-]);
+const { refresh, list, loading, loadMore } = usePager((params) => {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			resolve({
+				list: [
+					{
+						id: id++,
+						title: "春日樱花盛开时节,粉色花瓣如诗如画般飘洒",
+						image: "https://unix.cool-js.com/images/demo/1.jpg"
+					},
+					{
+						id: id++,
+						title: "夕阳西下的海滩边,金色阳光温柔地洒在波光粼粼的海面上,构成令人心旷神怡的日落美景",
+						image: "https://unix.cool-js.com/images/demo/2.jpg"
+					},
+					{
+						id: id++,
+						title: "寒冬腊月时分,洁白雪花纷纷扬扬地覆盖着整个世界,感受冬日的宁静与美好",
+						image: "https://unix.cool-js.com/images/demo/3.jpg"
+					},
+					{
+						id: id++,
+						title: "都市夜景霓虹闪烁,五彩斑斓光芒照亮城市营造梦幻般景象",
+						image: "https://unix.cool-js.com/images/demo/5.jpg"
+					},
+					{
+						id: id++,
+						title: "云雾缭绕的山间风光如诗如画让人心旷神怡,微风轻抚树梢带来阵阵清香,鸟儿在林间自由歌唱",
+						image: "https://unix.cool-js.com/images/demo/6.jpg"
+					},
+					{
+						id: id++,
+						title: "古老建筑与现代摩天大楼交相辉映,传统与现代完美融合创造独特城市景观",
+						image: "https://unix.cool-js.com/images/demo/7.jpg"
+					},
+					{
+						id: id++,
+						title: "广袤田野绿意盎然风光无限,金黄麦浪在微风中轻柔摇曳,农家炊烟袅袅升起",
+						image: "https://unix.cool-js.com/images/demo/8.jpg"
+					},
+					{
+						id: id++,
+						title: "璀璨星空下银河横跨天际,繁星闪烁神秘光芒营造浪漫夜空美景",
+						image: "https://unix.cool-js.com/images/demo/9.jpg"
+					},
+					{
+						id: id++,
+						title: "雄伟瀑布从高耸悬崖飞流直下激起千层浪花,彩虹在水雾中若隐若现如梦如幻",
+						image: "https://unix.cool-js.com/images/demo/10.jpg"
+					}
+				],
+				pagination: {
+					page: params["page"],
+					size: params["size"],
+					total: 100
+				}
+			});
+
+			ui.hideLoading();
+		}, 1000);
+	});
+});
+
+const data = computed<ClListViewItem[]>(() => {
+	return useListView(list.value);
+});
+
+async function onPull() {
+	await refresh({ page: 1 });
+	listViewRef.value!.stopRefresh();
+}
+
+onReady(() => {
+	ui.showLoading("加载中");
+	refresh({});
+});
 </script>

+ 1 - 1
pages/index/home.uvue

@@ -263,7 +263,7 @@ const data = computed<Item[]>(() => {
 					label: t("列表刷新"),
 					icon: "refresh-line",
 					path: "/pages/demo/data/list-view-refresh",
-					disabled: true
+					disabled: false
 				},
 				{
 					label: t("瀑布流"),

+ 18 - 0
types/uni-app.d.ts

@@ -179,6 +179,24 @@ declare interface UniScrollEvent extends UniEvent {
 	};
 }
 
+declare interface UniScrollToUpperEvent extends UniEvent {
+	detail: {
+		direction: string;
+	};
+}
+
+declare interface UniScrollToLowerEvent extends UniEvent {
+	detail: {
+		direction: string;
+	};
+}
+
+declare interface UniRefresherEvent extends UniEvent {
+	detail: {
+		dy: number;
+	};
+}
+
 declare interface UniSwiperChangeEvent extends UniEvent {
 	detail: {
 		current: number;

+ 214 - 29
uni_modules/cool-ui/components/cl-list-view/cl-list-view.uvue

@@ -1,5 +1,5 @@
 <template>
-	<view class="cl-list-view">
+	<view class="cl-list-view" :class="[pt.className]">
 		<cl-index-bar
 			v-if="hasIndex"
 			v-model="activeIndex"
@@ -13,21 +13,61 @@
 
 		<scroll-view
 			class="cl-list-view__scroller"
+			:class="[pt.scroller?.className]"
 			:scroll-top="targetScrollTop"
-			:show-scrollbar="false"
+			:scroll-into-view="scrollIntoView"
+			:scroll-with-animation="scrollWithAnimation"
+			:show-scrollbar="showScrollbar"
+			:refresher-triggered="refreshTriggered"
+			:refresher-enabled="refresherEnabled"
+			:refresher-threshold="refresherThreshold"
+			:refresher-background="refresherBackground"
+			refresher-default-style="none"
 			direction="vertical"
+			@scrolltoupper="onScrollToUpper"
+			@scrolltolower="onScrollToLower"
 			@scroll="onScroll"
+			@scrollend="onScrollEnd"
+			@refresherpulling="onRefresherPulling"
+			@refresherrefresh="onRefresherRefresh"
+			@refresherrestore="onRefresherRestore"
+			@refresherabort="onRefresherAbort"
 		>
 			<view
+				slot="refresher"
+				class="cl-list-view__refresher"
+				:class="[
+					{
+						'is-pulling': refresherStatus === 'pulling',
+						'is-refreshing': refresherStatus === 'refreshing'
+					},
+					pt.refresher?.className
+				]"
+				:style="{
+					height: refresherThreshold + 'px'
+				}"
+			>
+				<cl-loading
+					v-if="refresherStatus === 'refreshing'"
+					:size="28"
+					:pt="{
+						className: 'mr-2'
+					}"
+				></cl-loading>
+				<cl-text> {{ refresherText }} </cl-text>
+			</view>
+
+			<view
 				class="cl-list-view__virtual-list"
-				:style="{ height: virtual ? listHeight + 'px' : 'auto' }"
+				:class="[pt.list?.className]"
+				:style="listStyle"
 			>
-				<view class="cl-list-view__spacer-top" :style="{ height: spacerTopHeight + 'px' }">
+				<view class="cl-list-view__spacer-top" :style="spacerTopStyle">
 					<slot name="top"></slot>
 				</view>
 
 				<view
-					v-for="item in visibleItems"
+					v-for="(item, index) in visibleItems"
 					:key="item.key"
 					class="cl-list-view__virtual-item"
 				>
@@ -62,7 +102,13 @@
 						}"
 						@tap="onItemTap(item)"
 					>
-						<slot name="item" :item="item" :data="item.data" :value="item.data.value">
+						<slot
+							name="item"
+							:item="item"
+							:data="item.data"
+							:value="item.data.value"
+							:index="index"
+						>
 							<view class="cl-list-view__item-inner">
 								<cl-text> {{ item.data.label }} </cl-text>
 							</view>
@@ -70,10 +116,7 @@
 					</view>
 				</view>
 
-				<view
-					class="cl-list-view__spacer-bottom"
-					:style="{ height: spacerBottomHeight + 'px' }"
-				>
+				<view class="cl-list-view__spacer-bottom" :style="spacerBottomStyle">
 					<slot name="bottom"></slot>
 				</view>
 			</view>
@@ -134,7 +177,7 @@ defineSlots<{
 	// 分组头部插槽
 	header(props: { index: string }): any;
 	// 列表项插槽
-	item(props: { data: ClListViewItem; item: VirtualItem; value: any | null }): any;
+	item(props: { data: ClListViewItem; item: VirtualItem; value: any | null; index: number }): any;
 	// 底部插槽
 	bottom(): any;
 	// 索引插槽
@@ -181,18 +224,80 @@ const props = defineProps({
 	virtual: {
 		type: Boolean,
 		default: true
+	},
+	// 滚动到指定位置
+	scrollIntoView: {
+		type: String,
+		default: ""
+	},
+	// 是否启用滚动动画
+	scrollWithAnimation: {
+		type: Boolean,
+		default: false
+	},
+	// 是否显示滚动条
+	showScrollbar: {
+		type: Boolean,
+		default: false
+	},
+	// 是否启用下拉刷新
+	refresherEnabled: {
+		type: Boolean,
+		default: false
+	},
+	// 下拉刷新触发距离,相当于下拉内容高度
+	refresherThreshold: {
+		type: Number,
+		default: 45
+	},
+	// 下拉刷新区域背景色
+	refresherBackground: {
+		type: String,
+		default: "transparent"
+	},
+	// 下拉刷新默认文案
+	refresherDefaultText: {
+		type: String,
+		default: "下拉刷新"
+	},
+	// 释放刷新文案
+	refresherPullingText: {
+		type: String,
+		default: "释放立即刷新"
+	},
+	// 正在刷新文案
+	refresherRefreshingText: {
+		type: String,
+		default: "加载中"
 	}
 });
 
-const emit = defineEmits(["item-tap"]);
+const emit = defineEmits([
+	"item-tap",
+	"refresher-pulling",
+	"refresher-refresh",
+	"refresher-restore",
+	"refresher-abort",
+	"scrolltoupper",
+	"scrolltolower",
+	"scroll",
+	"scrollend",
+	"pull",
+	"top",
+	"bottom"
+]);
 
 // 获取当前组件实例,用于后续DOM操作
 const { proxy } = getCurrentInstance()!;
 
+// 透传样式配置类型
 type PassThrough = {
 	className?: string;
 	item?: PassThroughProps;
+	list?: PassThroughProps;
 	indexBar?: PassThroughProps;
+	scroller?: PassThroughProps;
+	refresher?: PassThroughProps;
 };
 
 // 解析透传样式配置
@@ -371,10 +476,6 @@ const spacerTopHeight = computed<number>(() => {
 	if (isEmpty(visibleItems.value)) {
 		return 0;
 	}
-	// 如果未启用虚拟列表,返回0
-	if (!props.virtual) {
-		return 0;
-	}
 	// 返回第一个可见项目的顶部位置
 	return visibleItems.value[0].top;
 });
@@ -385,16 +486,33 @@ const spacerBottomHeight = computed<number>(() => {
 	if (isEmpty(visibleItems.value)) {
 		return 0;
 	}
-	// 如果未启用虚拟列表,返回0
-	if (!props.virtual) {
-		return 0;
-	}
 	// 获取最后一个可见项目
 	const lastItem = visibleItems.value[visibleItems.value.length - 1];
 	// 计算下方占位高度
 	return listHeight.value - (lastItem.top + lastItem.height);
 });
 
+// 列表样式
+const listStyle = computed(() => {
+	return {
+		height: props.virtual ? `${listHeight.value}px` : "auto"
+	};
+});
+
+// 上方占位容器样式
+const spacerTopStyle = computed(() => {
+	return {
+		height: props.virtual ? `${spacerTopHeight.value}px` : "auto"
+	};
+});
+
+// 下方占位容器样式
+const spacerBottomStyle = computed(() => {
+	return {
+		height: props.virtual ? `${spacerBottomHeight.value}px` : "auto"
+	};
+});
+
 // 存储每个分组头部距离顶部的位置数组
 const tops = ref<number[]>([]);
 
@@ -418,6 +536,42 @@ function getTops() {
 	tops.value = arr;
 }
 
+// 下拉刷新触发标志
+const refreshTriggered = ref(false);
+
+// 下拉刷新相关状态
+const refresherStatus = ref<"default" | "pulling" | "refreshing">("default");
+
+// 下拉刷新文案
+const refresherText = computed(() => {
+	switch (refresherStatus.value) {
+		case "pulling":
+			return props.refresherPullingText;
+		case "refreshing":
+			return props.refresherRefreshingText;
+		default:
+			return props.refresherDefaultText;
+	}
+});
+
+// 停止下拉刷新
+function stopRefresh() {
+	refreshTriggered.value = false;
+	refresherStatus.value = "default";
+}
+
+// 滚动到顶部事件处理函数
+function onScrollToUpper(e: UniScrollToUpperEvent) {
+	emit("scrolltoupper", e);
+	emit("top");
+}
+
+// 滚动到底部事件处理函数
+function onScrollToLower(e: UniScrollToLowerEvent) {
+	emit("scrolltolower", e);
+	emit("bottom");
+}
+
 // 滚动锁定标志,用于防止滚动时触发不必要的计算
 let scrollLock = false;
 
@@ -435,6 +589,13 @@ function onScroll(e: UniScrollEvent) {
 			activeIndex.value = index;
 		}
 	});
+
+	emit("scroll", e);
+}
+
+// 滚动结束事件处理函数
+function onScrollEnd(e: UniScrollEvent) {
+	emit("scrollend", e);
 }
 
 // 行点击事件处理函数
@@ -456,6 +617,31 @@ function onIndexChange(index: number) {
 	}, 300);
 }
 
+// 下拉刷新事件处理函数
+function onRefresherPulling(e: UniRefresherEvent) {
+	if (e.detail.dy > props.refresherThreshold * 1.5) {
+		refresherStatus.value = "pulling";
+	}
+	emit("refresher-pulling", e);
+}
+
+function onRefresherRefresh(e: UniRefresherEvent) {
+	refresherStatus.value = "refreshing";
+	refreshTriggered.value = true;
+	emit("refresher-refresh", e);
+	emit("pull", e);
+}
+
+function onRefresherRestore(e: UniRefresherEvent) {
+	refresherStatus.value = "default";
+	emit("refresher-restore", e);
+}
+
+function onRefresherAbort(e: UniRefresherEvent) {
+	refresherStatus.value = "default";
+	emit("refresher-abort", e);
+}
+
 // 获取滚动容器的高度
 function getScrollerHeight() {
 	setTimeout(() => {
@@ -493,7 +679,8 @@ onMounted(() => {
 });
 
 defineExpose({
-	data
+	data,
+	stopRefresh
 });
 </script>
 
@@ -516,10 +703,7 @@ defineExpose({
 
 	&__index {
 		@apply flex flex-row items-center bg-white;
-		@apply absolute top-0 left-0 w-full;
-		top: 0px;
-		padding: 0 20rpx;
-		z-index: 11;
+		@apply absolute top-0 left-0 w-full px-[20rpx] z-20;
 
 		&.is-dark {
 			@apply bg-surface-600 border-none;
@@ -531,10 +715,7 @@ defineExpose({
 	}
 
 	&__header {
-		@apply flex flex-row items-center;
-		padding: 0 20rpx;
-		position: relative;
-		z-index: 10;
+		@apply flex flex-row items-center relative px-[20rpx] z-10;
 	}
 
 	&__item {
@@ -542,5 +723,9 @@ defineExpose({
 			@apply flex flex-row items-center px-[20rpx] h-full;
 		}
 	}
+
+	&__refresher {
+		@apply flex flex-row items-center justify-center w-full h-full;
+	}
 }
 </style>

+ 8 - 0
uni_modules/cool-ui/components/cl-list-view/props.ts

@@ -16,4 +16,12 @@ export type ClListViewProps = {
 	bottomHeight?: number;
 	bufferSize?: number;
 	virtual?: boolean;
+	// 下拉刷新相关属性
+	refresherEnabled?: boolean;
+	refresherThreshold?: number;
+	refresherTriggered?: boolean;
+	refresherBackground?: string;
+	refresherDefaultText?: string;
+	refresherPullingText?: string;
+	refresherRefreshingText?: string;
 };

+ 2 - 2
uni_modules/cool-ui/hooks/form.ts

@@ -2,7 +2,7 @@ import { computed, ref, type ComputedRef } from "vue";
 import type { ClFormRule, ClFormValidateError } from "../types";
 import { useParent } from "@/cool";
 
-class UseForm {
+class Form {
 	public formRef = ref<ClFormComponentPublicInstance | null>(null);
 	public disabled: ComputedRef<boolean>;
 
@@ -82,5 +82,5 @@ class UseForm {
 }
 
 export function useForm() {
-	return new UseForm();
+	return new Form();
 }

+ 19 - 0
uni_modules/cool-ui/hooks/ui.ts

@@ -79,6 +79,25 @@ class Ui {
 			instance.showToast(options);
 		}
 	}
+
+	/**
+	 * 显示加载中弹窗
+	 * @param title 提示内容
+	 * @param mask 是否显示蒙层
+	 */
+	showLoading(title: string, mask: boolean | null = null): void {
+		uni.showLoading({
+			title,
+			mask: mask ?? true
+		});
+	}
+
+	/**
+	 * 隐藏加载中弹窗
+	 */
+	hideLoading(): void {
+		uni.hideLoading();
+	}
 }
 
 /**

+ 1 - 0
uni_modules/cool-ui/types/component.d.ts

@@ -128,6 +128,7 @@ declare type ClListItemComponentPublicInstance = {
 
 declare type ClListViewComponentPublicInstance = {
 	data: ClListViewItem[];
+	stopRefresh: () => void;
 };
 
 declare type ClCascaderComponentPublicInstance = {