100 lines
2.8 KiB
Vue
100 lines
2.8 KiB
Vue
<script setup>
|
||
import { postApi } from '@/api/postApi'
|
||
import ArticleListItem from '@/components/ArticleListItem.vue'
|
||
import { useIntersectionObserver } from '@vueuse/core'
|
||
import { onMounted, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
|
||
const router = useRouter()
|
||
const searchInput = ref('')
|
||
const loadingTrigger = ref(null)
|
||
const articles = ref([])
|
||
const loading = ref(false)
|
||
const hasMore = ref(true)
|
||
const currentPage = ref(1)
|
||
const limit = 10
|
||
|
||
const fetchArticles = async () => {
|
||
if (loading.value) return
|
||
loading.value = true
|
||
try {
|
||
const { data } = await postApi.list({
|
||
page: currentPage.value,
|
||
limit,
|
||
keyword: searchInput.value
|
||
})
|
||
if (data.items?.length === 0) {
|
||
hasMore.value = false
|
||
} else {
|
||
articles.value.push(...data.items)
|
||
currentPage.value += 1
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch articles:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
useIntersectionObserver(
|
||
loadingTrigger,
|
||
([{ isIntersecting }]) => {
|
||
if (isIntersecting && !loading.value && hasMore.value) {
|
||
fetchArticles()
|
||
}
|
||
},
|
||
{
|
||
threshold: 0,
|
||
rootMargin: '100px'
|
||
}
|
||
)
|
||
|
||
|
||
const handleSearch = () => {
|
||
articles.value = []
|
||
currentPage.value = 1
|
||
hasMore.value = true
|
||
fetchArticles()
|
||
}
|
||
|
||
const handleKeyup = (e) => {
|
||
if (e.key === 'Enter') {
|
||
handleSearch()
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchArticles()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="h-full flex flex-col">
|
||
<div class="flex-none bg-white border-b border-gray-200 z-50 shadow">
|
||
<div class="p-4">
|
||
<input type="search" v-model="searchInput" @keyup="handleKeyup" placeholder="搜索文章"
|
||
class="w-full px-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto">
|
||
<div class="p-4">
|
||
<ArticleListItem v-for="article in articles" :key="article.id" :article="article" class="mb-4" />
|
||
|
||
<div ref="loadingTrigger" class="py-4 text-center" v-show="hasMore || loading">
|
||
<div v-if="loading"
|
||
class="animate-spin rounded-full h-8 w-8 border-4 border-gray-200 border-t-blue-600 mx-auto">
|
||
</div>
|
||
<div v-else class="h-8">
|
||
<!-- 空白占位,保持触发器可见 -->
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!hasMore && articles.length > 0" class="text-center text-gray-500 py-4">
|
||
没有更多文章了
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|