diff --git a/app/links/page.tsx b/app/links/page.tsx index cbc2b16..550f6ad 100644 --- a/app/links/page.tsx +++ b/app/links/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import CreateLinkModal from '../components/ui/CreateLinkModal'; import { Link, StatsOverview, Tag } from '../api/types'; @@ -48,6 +48,13 @@ export default function LinksPage() { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + // 无限加载相关状态 + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const observer = useRef(null); + const lastLinkElementRef = useRef(null); + // 映射API数据到UI所需格式的函数 const mapApiLinkToUiLink = (apiLink: Link): UILink => { // 生成短URL显示 - 因为数据库中没有short_url字段 @@ -91,49 +98,116 @@ export default function LinksPage() { }; // 获取链接数据 - useEffect(() => { - const fetchLinks = async () => { - try { + const fetchLinks = useCallback(async (pageNum: number, isInitialLoad: boolean = false) => { + try { + if (isInitialLoad) { setIsLoading(true); - setError(null); - - // 获取链接列表 - const linksResponse = await fetch('/api/links'); - if (!linksResponse.ok) { - throw new Error(`Failed to fetch links: ${linksResponse.statusText}`); - } - const linksData = await linksResponse.json(); - - // 获取标签列表 + } else { + setIsLoadingMore(true); + } + setError(null); + + // 获取链接列表 + const linksResponse = await fetch(`/api/links?page=${pageNum}&limit=20${searchQuery ? `&search=${encodeURIComponent(searchQuery)}` : ''}`); + if (!linksResponse.ok) { + throw new Error(`Failed to fetch links: ${linksResponse.statusText}`); + } + const linksData = await linksResponse.json(); + + const uiLinks = linksData.data.map(mapApiLinkToUiLink); + + if (isInitialLoad) { + setLinks(uiLinks); + } else { + setLinks(prevLinks => [...prevLinks, ...uiLinks]); + } + + // 检查是否还有更多数据可加载 + const { pagination } = linksData; + setHasMore(pagination.page < pagination.totalPages); + + if (isInitialLoad) { + // 只在初始加载时获取标签和统计数据 const tagsResponse = await fetch('/api/tags'); if (!tagsResponse.ok) { throw new Error(`Failed to fetch tags: ${tagsResponse.statusText}`); } const tagsData = await tagsResponse.json(); - // 获取统计数据 const statsResponse = await fetch('/api/stats'); if (!statsResponse.ok) { throw new Error(`Failed to fetch stats: ${statsResponse.statusText}`); } const statsData = await statsResponse.json(); - // 处理并设置数据 - const uiLinks = linksData.data.map(mapApiLinkToUiLink); - setLinks(uiLinks); setAllTags(tagsData); setStats(statsData); - - } catch (err) { - console.error('Data loading failed:', err); - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { + } + + } catch (err) { + console.error('Data loading failed:', err); + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + if (isInitialLoad) { setIsLoading(false); + } else { + setIsLoadingMore(false); + } + } + }, [searchQuery]); + + // 初始加载 + useEffect(() => { + setPage(1); + fetchLinks(1, true); + }, [fetchLinks]); + + // 搜索过滤变化时重新加载数据 + useEffect(() => { + // 当搜索关键词变化时,重置页码和链接列表,然后重新获取数据 + setLinks([]); + setPage(1); + fetchLinks(1, true); + }, [searchQuery, fetchLinks]); + + // 设置Intersection Observer来检测滚动并加载更多数据 + useEffect(() => { + // 如果正在加载或没有更多数据,则不设置observer + if (isLoading || isLoadingMore || !hasMore) return; + + // 断开之前的observer连接 + if (observer.current) { + observer.current.disconnect(); + } + + observer.current = new IntersectionObserver(entries => { + if (entries[0].isIntersecting && hasMore) { + // 当最后一个元素可见且有更多数据时,加载下一页 + setPage(prevPage => prevPage + 1); + } + }, { + root: null, + rootMargin: '0px', + threshold: 0.5 + }); + + if (lastLinkElementRef.current) { + observer.current.observe(lastLinkElementRef.current); + } + + return () => { + if (observer.current) { + observer.current.disconnect(); } }; - - fetchLinks(); - }, []); + }, [isLoading, isLoadingMore, hasMore, links]); + + // 当页码变化时加载更多数据 + useEffect(() => { + if (page > 1) { + fetchLinks(page, false); + } + }, [page, fetchLinks]); const filteredLinks = links.filter(link => link.name.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -152,14 +226,8 @@ export default function LinksPage() { console.log('创建链接:', linkData); // 刷新链接列表 - const response = await fetch('/api/links'); - if (!response.ok) { - throw new Error(`刷新链接列表失败: ${response.statusText}`); - } - - const newData = await response.json(); - const uiLinks = newData.data.map(mapApiLinkToUiLink); - setLinks(uiLinks); + setPage(1); + fetchLinks(1, true); setShowCreateModal(false); } catch (err) { @@ -309,8 +377,8 @@ export default function LinksPage() { {/* Links Table */}
- - +
+ @@ -323,28 +391,20 @@ export default function LinksPage() { - - {isLoading && links.length === 0 ? ( - - - - ) : filteredLinks.length === 0 ? ( - - + {filteredLinks.length === 0 ? ( + + ) : ( - filteredLinks.map((link) => ( + filteredLinks.map((link, index) => ( handleOpenLinkDetails(link.id)} + className="transition-colors cursor-pointer hover:bg-card-bg-secondary" + ref={index === filteredLinks.length - 1 ? lastLinkElementRef : null} >
Link Info Visits
-
-
- Loading... -
-
- No links found matching your search criteria +
+ No links found. Create one to get started.
{link.name}
@@ -436,6 +496,21 @@ export default function LinksPage() {
+ + {/* Loading more indicator */} + {isLoadingMore && ( +
+
+

Loading more links...

+
+ )} + + {/* End of results message */} + {!hasMore && links.length > 0 && ( +
+ No more links to load. +
+ )}
{/* Tags Section */}