Files
fad-trade-next/app/[locale]/blog/page.tsx
Joshi 9758df608e style(blog): 优化文章卡片悬停和点击效果
添加悬停动画、阴影效果和点击反馈,提升用户体验
2025-11-21 14:58:41 +08:00

190 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Locale, LOCALES, Link as I18nLink } from "@/i18n/routing";
import { getPosts } from "@/lib/getBlogs";
import { constructMetadata } from "@/lib/metadata";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import Image from "next/image";
import dayjs from "dayjs";
import TablerEyeFilled from "@/components/icons/eye";
import ParallaxHero from "@/app/[locale]/blog/ParallaxHero";
type Params = Promise<{ locale: string }>;
type SearchParams = { page?: string };
type MetadataProps = {
params: Params;
};
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Blog" });
return constructMetadata({
page: "Blog",
title: t("title"),
description: t("description"),
locale: locale as Locale,
path: `/blog`,
canonicalUrl: `/blog`,
});
}
export default async function Page({
params,
searchParams,
}: {
params: Params;
searchParams?: SearchParams;
}) {
const { locale } = await params;
const { posts } = await getPosts(locale);
const t = await getTranslations("Blog");
const pageRaw = searchParams?.page;
const page = Math.max(1, parseInt(pageRaw || "1", 10));
const pageSize = 10;
const totalPages = Math.max(1, Math.ceil(posts.length / pageSize));
const start = (page - 1) * pageSize;
const end = start + pageSize;
const pagePosts = posts.slice(start, end);
return (
<div className="w-full">
<ParallaxHero />
{/* 顶部操作区:标签 + 面包屑 */}
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mt-4">
<div className="flex gap-3">
<button className="px-4 py-2 rounded-md border bg-white text-gray-700 shadow-sm hover:bg-gray-50">
</button>
<button className="px-4 py-2 rounded-md border bg-white text-gray-700 shadow-sm hover:bg-gray-50">
</button>
</div>
<div className="flex items-center gap-2 text-sm text-gray-500">
<I18nLink href="/" prefetch={false} className="hover:underline">
</I18nLink>
<span>/</span>
<I18nLink href="/blog" prefetch={false} className="hover:underline">
</I18nLink>
</div>
</div>
{/* 列表 */}
<div className="mt-4">
{pagePosts.map((post) => {
const year = dayjs(post.date).format("YYYY");
const monthDay = dayjs(post.date).format("MM-DD");
const views = (post.metadata?.views as number) || 0;
return (
<I18nLink
key={post.slug}
href={`/blog${post.slug}`}
prefetch={false}
className="block rounded-md mb-4 bg-gray-100 px-6 py-5 transition-all duration-200 shadow hover:bg-gray-200 hover:translate-y-[1px] hover:shadow-inner active:translate-y-[2px] active:shadow-inner focus:outline-none focus:ring-1 focus:ring-gray-300"
>
<div className="flex items-center">
<div className="flex-1 pr-4">
<div className="text-base md:text-lg font-medium text-gray-800">
{post.title}
</div>
<div className="mt-2 text-xs text-gray-600 flex items-center gap-1">
<TablerEyeFilled className="w-4 h-4" />
访{views}
</div>
</div>
<div className="w-24 md:w-28 text-right">
<div className="text-xs text-gray-500">{year}</div>
<div className="text-2xl md:text-3xl font-bold text-gray-500">{monthDay}</div>
</div>
</div>
</I18nLink>
);
})}
</div>
<div className="my-6 flex items-center justify-center gap-2">
<I18nLink
href={`/blog?page=${Math.max(1, page - 1)}`}
prefetch={false}
className={`px-2.5 py-1 border rounded-sm text-sm ${
page === 1 ? "bg-gray-100 text-gray-400" : "bg-white hover:bg-gray-50"
}`}
>
{"<"}
</I18nLink>
{(() => {
const items: (number | string)[] = [];
const showTotal = totalPages;
const maxNumbers = 4;
const startNum = 1;
const endNum = Math.min(showTotal, maxNumbers);
for (let n = startNum; n <= endNum; n++) items.push(n);
if (showTotal > maxNumbers + 1) items.push("...");
if (showTotal > maxNumbers) items.push(showTotal);
return items.map((it, idx) => {
if (typeof it === "string")
return (
<span key={`dot-${idx}`} className="px-2.5 py-1 text-sm text-gray-500">
{it}
</span>
);
const isActive = it === page;
return (
<I18nLink
key={it}
href={`/blog?page=${it}`}
prefetch={false}
className={`px-2.5 py-1 border rounded-sm text-sm ${
isActive
? "bg-red-600 text-white border-red-600"
: "bg-white hover:bg-gray-50"
}`}
>
{it}
</I18nLink>
);
});
})()}
<I18nLink
href={`/blog?page=${Math.min(totalPages, page + 1)}`}
prefetch={false}
className={`px-2.5 py-1 border rounded-sm text-sm ${
page === totalPages ? "bg-gray-100 text-gray-400" : "bg-white hover:bg-gray-50"
}`}
>
{">"}
</I18nLink>
<span className="ml-2 text-sm text-gray-600"></span>
<form action="/blog" method="get" className="flex items-center gap-1">
<input
type="number"
name="page"
min={1}
max={totalPages}
defaultValue={page}
className="w-14 h-7 border rounded-sm px-2 text-sm"
/>
<button className="px-2.5 h-7 border rounded-sm text-sm bg-white hover:bg-gray-50" type="submit">
</button>
<span className="text-sm text-gray-600"></span>
</form>
</div>
</div>
</div>
);
}
export async function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}