Skip to content

Commit e65ab5b

Browse files
pagination (#30)
* pagination * update yulu post url
1 parent edf65a0 commit e65ab5b

File tree

6 files changed

+127
-80
lines changed

6 files changed

+127
-80
lines changed

AI_Document.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
## 概述
44

5+
\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| 文件 | 说明 |
56
GitHub Pages 支持 React Router,但需要特殊配置来处理客户端路由。本项目已配置完成,支持以下路由:
67

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

11-
## 实现原理
1212

1313
### 问题
1414
GitHub Pages 是静态文件托管服务,当用户直接访问 `/home``/about` 时,服务器会寻找对应的物理文件,但这些路由是由 React Router 在客户端处理的,不存在实际的文件,因此会返回 404 错误。

src/App.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import './App.css'
44
import blogsData from './assets/blogs.json'
55
import { resolveAvatar } from './services/avatarService'
66
import News from './components/News'
7+
import { usePagination } from './hooks/usePagination';
8+
import { Pagination } from './components/Pagination';
79
import { Footer } from './components/Footer'
810

911

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

1921
function Them() {
22+
const { currentItems, currentPage, totalPages, startIndex, endIndex, totalItems, setPage } = usePagination(blogs, 20);
2023
return (
2124
<div className='container mx-auto flex flex-col gap-4'>
25+
<div className='text-center text-sm text-gray-500 mb-2'>
26+
显示第 {startIndex + 1} - {endIndex} 项,共 {totalItems}
27+
</div>
2228
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
23-
{blogs.map((blog) => (
29+
{currentItems.map((blog) => (
2430
<Card key={blog.name} className="w-full">
2531
<div className="flex flex-col items-center justify-center gap-3">
2632
<div className="w-64 aspect-square overflow-hidden rounded-lg">
@@ -42,8 +48,9 @@ function Them() {
4248
</Card>
4349
))}
4450
</div>
51+
<Pagination currentPage={currentPage} totalPages={totalPages} onChange={setPage} />
4552
</div>
46-
)
53+
);
4754
}
4855

4956
function App() {

src/assets/blogs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[
22
{
33
"name": "littlecheesecake",
4-
"url": "https://littlecheesecake.me/",
4+
"url": "https://littlecheesecake.me/blog2/index.html",
55
"describe": "littlecheesecake的个人博客",
66
"avatar": "avatar/littlecheesecake.webp"
77
},

src/components/News.tsx

Lines changed: 7 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { useState } from 'react';
21
import rawItemData from '../assets/items.json'
32
import rawBlogData from '../assets/blogs.json'
43
import type { Blog } from '../App';
54
import { Link } from 'react-router';
65
import { resolveAvatar } from '../services/avatarService'
76

8-
import { Card, Button } from '@radix-ui/themes';
7+
import { Card } from '@radix-ui/themes';
8+
import { usePagination } from '../hooks/usePagination';
9+
import { Pagination } from './Pagination';
910

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

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

32-
blogs.forEach(blog => {
33-
blogMap[blog.name] = blog;
34-
});
35-
36-
// 计算总页数
37-
const totalPages = Math.ceil(items.length / itemsPerPage);
38-
39-
// 计算当前页的数据范围
40-
const startIndex = (currentPage - 1) * itemsPerPage;
41-
const endIndex = startIndex + itemsPerPage;
42-
const currentItems = items.slice(startIndex, endIndex);
43-
44-
const handlePageChange = (page: number) => {
45-
setCurrentPage(page);
46-
// 滚动到页面顶部
47-
window.scrollTo({ top: 0, behavior: 'smooth' });
48-
};
32+
const { currentItems, currentPage, totalPages, startIndex, endIndex, totalItems, setPage } = usePagination(items, 20);
4933

5034
return (
5135
<div className='container mx-auto flex flex-col gap-4'>
52-
{/* 页面信息 */}
5336
<div className='text-center text-sm text-gray-500 mb-4'>
54-
显示第 {startIndex + 1} - {Math.min(endIndex, items.length)} 项,共 {items.length}
37+
显示第 {startIndex + 1} - {endIndex} 项,共 {totalItems}
5538
</div>
56-
57-
{/* 博客列表 */}
5839
{currentItems.map(item => (
5940
<Card key={item.item_url}>
6041
<div className='flex flex-row gap-2'>
@@ -72,57 +53,7 @@ function News() {
7253
</div>
7354
</Card>
7455
))}
75-
76-
{/* 分页导航 */}
77-
<div className="flex justify-center items-center gap-2 mt-8 mb-4">
78-
{/* 上一页按钮,所有屏幕都显示 */}
79-
<Button
80-
variant="outline"
81-
disabled={currentPage === 1}
82-
onClick={() => handlePageChange(currentPage - 1)}
83-
>
84-
上一页
85-
</Button>
86-
{/* 只在大屏显示页码按钮 */}
87-
<div className="hidden sm:flex items-center gap-2">
88-
{/* 显示前几页 */}
89-
{currentPage > 3 && (
90-
<>
91-
<Button variant="outline" onClick={() => handlePageChange(1)}>1</Button>
92-
{currentPage > 4 && <span className='text-gray-500'>...</span>}
93-
</>
94-
)}
95-
{/* 显示当前页周围的页码 */}
96-
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
97-
const page = Math.max(1, Math.min(totalPages - 4, currentPage - 2)) + i;
98-
if (page > totalPages) return null;
99-
return (
100-
<Button
101-
key={page}
102-
variant={page === currentPage ? "solid" : "outline"}
103-
onClick={() => handlePageChange(page)}
104-
>
105-
{page}
106-
</Button>
107-
);
108-
})}
109-
{/* 显示后几页 */}
110-
{currentPage < totalPages - 2 && (
111-
<>
112-
{currentPage < totalPages - 3 && <span className='text-gray-500'>...</span>}
113-
<Button variant="outline" onClick={() => handlePageChange(totalPages)}>{totalPages}</Button>
114-
</>
115-
)}
116-
</div>
117-
{/* 下一页按钮,所有屏幕都显示 */}
118-
<Button
119-
variant="outline"
120-
disabled={currentPage === totalPages}
121-
onClick={() => handlePageChange(currentPage + 1)}
122-
>
123-
下一页
124-
</Button>
125-
</div>
56+
<Pagination currentPage={currentPage} totalPages={totalPages} onChange={setPage} />
12657
</div>
12758
);
12859
}

src/components/Pagination.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Button } from '@radix-ui/themes';
2+
import React from 'react';
3+
4+
interface PaginationProps {
5+
currentPage: number;
6+
totalPages: number;
7+
onChange: (page: number) => void;
8+
}
9+
10+
/**
11+
* Responsive pagination control replicating previous News component style.
12+
*/
13+
export const Pagination: React.FC<PaginationProps> = ({ currentPage, totalPages, onChange }) => {
14+
if (totalPages <= 1) return null;
15+
16+
const go = (p: number) => () => onChange(p);
17+
18+
// Compute window of up to 5 pages around current page (with shifting for edges)
19+
const pages: number[] = [];
20+
const windowSize = Math.min(5, totalPages);
21+
const start = Math.max(1, Math.min(totalPages - windowSize + 1, currentPage - Math.floor(windowSize / 2)));
22+
for (let i = 0; i < windowSize; i++) {
23+
const page = start + i;
24+
if (page <= totalPages) pages.push(page);
25+
}
26+
27+
return (
28+
<div className="flex justify-center items-center gap-2 mt-8 mb-4">
29+
<Button variant="outline" disabled={currentPage === 1} onClick={go(currentPage - 1)}>上一页</Button>
30+
<div className="hidden sm:flex items-center gap-2">
31+
{pages[0] > 1 && (
32+
<>
33+
<Button variant="outline" onClick={go(1)}>1</Button>
34+
{pages[0] > 2 && <span className='text-gray-500'>...</span>}
35+
</>
36+
)}
37+
{pages.map(p => (
38+
<Button key={p} variant={p === currentPage ? 'solid' : 'outline'} onClick={go(p)}>{p}</Button>
39+
))}
40+
{pages[pages.length - 1] < totalPages && (
41+
<>
42+
{pages[pages.length - 1] < totalPages - 1 && <span className='text-gray-500'>...</span>}
43+
<Button variant="outline" onClick={go(totalPages)}>{totalPages}</Button>
44+
</>
45+
)}
46+
</div>
47+
<Button variant="outline" disabled={currentPage === totalPages} onClick={go(currentPage + 1)}>下一页</Button>
48+
</div>
49+
);
50+
};

src/hooks/usePagination.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useState, useMemo, useCallback } from 'react';
2+
3+
export interface PaginationResult<T> {
4+
currentPage: number;
5+
totalPages: number;
6+
pageSize: number;
7+
totalItems: number;
8+
startIndex: number; // inclusive
9+
endIndex: number; // exclusive (raw slice end)
10+
currentItems: T[];
11+
setPage: (page: number) => void;
12+
nextPage: () => void;
13+
prevPage: () => void;
14+
}
15+
16+
/**
17+
* Generic pagination hook with stable memoized slices and convenience helpers.
18+
* Automatically scrolls to top on page change (smooth).
19+
*/
20+
export function usePagination<T>(items: readonly T[], pageSize: number = 20): PaginationResult<T> {
21+
const [currentPage, setCurrentPage] = useState(1);
22+
23+
const totalItems = items.length;
24+
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
25+
26+
const safeSetPage = useCallback((page: number) => {
27+
setCurrentPage(prev => {
28+
const next = Math.min(totalPages, Math.max(1, page));
29+
if (prev !== next) {
30+
// Smooth scroll to top on page change
31+
if (typeof window !== 'undefined') {
32+
window.scrollTo({ top: 0, behavior: 'smooth' });
33+
}
34+
}
35+
return next;
36+
});
37+
}, [totalPages]);
38+
39+
const startIndex = (currentPage - 1) * pageSize;
40+
const endIndex = Math.min(startIndex + pageSize, totalItems);
41+
42+
const currentItems = useMemo(() => items.slice(startIndex, endIndex), [items, startIndex, endIndex]);
43+
44+
const nextPage = useCallback(() => safeSetPage(currentPage + 1), [currentPage, safeSetPage]);
45+
const prevPage = useCallback(() => safeSetPage(currentPage - 1), [currentPage, safeSetPage]);
46+
47+
return {
48+
currentPage,
49+
totalPages,
50+
pageSize,
51+
totalItems,
52+
startIndex,
53+
endIndex,
54+
currentItems,
55+
setPage: safeSetPage,
56+
nextPage,
57+
prevPage,
58+
};
59+
}

0 commit comments

Comments
 (0)