"use client"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import type { PublicPost } from "@/lib/store"; import { formatDate, readingTimeLabel } from "@/lib/utils"; import { useGsapAnimation } from "./useGsapAnimation"; import { ChevronLeft, ChevronRight } from "lucide-react"; gsap.registerPlugin(ScrollTrigger); interface BlogListProps { posts: PublicPost[]; } export default function BlogList({ posts }: BlogListProps) { const router = useRouter(); const searchParams = useSearchParams(); const activeCategory = searchParams.get("category") || ""; const activeTag = searchParams.get("tag") || ""; const page = Number(searchParams.get("page")) || 1; const pageSize = 10; const headerRef = useGsapAnimation((_, isReduced) => { if (isReduced) return; gsap.from(".blog-header-el", { y: 30, opacity: 0, duration: 0.7, stagger: 0.1, ease: "power3.out", }); }, []); const listRef = useGsapAnimation((scope, isReduced) => { if (isReduced) return; gsap.from(".blog-list-item", { y: 40, opacity: 0, duration: 0.7, stagger: 0.08, ease: "power3.out", scrollTrigger: { trigger: scope, start: "top 88%" }, }); }, [activeCategory, activeTag]); // 从文章动态聚合分类与标签 const { categories, tags } = useMemo(() => { const catSet = new Map(); const tagSet = new Map(); for (const p of posts) { catSet.set(p.category, (catSet.get(p.category) || 0) + 1); for (const t of p.tags) tagSet.set(t, (tagSet.get(t) || 0) + 1); } return { categories: [...catSet.entries()].sort((a, b) => b[1] - a[1]).map(([name]) => name), tags: [...tagSet.entries()].sort((a, b) => b[1] - a[1]).map(([name]) => name), }; }, [posts]); const filtered = useMemo(() => { return posts.filter((p) => { if (activeCategory && p.category !== activeCategory) return false; if (activeTag && !p.tags.includes(activeTag)) return false; return true; }); }, [posts, activeCategory, activeTag]); const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); const currentPage = Math.min(page, totalPages); const paged = useMemo(() => { const start = (currentPage - 1) * pageSize; return filtered.slice(start, start + pageSize); }, [filtered, currentPage]); function setFilter(key: "category" | "tag", value: string) { const params = new URLSearchParams(searchParams.toString()); if (value) params.set(key, value); else params.delete(key); // 切换一个维度时清除另一个,避免组合空结果困惑 if (key === "category") params.delete("tag"); if (key === "tag") params.delete("category"); params.delete("page"); // 切换筛选时重置分页 const qs = params.toString(); router.push(qs ? `/blog?${qs}` : "/blog", { scroll: false }); } function setPage(p: number) { const params = new URLSearchParams(searchParams.toString()); if (p > 1) params.set("page", String(p)); else params.delete("page"); const qs = params.toString(); router.push(qs ? `/blog?${qs}` : "/blog", { scroll: false }); } const hasFilter = Boolean(activeCategory || activeTag); return (
{/* Header */}
} className="mb-10">

文章

所有的文字,按时间排列。通过下方的分类或标签来筛选探索。

{/* Filters */}
{/* Category filter */}
分类 setFilter("category", "")}> 全部 {categories.map((cat) => ( setFilter("category", cat)}> {cat} ))}
{/* Tag filter */}
标签 setFilter("tag", "")}> 全部 {tags.slice(0, 12).map((tag) => ( setFilter("tag", tag)}> {tag} ))}
{/* Result meta */}
{hasFilter ? ( {activeCategory && <>分类:{activeCategory} } {activeTag && <>标签:#{activeTag} } · 共 {filtered.length} 篇 ) : ( 共 {filtered.length} 篇文章 )}
{/* Post list */} {paged.length > 0 ? (
} className="space-y-0"> {paged.map((post) => (
{post.category} {readingTimeLabel(post.readingTime)}

{post.title}

{post.excerpt}

{post.tags.slice(0, 4).map((tag) => ( {tag} ))}
{post.coverImage && (
{/* eslint-disable-next-line @next/next/no-img-element */}
)}
))}
) : (

空空如也

该筛选条件下暂无文章。

)} {/* Pagination */} {totalPages > 1 && (
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ( ))}
)}
); } function FilterChip({ active, onClick, children, }: { active: boolean; onClick: () => void; children: React.ReactNode; }) { return ( ); }