ch-xymy-h5/src/components/BaseList.vue
2026-07-23 08:57:01 +08:00

145 lines
3.8 KiB
Vue

<template>
<div class="base_list" ref="listRef">
<div v-if="loading && list.length === 0" class="base_list__loading">
<van-loading size="24px">加载中...</van-loading>
</div>
<template v-for="(item, index) in list" :key="item.id">
<slot :item="item" :index="index" />
</template>
<div v-if="loading && list.length > 0" class="base_list__loading">
<van-loading size="24px">加载中...</van-loading>
</div>
<div v-if="finished && list.length > 0" class="base_list__finished">{{ finishedText }}</div>
<div v-if="finished && list.length === 0 && !loading" class="base_list__empty">暂无数据</div>
<div ref="base_list__sentinel" class="base_list__sentinel"></div>
</div>
</template>
<script>
export default {
name: 'BaseList',
props: {
url: { type: String, required: true },
method: { type: String, default: 'get' },
params: { type: Object, default: () => ({}) },
pageSize: { type: Number, default: 10 },
finishedText: { type: String, default: '没有更多了' },
parseData: { type: Function, default: (res) => res.data.items },
},
data() {
return {
loading: false,
finished: false,
list: [],
page: 1,
requestId: 0,
observer: null,
}
},
emits: ['update:list', 'load', 'refresh'],
expose: ['refresh'],
watch: {
url() {
this.refresh()
},
params: {
handler() {
if (this._skipWatch) return
this.refresh()
},
deep: true
}
},
mounted() {
this.$nextTick(() => {
this.observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !this.loading && !this.finished) {
this.loadMore()
}
}, { rootMargin: '100px' })
if (this.$refs.base_list__sentinel) {
this.observer.observe(this.$refs.base_list__sentinel)
}
})
},
methods: {
loadMore() {
if (this.loading) return
const currentRequestId = this.requestId
this.loading = true
const request = this.method === 'get' ? this.$get : this.$post
request(this.url, { page: this.page, pageSize: this.pageSize, ...this.params })
.then(res => {
if (currentRequestId !== this.requestId) return
const data = this.parseData(res)
const safeData = data || []
if (this.page === 1) {
this.list = safeData
} else {
this.list.push(...safeData)
}
this.page++
this.finished = safeData.length < this.pageSize
this.$emit('update:list', this.list)
this.$emit('load', this.list)
})
.catch(err => {
if (currentRequestId !== this.requestId) return
this.$showFailToast(err.message || '加载失败')
})
.finally(() => { this.loading = false })
},
refresh() {
this.requestId++
this.list = []
this.page = 1
this.finished = false
this.loading = false
this._skipWatch = true
this.loadMore()
this.$emit('refresh', this.list)
// 等 params 的 deep watcher 排队回调跑完再解锁,避免同一轮 tick 里被再触发一次
this.$nextTick(() => { this._skipWatch = false })
},
},
}
</script>
<style scoped>
.base_list {
min-height: 200px;
display: flex;
flex-direction: column;
}
.base_list__loading {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
width: 100%;
box-sizing: border-box;
}
.base_list__loading :deep(.van-loading) {
display: inline-flex;
align-items: center;
flex-direction: row;
white-space: nowrap;
float: none !important;
}
.base_list__loading :deep(.van-loading__text) {
float: none !important;
}
.base_list__finished,
.base_list__empty {
width: 100%;
text-align: center;
padding: 15px;
color: #969799;
font-size: 14px;
}
</style>