后端 Go+Gin:认证/CSRF/限流、板块、帖子、评论、点赞、通知、用户资料、置顶推荐;前端 Next.js 16:发帖/编辑/删除、搜索、分页、点赞、通知中心、设置;基础设施 docker-compose 与配置模板;添加 .gitignore 与专有许可证(保留所有权利)
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
|
|
interface PaginationProps {
|
|
total: number;
|
|
page: number;
|
|
size: number;
|
|
basePath: string; // e.g. "/" or "/board/1"
|
|
query?: Record<string, string>; // additional query params (e.g. q for search)
|
|
onChange?: (page: number) => void; // 客户端分页回调;提供时不渲染 Link 而是按钮
|
|
}
|
|
|
|
export default function Pagination({ total, page, size, basePath, query = {}, onChange }: PaginationProps) {
|
|
const totalPages = Math.max(1, Math.ceil(total / size));
|
|
if (totalPages <= 1) return null;
|
|
|
|
const buildHref = (p: number) => {
|
|
const params = new URLSearchParams(query);
|
|
params.set("page", String(p));
|
|
return `${basePath}?${params.toString()}`;
|
|
};
|
|
|
|
const handleClick = (p: number, e: React.MouseEvent) => {
|
|
if (onChange) {
|
|
e.preventDefault();
|
|
onChange(p);
|
|
}
|
|
};
|
|
|
|
// 生成页码按钮(最多显示 7 个,带省略号)
|
|
const pages: (number | "...")[] = [];
|
|
const add = (p: number | "...") => pages.push(p);
|
|
|
|
if (totalPages <= 7) {
|
|
for (let i = 1; i <= totalPages; i++) add(i);
|
|
} else {
|
|
add(1);
|
|
if (page > 4) add("...");
|
|
const start = Math.max(2, page - 1);
|
|
const end = Math.min(totalPages - 1, page + 1);
|
|
for (let i = start; i <= end; i++) add(i);
|
|
if (page < totalPages - 3) add("...");
|
|
add(totalPages);
|
|
}
|
|
|
|
return (
|
|
<nav className="flex items-center justify-center gap-1 mt-8">
|
|
{page > 1 && (
|
|
<Link
|
|
href={buildHref(page - 1)}
|
|
onClick={(e) => handleClick(page - 1, e)}
|
|
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm hover:opacity-80"
|
|
style={{ background: "var(--bg-secondary)", color: "var(--text)" }}
|
|
>
|
|
<ChevronLeft size={16} /> 上一页
|
|
</Link>
|
|
)}
|
|
|
|
{pages.map((p, i) =>
|
|
p === "..." ? (
|
|
<span key={`e${i}`} className="px-2" style={{ color: "var(--text-secondary)" }}>…</span>
|
|
) : (
|
|
<Link
|
|
key={p}
|
|
href={buildHref(p)}
|
|
onClick={(e) => handleClick(p, e)}
|
|
className={`min-w-[36px] h-9 flex items-center justify-center rounded-lg text-sm ${
|
|
p === page ? "text-white" : "hover:opacity-80"
|
|
}`}
|
|
style={
|
|
p === page
|
|
? { background: "var(--primary)" }
|
|
: { background: "var(--bg-secondary)", color: "var(--text)" }
|
|
}
|
|
>
|
|
{p}
|
|
</Link>
|
|
)
|
|
)}
|
|
|
|
{page < totalPages && (
|
|
<Link
|
|
href={buildHref(page + 1)}
|
|
onClick={(e) => handleClick(page + 1, e)}
|
|
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm hover:opacity-80"
|
|
style={{ background: "var(--bg-secondary)", color: "var(--text)" }}
|
|
>
|
|
下一页 <ChevronRight size={16} />
|
|
</Link>
|
|
)}
|
|
</nav>
|
|
);
|
|
}
|