mrr.sj.front/components/common/CommonList.vue

232 lines
6.2 KiB
Vue
Raw Normal View History

2026-03-24 11:45:13 +08:00
<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=1limit=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 20rpx;
}
/* 加载提示 */
.load-tips {
text-align: center;
font-size: 24rpx;
color: #cecece;
padding: 24rpx 0;
margin-top: 30rpx;
}
</style>