Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AI_Document.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

## 概述

\n---\n\n# 统一分页逻辑重构记录 (2025-09-21)\n\n## 背景\n原 `News.tsx` 中直接内联了分页逻辑(当前页、计算切片、页码按钮、滚动处理等)。`Them` 组件(友链列表)需要同样的 20 条/页分页功能,如果继续复制粘贴会造成:\n- 逻辑重复(修改规则需同步两处)。\n- 维护成本增加。\n- 单元测试或未来抽象难度增大。\n\n## 重构目标\n1. 提供一个通用分页逻辑 Hook:`usePagination`。\n2. 提供一个可复用的分页 UI:`<Pagination />`。\n3. `News` 与 `Them` 共用,不再出现分页实现重复代码。\n4. 维持原有交互体验(平滑滚动、页码省略号规则、上一页/下一页)。\n\n## 新增文件\n| 文件 | 说明 |
GitHub Pages 支持 React Router,但需要特殊配置来处理客户端路由。本项目已配置完成,支持以下路由:

- `/` - 友链列表页(原首页内容)
- `/home` - 主页
- `/about` - 关于页面

## 实现原理

### 问题
GitHub Pages 是静态文件托管服务,当用户直接访问 `/home` 或 `/about` 时,服务器会寻找对应的物理文件,但这些路由是由 React Router 在客户端处理的,不存在实际的文件,因此会返回 404 错误。
Expand Down
11 changes: 9 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import './App.css'
import blogsData from './assets/blogs.json'
import { resolveAvatar } from './services/avatarService'
import News from './components/News'
import { usePagination } from './hooks/usePagination';
import { Pagination } from './components/Pagination';
import { Footer } from './components/Footer'


Expand All @@ -17,10 +19,14 @@ interface Blog {
const blogs: Blog[] = blogsData as Blog[];

function Them() {
const { currentItems, currentPage, totalPages, startIndex, endIndex, totalItems, setPage } = usePagination(blogs, 20);
return (
<div className='container mx-auto flex flex-col gap-4'>
<div className='text-center text-sm text-gray-500 mb-2'>
显示第 {startIndex + 1} - {endIndex} 项,共 {totalItems} 项
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{blogs.map((blog) => (
{currentItems.map((blog) => (
<Card key={blog.name} className="w-full">
<div className="flex flex-col items-center justify-center gap-3">
<div className="w-64 aspect-square overflow-hidden rounded-lg">
Expand All @@ -42,8 +48,9 @@ function Them() {
</Card>
))}
</div>
<Pagination currentPage={currentPage} totalPages={totalPages} onChange={setPage} />
</div>
)
);
}

function App() {
Expand Down
2 changes: 1 addition & 1 deletion src/assets/blogs.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"name": "littlecheesecake",
"url": "https://littlecheesecake.me/",
"url": "https://littlecheesecake.me/blog2/index.html",
"describe": "littlecheesecake的个人博客",
"avatar": "avatar/littlecheesecake.webp"
},
Expand Down
83 changes: 7 additions & 76 deletions src/components/News.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState } from 'react';
import rawItemData from '../assets/items.json'
import rawBlogData from '../assets/blogs.json'
import type { Blog } from '../App';
import { Link } from 'react-router';
import { resolveAvatar } from '../services/avatarService'

import { Card, Button } from '@radix-ui/themes';
import { Card } from '@radix-ui/themes';
import { usePagination } from '../hooks/usePagination';
import { Pagination } from './Pagination';

interface Item {
blog_id: string;
Expand All @@ -26,35 +27,15 @@ const blogs: Blog[] = rawBlogData as Blog[];
const blogMap: Record<string, Blog> = {};

function News() {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 20;
blogs.forEach(blog => { blogMap[blog.name] = blog; });

blogs.forEach(blog => {
blogMap[blog.name] = blog;
});

// 计算总页数
const totalPages = Math.ceil(items.length / itemsPerPage);

// 计算当前页的数据范围
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentItems = items.slice(startIndex, endIndex);

const handlePageChange = (page: number) => {
setCurrentPage(page);
// 滚动到页面顶部
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const { currentItems, currentPage, totalPages, startIndex, endIndex, totalItems, setPage } = usePagination(items, 20);

return (
<div className='container mx-auto flex flex-col gap-4'>
{/* 页面信息 */}
<div className='text-center text-sm text-gray-500 mb-4'>
显示第 {startIndex + 1} - {Math.min(endIndex, items.length)} 项,共 {items.length} 项
显示第 {startIndex + 1} - {endIndex} 项,共 {totalItems} 项
</div>

{/* 博客列表 */}
{currentItems.map(item => (
<Card key={item.item_url}>
<div className='flex flex-row gap-2'>
Expand All @@ -72,57 +53,7 @@ function News() {
</div>
</Card>
))}

{/* 分页导航 */}
<div className="flex justify-center items-center gap-2 mt-8 mb-4">
{/* 上一页按钮,所有屏幕都显示 */}
<Button
variant="outline"
disabled={currentPage === 1}
onClick={() => handlePageChange(currentPage - 1)}
>
上一页
</Button>
{/* 只在大屏显示页码按钮 */}
<div className="hidden sm:flex items-center gap-2">
{/* 显示前几页 */}
{currentPage > 3 && (
<>
<Button variant="outline" onClick={() => handlePageChange(1)}>1</Button>
{currentPage > 4 && <span className='text-gray-500'>...</span>}
</>
)}
{/* 显示当前页周围的页码 */}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const page = Math.max(1, Math.min(totalPages - 4, currentPage - 2)) + i;
if (page > totalPages) return null;
return (
<Button
key={page}
variant={page === currentPage ? "solid" : "outline"}
onClick={() => handlePageChange(page)}
>
{page}
</Button>
);
})}
{/* 显示后几页 */}
{currentPage < totalPages - 2 && (
<>
{currentPage < totalPages - 3 && <span className='text-gray-500'>...</span>}
<Button variant="outline" onClick={() => handlePageChange(totalPages)}>{totalPages}</Button>
</>
)}
</div>
{/* 下一页按钮,所有屏幕都显示 */}
<Button
variant="outline"
disabled={currentPage === totalPages}
onClick={() => handlePageChange(currentPage + 1)}
>
下一页
</Button>
</div>
<Pagination currentPage={currentPage} totalPages={totalPages} onChange={setPage} />
</div>
);
}
Expand Down
50 changes: 50 additions & 0 deletions src/components/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Button } from '@radix-ui/themes';
import React from 'react';

interface PaginationProps {
currentPage: number;
totalPages: number;
onChange: (page: number) => void;
}

/**
* Responsive pagination control replicating previous News component style.
*/
export const Pagination: React.FC<PaginationProps> = ({ currentPage, totalPages, onChange }) => {
if (totalPages <= 1) return null;

const go = (p: number) => () => onChange(p);

// Compute window of up to 5 pages around current page (with shifting for edges)
const pages: number[] = [];
const windowSize = Math.min(5, totalPages);
const start = Math.max(1, Math.min(totalPages - windowSize + 1, currentPage - Math.floor(windowSize / 2)));
for (let i = 0; i < windowSize; i++) {
const page = start + i;
if (page <= totalPages) pages.push(page);
}

return (
<div className="flex justify-center items-center gap-2 mt-8 mb-4">
<Button variant="outline" disabled={currentPage === 1} onClick={go(currentPage - 1)}>上一页</Button>
<div className="hidden sm:flex items-center gap-2">
{pages[0] > 1 && (
<>
<Button variant="outline" onClick={go(1)}>1</Button>
{pages[0] > 2 && <span className='text-gray-500'>...</span>}
</>
)}
{pages.map(p => (
<Button key={p} variant={p === currentPage ? 'solid' : 'outline'} onClick={go(p)}>{p}</Button>
))}
{pages[pages.length - 1] < totalPages && (
<>
{pages[pages.length - 1] < totalPages - 1 && <span className='text-gray-500'>...</span>}
<Button variant="outline" onClick={go(totalPages)}>{totalPages}</Button>
</>
)}
</div>
<Button variant="outline" disabled={currentPage === totalPages} onClick={go(currentPage + 1)}>下一页</Button>
</div>
);
};
59 changes: 59 additions & 0 deletions src/hooks/usePagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState, useMemo, useCallback } from 'react';

export interface PaginationResult<T> {
currentPage: number;
totalPages: number;
pageSize: number;
totalItems: number;
startIndex: number; // inclusive
endIndex: number; // exclusive (raw slice end)
currentItems: T[];
setPage: (page: number) => void;
nextPage: () => void;
prevPage: () => void;
}

/**
* Generic pagination hook with stable memoized slices and convenience helpers.
* Automatically scrolls to top on page change (smooth).
*/
export function usePagination<T>(items: readonly T[], pageSize: number = 20): PaginationResult<T> {
const [currentPage, setCurrentPage] = useState(1);

const totalItems = items.length;
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));

const safeSetPage = useCallback((page: number) => {
setCurrentPage(prev => {
const next = Math.min(totalPages, Math.max(1, page));
if (prev !== next) {
// Smooth scroll to top on page change
if (typeof window !== 'undefined') {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
return next;
});
}, [totalPages]);

const startIndex = (currentPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, totalItems);

const currentItems = useMemo(() => items.slice(startIndex, endIndex), [items, startIndex, endIndex]);

const nextPage = useCallback(() => safeSetPage(currentPage + 1), [currentPage, safeSetPage]);
const prevPage = useCallback(() => safeSetPage(currentPage - 1), [currentPage, safeSetPage]);

return {
currentPage,
totalPages,
pageSize,
totalItems,
startIndex,
endIndex,
currentItems,
setPage: safeSetPage,
nextPage,
prevPage,
};
}