新增:列表增加下拉刷新和加载更多功能
This commit is contained in:
parent
ae495ef9ab
commit
3117e34238
@ -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',
|
||||||
|
|||||||
@ -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>
|
||||||
<div class="home-list">
|
<!-- 可滚动容器作为 v-pull-to-refresh 的父元素,组件据此判断 scrollTop 仅在顶部时才响应下拉 -->
|
||||||
<MarketCard v-for="value in 30" :key="value"></MarketCard>
|
<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">
|
||||||
|
<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 */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user