添加列表刷新组件
This commit is contained in:
5
App.uvue
5
App.uvue
@@ -48,4 +48,9 @@ export default {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uni-toast {
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./refs";
|
export * from "./refs";
|
||||||
export * from "./page";
|
export * from "./page";
|
||||||
|
export * from "./pager";
|
||||||
export * from "./long-press";
|
export * from "./long-press";
|
||||||
export * from "./cache";
|
export * from "./cache";
|
||||||
export * from "./parent";
|
export * from "./parent";
|
||||||
|
|||||||
94
cool/hooks/pager.ts
Normal file
94
cool/hooks/pager.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ export class User {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.logout();
|
// this.logout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="w-full p-3 bg-white rounded-xl mb-3 dark:bg-surface-800">
|
<view class="p-3 pb-0">
|
||||||
<cl-image :src="item.image" mode="widthFix" width="100%" height="250rpx"></cl-image>
|
<view class="w-full p-3 bg-white rounded-xl dark:bg-surface-800">
|
||||||
<cl-text :pt="{ className: 'mt-2' }">{{ item.title }}</cl-text>
|
<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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, type PropType } from "vue";
|
import { computed } from "vue";
|
||||||
import { useParent } from "@/cool";
|
import { parse } from "@/cool";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "goods-item"
|
name: "goods-item"
|
||||||
@@ -15,15 +17,16 @@ defineOptions({
|
|||||||
|
|
||||||
type GoodsItem = {
|
type GoodsItem = {
|
||||||
id: number;
|
id: number;
|
||||||
likeCount: number;
|
|
||||||
title: string;
|
title: string;
|
||||||
image: string;
|
image: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
item: {
|
value: {
|
||||||
type: Object as PropType<GoodsItem>,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const item = computed(() => parse<GoodsItem>(props.value));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,77 +1,117 @@
|
|||||||
<template>
|
<template>
|
||||||
<cl-page>
|
<cl-page>
|
||||||
<view class="p-3">
|
<cl-list-view
|
||||||
<cl-list-view :data="data" :virtual="false">
|
ref="listViewRef"
|
||||||
|
:data="data"
|
||||||
|
:virtual="false"
|
||||||
|
:pt="{
|
||||||
|
refresher: {
|
||||||
|
className: 'pt-3'
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
:refresher-enabled="true"
|
||||||
|
@pull="onPull"
|
||||||
|
@bottom="loadMore"
|
||||||
|
>
|
||||||
<template #item="{ value }">
|
<template #item="{ value }">
|
||||||
<goods-item :item="value"></goods-item>
|
<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>
|
</template>
|
||||||
</cl-list-view>
|
</cl-list-view>
|
||||||
</view>
|
|
||||||
</cl-page>
|
</cl-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useListView } from "@/uni_modules/cool-ui";
|
import { useListView, useUi, type ClListViewItem } from "@/uni_modules/cool-ui";
|
||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { random } from "@/cool";
|
import { usePager } from "@/cool";
|
||||||
import GoodsItem from "../components/goods-item.uvue";
|
import GoodsItem from "../components/goods-item.uvue";
|
||||||
|
|
||||||
|
const ui = useUi();
|
||||||
|
|
||||||
|
const listViewRef = ref<ClListViewComponentPublicInstance | null>(null);
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
|
|
||||||
const data = useListView([
|
const { refresh, list, loading, loadMore } = usePager((params) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
list: [
|
||||||
{
|
{
|
||||||
id: id++,
|
id: id++,
|
||||||
likeCount: random(100, 1000),
|
|
||||||
title: "春日樱花盛开时节,粉色花瓣如诗如画般飘洒",
|
title: "春日樱花盛开时节,粉色花瓣如诗如画般飘洒",
|
||||||
image: "https://unix.cool-js.com/images/demo/1.jpg"
|
image: "https://unix.cool-js.com/images/demo/1.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id++,
|
id: id++,
|
||||||
likeCount: random(100, 1000),
|
|
||||||
title: "夕阳西下的海滩边,金色阳光温柔地洒在波光粼粼的海面上,构成令人心旷神怡的日落美景",
|
title: "夕阳西下的海滩边,金色阳光温柔地洒在波光粼粼的海面上,构成令人心旷神怡的日落美景",
|
||||||
image: "https://unix.cool-js.com/images/demo/2.jpg"
|
image: "https://unix.cool-js.com/images/demo/2.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id++,
|
id: id++,
|
||||||
likeCount: random(100, 1000),
|
|
||||||
title: "寒冬腊月时分,洁白雪花纷纷扬扬地覆盖着整个世界,感受冬日的宁静与美好",
|
title: "寒冬腊月时分,洁白雪花纷纷扬扬地覆盖着整个世界,感受冬日的宁静与美好",
|
||||||
image: "https://unix.cool-js.com/images/demo/3.jpg"
|
image: "https://unix.cool-js.com/images/demo/3.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id++,
|
id: id++,
|
||||||
likeCount: random(100, 1000),
|
|
||||||
title: "都市夜景霓虹闪烁,五彩斑斓光芒照亮城市营造梦幻般景象",
|
title: "都市夜景霓虹闪烁,五彩斑斓光芒照亮城市营造梦幻般景象",
|
||||||
image: "https://unix.cool-js.com/images/demo/5.jpg"
|
image: "https://unix.cool-js.com/images/demo/5.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id++,
|
id: id++,
|
||||||
likeCount: random(100, 1000),
|
|
||||||
title: "云雾缭绕的山间风光如诗如画让人心旷神怡,微风轻抚树梢带来阵阵清香,鸟儿在林间自由歌唱",
|
title: "云雾缭绕的山间风光如诗如画让人心旷神怡,微风轻抚树梢带来阵阵清香,鸟儿在林间自由歌唱",
|
||||||
image: "https://unix.cool-js.com/images/demo/6.jpg"
|
image: "https://unix.cool-js.com/images/demo/6.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id++,
|
id: id++,
|
||||||
likeCount: random(100, 1000),
|
|
||||||
title: "古老建筑与现代摩天大楼交相辉映,传统与现代完美融合创造独特城市景观",
|
title: "古老建筑与现代摩天大楼交相辉映,传统与现代完美融合创造独特城市景观",
|
||||||
image: "https://unix.cool-js.com/images/demo/7.jpg"
|
image: "https://unix.cool-js.com/images/demo/7.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id++,
|
id: id++,
|
||||||
likeCount: random(100, 1000),
|
|
||||||
title: "广袤田野绿意盎然风光无限,金黄麦浪在微风中轻柔摇曳,农家炊烟袅袅升起",
|
title: "广袤田野绿意盎然风光无限,金黄麦浪在微风中轻柔摇曳,农家炊烟袅袅升起",
|
||||||
image: "https://unix.cool-js.com/images/demo/8.jpg"
|
image: "https://unix.cool-js.com/images/demo/8.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id++,
|
id: id++,
|
||||||
likeCount: random(100, 1000),
|
|
||||||
title: "璀璨星空下银河横跨天际,繁星闪烁神秘光芒营造浪漫夜空美景",
|
title: "璀璨星空下银河横跨天际,繁星闪烁神秘光芒营造浪漫夜空美景",
|
||||||
image: "https://unix.cool-js.com/images/demo/9.jpg"
|
image: "https://unix.cool-js.com/images/demo/9.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: id++,
|
id: id++,
|
||||||
likeCount: random(100, 1000),
|
|
||||||
title: "雄伟瀑布从高耸悬崖飞流直下激起千层浪花,彩虹在水雾中若隐若现如梦如幻",
|
title: "雄伟瀑布从高耸悬崖飞流直下激起千层浪花,彩虹在水雾中若隐若现如梦如幻",
|
||||||
image: "https://unix.cool-js.com/images/demo/10.jpg"
|
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>
|
</script>
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ const data = computed<Item[]>(() => {
|
|||||||
label: t("列表刷新"),
|
label: t("列表刷新"),
|
||||||
icon: "refresh-line",
|
icon: "refresh-line",
|
||||||
path: "/pages/demo/data/list-view-refresh",
|
path: "/pages/demo/data/list-view-refresh",
|
||||||
disabled: true
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("瀑布流"),
|
label: t("瀑布流"),
|
||||||
|
|||||||
18
types/uni-app.d.ts
vendored
18
types/uni-app.d.ts
vendored
@@ -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 {
|
declare interface UniSwiperChangeEvent extends UniEvent {
|
||||||
detail: {
|
detail: {
|
||||||
current: number;
|
current: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="cl-list-view">
|
<view class="cl-list-view" :class="[pt.className]">
|
||||||
<cl-index-bar
|
<cl-index-bar
|
||||||
v-if="hasIndex"
|
v-if="hasIndex"
|
||||||
v-model="activeIndex"
|
v-model="activeIndex"
|
||||||
@@ -13,21 +13,61 @@
|
|||||||
|
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="cl-list-view__scroller"
|
class="cl-list-view__scroller"
|
||||||
|
:class="[pt.scroller?.className]"
|
||||||
:scroll-top="targetScrollTop"
|
: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"
|
direction="vertical"
|
||||||
|
@scrolltoupper="onScrollToUpper"
|
||||||
|
@scrolltolower="onScrollToLower"
|
||||||
@scroll="onScroll"
|
@scroll="onScroll"
|
||||||
|
@scrollend="onScrollEnd"
|
||||||
|
@refresherpulling="onRefresherPulling"
|
||||||
|
@refresherrefresh="onRefresherRefresh"
|
||||||
|
@refresherrestore="onRefresherRestore"
|
||||||
|
@refresherabort="onRefresherAbort"
|
||||||
>
|
>
|
||||||
<view
|
<view
|
||||||
class="cl-list-view__virtual-list"
|
slot="refresher"
|
||||||
:style="{ height: virtual ? listHeight + 'px' : 'auto' }"
|
class="cl-list-view__refresher"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'is-pulling': refresherStatus === 'pulling',
|
||||||
|
'is-refreshing': refresherStatus === 'refreshing'
|
||||||
|
},
|
||||||
|
pt.refresher?.className
|
||||||
|
]"
|
||||||
|
:style="{
|
||||||
|
height: refresherThreshold + 'px'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<view class="cl-list-view__spacer-top" :style="{ height: spacerTopHeight + '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"
|
||||||
|
:class="[pt.list?.className]"
|
||||||
|
:style="listStyle"
|
||||||
|
>
|
||||||
|
<view class="cl-list-view__spacer-top" :style="spacerTopStyle">
|
||||||
<slot name="top"></slot>
|
<slot name="top"></slot>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
v-for="item in visibleItems"
|
v-for="(item, index) in visibleItems"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
class="cl-list-view__virtual-item"
|
class="cl-list-view__virtual-item"
|
||||||
>
|
>
|
||||||
@@ -62,7 +102,13 @@
|
|||||||
}"
|
}"
|
||||||
@tap="onItemTap(item)"
|
@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">
|
<view class="cl-list-view__item-inner">
|
||||||
<cl-text> {{ item.data.label }} </cl-text>
|
<cl-text> {{ item.data.label }} </cl-text>
|
||||||
</view>
|
</view>
|
||||||
@@ -70,10 +116,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view class="cl-list-view__spacer-bottom" :style="spacerBottomStyle">
|
||||||
class="cl-list-view__spacer-bottom"
|
|
||||||
:style="{ height: spacerBottomHeight + 'px' }"
|
|
||||||
>
|
|
||||||
<slot name="bottom"></slot>
|
<slot name="bottom"></slot>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -134,7 +177,7 @@ defineSlots<{
|
|||||||
// 分组头部插槽
|
// 分组头部插槽
|
||||||
header(props: { index: string }): any;
|
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;
|
bottom(): any;
|
||||||
// 索引插槽
|
// 索引插槽
|
||||||
@@ -181,18 +224,80 @@ const props = defineProps({
|
|||||||
virtual: {
|
virtual: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
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操作
|
// 获取当前组件实例,用于后续DOM操作
|
||||||
const { proxy } = getCurrentInstance()!;
|
const { proxy } = getCurrentInstance()!;
|
||||||
|
|
||||||
|
// 透传样式配置类型
|
||||||
type PassThrough = {
|
type PassThrough = {
|
||||||
className?: string;
|
className?: string;
|
||||||
item?: PassThroughProps;
|
item?: PassThroughProps;
|
||||||
|
list?: PassThroughProps;
|
||||||
indexBar?: PassThroughProps;
|
indexBar?: PassThroughProps;
|
||||||
|
scroller?: PassThroughProps;
|
||||||
|
refresher?: PassThroughProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解析透传样式配置
|
// 解析透传样式配置
|
||||||
@@ -371,10 +476,6 @@ const spacerTopHeight = computed<number>(() => {
|
|||||||
if (isEmpty(visibleItems.value)) {
|
if (isEmpty(visibleItems.value)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
// 如果未启用虚拟列表,返回0
|
|
||||||
if (!props.virtual) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
// 返回第一个可见项目的顶部位置
|
// 返回第一个可见项目的顶部位置
|
||||||
return visibleItems.value[0].top;
|
return visibleItems.value[0].top;
|
||||||
});
|
});
|
||||||
@@ -385,16 +486,33 @@ const spacerBottomHeight = computed<number>(() => {
|
|||||||
if (isEmpty(visibleItems.value)) {
|
if (isEmpty(visibleItems.value)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
// 如果未启用虚拟列表,返回0
|
|
||||||
if (!props.virtual) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
// 获取最后一个可见项目
|
// 获取最后一个可见项目
|
||||||
const lastItem = visibleItems.value[visibleItems.value.length - 1];
|
const lastItem = visibleItems.value[visibleItems.value.length - 1];
|
||||||
// 计算下方占位高度
|
// 计算下方占位高度
|
||||||
return listHeight.value - (lastItem.top + lastItem.height);
|
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[]>([]);
|
const tops = ref<number[]>([]);
|
||||||
|
|
||||||
@@ -418,6 +536,42 @@ function getTops() {
|
|||||||
tops.value = arr;
|
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;
|
let scrollLock = false;
|
||||||
|
|
||||||
@@ -435,6 +589,13 @@ function onScroll(e: UniScrollEvent) {
|
|||||||
activeIndex.value = index;
|
activeIndex.value = index;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
emit("scroll", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动结束事件处理函数
|
||||||
|
function onScrollEnd(e: UniScrollEvent) {
|
||||||
|
emit("scrollend", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 行点击事件处理函数
|
// 行点击事件处理函数
|
||||||
@@ -456,6 +617,31 @@ function onIndexChange(index: number) {
|
|||||||
}, 300);
|
}, 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() {
|
function getScrollerHeight() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -493,7 +679,8 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
data
|
data,
|
||||||
|
stopRefresh
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -516,10 +703,7 @@ defineExpose({
|
|||||||
|
|
||||||
&__index {
|
&__index {
|
||||||
@apply flex flex-row items-center bg-white;
|
@apply flex flex-row items-center bg-white;
|
||||||
@apply absolute top-0 left-0 w-full;
|
@apply absolute top-0 left-0 w-full px-[20rpx] z-20;
|
||||||
top: 0px;
|
|
||||||
padding: 0 20rpx;
|
|
||||||
z-index: 11;
|
|
||||||
|
|
||||||
&.is-dark {
|
&.is-dark {
|
||||||
@apply bg-surface-600 border-none;
|
@apply bg-surface-600 border-none;
|
||||||
@@ -531,10 +715,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
@apply flex flex-row items-center;
|
@apply flex flex-row items-center relative px-[20rpx] z-10;
|
||||||
padding: 0 20rpx;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
@@ -542,5 +723,9 @@ defineExpose({
|
|||||||
@apply flex flex-row items-center px-[20rpx] h-full;
|
@apply flex flex-row items-center px-[20rpx] h-full;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__refresher {
|
||||||
|
@apply flex flex-row items-center justify-center w-full h-full;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,4 +16,12 @@ export type ClListViewProps = {
|
|||||||
bottomHeight?: number;
|
bottomHeight?: number;
|
||||||
bufferSize?: number;
|
bufferSize?: number;
|
||||||
virtual?: boolean;
|
virtual?: boolean;
|
||||||
|
// 下拉刷新相关属性
|
||||||
|
refresherEnabled?: boolean;
|
||||||
|
refresherThreshold?: number;
|
||||||
|
refresherTriggered?: boolean;
|
||||||
|
refresherBackground?: string;
|
||||||
|
refresherDefaultText?: string;
|
||||||
|
refresherPullingText?: string;
|
||||||
|
refresherRefreshingText?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { computed, ref, type ComputedRef } from "vue";
|
|||||||
import type { ClFormRule, ClFormValidateError } from "../types";
|
import type { ClFormRule, ClFormValidateError } from "../types";
|
||||||
import { useParent } from "@/cool";
|
import { useParent } from "@/cool";
|
||||||
|
|
||||||
class UseForm {
|
class Form {
|
||||||
public formRef = ref<ClFormComponentPublicInstance | null>(null);
|
public formRef = ref<ClFormComponentPublicInstance | null>(null);
|
||||||
public disabled: ComputedRef<boolean>;
|
public disabled: ComputedRef<boolean>;
|
||||||
|
|
||||||
@@ -82,5 +82,5 @@ class UseForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useForm() {
|
export function useForm() {
|
||||||
return new UseForm();
|
return new Form();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,25 @@ class Ui {
|
|||||||
instance.showToast(options);
|
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
uni_modules/cool-ui/types/component.d.ts
vendored
1
uni_modules/cool-ui/types/component.d.ts
vendored
@@ -128,6 +128,7 @@ declare type ClListItemComponentPublicInstance = {
|
|||||||
|
|
||||||
declare type ClListViewComponentPublicInstance = {
|
declare type ClListViewComponentPublicInstance = {
|
||||||
data: ClListViewItem[];
|
data: ClListViewItem[];
|
||||||
|
stopRefresh: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type ClCascaderComponentPublicInstance = {
|
declare type ClCascaderComponentPublicInstance = {
|
||||||
|
|||||||
Reference in New Issue
Block a user