Files
sage-home/app/[locale]/product/page.tsx
砂糖 86c20e5ef7 feat(ui): 更新车间和产品页面UI设计并添加新图片资源
更新多个页面的UI设计,包括产品卡片、车间详情页和列表页,采用现代化卡片设计和动画效果
添加新的车间图片资源并更新相关页面的图片引用路径
优化响应式布局和交互体验,增强视觉吸引力
2026-02-03 14:39:36 +08:00

196 lines
7.5 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 { 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"
>
<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>
</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,
}));
}