links infinite scroll
This commit is contained in:
@@ -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 () => {
|
||||
try {
|
||||
const fetchLinks = useCallback(async (pageNum: number, isInitialLoad: boolean = false) => {
|
||||
try {
|
||||
if (isInitialLoad) {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
} else {
|
||||
setIsLoadingMore(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();
|
||||
// 获取链接列表
|
||||
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();
|
||||
}
|
||||
};
|
||||
}, [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 */}
|
||||
|
||||
Reference in New Issue
Block a user