简略新建 成员一览 页面

This commit is contained in:
2026-09-24 09:23:56 +08:00
parent f862b76803
commit 491dd99cd3
5 changed files with 184 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

+15
View File
@@ -0,0 +1,15 @@
import { notFound } from "next/navigation";
import { MembersPage } from "@/components/members/members-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.columns[1].links[1] };
}
export default async function MembersRoute({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <MembersPage locale={locale} />;
}
+28
View File
@@ -63,6 +63,7 @@ a { color: inherit; text-decoration: none; }
.friendly-links-content { width: min(1214px, 100%); margin: 0 auto; } .friendly-links-content { width: min(1214px, 100%); margin: 0 auto; }
.friendly-links-section + .friendly-links-section { margin-top: 50px; } .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-section h2 { margin: 0 0 20px; color: #181c20; font-size: 40px; font-weight: 700; line-height: 48px; }
.friendly-links-section h2::after { display: block; width: 28.875px; height: 5.25px; margin-top: 17.5px; background: #8f56a2; content: ""; }
.friendly-links-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } .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; } .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); } 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); }
@@ -73,6 +74,24 @@ a.friendly-link-card:hover, a.friendly-link-card:focus-visible { border-color: #
.friendly-link-card-description { color: #8d949a; font-size: 16.42px; line-height: 21.89px; text-align: center; } .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-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; } .friendly-links-page .page-bottom-nav { margin-top: 176px; }
.members-page-inner { gap: 50px; }
.members-hero { background: var(--policy-hero-image) center 47% / 100% auto no-repeat; }
.members-sections { width: min(1200px, 100%); }
.member-section + .member-section { margin-top: 46px; }
.member-section-heading { margin: 0 0 20px; }
.member-section-heading h2 { margin: 0; color: #202326; font-size: 30px; font-weight: 700; line-height: 1.35; letter-spacing: -.03em; }
.member-section-heading span { display: block; width: 28px; height: 5px; margin-top: 12px; background: #8f56a2; }
.member-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 28px 20px; }
.member-card { display: block; min-width: 0; color: #202326; }
a.member-card { transition: opacity .2s ease, transform .2s ease; }
a.member-card:hover, a.member-card:focus-visible { opacity: .82; transform: translateY(-2px); }
.member-card-avatar { display: flex; aspect-ratio: 1; align-items: center; justify-content: center; overflow: hidden; border-radius: 4px; background: #d9d9d9; color: #8f56a2; font-size: 72px; font-weight: 700; }
.member-card-avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
.member-card-copy { padding-top: 12px; }
.member-card-copy h3 { margin: 0; font-size: 20px; font-weight: 700; line-height: 1.4; letter-spacing: -.03em; }
.member-card-copy p { margin: 4px 0 0; color: #5f6464; font-size: 14px; line-height: 1.45; }
.member-empty { margin: 0; padding: 40px 0; color: #697684; font-size: 16px; }
.members-page .page-bottom-nav { margin-top: 80px; }
.news-page-inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; } .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 { 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; } .news-page-header h1 { margin: 0; font-size: clamp(52px, 5vw, 88px); letter-spacing: -0.07em; line-height: 1; font-weight: 800; }
@@ -224,6 +243,9 @@ html[data-theme="dark"] .site-footer { color: #afbbc6; background: #0b1016; }
html[data-theme="dark"] .site-footer-top, html[data-theme="dark"] .site-footer-meta { border-color: rgba(255, 255, 255, 0.1); } html[data-theme="dark"] .site-footer-top, html[data-theme="dark"] .site-footer-meta { border-color: rgba(255, 255, 255, 0.1); }
html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer-column h3, html[data-theme="dark"] .site-footer-column a, html[data-theme="dark"] .site-footer-icon, html[data-theme="dark"] .site-footer-legal-row, html[data-theme="dark"] .site-footer-copyright-row, html[data-theme="dark"] .site-footer-meta p { color: #afbbc6; } html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer-column h3, html[data-theme="dark"] .site-footer-column a, html[data-theme="dark"] .site-footer-icon, html[data-theme="dark"] .site-footer-legal-row, html[data-theme="dark"] .site-footer-copyright-row, html[data-theme="dark"] .site-footer-meta p { color: #afbbc6; }
html[data-theme="dark"] .site-footer-icon img { filter: brightness(0) invert(1); opacity: .78; } html[data-theme="dark"] .site-footer-icon img { filter: brightness(0) invert(1); opacity: .78; }
html[data-theme="dark"] .member-section-heading h2, html[data-theme="dark"] .member-card { color: var(--ink); }
html[data-theme="dark"] .member-card-avatar { background: #263240; }
html[data-theme="dark"] .member-card-copy p, html[data-theme="dark"] .member-empty { color: #afbbc6; }
/* The static HTML has no theme attribute. Apply the system theme before React hydrates. */ /* The static HTML has no theme attribute. Apply the system theme before React hydrates. */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@@ -264,6 +286,9 @@ html[data-theme="dark"] .site-footer-icon img { filter: brightness(0) invert(1);
html:not([data-theme]) .site-footer-top, html:not([data-theme]) .site-footer-meta { border-color: rgba(255, 255, 255, 0.1); } html:not([data-theme]) .site-footer-top, html:not([data-theme]) .site-footer-meta { border-color: rgba(255, 255, 255, 0.1); }
html:not([data-theme]) .site-footer-title, html:not([data-theme]) .site-footer-column h3, html:not([data-theme]) .site-footer-column a, html:not([data-theme]) .site-footer-icon, html:not([data-theme]) .site-footer-legal-row, html:not([data-theme]) .site-footer-copyright-row, html:not([data-theme]) .site-footer-meta p { color: #afbbc6; } html:not([data-theme]) .site-footer-title, html:not([data-theme]) .site-footer-column h3, html:not([data-theme]) .site-footer-column a, html:not([data-theme]) .site-footer-icon, html:not([data-theme]) .site-footer-legal-row, html:not([data-theme]) .site-footer-copyright-row, html:not([data-theme]) .site-footer-meta p { color: #afbbc6; }
html:not([data-theme]) .site-footer-icon img { filter: brightness(0) invert(1); opacity: .78; } html:not([data-theme]) .site-footer-icon img { filter: brightness(0) invert(1); opacity: .78; }
html:not([data-theme]) .member-section-heading h2, html:not([data-theme]) .member-card { color: var(--ink); }
html:not([data-theme]) .member-card-avatar { background: #263240; }
html:not([data-theme]) .member-card-copy p, html:not([data-theme]) .member-empty { color: #afbbc6; }
} }
@media (max-width: 900px) { @media (max-width: 900px) {
@@ -332,6 +357,7 @@ html[data-theme="dark"] .site-footer-icon img { filter: brightness(0) invert(1);
.site-footer-brand-wrap { display: none; } .site-footer-brand-wrap { display: none; }
.site-footer-socials { flex-direction: row; align-items: center; } .site-footer-socials { flex-direction: row; align-items: center; }
.site-footer-top { align-items: center; } .site-footer-top { align-items: center; }
.member-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 16px; }
.site-footer-legal-row { gap: 8px 12px; } .site-footer-legal-row { gap: 8px 12px; }
.site-footer { height: auto; padding: 28px 0 22px; } .site-footer { height: auto; padding: 28px 0 22px; }
.site-footer-meta p { font-size: 12px; } .site-footer-meta p { font-size: 12px; }
@@ -423,6 +449,8 @@ html[data-theme="dark"] .site-footer-icon img { filter: brightness(0) invert(1);
.site-footer-socials { gap: 18px; } .site-footer-socials { gap: 18px; }
.site-footer-title { width: auto; font-size: 18px; } .site-footer-title { width: auto; font-size: 18px; }
.site-footer-icons { gap: 14px; } .site-footer-icons { gap: 14px; }
.member-section-heading h2 { font-size: 26px; }
.member-grid { grid-template-columns: 1fr; gap: 24px; }
.site-footer-main { padding-top: 22px; } .site-footer-main { padding-top: 22px; }
.site-footer-columns { gap: 18px; } .site-footer-columns { gap: 18px; }
.site-footer-column h3 { font-size: 18px; } .site-footer-column h3 { font-size: 18px; }
+70
View File
@@ -0,0 +1,70 @@
import members from "@/data/members.json";
import type { CSSProperties } from "react";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { getLocaleMessages, type Locale } from "@/lib/i18n";
type LocalizedText = Record<Locale, string>;
type MemberEntry = {
name: string;
role: string;
avatar?: string;
url?: string;
i18n?: { name?: Partial<LocalizedText>; role?: Partial<LocalizedText> };
};
type MemberSection = { id: string; title: LocalizedText; members: MemberEntry[] };
type MembersData = { title: LocalizedText; empty: LocalizedText; sections: MemberSection[] };
const memberData = members as MembersData;
function localized(value: string, translations: Partial<LocalizedText> | undefined, locale: Locale) {
return translations?.[locale] ?? value;
}
function MemberCard({ member, locale }: { member: MemberEntry; locale: Locale }) {
const name = localized(member.name, member.i18n?.name, locale);
const role = localized(member.role, member.i18n?.role, locale);
const content = (
<>
<div className="member-card-avatar">
{member.avatar ? (
// eslint-disable-next-line @next/next/no-img-element -- Member data may use static or remote avatar URLs in a static export.
<img src={member.avatar} alt="" width={285} height={285} />
) : <span aria-hidden="true">{name.slice(0, 1)}</span>}
</div>
<div className="member-card-copy"><h3>{name}</h3>{role && <p>{role}</p>}</div>
</>
);
return member.url ? (
<a className="member-card" href={member.url} target="_blank" rel="noreferrer" aria-label={name}>{content}</a>
) : <article className="member-card">{content}</article>;
}
export function MembersPage({ locale }: { locale: Locale }) {
const messages = getLocaleMessages(locale);
const pageTitle = memberData.title[locale];
const heroStyle = { "--policy-hero-image": 'url("/members/members-hero.png")' } as CSSProperties;
return (
<main className="policy-page members-page">
<div className="policy-page-inner members-page-inner">
<section className="policy-hero members-hero" aria-labelledby="members-page-title" style={heroStyle}>
<div className="policy-hero-inner"><h1 id="members-page-title">{pageTitle}</h1></div>
</section>
<div className="members-sections">
{memberData.sections.map((section) => (
<section className="member-section" key={section.id} aria-labelledby={`members-${section.id}`}>
<header className="member-section-heading"><h2 id={`members-${section.id}`}>{section.title[locale]}</h2><span aria-hidden="true" /></header>
{section.members.length ? (
<div className="member-grid">
{section.members.map((member, index) => <MemberCard key={`${section.id}-${member.name}-${index}`} member={member} locale={locale} />)}
</div>
) : <p className="member-empty">{memberData.empty[locale]}</p>}
</section>
))}
</div>
</div>
<div className="page-bottom-nav"><div className="page-bottom-nav-inner"><PageBreadcrumb items={[{ label: messages.common.home, href: "/" }, { label: messages.footer.columns[1].title }, { label: pageTitle }]} /></div></div>
</main>
);
}
+71
View File
@@ -0,0 +1,71 @@
{
"title": {
"zh-Hans": "成员一览",
"zh-Hant": "成員一覽",
"en": "Members",
"jp": "メンバー"
},
"empty": {
"zh-Hans": "成员资料正在整理中。",
"zh-Hant": "成員資料正在整理中。",
"en": "Member profiles are being prepared.",
"jp": "メンバー情報は現在準備中です。"
},
"sections": [
{
"id": "leadership",
"title": {
"zh-Hans": "领导团队",
"zh-Hant": "領導團隊",
"en": "Leadership",
"jp": "運営チーム"
},
"members": [
{
"name": "IF-Chan",
"role": "服主&主要出资者",
"avatar": "/icon/avatar/if-chan.png",
"url": "https://if-chan.mika.vin/",
"i18n": {
"name": {
"en": "IF-Chan",
"jp": "IF-Chan"
},
"role": {
"zh-Hant": "服主&主要出資者",
"en": "Server Owner & Main Investor",
"jp": "サーバーオーナー&主要投資家"
}
}
},
{
"name": "MisakaAkio",
"role": "主要网络维护者",
"avatar": "https://webstatic.blueakio.com/dynamic/user/NiuBoss123.jpg",
"url": "https://www.akio.top/",
"i18n": {
"name": {
"en": "MisakaAkio",
"jp": "MisakaAkio"
},
"role": {
"zh-Hant": "主要網絡維護者",
"en": "Main Network Maintainer",
"jp": "主要ネットワークメンテナー"
}
}
}
]
},
{
"id": "members",
"title": {
"zh-Hans": "主要玩家",
"zh-Hant": "主要玩家",
"en": "Members",
"jp": "メンバー"
},
"members": []
}
]
}