232 lines
6.2 KiB
Vue
232 lines
6.2 KiB
Vue
<template>
|
||
<view class="common-list">
|
||
<!-- 头部插槽:父组件可插入独立的Tab、筛选组件等 -->
|
||
<slot name="header"></slot>
|
||
|
||
<!-- 核心列表区域(scroll-view 实现刷新加载) -->
|
||
<scroll-view ref="listScrollRef" :scroll-top="scrollTop" class="list-scroll" scroll-y
|
||
:style="{ height: listScrollHeight }" :refresher-enabled="refresherEnabled" :refresher-triggered="isRefreshing"
|
||
@refresherrefresh="handleRefresh" @scrolltolower="handleLoadMore" :show-scrollbar="false"
|
||
:refresher-threshold="20" lower-threshold="130">
|
||
<!-- 滑动区域头部插槽:父组件可插入独立的Tab、筛选组件等 -->
|
||
<slot name="headerScroll"></slot>
|
||
<!-- 列表项插槽:父组件自定义列表内容 -->
|
||
<view class="list-container" v-if="listData.length">
|
||
<slot name="listData" :list="listData"></slot>
|
||
</view>
|
||
|
||
<!-- 空数据插槽:父组件自定义空状态 -->
|
||
<slot name="empty" v-else></slot>
|
||
|
||
<!-- 加载提示 -->
|
||
<view class="load-tips" v-if="listData.length">
|
||
<text v-if="hasMore && !isLoading">{{ loadingText }}</text>
|
||
<text v-else>{{ !hasMore ? noMoreText : "已加载完成" }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import request from "@/utils/request";
|
||
|
||
export default {
|
||
name: "CommonList",
|
||
props: {
|
||
//是否开启下拉刷新
|
||
refresherEnabled: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
// 必需:列表数据请求地址
|
||
apiUrl: {
|
||
type: String,
|
||
required: true,
|
||
default: "",
|
||
},
|
||
// 初始查询参数(如分页、筛选条件等,由父组件传入)
|
||
initialQuery: {
|
||
type: Object,
|
||
default: () => ({}),
|
||
},
|
||
// 列表滚动高度(父组件根据布局动态计算)
|
||
listScrollHeight: {
|
||
type: String,
|
||
default: "calc(100vh - 200rpx)",
|
||
},
|
||
// 自定义提示文本(可选)
|
||
loadingText: {
|
||
type: String,
|
||
default: "加载中...",
|
||
},
|
||
noMoreText: {
|
||
type: String,
|
||
default: "~暂时只有这些了~",
|
||
},
|
||
// 分页配置(可选,默认page=1,limit=10)
|
||
pageConfig: {
|
||
type: Object,
|
||
default: () => ({
|
||
pageKey: "page", // 接口分页参数名(如“page”)
|
||
limitKey: "limit", // 接口每页条数参数名(如“limit”)
|
||
page: 1, // 初始页码
|
||
limit: 10, // 每页条数
|
||
}),
|
||
},
|
||
// 接口数据结构映射(适配不同接口返回格式)
|
||
responseMap: {
|
||
type: Object,
|
||
default: () => ({
|
||
list: "data", // 列表数据在响应中的字段名
|
||
total: "count", // 总条数在响应中的字段名
|
||
}),
|
||
},
|
||
},
|
||
data() {
|
||
const {
|
||
pageKey,
|
||
limitKey,
|
||
page,
|
||
limit
|
||
} = this.pageConfig;
|
||
return {
|
||
queryData: {
|
||
[pageKey]: page,
|
||
[limitKey]: limit,
|
||
...this.initialQuery,
|
||
},
|
||
scrollTop: 0,
|
||
listData: [], // 列表数据
|
||
isRefreshing: false, // 下拉刷新状态(控制动画)
|
||
isLoading: false, // 加载中状态(防止重复请求)
|
||
hasMore: true, // 是否有更多数据
|
||
};
|
||
},
|
||
created() {
|
||
// 初始化加载列表
|
||
// this.fetchListData();
|
||
},
|
||
methods: {
|
||
/** 核心:请求列表数据 */
|
||
async fetchListData() {
|
||
// 防止重复请求
|
||
if (this.isLoading) return;
|
||
this.isLoading = true;
|
||
|
||
try {
|
||
const res = await request.post(this.apiUrl, this.queryData);
|
||
const {
|
||
list: listField,
|
||
total: totalField
|
||
} = this.responseMap;
|
||
// 从响应中提取列表数据和总条数(适配不同接口格式)
|
||
|
||
let list = res[listField] || [];
|
||
let total = res[totalField] || 0;
|
||
if (list.list) {
|
||
total = list[totalField] || 0;
|
||
list = list.list || [];
|
||
}
|
||
|
||
// 分页逻辑:第一页重置,后续页追加
|
||
if (this.queryData[this.pageConfig.pageKey] === 1) {
|
||
this.scrollToTop();
|
||
this.listData = list;
|
||
} else {
|
||
this.listData = [...this.listData, ...list];
|
||
}
|
||
// 判断是否有更多数据(当前列表长度 >= 总条数则无更多)
|
||
this.hasMore = this.listData.length < total;
|
||
|
||
// 向父组件传递加载成功事件
|
||
this.$emit("load-success", {
|
||
list: this.listData,
|
||
total,
|
||
currentPage: this.queryData[this.pageConfig.pageKey],
|
||
});
|
||
this.$emit("load-success-all", res);
|
||
} catch (err) {
|
||
this.$emit("load-fail", err);
|
||
console.error("列表请求失败:", err);
|
||
} finally {
|
||
// 关键修复:用$nextTick确保状态更新同步到DOM
|
||
this.$nextTick(() => {
|
||
this.isRefreshing = false; // 结束下拉刷新动画
|
||
this.isLoading = false; // 结束加载状态
|
||
});
|
||
}
|
||
},
|
||
|
||
/** 下拉刷新:重置页码为1,重新加载 */
|
||
handleRefresh() {
|
||
// 关键修复:明确开启刷新状态(与scroll-view双向绑定同步)
|
||
this.isRefreshing = true;
|
||
// 重置页码为1
|
||
this.queryData[this.pageConfig.pageKey] = 1;
|
||
// 触发数据请求
|
||
this.fetchListData();
|
||
},
|
||
|
||
/** 上拉加载更多:页码+1,追加数据 */
|
||
handleLoadMore() {
|
||
// 无更多数据/正在加载/正在刷新时,不触发加载
|
||
if (!this.hasMore || this.isLoading || this.isRefreshing) return;
|
||
this.queryData[this.pageConfig.pageKey] += 1;
|
||
this.fetchListData();
|
||
},
|
||
|
||
/** 对外暴露:手动刷新列表(父组件可通过ref调用) */
|
||
manualRefresh(newQuery = {}) {
|
||
this.queryData = {
|
||
...this.queryData,
|
||
[this.pageConfig.pageKey]: 1, // 重置为第一页
|
||
...newQuery,
|
||
};
|
||
this.fetchListData();
|
||
},
|
||
|
||
/** 对外暴露:清空列表数据 */
|
||
clearList() {
|
||
this.listData = [];
|
||
this.queryData[this.pageConfig.pageKey] = 1;
|
||
this.hasMore = true;
|
||
},
|
||
/** 工具方法:滚动到顶部(复用逻辑) */
|
||
scrollToTop() {
|
||
if (this.scrollTop == 0) {
|
||
this.scrollTop = 0.1;
|
||
} else {
|
||
this.scrollTop = 0;
|
||
}
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.common-list {
|
||
width: 100%;
|
||
background-color: #fff;
|
||
padding-bottom: 30rpx;
|
||
}
|
||
|
||
/* 列表滚动容器 */
|
||
.list-scroll {
|
||
width: 100%;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
/* 列表容器 */
|
||
.list-container {
|
||
padding: 0 24rpx;
|
||
}
|
||
|
||
/* 加载提示 */
|
||
.load-tips {
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: #cecece;
|
||
padding: 24rpx 0;
|
||
margin-top: 30rpx;
|
||
}
|
||
</style> |