优化代码

This commit is contained in:
2026-09-17 04:14:24 +08:00
parent d76d476e75
commit 7f6323f64c
18 changed files with 443 additions and 618 deletions
+1
View File
@@ -0,0 +1 @@
22.11.0
+1
View File
@@ -1,4 +1,5 @@
{
"nodeVersion": "22.11.0",
"redirects": [
{ "source": "/zh-CN", "destination": "/zh-Hans", "statusCode": 301 },
{ "source": "/zh-CN/*", "destination": "/zh-Hans/:splat", "statusCode": 301 },
+12 -12
View File
@@ -13,20 +13,20 @@
"preview": "wrangler dev"
},
"dependencies": {
"@fontsource-variable/noto-sans": "^5.3.0",
"@fontsource-variable/noto-sans-sc": "^5.3.0",
"marked": "^18.0.11",
"next": "latest",
"react": "latest",
"react-dom": "latest"
"@fontsource-variable/noto-sans": "5.3.0",
"@fontsource-variable/noto-sans-sc": "5.3.0",
"marked": "18.0.11",
"next": "16.3.5",
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@types/node": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"eslint": "latest",
"eslint-config-next": "latest",
"typescript": "latest",
"@types/node": "22.20.2",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"eslint": "9.39.1",
"eslint-config-next": "16.3.5",
"typescript": "5.9.3",
"wrangler": "4.131.1"
}
}
+298 -434
View File
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
import type { Metadata } from "next";
import { FriendlyLinksPage } from "@/components/links/friendly-links-page";
import { getLocaleMessages, isLocale } from "@/lib/i18n";
export async function generateMetadata({ params }: { params: Promise<{ locale?: string }> }): Promise<Metadata> {
const { locale } = await params;
return { title: getLocaleMessages(isLocale(locale) ? locale : "zh-Hans").footer.legal[2] };
}
export default async function LinksPage({ params }: { params: Promise<{ locale?: string }> }) {
const { locale } = await params;
return <FriendlyLinksPage locale={isLocale(locale) ? locale : "zh-Hans"} />;
}
-32
View File
@@ -1,32 +0,0 @@
import type { Metadata } from "next";
import { getNewsBySlug, getNewsCatalog, getNewsList } from "@/data/news";
import { NewsDetailClient } from "@/components/news/news-detail-client";
import { isLocale } from "@/lib/i18n";
export async function generateStaticParams() {
const news = await getNewsList();
return news.map((item) => ({ slug: item.slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string; locale?: string }>;
}): Promise<Metadata> {
const { slug, locale } = await params;
const item = await getNewsBySlug(slug, isLocale(locale) ? locale : "zh-Hans");
return {
title: item?.title ?? "新闻",
};
}
export default async function NewsDetailPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <NewsDetailClient catalog={await getNewsCatalog()} slug={slug} />;
}
-34
View File
@@ -1,34 +0,0 @@
import type { Metadata } from "next";
import { getNewsCatalog } from "@/data/news";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { NewsList } from "@/components/news/news-list";
import { NewsPageHeading } from "@/components/news/news-page-heading";
import { getLocaleMessages, isLocale } from "@/lib/i18n";
export async function generateMetadata({ params }: { params: Promise<{ locale?: string }> }): Promise<Metadata> {
const { locale } = await params;
return { title: getLocaleMessages(isLocale(locale) ? locale : "zh-Hans").common.news };
}
export default async function NewsPage({ params }: { params: Promise<{ locale?: string }> }) {
const { locale } = await params;
const messages = getLocaleMessages(isLocale(locale) ? locale : "zh-Hans");
const catalog = await getNewsCatalog();
return (
<main className="news-page">
<div className="news-page-inner">
<header className="news-page-header">
<NewsPageHeading />
</header>
<NewsList catalog={catalog} />
</div>
<div className="page-bottom-nav">
<div className="page-bottom-nav-inner">
<PageBreadcrumb items={[{ label: messages.common.home, href: "/" }, { label: messages.common.news }]} />
</div>
</div>
</main>
);
}
-31
View File
@@ -1,31 +0,0 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { PolicyPageContent } from "@/components/policy/policy-page-content";
import { getPolicyCatalog, getPolicySlugs } from "@/data/policy";
import { isLocale } from "@/lib/i18n";
export const dynamicParams = false;
export function generateStaticParams() {
return getPolicySlugs();
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string; locale?: string }> }): Promise<Metadata> {
const { slug, locale } = await params;
const catalog = await getPolicyCatalog(slug);
return { title: catalog[isLocale(locale) ? locale : "zh-Hans"]?.title ?? "法律信息" };
}
export default async function PolicyPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const catalog = await getPolicyCatalog(slug);
if (!catalog["zh-Hans"]) notFound();
return (
<main className="policy-page">
<div className="policy-page-inner">
<PolicyPageContent catalog={catalog} />
</div>
</main>
);
}
+15 -1
View File
@@ -1 +1,15 @@
export { default, generateMetadata } from "@/app/(legacy)/links/page";
import { notFound } from "next/navigation";
import { FriendlyLinksPage } from "@/components/links/friendly-links-page";
import { getLocaleMessages, isLocale } from "@/lib/i18n";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return { title: getLocaleMessages(locale).footer.legal[2] };
}
export default async function LinksPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <FriendlyLinksPage locale={locale} />;
}
+21 -1
View File
@@ -1 +1,21 @@
export { default, generateMetadata, generateStaticParams } from "@/app/(legacy)/news/[slug]/page";
import { notFound } from "next/navigation";
import { getNewsBySlug, getNewsList } from "@/data/news";
import { NewsDetailClient } from "@/components/news/news-detail-client";
import { isLocale } from "@/lib/i18n";
export async function generateStaticParams() {
return (await getNewsList()).map((item) => ({ slug: item.slug }));
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string; locale: string }> }) {
const { slug, locale } = await params;
if (!isLocale(locale)) notFound();
const item = await getNewsBySlug(slug, locale);
return { title: item?.title ?? "新闻" };
}
export default async function NewsDetailPage({ params }: { params: Promise<{ slug: string; locale: string }> }) {
const { slug, locale } = await params;
if (!isLocale(locale)) notFound();
return <NewsDetailClient item={await getNewsBySlug(slug, locale)} locale={locale} />;
}
+28 -1
View File
@@ -1 +1,28 @@
export { default, generateMetadata } from "@/app/(legacy)/news/page";
import { notFound } from "next/navigation";
import { getNewsList } from "@/data/news";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { NewsList } from "@/components/news/news-list";
import { NewsPageHeading } from "@/components/news/news-page-heading";
import { getLocaleMessages, isLocale } from "@/lib/i18n";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return { title: getLocaleMessages(locale).common.news };
}
export default async function NewsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
const messages = getLocaleMessages(locale);
return (
<main className="news-page">
<div className="news-page-inner">
<header className="news-page-header"><NewsPageHeading /></header>
<NewsList news={await getNewsList(locale)} />
</div>
<div className="page-bottom-nav"><div className="page-bottom-nav-inner"><PageBreadcrumb items={[{ label: messages.common.home, href: "/" }, { label: messages.common.news }]} /></div></div>
</main>
);
}
+26 -1
View File
@@ -1 +1,26 @@
export { default, generateMetadata, generateStaticParams } from "@/app/(legacy)/policy/[slug]/page";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { PolicyPageContent } from "@/components/policy/policy-page-content";
import { getPolicyCatalog, getPolicySlugs } from "@/data/policy";
import { isLocale } from "@/lib/i18n";
export const dynamicParams = false;
export function generateStaticParams() {
return getPolicySlugs();
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string; locale: string }> }): Promise<Metadata> {
const { slug, locale } = await params;
if (!isLocale(locale)) notFound();
const catalog = await getPolicyCatalog(slug);
return { title: catalog[locale]?.title ?? "法律信息" };
}
export default async function PolicyPage({ params }: { params: Promise<{ slug: string; locale: string }> }) {
const { slug, locale } = await params;
if (!isLocale(locale)) notFound();
const catalog = await getPolicyCatalog(slug);
if (!catalog["zh-Hans"]) notFound();
return <main className="policy-page"><div className="policy-page-inner"><PolicyPageContent catalog={catalog} /></div></main>;
}
+1 -2
View File
@@ -21,7 +21,7 @@ function isIcpVisibleHost(hostname: string) {
export function SiteFooter() {
const [locale, setLocale] = useState<Locale>(useRouteLocale());
const [showIcp, setShowIcp] = useState(false);
const [showIcp] = useState(() => typeof window !== "undefined" && isIcpVisibleHost(window.location.hostname));
useEffect(() => {
const syncLocale = (nextLocale: Locale) => setLocale(nextLocale);
@@ -37,7 +37,6 @@ export function SiteFooter() {
const currentLocale = getInitialLocale();
syncLocale(currentLocale);
applyLocale(currentLocale);
setShowIcp(isIcpVisibleHost(window.location.hostname));
window.addEventListener("kr-locale-change", handleLocaleChange);
window.addEventListener("storage", handleStorage);
+4 -1
View File
@@ -27,7 +27,10 @@ function FriendlyLinkCard({ link, locale, visitLabel }: { link: LinkEntry; local
<>
<span className="friendly-link-card-content">
<span className="friendly-link-card-heading">
<span className="friendly-link-card-logo"><img src={link.logo} alt="" width={66} height={66} /></span>
<span className="friendly-link-card-logo">
{/* eslint-disable-next-line @next/next/no-img-element -- Static export has no Image Optimization API. */}
<img src={link.logo} alt="" width={66} height={66} />
</span>
<span>{name}</span>
</span>
<span className="friendly-link-card-description">{description}</span>
+22 -40
View File
@@ -1,50 +1,32 @@
"use client";
import { useRouteLocale } from "@/components/layout/locale-context";
import Link from "next/link";
import { useEffect, useState } from "react";
import type { NewsItem } from "@/data/news";
import { getAiTranslationNotice, getInitialLocale, withLocale, getLocaleMessages, type Locale } from "@/lib/i18n";
import { getAiTranslationNotice, getLocaleMessages, withLocale, type Locale } from "@/lib/i18n";
import { markdownToHtml } from "@/lib/markdown";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { NewsShare } from "@/components/news/news-share";
export function NewsDetailClient({ catalog, slug }: { catalog: Record<Locale, NewsItem[]>; slug: string }) {
const [locale, setLocale] = useState<Locale>(useRouteLocale());
export function NewsDetailClient({ item, locale }: { item: NewsItem | null; locale: Locale }) {
const messages = getLocaleMessages(locale);
const item = catalog[locale].find((entry) => entry.slug === slug) ?? catalog["zh-Hans"].find((entry) => entry.slug === slug);
useEffect(() => {
const syncLocale = () => setLocale(getInitialLocale());
syncLocale();
window.addEventListener("kr-locale-change", syncLocale);
window.addEventListener("storage", syncLocale);
return () => {
window.removeEventListener("kr-locale-change", syncLocale);
window.removeEventListener("storage", syncLocale);
};
}, []);
if (!item) {
return <main className="news-detail-page"><div className="news-detail-inner"><p>未找到该文章。</p><Link href={withLocale("/news", locale)}>{messages.common.backToNews}</Link></div></main>;
}
if (!item) return <main className="news-detail-page"><div className="news-detail-inner"><p>未找到该文章。</p><Link href={withLocale("/news", locale)}>{messages.common.backToNews}</Link></div></main>;
return <main className="news-detail-page">
<div className="news-detail-inner">
<Link href={withLocale("/news", locale)} className="back-link">← {messages.common.backToNews}</Link>
<article className="news-detail-card">
<p className="news-detail-tag">{item.category}</p>
<h1>{item.title}</h1>
<time>{item.date}</time>
{locale !== "zh-Hans" && (
<div className="news-detail-ai-banner" aria-label="AI translated content">
{getAiTranslationNotice(locale)}
</div>
)}
<div className="news-detail-cover" style={{ backgroundImage: `url(${item.cover})` }} />
<div className="news-detail-body" dangerouslySetInnerHTML={{ __html: markdownToHtml(item.body) }} />
<NewsShare title={item.title} />
</article>
</div>
<div className="page-bottom-nav"><div className="page-bottom-nav-inner"><PageBreadcrumb ariaLabel={messages.common.pagePosition} items={[{ label: messages.common.home, href: "/" }, { label: messages.common.news, href: "/news" }, { label: item.title }]} /></div></div>
</main>;
return (
<main className="news-detail-page">
<div className="news-detail-inner">
<Link href={withLocale("/news", locale)} className="back-link">← {messages.common.backToNews}</Link>
<article className="news-detail-card">
<p className="news-detail-tag">{item.category}</p>
<h1>{item.title}</h1>
<time>{item.date}</time>
{locale !== "zh-Hans" && <div className="news-detail-ai-banner" aria-label="AI translated content">{getAiTranslationNotice(locale)}</div>}
<div className="news-detail-cover" style={{ backgroundImage: `url(${item.cover})` }} />
<div className="news-detail-body" dangerouslySetInnerHTML={{ __html: markdownToHtml(item.body) }} />
<NewsShare title={item.title} />
</article>
</div>
<div className="page-bottom-nav"><div className="page-bottom-nav-inner"><PageBreadcrumb ariaLabel={messages.common.pagePosition} items={[{ label: messages.common.home, href: "/" }, { label: messages.common.news, href: "/news" }, { label: item.title }]} /></div></div>
</main>
);
}
+8 -8
View File
@@ -9,12 +9,11 @@ import { getInitialLocale, withLocale, getLocaleMessages, type Locale } from "@/
const ITEMS_PER_PAGE = 12;
export function NewsList({ catalog }: { catalog: Record<Locale, NewsItem[]> }) {
export function NewsList({ news }: { news: NewsItem[] }) {
const [activeCategory, setActiveCategory] = useState<string | null>(null);
const [locale, setLocale] = useState<Locale>(useRouteLocale());
const [currentPage, setCurrentPage] = useState(1);
const messages = getLocaleMessages(locale);
const news = catalog[locale];
useEffect(() => {
const syncLocale = () => setLocale(getInitialLocale());
@@ -27,10 +26,6 @@ export function NewsList({ catalog }: { catalog: Record<Locale, NewsItem[]> }) {
};
}, []);
useEffect(() => {
setCurrentPage(1);
}, [activeCategory]);
const categories = Array.from(new Set(news.map((item) => item.category).filter(Boolean)));
const visibleNews = activeCategory
? news.filter((item) => item.category === activeCategory)
@@ -48,6 +43,11 @@ export function NewsList({ catalog }: { catalog: Record<Locale, NewsItem[]> }) {
}
};
const handleCategoryChange = (category: string | null) => {
setActiveCategory(category);
setCurrentPage(1);
};
const handleNext = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
@@ -67,7 +67,7 @@ export function NewsList({ catalog }: { catalog: Record<Locale, NewsItem[]> }) {
<button
type="button"
className={`news-filter-button${activeCategory === null ? " active" : ""}`}
onClick={() => setActiveCategory(null)}
onClick={() => handleCategoryChange(null)}
>
{messages.common.all}
</button>
@@ -76,7 +76,7 @@ export function NewsList({ catalog }: { catalog: Record<Locale, NewsItem[]> }) {
key={category}
type="button"
className={`news-filter-button${activeCategory === category ? " active" : ""}`}
onClick={() => setActiveCategory(category)}
onClick={() => handleCategoryChange(category)}
>
{category}
</button>
+5 -5
View File
@@ -3,7 +3,7 @@
import { useRouteLocale } from "@/components/layout/locale-context";
import Image from "next/image";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { getInitialLocale, withLocale, type Locale } from "@/lib/i18n";
export type HttpStatusCode = 403 | 404 | 500 | 502 | 503 | 504;
@@ -72,7 +72,6 @@ function parseLocalizedMessages(source: string): Partial<Record<Locale, string[]
export function HttpStatusPage({ code }: { code: HttpStatusCode }) {
const [locale, setLocale] = useState<Locale>(useRouteLocale());
const [randomMessages, setRandomMessages] = useState<Partial<Record<Locale, string[]>>>({});
const [randomMessage, setRandomMessage] = useState<string | null>(null);
useEffect(() => {
const syncLocale = () => setLocale(getInitialLocale());
@@ -94,10 +93,11 @@ export function HttpStatusPage({ code }: { code: HttpStatusCode }) {
.catch(() => setRandomMessages({}));
}, [code]);
useEffect(() => {
const randomMessage = useMemo(() => {
const messages = randomMessages[locale] ?? randomMessages["zh-Hans"];
if (!messages?.length) return;
setRandomMessage(messages[Math.floor(Math.random() * messages.length)]);
if (!messages?.length) return null;
const index = Array.from(`${locale}:${messages.join("|")}`).reduce((total, character) => total + character.codePointAt(0)!, 0) % messages.length;
return messages[index];
}, [locale, randomMessages]);
const returnToPreviousPage = () => {
-1
View File
@@ -1,7 +1,6 @@
import fs from "fs/promises";
import path from "path";
import { localeContentDirectory, type Locale } from "@/lib/i18n";
import { markdownToHtml } from "@/lib/markdown";
import { imageMap } from "@/lib/image-map.generated";
export type NewsItem = {