Files
fad-trade-next/app/[locale]/blog/page.tsx
砂糖 d8ec1d4384 feat: 添加产品中心功能并优化博客系统
- 新增产品中心功能,包括产品列表页和详情页
- 实现产品多语言支持(中文、英文、越南语)
- 重构博客系统,从API获取改为本地MDX文件管理
- 更新favicon为PNG格式并修复相关引用
- 添加产品类型定义和获取逻辑
- 优化首页应用场景图片和链接
- 完善国际化配置和翻译
- 新增产品详情页标签切换组件
- 修复代理配置中的favicon路径问题
2025-12-10 11:32:50 +08:00

189 lines
6.8 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 ParallaxHero from "@/app/[locale]/blog/ParallaxHero";
import TablerEyeFilled from "@/components/icons/eye";
import { Link as I18nLink, Locale, LOCALES } from "@/i18n/routing";
import { getPosts } from "@/lib/getBlogs";
import { constructMetadata } from "@/lib/metadata";
import dayjs from "dayjs";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
type Params = Promise<{ locale: string }>;
type SearchParams = { page?: string; category?: 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: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const { locale } = await params;
const resolvedSearchParams = await searchParams;
const category = resolvedSearchParams.category as string || "";
let { posts } = await getPosts(locale);
const t = await getTranslations("Blog");
const pageRaw = resolvedSearchParams.page as string || "1";
const page = Math.max(1, parseInt(pageRaw, 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 }));
}