增加友情链接页面

This commit is contained in:
2026-09-16 23:53:27 +08:00
parent 651f860703
commit 481e76cb16
12 changed files with 145 additions and 70 deletions
+13
View File
@@ -0,0 +1,13 @@
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"} />;
}
+1
View File
@@ -0,0 +1 @@
export { default, generateMetadata } from "@/app/(legacy)/links/page";
+26 -1
View File
@@ -55,6 +55,20 @@ a { color: inherit; text-decoration: none; }
.home-collage { position: relative; height: 648px; margin-top: 50px; }.home-collage-tile { position: absolute; border-radius: 13px; background: #d9d9d9; }.home-collage-wide { top: 0; left: 0; width: 41.4%; height: 359px; }.home-collage-medium { top: 0; left: 42.57%; width: 28.87%; height: 359px; }.home-collage-small { top: 376px; left: 27.28%; width: 44.3%; height: 272px; }.home-collage-bottom { top: 376px; left: 0; width: 25.83%; height: 272px; }.home-collage-tall { top: 0; right: 0; width: 27.1%; height: 648px; }
.news-page { padding: 30px 0 60px; }
.friendly-links-hero { background: var(--policy-hero-image) center 26.71% / 100% auto no-repeat; }
.friendly-links-page-inner { gap: 50px; }
.friendly-links-content { width: min(1214px, 100%); margin: 0 auto; }
.friendly-links-section + .friendly-links-section { margin-top: 50px; }
.friendly-links-section h2 { margin: 0 0 20px; color: #181c20; font-size: 40px; font-weight: 700; line-height: 48px; }
.friendly-links-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.friendly-link-card { position: relative; display: flex; align-items: stretch; min-width: 0; min-height: 186px; overflow: hidden; border: 1.1px solid #e3e6e8; border-radius: 16.42px; background: #fff; color: #181c20; transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease; }
a.friendly-link-card:hover, a.friendly-link-card:focus-visible { border-color: #8f56a2; box-shadow: 0 8px 20px rgba(40, 29, 47, .1); transform: translateY(-2px); }
.friendly-link-card-content { display: flex; flex: 1; flex-direction: column; align-items: center; justify-content: center; gap: 16.42px; padding: 32.83px 10.94px; }
.friendly-link-card-heading { display: flex; align-items: center; justify-content: center; gap: 16px; min-width: 0; font-size: 27.36px; font-weight: 400; line-height: normal; white-space: nowrap; }
.friendly-link-card-heading img { width: 65.67px; height: 65.67px; overflow: hidden; border-radius: 8px; object-fit: cover; }
.friendly-link-card-description { color: #8d949a; font-size: 16.42px; line-height: 21.89px; text-align: center; }
.friendly-link-card-archive { position: absolute; top: 13px; right: 5px; padding: 4px 6px; border-radius: 4px; background: #e72329; color: #fff; font-size: 10px; line-height: 12px; }
.friendly-links-page .page-bottom-nav { margin-top: 176px; }
.news-page-inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.news-page-header { display: flex; justify-content: center; align-items: center; margin: 0 0 30px; }
.news-page-header h1 { margin: 0; font-size: clamp(52px, 5vw, 88px); letter-spacing: -0.07em; line-height: 1; font-weight: 800; }
@@ -104,7 +118,7 @@ a { color: inherit; text-decoration: none; }
.news-detail-body a:hover, .policy-body a:hover { color: #8f56a2; }
.policy-page { padding: 0 0 48px; background: #fff; }
.policy-page-inner { display: flex; flex-direction: column; gap: 50px; align-items: center; padding: 0 40px 48px; }
.policy-hero { width: min(1200px, 100%); min-height: 322px; display: flex; align-items: flex-start; padding: 156px 48px 0; border-radius: 10px; background: linear-gradient(110deg, rgba(143, 86, 162, 0.82), rgba(143, 86, 162, 0.18)), url("/policy-hero.png") center / cover; overflow: hidden; }
.policy-hero { width: min(1200px, 100%); min-height: 322px; display: flex; align-items: flex-start; padding: 156px 48px 0; border-radius: 10px; background: linear-gradient(110deg, rgba(143, 86, 162, 0.82), rgba(143, 86, 162, 0.18)), var(--policy-hero-image, url("/policy-hero.png")) center / cover; overflow: hidden; }
.policy-hero-inner { margin-left: -48px; padding: 10px 46px; background: #8f56a2; }
.policy-hero h1 { margin: 0; color: #fff; font-size: clamp(38px, 4vw, 62px); line-height: 1.3; letter-spacing: 0.25px; }
.policy-card { width: min(1200px, 100%); padding: 28px; border-radius: 26px; background: #fefdfa; box-shadow: 0 10px 15px rgba(15, 23, 42, 0.04); }
@@ -188,6 +202,9 @@ html[data-theme="dark"] .theme-toggle button:hover, html[data-theme="dark"] .the
html[data-theme="dark"] .theme-toggle button.active:hover, html[data-theme="dark"] .theme-toggle button.active:focus-visible { background: #2b3746; }
html[data-theme="dark"] .theme-toggle img { filter: brightness(0) invert(1); }
html[data-theme="dark"] .news-toolbar { border-color: rgba(255, 255, 255, 0.16); }
html[data-theme="dark"] .friendly-links-section h2 { color: var(--ink); }
html[data-theme="dark"] .friendly-link-card { border-color: #394553; background: #19222d; color: var(--ink); }
html[data-theme="dark"] .friendly-link-card-description { color: #a9b5c1; }
html[data-theme="dark"] .news-filter-button, html[data-theme="dark"] .news-view-toggle { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.04); }
html[data-theme="dark"] .news-pagination-button, html[data-theme="dark"] .news-share-button { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: #1b2531; }
html[data-theme="dark"] .news-detail-card { background: #19222d; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }
@@ -224,6 +241,9 @@ html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer
html:not([data-theme]) .theme-toggle button.active:hover, html:not([data-theme]) .theme-toggle button.active:focus-visible { background: #2b3746; }
html:not([data-theme]) .theme-toggle img { filter: brightness(0) invert(1); }
html:not([data-theme]) .news-toolbar { border-color: rgba(255, 255, 255, 0.16); }
html:not([data-theme]) .friendly-links-section h2 { color: var(--ink); }
html:not([data-theme]) .friendly-link-card { border-color: #394553; background: #19222d; color: var(--ink); }
html:not([data-theme]) .friendly-link-card-description { color: #a9b5c1; }
html:not([data-theme]) .news-filter-button, html:not([data-theme]) .news-view-toggle { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.04); }
html:not([data-theme]) .news-pagination-button, html:not([data-theme]) .news-share-button { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: #1b2531; }
html:not([data-theme]) .news-detail-card { background: #19222d; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }
@@ -277,6 +297,7 @@ html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer
.home-collage-small { top: 202px; left: 39%; width: 61%; height: 158px; }
.home-collage-tall { display: none; }
.news-page-inner { padding: 0 16px; }
.friendly-links-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.news-page-header h1 { font-size: clamp(46px, 12vw, 72px); }
.news-toolbar { align-items: flex-start; flex-wrap: wrap; gap: 16px; }
.news-toolbar-right { margin-left: auto; }
@@ -337,6 +358,10 @@ html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer
.home-collage-bottom, .home-collage-small { top: 160px; height: 120px; }
.news-page { padding-top: 20px; }
.friendly-links-content { width: 100%; }
.friendly-links-section + .friendly-links-section { margin-top: 36px; }
.friendly-links-section h2 { margin-bottom: 16px; font-size: 30px; line-height: 1.35; }
.friendly-links-grid { grid-template-columns: 1fr; gap: 12px; }
.news-page-inner { padding: 0 12px; }
.news-page-header { margin-bottom: 20px; }
.news-page-header h1 { font-size: 52px; }
+11 -2
View File
@@ -1,7 +1,16 @@
import SiteDocument from "@/components/layout/site-document";
import Script from "next/script";
import "@/app/globals.css";
export { metadata, viewport } from "@/components/layout/site-document";
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return <SiteDocument>{children}</SiteDocument>;
return (
<html lang="zh-Hans" suppressHydrationWarning>
<body>
{children}
<Script id="theme-preference" src="/init-theme.js" strategy="beforeInteractive" />
<Script id="locale-canonicalization" src="/init-locale.js" strategy="beforeInteractive" />
</body>
</html>
);
}
+1 -67
View File
@@ -1,5 +1,4 @@
import type { Metadata, Viewport } from "next";
import "@/app/globals.css";
export const metadata: Metadata = {
title: {
@@ -7,75 +6,10 @@ export const metadata: Metadata = {
template: "%s | 基沃托斯铁道公团 KivotosRailway",
},
description: "Kivotos Railway website",
icons: {
icon: "/icon/icon.svg",
},
icons: { icon: "/icon/icon.svg" },
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-Hans" suppressHydrationWarning>
<head>
{/* Run during HTML parsing, before the body can paint. */}
<script
id="theme-preference"
dangerouslySetInnerHTML={{ __html: `(() => {
const isTheme = (value) => value === 'light' || value === 'system' || value === 'dark';
let preference = 'system';
try {
const cookieTheme = document.cookie.split(';').map((entry) => entry.trim()).find((entry) => entry.startsWith('kr-theme='))?.slice('kr-theme='.length);
const savedTheme = isTheme(cookieTheme) ? cookieTheme : localStorage.getItem('kr-theme');
if (isTheme(savedTheme)) preference = savedTheme;
} catch {}
const theme = preference === 'system' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : preference;
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
})();` }}
/>
<script
id="locale-canonicalization"
dangerouslySetInnerHTML={{ __html: `(() => {
const canonicalLocale = (value) => {
const language = value?.toLowerCase().replaceAll('_', '-');
if (!language) return null;
if (/^zh(?:-|$)/.test(language)) return /^zh-(tw|hk|mo)(-|$)|-hant(?:-|$)/.test(language) ? 'zh-Hant' : 'zh-Hans';
if (/^en(?:-|$)/.test(language)) return 'en';
if (/^(ja|jp)(?:-|$)/.test(language)) return 'jp';
return null;
};
const parts = location.pathname.split('/');
const firstSegment = parts[1];
const pathLocale = canonicalLocale(firstSegment);
const isUnknownLanguagePath = !pathLocale && /^[a-z]{2,3}(?:-[a-z0-9]+)*$/i.test(firstSegment ?? '');
let locale = pathLocale;
if (!locale) {
try { locale = canonicalLocale(localStorage.getItem('kr-locale')); } catch {}
}
if (!locale) {
for (const language of navigator.languages?.length ? navigator.languages : [navigator.language]) {
locale = canonicalLocale(language);
if (locale) break;
}
}
locale ??= 'en';
const path = pathLocale || isUnknownLanguagePath ? '/' + parts.slice(2).join('/') : location.pathname;
const normalizedPath = path.replace(/\\/$/, '');
const destination = '/' + locale + normalizedPath + '/';
const search = new URLSearchParams(location.search);
search.delete('lang');
const canonicalUrl = destination + (search.size ? '?' + search : '') + location.hash;
if (location.pathname + location.search + location.hash !== canonicalUrl) location.replace(canonicalUrl);
})();` }}
/>
</head>
<body>{children}</body>
</html>
);
}
@@ -0,0 +1,20 @@
import links from "@/data/links.json";
import type { CSSProperties } from "react";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { getLocaleMessages, type Locale } from "@/lib/i18n";
type LinkEntry = { name: string; description: string; logo: string; url?: string; archived?: boolean };
type LocalizedText = Record<Locale, string>;
type LinkSection = { id: string; title: LocalizedText; links: LinkEntry[] };
const linkSections = links.sections as LinkSection[];
function FriendlyLinkCard({ link }: { link: LinkEntry }) {
const content = <><span className="friendly-link-card-content"><span className="friendly-link-card-heading"><img src={link.logo} alt="" width={48} height={48} /><span>{link.name}</span></span><span className="friendly-link-card-description">{link.description}</span></span>{link.archived && <span className="friendly-link-card-archive">已归档</span>}</>;
return link.url ? <a className="friendly-link-card" href={link.url} target="_blank" rel="noreferrer" aria-label={`访问 ${link.name}`}>{content}</a> : <article className="friendly-link-card">{content}</article>;
}
export function FriendlyLinksPage({ locale }: { locale: Locale }) {
const messages = getLocaleMessages(locale);
const heroStyle = { "--policy-hero-image": 'url("/links/friendly-links-hero.png")' } as CSSProperties;
return <main className="policy-page friendly-links-page"><div className="policy-page-inner friendly-links-page-inner"><section className="policy-hero friendly-links-hero" aria-labelledby="friendly-links-title" style={heroStyle}><div className="policy-hero-inner"><h1 id="friendly-links-title">{messages.footer.legal[2]}</h1></div></section><div className="friendly-links-content">{linkSections.map((section) => <section className="friendly-links-section" key={section.id} aria-labelledby={`friendly-links-${section.id}`}><h2 id={`friendly-links-${section.id}`}>{section.title[locale]}</h2><div className="friendly-links-grid">{section.links.map((link, index) => <FriendlyLinkCard key={`${section.id}-${link.name}-${index}`} link={link} />)}</div></section>)}</div></div><div className="page-bottom-nav"><div className="page-bottom-nav-inner"><PageBreadcrumb items={[{ label: messages.common.home, href: "/" }, { label: messages.footer.legal[2] }]} /></div></div></main>;
}
+25
View File
@@ -0,0 +1,25 @@
{
"sections": [
{
"id": "services",
"title": { "zh-Hans": "服务提供", "zh-Hant": "服務提供", "en": "Service providers", "jp": "サービス提供" },
"links": [
{ "name": "德威云", "description": "Minecraft 服务器提供方", "logo": "/links/fetarute.png", "url": "https://www.deviy.cn/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" },
{ "name": "蓝色秋生", "description": "网页服务提供方,对象存储、CDN 维护方", "logo": "/links/blueakio.svg", "url": "https://www.blueakio.com/" }
]
},
{
"id": "friends",
"title": { "zh-Hans": "朋友们", "zh-Hant": "朋友們", "en": "Friends", "jp": "友だち" },
"links": [
{ "name": "德威云", "description": "Minecraft 服务器提供方", "logo": "/links/fetarute.png", "url": "https://deviy.cn/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" },
{ "name": "蓝色秋生", "description": "网页服务提供方,对象存储、CDN 维护方", "logo": "/links/fetarute.png", "url": "https://liqiusheng.cn/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" }
]
}
]
}