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

232 lines
6.2 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>