Files
sage-home/app/[locale]/line/page.tsx

140 lines
4.9 KiB
TypeScript
Raw Normal View History

2026-01-24 16:54:44 +08:00
import { Link as I18nLink, LOCALES } from "@/i18n/routing";
import { getLines } from "@/lib/lines";
import { constructMetadata } from "@/lib/metadata";
import { Line } from "@/types/line";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
// 强制静态生成
export const dynamic = "force-static";
// 明确 Params 类型(静态生成的参数)
type Params = { locale: string };
type MetadataProps = {
params: Params;
};
// 生成页面元数据(确保服务端/客户端翻译一致)
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const { locale } = await params;
2026-01-24 16:54:44 +08:00
const t = await getTranslations({ locale, namespace: "Workshop" });
return constructMetadata({
page: "Workshop",
title: t("pageTitle", { defaultValue: "车间展示" }), // 使用 next-intl 官方的 defaultValue
description: t("pageDesc", { defaultValue: "公司产品线展示" }),
locale: locale,
path: `/line`,
canonicalUrl: `/line`,
});
}
// 生成静态路由参数(多语言)
export async function generateStaticParams() {
return LOCALES.map((locale) => ({
locale,
}));
}
// 空数据组件(纯静态,无动态属性)
function EmptyState() {
return <div className="py-10 text-center text-gray-500">线</div>;
}
// 单个车间卡片组件Client Component 标记,避免 Hydration 冲突)
// 'use client'; // 仅当需要添加交互时启用,当前纯展示可不用
function ProductCard({ product }: { product: Line }) {
// 为 Image 组件添加默认值,避免属性缺失导致不匹配
const coverSrc = product.cover || "/default-workshop-cover.png"; // 兜底封面图
const coverAlt = product.title || "产品线封面";
return (
<I18nLink
href={`/line/${product.slug}`}
locale={product.locale}
>
<div
className="mb-4 relative flex w-full 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
src={coverSrc}
alt={coverAlt}
className="w-full h-full text-white/90 transform transition-transform group-hover:scale-110 duration-300"
/>
</div>
2026-01-24 16:54:44 +08:00
</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 overflow-hidden"
style={{
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 4,
overflow: 'hidden', // 确保文本溢出时隐藏
}}
>
{product.desc}
</p>
2026-01-24 16:54:44 +08:00
</div>
</div>
2026-01-24 16:54:44 +08:00
</I18nLink>
);
}
// 页面主组件Server Component
export default async function Page({
params,
}: {
params: Params;
}) {
const { locale } = await params;
// 获取翻译(确保服务端/客户端一致)
const t = await getTranslations({ locale, namespace: "Workshop" });
// 获取产品线数据(顶层 awaitServer Component 原生支持)
const products: Line[] = await getLines(locale);
// 页面标题翻译兜底
const pageTitle = t("pageTitle", { defaultValue: "产品线展示" });
return (
<main className="container mx-auto py-8 px-4">
{/* 页面标题(纯静态渲染,无动态属性) */}
<h1 className="text-3xl font-bold mb-8 text-center">{pageTitle}</h1>
{/* 移除不必要的 SuspenseServer Component 顶层 await 无需 Suspense */}
{products.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map((product) => {
// 用稳定的 key优先用唯一标识如 id无 id 则用 title+locale 避免 index 导致的问题)
const stableKey = `${product.title}-${product.locale}`;
return <ProductCard key={stableKey} product={product} />;
})}
</div>
) : (
<EmptyState />
)}
</main>
);
}