新增:列表增加下拉刷新和加载更多功能

This commit is contained in:
ivan 2026-02-08 14:39:33 +08:00
parent ae495ef9ab
commit 3117e34238
2 changed files with 117 additions and 4 deletions

View File

@ -1,11 +1,15 @@
import { createVuetify } from 'vuetify' import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components' import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives' import * as directives from 'vuetify/directives'
import { VPullToRefresh } from 'vuetify/labs/components'
import 'vuetify/styles' import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css' import '@mdi/font/css/materialdesignicons.css'
export default createVuetify({ export default createVuetify({
components, components: {
...components,
VPullToRefresh,
},
directives, directives,
theme: { theme: {
defaultTheme: 'light', defaultTheme: 'light',

View File

@ -7,17 +7,77 @@
<v-tab value="portfolio">Portfolio</v-tab> <v-tab value="portfolio">Portfolio</v-tab>
</v-tabs> </v-tabs>
</v-row> </v-row>
<!-- 可滚动容器作为 v-pull-to-refresh 的父元素组件据此判断 scrollTop 仅在顶部时才响应下拉 -->
<div ref="scrollRef" class="home-list-scroll">
<v-pull-to-refresh class="pull-to-refresh" @load="onRefresh">
<div class="pull-to-refresh-inner">
<div class="home-list"> <div class="home-list">
<MarketCard v-for="value in 30" :key="value"></MarketCard> <MarketCard v-for="id in listLength" :key="id" :id="String(id)" />
</div>
<div class="load-more-footer">
<div ref="sentinelRef" class="load-more-sentinel" aria-hidden="true" />
<div v-if="loadingMore" class="load-more-indicator">
<v-progress-circular indeterminate size="24" width="2" />
<span>加载中...</span>
</div>
<div v-else-if="listLength >= maxItems" class="no-more-tip">没有更多了</div>
</div>
</div>
</v-pull-to-refresh>
</div> </div>
</v-container> </v-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import MarketCard from '../components/MarketCard.vue' import MarketCard from '../components/MarketCard.vue'
const activeTab = ref('overview') const activeTab = ref('overview')
const INITIAL_COUNT = 10
const PAGE_SIZE = 10
const maxItems = 50
const listLength = ref(INITIAL_COUNT)
const loadingMore = ref(false)
const scrollRef = ref<HTMLElement | null>(null)
const sentinelRef = ref<HTMLElement | null>(null)
let observer: IntersectionObserver | null = null
function onRefresh({ done }: { done: () => void }) {
setTimeout(() => {
listLength.value = INITIAL_COUNT
done()
}, 600)
}
function loadMore() {
if (loadingMore.value || listLength.value >= maxItems) return
loadingMore.value = true
setTimeout(() => {
listLength.value = Math.min(listLength.value + PAGE_SIZE, maxItems)
loadingMore.value = false
}, 400)
}
onMounted(() => {
nextTick(() => {
if (!sentinelRef.value || !scrollRef.value) return
observer = new IntersectionObserver(
(entries) => {
if (!entries[0]?.isIntersecting) return
loadMore()
},
{ root: scrollRef.value, rootMargin: '80px', threshold: 0 }
)
observer.observe(sentinelRef.value)
})
})
onUnmounted(() => {
if (observer && sentinelRef.value) observer.unobserve(sentinelRef.value)
observer = null
})
</script> </script>
<style scoped> <style scoped>
@ -36,11 +96,60 @@ const activeTab = ref('overview')
margin: 0; margin: 0;
} }
.pull-to-refresh {
width: 100%;
margin-top: 8px;
min-height: 100%;
}
.pull-to-refresh-inner {
min-height: 100%;
}
.home-list-scroll {
width: 100%;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
max-height: calc(100vh - 64px - 48px - 32px);
}
.home-list { .home-list {
width: 100%; width: 100%;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px; gap: 20px;
padding-bottom: 8px;
}
.load-more-footer {
min-height: 56px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.load-more-sentinel {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
pointer-events: none;
opacity: 0;
}
.load-more-indicator,
.no-more-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
color: #666;
font-size: 14px;
} }
/* When only one column fits */ /* When only one column fits */