From a79afcb0a5d9f6369fb28b3ac06a3666dc8eeb1b Mon Sep 17 00:00:00 2001 From: bill lau Date: Sun, 21 Sep 2025 21:49:52 +0800 Subject: [PATCH 1/2] pagination --- AI_Document.md | 2 +- src/App.tsx | 11 ++++- src/components/News.tsx | 83 +++-------------------------------- src/components/Pagination.tsx | 50 +++++++++++++++++++++ src/hooks/usePagination.ts | 59 +++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 79 deletions(-) create mode 100644 src/components/Pagination.tsx create mode 100644 src/hooks/usePagination.ts diff --git a/AI_Document.md b/AI_Document.md index 2b1637e..3486fb4 100644 --- a/AI_Document.md +++ b/AI_Document.md @@ -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:``。\n3. `News` 与 `Them` 共用,不再出现分页实现重复代码。\n4. 维持原有交互体验(平滑滚动、页码省略号规则、上一页/下一页)。\n\n## 新增文件\n| 文件 | 说明 | GitHub Pages 支持 React Router,但需要特殊配置来处理客户端路由。本项目已配置完成,支持以下路由: - `/` - 友链列表页(原首页内容) - `/home` - 主页 - `/about` - 关于页面 -## 实现原理 ### 问题 GitHub Pages 是静态文件托管服务,当用户直接访问 `/home` 或 `/about` 时,服务器会寻找对应的物理文件,但这些路由是由 React Router 在客户端处理的,不存在实际的文件,因此会返回 404 错误。 diff --git a/src/App.tsx b/src/App.tsx index 6a004bb..83e9402 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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' @@ -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 (
+
+ 显示第 {startIndex + 1} - {endIndex} 项,共 {totalItems} 项 +
- {blogs.map((blog) => ( + {currentItems.map((blog) => (
@@ -42,8 +48,9 @@ function Them() { ))}
+
- ) + ); } function App() { diff --git a/src/components/News.tsx b/src/components/News.tsx index 0bd1c54..01e361a 100644 --- a/src/components/News.tsx +++ b/src/components/News.tsx @@ -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; @@ -26,35 +27,15 @@ const blogs: Blog[] = rawBlogData as Blog[]; const blogMap: Record = {}; 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 (
- {/* 页面信息 */}
- 显示第 {startIndex + 1} - {Math.min(endIndex, items.length)} 项,共 {items.length} 项 + 显示第 {startIndex + 1} - {endIndex} 项,共 {totalItems} 项
- - {/* 博客列表 */} {currentItems.map(item => (
@@ -72,57 +53,7 @@ function News() {
))} - - {/* 分页导航 */} -
- {/* 上一页按钮,所有屏幕都显示 */} - - {/* 只在大屏显示页码按钮 */} -
- {/* 显示前几页 */} - {currentPage > 3 && ( - <> - - {currentPage > 4 && ...} - - )} - {/* 显示当前页周围的页码 */} - {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 ( - - ); - })} - {/* 显示后几页 */} - {currentPage < totalPages - 2 && ( - <> - {currentPage < totalPages - 3 && ...} - - - )} -
- {/* 下一页按钮,所有屏幕都显示 */} - -
+
); } diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx new file mode 100644 index 0000000..21bd805 --- /dev/null +++ b/src/components/Pagination.tsx @@ -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 = ({ 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 ( +
+ +
+ {pages[0] > 1 && ( + <> + + {pages[0] > 2 && ...} + + )} + {pages.map(p => ( + + ))} + {pages[pages.length - 1] < totalPages && ( + <> + {pages[pages.length - 1] < totalPages - 1 && ...} + + + )} +
+ +
+ ); +}; diff --git a/src/hooks/usePagination.ts b/src/hooks/usePagination.ts new file mode 100644 index 0000000..15b756a --- /dev/null +++ b/src/hooks/usePagination.ts @@ -0,0 +1,59 @@ +import { useState, useMemo, useCallback } from 'react'; + +export interface PaginationResult { + 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(items: readonly T[], pageSize: number = 20): PaginationResult { + 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, + }; +} From 06d72a017b2c47a7d0849319aa2ef520885521ff Mon Sep 17 00:00:00 2001 From: bill lau Date: Sun, 21 Sep 2025 21:51:05 +0800 Subject: [PATCH 2/2] update yulu post url --- src/assets/blogs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/blogs.json b/src/assets/blogs.json index f73e525..90056b1 100644 --- a/src/assets/blogs.json +++ b/src/assets/blogs.json @@ -1,7 +1,7 @@ [ { "name": "littlecheesecake", - "url": "https://littlecheesecake.me/", + "url": "https://littlecheesecake.me/blog2/index.html", "describe": "littlecheesecake的个人博客", "avatar": "avatar/littlecheesecake.webp" },