links infinite scroll

This commit is contained in:
2025-03-21 23:51:09 +08:00
parent bf3bdc63f5
commit 6d48b53cba

View File

@@ -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<string | null>(null);
// 无限加载相关状态
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const observer = useRef<IntersectionObserver | null>(null);
const lastLinkElementRef = useRef<HTMLTableRowElement | null>(null);
// 映射API数据到UI所需格式的函数
const mapApiLinkToUiLink = (apiLink: Link): UILink => {
// 生成短URL显示 - 因为数据库中没有short_url字段
@@ -91,49 +98,116 @@ export default function LinksPage() {
};
// 获取链接数据
useEffect(() => {
const fetchLinks = async () => {
const fetchLinks = useCallback(async (pageNum: number, isInitialLoad: boolean = false) => {
try {
if (isInitialLoad) {
setIsLoading(true);
} else {
setIsLoadingMore(true);
}
setError(null);
// 获取链接列表
const linksResponse = await fetch('/api/links');
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 {
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();
}
};
}, [isLoading, isLoadingMore, hasMore, links]);
fetchLinks();
}, []);
// 当页码变化时加载更多数据
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 */}
<div className="overflow-hidden border rounded-lg shadow bg-card-bg border-card-border">
<div className="overflow-x-auto">
<table className="w-full text-sm text-left text-text-secondary">
<thead className="text-xs uppercase border-b bg-card-bg/60 text-text-secondary border-card-border">
<table className="min-w-full divide-y divide-card-border">
<thead className="bg-card-bg-secondary">
<tr>
<th scope="col" className="px-6 py-3">Link Info</th>
<th scope="col" className="px-6 py-3">Visits</th>
@@ -323,28 +391,20 @@ export default function LinksPage() {
</th>
</tr>
</thead>
<tbody>
{isLoading && links.length === 0 ? (
<tr className="border-b bg-card-bg border-card-border">
<td colSpan={7} className="px-6 py-4 text-center text-text-secondary">
<div className="flex items-center justify-center">
<div className="w-6 h-6 border-2 rounded-full border-accent-blue border-t-transparent animate-spin"></div>
<span className="ml-2">Loading...</span>
</div>
</td>
</tr>
) : filteredLinks.length === 0 ? (
<tr className="border-b bg-card-bg border-card-border">
<td colSpan={7} className="px-6 py-4 text-center text-text-secondary">
No links found matching your search criteria
<tbody className="divide-y divide-card-border">
{filteredLinks.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-12 text-center text-text-secondary">
No links found. Create one to get started.
</td>
</tr>
) : (
filteredLinks.map((link) => (
filteredLinks.map((link, index) => (
<tr
key={link.id}
className="border-b cursor-pointer bg-card-bg border-card-border hover:bg-card-bg/80"
onClick={() => handleOpenLinkDetails(link.id)}
className="transition-colors cursor-pointer hover:bg-card-bg-secondary"
ref={index === filteredLinks.length - 1 ? lastLinkElementRef : null}
>
<td className="px-6 py-4">
<div className="font-medium text-foreground">{link.name}</div>
@@ -436,6 +496,21 @@ export default function LinksPage() {
</tbody>
</table>
</div>
{/* Loading more indicator */}
{isLoadingMore && (
<div className="p-4 text-center">
<div className="inline-block w-6 h-6 border-2 rounded-full border-accent-blue border-t-transparent animate-spin"></div>
<p className="mt-2 text-sm text-text-secondary">Loading more links...</p>
</div>
)}
{/* End of results message */}
{!hasMore && links.length > 0 && (
<div className="p-4 text-center text-sm text-text-secondary">
No more links to load.
</div>
)}
</div>
{/* Tags Section */}