首次提交:姜十三论坛

后端 Go+Gin:认证/CSRF/限流、板块、帖子、评论、点赞、通知、用户资料、置顶推荐;前端 Next.js 16:发帖/编辑/删除、搜索、分页、点赞、通知中心、设置;基础设施 docker-compose 与配置模板;添加 .gitignore 与专有许可证(保留所有权利)
This commit is contained in:
2026-09-12 01:11:05 +08:00
commit 0c2b0d2d6a
62 changed files with 8289 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
"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>
);
}