235 lines
6.7 KiB
TypeScript
235 lines
6.7 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { getTranslations, setRequestLocale } from "next-intl/server";
|
|
import {
|
|
Briefcase,
|
|
MapPin,
|
|
Mail,
|
|
Code2,
|
|
Gauge,
|
|
Palette,
|
|
Wrench,
|
|
} from "lucide-react";
|
|
import { Container, Section } from "@/components/ui/layout";
|
|
import { Card } from "@/components/ui/card";
|
|
import { buttonClassName } from "@/components/ui/button-variants";
|
|
import { Reveal } from "@/components/motion/reveal";
|
|
import { siteConfig } from "@/lib/site-config";
|
|
|
|
/**
|
|
* 关于页渲染策略:ISR 缓存 60 秒。
|
|
* 路由段配置必须是字面量(Next.js 会静态分析)。
|
|
*/
|
|
export const revalidate = 60;
|
|
|
|
type AboutPageProps = {
|
|
params: Promise<{ locale: string }>;
|
|
};
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: AboutPageProps): Promise<Metadata> {
|
|
const { locale } = await params;
|
|
const t = await getTranslations({ locale, namespace: "About" });
|
|
|
|
return {
|
|
title: t("title"),
|
|
description: t("description"),
|
|
alternates: { canonical: `/${locale}/about` },
|
|
openGraph: {
|
|
type: "profile",
|
|
title: t("title"),
|
|
description: t("description"),
|
|
url: `${siteConfig.url}/${locale}/about`,
|
|
images: [{ url: siteConfig.ogImage }],
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 关注方向 —— 图标仅为装饰,语义由文本承载 */
|
|
const FOCUS_AREAS = [
|
|
{
|
|
icon: Gauge,
|
|
title: "Web 性能",
|
|
description:
|
|
"关注核心网页指标、渲染路径与打包体积,把「快」当作可度量的工程目标而不是感觉。",
|
|
},
|
|
{
|
|
icon: Code2,
|
|
title: "开发者体验",
|
|
description:
|
|
"好的工具应该让正确的事情变简单。投入最多的地方是构建流程、类型系统与文档。",
|
|
},
|
|
{
|
|
icon: Palette,
|
|
title: "设计系统",
|
|
description:
|
|
"把设计规范翻译成可执行的代码约束,让一致性由类型系统保证,而不是靠代码评审。",
|
|
},
|
|
{
|
|
icon: Wrench,
|
|
title: "工程化实践",
|
|
description:
|
|
"测试策略、CI/CD、可观测性——那些看不见但决定了项目能走多远的基建。",
|
|
},
|
|
] as const;
|
|
|
|
export default async function AboutPage({ params }: AboutPageProps) {
|
|
const { locale } = await params;
|
|
setRequestLocale(locale);
|
|
|
|
const t = await getTranslations("About");
|
|
const { author } = siteConfig;
|
|
|
|
const facts = [
|
|
{ icon: Briefcase, label: t("role"), value: author.jobTitle },
|
|
{ icon: MapPin, label: t("location"), value: author.location },
|
|
{ icon: Mail, label: t("email"), value: author.email },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
{/* 页头 */}
|
|
<Section
|
|
spacing="sm"
|
|
className="border-b border-hairline-soft bg-surface-soft"
|
|
>
|
|
<Container size="narrow">
|
|
<p className="text-micro-uppercase font-semibold uppercase tracking-[0.5px] text-steel">
|
|
{t("eyebrow")}
|
|
</p>
|
|
<h1 className="mt-sm text-heading-1 font-semibold text-ink">
|
|
{t("title")}
|
|
</h1>
|
|
<p className="mt-md max-w-[640px] text-subtitle text-steel text-balance-pretty">
|
|
{t("description")}
|
|
</p>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section spacing="default">
|
|
<Container size="narrow">
|
|
<div className="grid grid-cols-1 gap-xxl lg:grid-cols-[minmax(0,1fr)_280px]">
|
|
{/* 主栏:简介 + 关注方向 */}
|
|
<div className="flex flex-col gap-section">
|
|
<section aria-labelledby="intro-heading">
|
|
<h2
|
|
id="intro-heading"
|
|
className="text-heading-3 font-semibold text-ink"
|
|
>
|
|
{t("introTitle")}
|
|
</h2>
|
|
|
|
<div className="mt-md flex flex-col gap-md text-body-md leading-[1.75] text-slate">
|
|
<p>{author.bio}</p>
|
|
<p>
|
|
这个博客用来记录我在日常工作中遇到的问题和解决思路。
|
|
我倾向于写下那些「当时找不到答案、后来自己想明白了」的内容,
|
|
因为这类经验对别人往往也最有价值。
|
|
</p>
|
|
<p>
|
|
写作风格上,我更在意准确而不是完整:与其罗列所有可能性,
|
|
不如把一条路径讲透,并说清楚它为什么成立、在什么情况下会失效。
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section aria-labelledby="focus-heading">
|
|
<h2
|
|
id="focus-heading"
|
|
className="text-heading-3 font-semibold text-ink"
|
|
>
|
|
{t("techTitle")}
|
|
</h2>
|
|
<p className="mt-xs text-body-sm text-steel">
|
|
{t("techDescription")}
|
|
</p>
|
|
|
|
<div className="mt-lg grid grid-cols-1 gap-md sm:grid-cols-2">
|
|
{FOCUS_AREAS.map((area, index) => (
|
|
<Reveal key={area.title} delay={index * 0.05}>
|
|
<Card className="flex h-full flex-col gap-xs" padding="md">
|
|
<area.icon
|
|
className="size-5 text-brand-green-deep"
|
|
aria-hidden="true"
|
|
/>
|
|
<h3 className="text-heading-5 font-semibold text-ink">
|
|
{area.title}
|
|
</h3>
|
|
<p className="text-body-sm text-steel">
|
|
{area.description}
|
|
</p>
|
|
</Card>
|
|
</Reveal>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
{/* 侧栏:基本信息 + 联系方式 */}
|
|
<aside className="flex flex-col gap-xl">
|
|
<Card
|
|
padding="md"
|
|
variant="feature"
|
|
className="flex flex-col gap-md"
|
|
>
|
|
<h2 className="text-heading-5 font-semibold text-ink">
|
|
基本信息
|
|
</h2>
|
|
|
|
<dl className="flex flex-col gap-sm">
|
|
{facts.map((fact) => (
|
|
<div key={fact.label} className="flex items-start gap-sm">
|
|
<fact.icon
|
|
className="mt-[2px] size-4 shrink-0 text-steel"
|
|
aria-hidden="true"
|
|
/>
|
|
<div className="min-w-0">
|
|
<dt className="text-caption text-steel">
|
|
{fact.label}
|
|
</dt>
|
|
<dd className="break-words text-body-sm text-ink">
|
|
{fact.value}
|
|
</dd>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</Card>
|
|
|
|
<Card padding="md" className="flex flex-col gap-sm">
|
|
<h2 className="text-heading-5 font-semibold text-ink">
|
|
{t("contactTitle")}
|
|
</h2>
|
|
<p className="text-body-sm text-steel">
|
|
{t("contactDescription")}
|
|
</p>
|
|
|
|
<ul className="mt-xs flex flex-col gap-xs">
|
|
{siteConfig.social.map((item) => (
|
|
<li key={item.label}>
|
|
<a
|
|
href={item.href}
|
|
target={
|
|
item.href.startsWith("mailto:") ? undefined : "_blank"
|
|
}
|
|
rel="noopener noreferrer"
|
|
className={buttonClassName({
|
|
variant: "secondary",
|
|
size: "sm",
|
|
className: "w-full justify-start",
|
|
})}
|
|
>
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</Card>
|
|
</aside>
|
|
</div>
|
|
</Container>
|
|
</Section>
|
|
</>
|
|
);
|
|
}
|