2026-01-24 16:54:44 +08:00
|
|
|
|
import { Link as I18nLink, Locale, LOCALES } from "@/i18n/routing";
|
|
|
|
|
|
import { getProducts } from "@/lib/getProducts";
|
|
|
|
|
|
import { constructMetadata } from "@/lib/metadata";
|
|
|
|
|
|
import { Product } from "@/types/product";
|
|
|
|
|
|
import { Metadata } from "next";
|
|
|
|
|
|
import { getTranslations } from "next-intl/server";
|
|
|
|
|
|
|
|
|
|
|
|
type Params = Promise<{ locale: string }>;
|
|
|
|
|
|
|
|
|
|
|
|
type MetadataProps = {
|
|
|
|
|
|
params: Params;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export async function generateMetadata({
|
|
|
|
|
|
params,
|
|
|
|
|
|
}: MetadataProps): Promise<Metadata> {
|
|
|
|
|
|
const { locale } = await params;
|
|
|
|
|
|
const t = await getTranslations({ locale, namespace: "Product" });
|
|
|
|
|
|
|
|
|
|
|
|
return constructMetadata({
|
|
|
|
|
|
page: "Product",
|
|
|
|
|
|
title: "产品中心",
|
|
|
|
|
|
description: "公司产品展示",
|
|
|
|
|
|
locale: locale as Locale,
|
|
|
|
|
|
path: `/product`,
|
|
|
|
|
|
canonicalUrl: `/product`,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 每页产品数量
|
|
|
|
|
|
const PRODUCTS_PER_PAGE = 8;
|
|
|
|
|
|
|
|
|
|
|
|
export default async function Page({
|
|
|
|
|
|
params,
|
|
|
|
|
|
searchParams,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
params: Params;
|
|
|
|
|
|
searchParams: { page?: string };
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const { locale } = await params;
|
|
|
|
|
|
const t = await getTranslations("Product");
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前页码,默认第1页
|
|
|
|
|
|
let currentPage = parseInt(searchParams.page || "1");
|
|
|
|
|
|
if (isNaN(currentPage) || currentPage < 1) {
|
|
|
|
|
|
currentPage = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取产品数据
|
|
|
|
|
|
const { products }: { products: Product[] } = await getProducts(locale);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算总页数
|
|
|
|
|
|
const totalProducts = products.length;
|
|
|
|
|
|
const totalPages = Math.ceil(totalProducts / PRODUCTS_PER_PAGE);
|
|
|
|
|
|
|
|
|
|
|
|
// 确保当前页码不超过总页数
|
|
|
|
|
|
const safeCurrentPage = Math.min(currentPage, totalPages);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算当前页的产品数据
|
|
|
|
|
|
const startIndex = (safeCurrentPage - 1) * PRODUCTS_PER_PAGE;
|
|
|
|
|
|
const endIndex = startIndex + PRODUCTS_PER_PAGE;
|
|
|
|
|
|
const currentProducts = products.slice(startIndex, endIndex);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="w-full">
|
|
|
|
|
|
{/* 面包屑导航 */}
|
|
|
|
|
|
<div className="bg-gray-100 py-4">
|
|
|
|
|
|
<div className="container mx-auto px-4">
|
|
|
|
|
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
|
|
|
|
|
<I18nLink href="/" prefetch={false} className="hover:underline">
|
|
|
|
|
|
{t("breadcrumb.home")}
|
|
|
|
|
|
</I18nLink>
|
|
|
|
|
|
<span>/</span>
|
|
|
|
|
|
<span className="text-gray-700">{t("breadcrumb.product")}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 产品标题区域 */}
|
|
|
|
|
|
<div className="container mx-auto px-4 py-8">
|
|
|
|
|
|
<h1 className="text-3xl font-bold text-center text-gray-800 mb-8">{t("title")}</h1>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 产品网格 */}
|
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
|
|
|
|
{currentProducts.map((product) => (
|
|
|
|
|
|
<I18nLink
|
|
|
|
|
|
key={product.slug}
|
|
|
|
|
|
href={`/product/${product.slug}`}
|
|
|
|
|
|
prefetch={false}
|
|
|
|
|
|
className="block bg-white border border-gray-100 rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-all duration-300"
|
|
|
|
|
|
>
|
2026-02-03 14:39:36 +08:00
|
|
|
|
<div
|
|
|
|
|
|
className="relative flex w-80 flex-col rounded-xl bg-gradient-to-br from-white to-gray-50 bg-clip-border text-gray-700 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="relative mx-4 -mt-6 h-40 overflow-hidden rounded-xl bg-clip-border shadow-lg group"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute inset-0 bg-gradient-to-r from-blue-600 via-blue-500 to-indigo-600 opacity-90"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute inset-0 bg-[linear-gradient(to_right,rgba(255,255,255,0.1)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.1)_1px,transparent_1px)] bg-[size:20px_20px] animate-pulse"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
|
|
|
|
<img className="w-20 h-20" src={product.images[0]} alt="" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
|
<h5
|
|
|
|
|
|
className="mb-2 block font-sans text-xl font-semibold leading-snug tracking-normal text-gray-900 antialiased group-hover:text-blue-600 transition-colors duration-300"
|
|
|
|
|
|
>
|
|
|
|
|
|
{product.title}
|
|
|
|
|
|
</h5>
|
|
|
|
|
|
<p
|
|
|
|
|
|
className="block font-sans text-base font-light leading-relaxed text-gray-700 antialiased"
|
|
|
|
|
|
>
|
|
|
|
|
|
{product.detail}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="p-6 pt-0">
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="group relative w-full inline-flex items-center justify-center px-6 py-3 font-bold text-white rounded-lg bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 shadow-lg shadow-blue-500/30 hover:shadow-blue-500/40 transition-all duration-300 hover:-translate-y-0.5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="relative flex items-center gap-2">
|
|
|
|
|
|
Read More
|
|
|
|
|
|
<svg
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
className="w-5 h-5 transform transition-transform group-hover:translate-x-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M17 8l4 4m0 0l-4 4m4-4H3"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
2026-01-24 16:54:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</I18nLink>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 分页组件 */}
|
|
|
|
|
|
{totalPages > 1 && (
|
|
|
|
|
|
<div className="mt-12 flex justify-center">
|
|
|
|
|
|
<nav className="inline-flex items-center -space-x-px">
|
|
|
|
|
|
{/* 上一页按钮 */}
|
|
|
|
|
|
<I18nLink
|
|
|
|
|
|
href={`/product?page=${safeCurrentPage - 1}`}
|
|
|
|
|
|
prefetch={false}
|
|
|
|
|
|
className={`px-3 py-2 rounded-l-lg border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 ${safeCurrentPage === 1 ? 'cursor-not-allowed opacity-50' : ''}`}
|
|
|
|
|
|
aria-disabled={safeCurrentPage === 1}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t("pagination.previous")}
|
|
|
|
|
|
</I18nLink>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 页码按钮 */}
|
|
|
|
|
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
|
|
|
|
|
<I18nLink
|
|
|
|
|
|
key={page}
|
|
|
|
|
|
href={`/product?page=${page}`}
|
|
|
|
|
|
prefetch={false}
|
|
|
|
|
|
className={`px-3 py-2 border-y border-gray-300 bg-white text-sm font-medium ${page === safeCurrentPage ? 'bg-red-50 text-red-600' : 'text-gray-500 hover:bg-gray-50'}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{page}
|
|
|
|
|
|
</I18nLink>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 下一页按钮 */}
|
|
|
|
|
|
<I18nLink
|
|
|
|
|
|
href={`/product?page=${safeCurrentPage + 1}`}
|
|
|
|
|
|
prefetch={false}
|
|
|
|
|
|
className={`px-3 py-2 rounded-r-lg border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 ${safeCurrentPage === totalPages ? 'cursor-not-allowed opacity-50' : ''}`}
|
|
|
|
|
|
aria-disabled={safeCurrentPage === totalPages}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t("pagination.next")}
|
|
|
|
|
|
</I18nLink>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function generateStaticParams() {
|
|
|
|
|
|
// 为每个语言生成首页
|
|
|
|
|
|
return LOCALES.map((locale) => ({
|
|
|
|
|
|
locale,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|