This commit is contained in:
toom1996
2026-10-22 10:36:49 +08:00
parent e7377a8771
commit 1adf71248f
6 changed files with 319 additions and 124 deletions

View File

@ -10,6 +10,7 @@
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"preview:full": "node scripts/local-proxy.mjs",
"check": "astro check", "check": "astro check",
"astro": "astro" "astro": "astro"
}, },

57
scripts/local-proxy.mjs Normal file
View File

@ -0,0 +1,57 @@
// 本地预览用的极简代理:模拟生产里 proxy(nginx) 的分流。
//
// /api/* → 后端(默认 127.0.0.1:8090)
// 其余 → astro preview(默认 127.0.0.1:4322)
//
// 为什么需要它:生产环境里浏览器只访问一个域名,/api 由 nginx 反代到后端;
// 本地没有 nginx,astro preview 只跑 Astro 自己的路由,/api 会 404。
// 这个脚本让本地的访问方式和生产一致(同源),不产生 CORS,也不需要改任何业务配置。
//
// 用法:
// node scripts/local-proxy.mjs # 默认 http://localhost:8099
// PROXY_PORT=9000 node scripts/local-proxy.mjs
// (先启动 astro preview,再启动它)
import http from 'node:http'
const PORT = Number(process.env.PROXY_PORT || 8099)
const WEB_ORIGIN = process.env.WEB_ORIGIN || 'http://127.0.0.1:4322'
const API_ORIGIN = process.env.API_ORIGIN || 'http://127.0.0.1:8090'
const server = http.createServer((req, res) => {
const url = req.url || '/'
const isApi = url === '/api' || url.startsWith('/api/')
const target = new URL(url, isApi ? API_ORIGIN : WEB_ORIGIN)
const proxyReq = http.request(
{
protocol: target.protocol,
hostname: target.hostname,
port: target.port,
path: target.pathname + target.search,
method: req.method,
headers: { ...req.headers, host: target.host },
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers)
proxyRes.pipe(res)
}
)
proxyReq.on('error', (err) => {
// 后端没起 / preview 没起时给出明确提示,而不是挂住
res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })
res.end(
`local-proxy: 连不上 ${isApi ? API_ORIGIN : WEB_ORIGIN}\n` +
`${err.message}\n\n` +
(isApi ? '先把后端跑起来(8090)\n' : '先跑 npm run preview(4322)\n')
)
})
req.pipe(proxyReq)
})
server.listen(PORT, () => {
console.log(`[local-proxy] http://localhost:${PORT}`)
console.log(` /api/* → ${API_ORIGIN}`)
console.log(` 其余 → ${WEB_ORIGIN}`)
})

View File

@ -7,23 +7,27 @@ interface Props {
moduleZh?: string moduleZh?: string
note?: string note?: string
} }
const { module = "This module", moduleZh = "", note = "" } = Astro.props const { module = "", moduleZh = "", note = "" } = Astro.props
const { locale } = getI18n(Astro) const { locale, t } = getI18n(Astro)
// 原实现整段是硬编码中文,英文站会直接显示中文。这里全部走词典,
// 模块名按语言取:中文优先 moduleZh,英文只取 module。
const moduleLabel = locale === 'cn' ? moduleZh || module : module || moduleZh
--- ---
<section class="cs-wrap"> <section class="cs-wrap">
<div class="cs-kicker"> <div class="cs-kicker">
<span>Coming Soon</span> <span>{t('Coming Soon')}</span>
<span>暂未上线</span> <span>{t('Archive')}</span>
</div> </div>
<h1 class="cs-title">暂未上线</h1> <h1 class="cs-title">{t('Coming Soon')}</h1>
<p class="cs-mod">{module}{moduleZh && ` · ${moduleZh}`}</p> <p class="cs-mod">{moduleLabel}</p>
<p class="cs-desc"> <p class="cs-desc">
{note || "该模块正在筹备中,尚未对外开放。站点正按模块逐个完善,你可以先浏览已上线的走秀档案。"} {note || t('This module is under construction and not open to the public yet. Browse the runway archive first.')}
</p> </p>
<div class="cs-actions"> <div class="cs-actions">
<a class="cs-btn" href={href(locale, ROUTES.runwayLooks)}>浏览走秀档案 →</a> <a class="cs-btn" href={href(locale, ROUTES.runwayLooks)}>{t('Browse runway archive →')}</a>
<a class="cs-btn-ghost" href={href(locale, ROUTES.home)}>返回首页</a> <a class="cs-btn-ghost" href={href(locale, ROUTES.home)}>{t('Back to home')}</a>
</div> </div>
</section> </section>

View File

@ -1,12 +1,17 @@
--- ---
// 登录页的 en/cn 共享实现(语言由 Astro.currentLocale 运行时决定)。 // 登录页的 en/cn 共享实现(语言由 Astro.currentLocale 运行时决定)。
// 对应页面:src/pages/{en,cn}/login.astro —— 仅做薄包装。 // 对应页面:src/pages/{en,cn}/login.astro —— 仅做薄包装。
// 注意:本页**不走 Layout**,自带完整 <html> 文档(独立极简版式,故保留 <!doctype html>)。 //
// 设计方向:把登录页当成「一期杂志的封面页」——整幅满血封面 + 竖排刊头 + 套准标记 +
// 极端字距的巨标题。延续全站的黑白 / 直角 / 粗线语言,但把张力拉满,避免"白底表单"的平庸。
// 本页**不走 Layout**,自带完整 <html> 文档。
import '@/styles/global.css' import '@/styles/global.css'
import { getI18n } from '@/lib/i18n' import { getI18n } from '@/lib/i18n'
import { href, ROUTES } from '@/lib/routes' import { href, ROUTES } from '@/lib/routes'
import { config } from '@/lib/config' import { config } from '@/lib/config'
import { LOCALE_NAMES } from '@/lib/i18n' import { LOCALE_NAMES } from '@/lib/i18n'
import { getSsgIndexRunway, getSsgStreetSnapPopular } from '@/lib/ssg'
import { toAbs } from '@/lib/request'
import { i18n } from 'astro:config/client'; import { i18n } from 'astro:config/client';
import { toCodes } from 'astro:i18n'; import { toCodes } from 'astro:i18n';
@ -24,7 +29,6 @@ const promptLabel = t('login prompt')
const accountLabel = t('username or email') const accountLabel = t('username or email')
const passwordLabel = t('password') const passwordLabel = t('password')
const failLabel = t('login failed') const failLabel = t('login failed')
const membersLabel = t('members')
const backLabel = t('back to site') const backLabel = t('back to site')
const homeUrl = href(locale, ROUTES.runwayLooks) const homeUrl = href(locale, ROUTES.runwayLooks)
const SITE_NAME = config.siteName ?? "Xisoa" const SITE_NAME = config.siteName ?? "Xisoa"
@ -35,7 +39,19 @@ const loginDesc = locale === 'cn'
const loginKeywords = locale === 'cn' const loginKeywords = locale === 'cn'
? `登录,账户,${SITE_NAME}` ? `登录,账户,${SITE_NAME}`
: `login,account,${SITE_NAME}` : `login,account,${SITE_NAME}`
// 封面区:库里原图统一是 240×360(爬虫入库下的是缩略图),
// 放大成整屏必然糊,所以改用「接触印样」版式——多张图按原始尺寸铺成网格,
// 既不会拉伸,也符合时尚档案的视觉习惯。取不到图则降级为纯黑排版块。
const showCovers = (await getSsgIndexRunway()).hotBrand
.filter((b) => b.cover)
.map((b) => toAbs(b.cover))
const snapCovers = (await getSsgStreetSnapPopular(8))
.filter((s) => s.cover)
.map((s) => toAbs(s.cover))
const sheet = [...new Set([...showCovers, ...snapCovers])].slice(0, 6)
--- ---
<!doctype html> <!doctype html>
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}> <html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
<head> <head>
@ -45,96 +61,187 @@ const loginKeywords = locale === 'cn'
<title>{pageTitle}</title> <title>{pageTitle}</title>
<meta name="description" content={loginDesc} /> <meta name="description" content={loginDesc} />
<meta name="keywords" content={loginKeywords} /> <meta name="keywords" content={loginKeywords} />
<style>
/* 印刷质感:极细扫描线,替代噪声图,零外部资源 */
.paper-grain {
background-image: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.035) 0 1px,
transparent 1px 3px
);
}
/* 逐行上浮:整页一次编排好的入场,而不是零散的微交互 */
@keyframes rise {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: none; }
}
.rise { animation: rise 0.62s cubic-bezier(0.22, 0.61, 0.36, 1) both; }
.rise-1 { animation-delay: 0.04s; }
.rise-2 { animation-delay: 0.12s; }
.rise-3 { animation-delay: 0.20s; }
.rise-4 { animation-delay: 0.28s; }
.rise-5 { animation-delay: 0.36s; }
@media (prefers-reduced-motion: reduce) {
.rise { animation: none; }
}
/* 竖排刊头(东方式版式,与"档案"气质一致) */
.vertical-label {
writing-mode: vertical-rl;
text-orientation: mixed;
}
/* 输入框:底线 2px,聚焦时整行压黑 */
.inkinput {
border: 0;
border-bottom: 2px solid rgba(0, 0, 0, 0.35);
background: transparent;
transition: border-color 0.18s ease;
}
.inkinput:focus { outline: none; border-bottom-color: #000; }
</style>
</head> </head>
<!-- 全站一致:白底 + 粗黑大标题 + 宽字距小标 + 粗黑细线,直角无阴影 -->
<body class="min-h-screen bg-white text-black antialiased selection:bg-black selection:text-white"> <body class="min-h-screen bg-white text-black antialiased selection:bg-black selection:text-white">
<main class="mx-auto w-full max-w-[1400px] px-5 py-10 md:px-10 md:py-14 font-sans"> <div class="mx-auto flex min-h-screen w-full max-w-[1700px] flex-col">
<!-- 顶部工具行:返回档案 + 语言切换 --> <!-- 顶栏:返回 / 期号 / 语言 -->
<header class="mb-12 flex items-center justify-between gap-6"> <header class="flex items-center justify-between gap-4 border-b border-black/15 px-5 py-4 md:px-10">
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.22em] text-black/50 transition-colors hover:text-black"> <a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.22em] text-black/55 transition-colors hover:text-black">
<span aria-hidden="true">←</span>{backLabel} <span aria-hidden="true">←</span>{backLabel}
</a> </a>
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false" @click.outside="open = false"> <div class="flex items-center gap-5">
<button type="button" @click="open = !open" class="inline-flex items-center gap-2 border border-black/15 px-3 py-2 text-[11px] uppercase tracking-[0.22em] text-black transition-colors hover:bg-black hover:text-white" aria-label={t('Language')} aria-haspopup="listbox" :aria-expanded="open"> <span class="hidden text-[10px] uppercase tracking-[0.3em] text-black/35 sm:inline">N° 04 / 2026</span>
<span>{LOCALE_NAMES[locale].nativeName}</span>
<svg viewBox="0 0 12 12" width="11" height="11" class="transition-transform duration-200" :class="open ? 'rotate-180' : ''" aria-hidden="true"> <div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false" @click.outside="open = false">
<path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/> <button type="button" @click="open = !open" class="inline-flex items-center gap-2 border border-black/20 px-3 py-2 text-[11px] uppercase tracking-[0.22em] text-black transition-colors hover:bg-black hover:text-white" aria-label={t('language')} aria-haspopup="listbox" :aria-expanded="open">
</svg> <span>{LOCALE_NAMES[locale].nativeName}</span>
</button> <svg viewBox="0 0 12 12" width="11" height="11" class="transition-transform duration-200" :class="open ? 'rotate-180' : ''" aria-hidden="true">
<ul x-show="open" x-cloak class="absolute right-0 top-full z-20 mt-2 min-w-[170px] border border-black/15 bg-white py-1"> <path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
{languages.map((l) => ( </svg>
<li> </button>
<a href={getRelativeLocaleUrl(l.code, pathWithoutLocale)} @click="open = false" class:list={['block px-4 py-2 text-[11px] uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white', l.code === locale ? 'font-bold text-black' : 'text-black/55']}>{l.label}</a> <ul x-show="open" x-cloak class="absolute right-0 top-full z-30 mt-2 min-w-[170px] border border-black/20 bg-white py-1">
</li> {languages.map((l) => (
))} <li>
</ul> <a href={getRelativeLocaleUrl(l.code, pathWithoutLocale)} @click="open = false" class:list={['block px-4 py-2 text-[11px] uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white', l.code === locale ? 'font-bold text-black' : 'text-black/55']}>{l.label}</a>
</li>
))}
</ul>
</div>
</div> </div>
</header> </header>
<!-- 报头:与账户页同一套 masthead(宽字距眉标 + 粗黑巨标题 + 档案标签 + 2px 粗黑线) --> <!-- 主体:左封面 / 右表单 -->
<section class="border-b-2 border-black pb-10"> <main class="grid flex-1 md:grid-cols-[1.08fr_1fr]">
<div class="flex flex-wrap items-end justify-between gap-x-16 gap-y-8">
<div> <!-- 左:整幅封面 -->
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{membersLabel}</p> <aside class="relative hidden overflow-hidden bg-black md:block">
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl">{loginLabel}</h1> {sheet.length > 0 ? (
<div class="grid h-full grid-cols-3 grid-rows-2 gap-px bg-black">
{sheet.map((src, i) => (
<div class="relative min-h-0 overflow-hidden">
<img
src={src}
alt=""
loading={i < 3 ? 'eager' : 'lazy'}
decoding="async"
class="absolute inset-0 h-full w-full object-cover"
/>
</div>
))}
</div>
) : (
<div class="h-full w-full bg-black"></div>
)}
<!-- 压暗 + 颗粒 -->
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/45 to-black/10"></div>
<div class="paper-grain absolute inset-0 opacity-60 mix-blend-overlay"></div>
<!-- 左侧竖排刊头 -->
<div class="absolute left-5 top-8 flex flex-col items-center gap-4">
<span class="vertical-label text-[10px] uppercase tracking-[0.42em] text-white/55">{t('fashion archive')}</span>
<span class="h-16 w-px bg-white/30"></span>
</div> </div>
<div class="pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-[var(--c-gray-600)]">
<p>{SITE_NAME}</p> <!-- 四角套准标记 -->
<p class="flex items-center justify-end gap-2 text-[var(--c-gray-700)]">Archive <span class="absolute left-5 top-5 h-3 w-3 border-l border-t border-white/40"></span>
<span class="bg-black px-1.5 py-0.5 text-[10px] font-bold tracking-widest text-white">2026</span> <span class="absolute right-5 top-5 h-3 w-3 border-r border-t border-white/40"></span>
</p> <span class="absolute bottom-5 left-5 h-3 w-3 border-b border-l border-white/40"></span>
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p> <span class="absolute bottom-5 right-5 h-3 w-3 border-b border-r border-white/40"></span>
<!-- 底部刊名(压在图上) -->
<div class="absolute inset-x-0 bottom-0 p-8 md:p-12">
<p class="text-[10px] uppercase tracking-[0.4em] text-white/60">{t('archive')}</p>
<p class="mt-3 text-5xl font-black uppercase leading-[0.8] tracking-[-0.04em] text-white lg:text-7xl">{SITE_NAME}</p>
<p class="mt-5 text-[10px] uppercase tracking-[0.34em] text-white/50">{t('paris · milan · tokyo')}</p>
</div> </div>
</div> </aside>
</section>
<!-- 主体:左表单 / 右档案说明(mobile 单栏堆叠) --> <!-- 右:表单 -->
<div class="grid gap-12 pt-12 md:grid-cols-[minmax(0,1fr)_320px] md:gap-16 md:pt-16"> <section class="relative flex flex-col justify-center px-5 py-10 md:px-14 md:py-14">
<span class="absolute right-5 top-5 hidden h-3 w-3 border-r border-t border-black/20 md:block"></span>
<div class="w-full max-w-md"> <p class="rise rise-1 flex items-center gap-2 text-[10px] uppercase tracking-[0.36em] text-black/45">
<p class="mb-10 text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]">{promptLabel}</p> <span class="inline-block h-[7px] w-[7px] bg-black"></span>{t('members only')}
</p>
<form x-data="loginForm" data-locale={locale} data-fail-label={failLabel} @submit.prevent="submit()" class="space-y-8" novalidate> <h1 class="rise rise-2 mt-5 text-[clamp(52px,8vw,104px)] font-black uppercase leading-[0.82] tracking-[-0.045em]">
<div> {loginLabel}
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-700)]">{accountLabel}</label> </h1>
<input type="text" x-model="account" autocomplete="username" required autofocus
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-3 text-sm text-black outline-none transition-colors focus:border-black focus:ring-0" /> <p class="rise rise-2 mt-6 max-w-sm text-sm leading-relaxed text-black/55">{promptLabel}</p>
<form x-data="loginForm" data-locale={locale} data-fail-label={failLabel} @submit.prevent="submit()" class="rise rise-3 mt-12 w-full max-w-md" novalidate>
<div class="flex items-baseline gap-4">
<span class="w-6 shrink-0 text-[10px] font-bold tracking-[0.2em] text-black/30">01</span>
<div class="flex-1">
<label for="lg-account" class="mb-1 block text-[10px] uppercase tracking-[0.28em] text-black/45">{accountLabel}</label>
<input id="lg-account" type="text" x-model="account" autocomplete="username" required autofocus
class="inkinput w-full py-3 text-base text-black md:text-lg" />
</div>
</div> </div>
<div> <div class="mt-9 flex items-baseline gap-4">
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-700)]">{passwordLabel}</label> <span class="w-6 shrink-0 text-[10px] font-bold tracking-[0.2em] text-black/30">02</span>
<input type="password" x-model="password" autocomplete="current-password" required <div class="flex-1">
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-3 text-sm text-black outline-none transition-colors focus:border-black focus:ring-0" /> <label for="lg-password" class="mb-1 block text-[10px] uppercase tracking-[0.28em] text-black/45">{passwordLabel}</label>
<input id="lg-password" type="password" x-model="password" autocomplete="current-password" required
class="inkinput w-full py-3 text-base text-black md:text-lg" />
</div>
</div> </div>
<p x-show="error" x-text="error" x-cloak class="text-[11px] uppercase tracking-[0.2em] text-red-600"></p> <p x-show="error" x-text="error" x-cloak
class="mt-7 border-l-4 border-black bg-black px-4 py-3 text-[11px] uppercase tracking-[0.18em] text-white"></p>
<button type="submit" :disabled="loading" <button type="submit" :disabled="loading"
class="w-full border border-black bg-black py-4 text-[12px] font-bold uppercase tracking-[0.2em] text-white transition-colors hover:bg-white hover:text-black disabled:cursor-not-allowed disabled:opacity-50"> class="group mt-10 flex w-full items-center justify-between border border-black bg-black px-6 py-5 text-[12px] font-bold uppercase tracking-[0.24em] text-white transition-colors hover:bg-white hover:text-black disabled:cursor-not-allowed disabled:opacity-50">
<span x-show="!loading">{loginLabel}</span> <span x-show="!loading">{t('enter the archive →')}</span>
<span x-show="loading" x-cloak>…</span> <span x-show="loading" x-cloak>{t('signing in…')}</span>
<span aria-hidden="true" class="text-base transition-transform duration-200 group-hover:translate-x-1">→</span>
</button> </button>
</form> </form>
<div class="mt-10 border-t border-black/15 pt-5"> <!-- 说明与补充入口 -->
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.2em] text-[var(--c-gray-600)] transition-colors hover:text-black"> <div class="rise rise-4 mt-12 max-w-md border-t border-black/10 pt-6">
<p class="text-[10px] uppercase tracking-[0.3em] text-black/40">{t('archive note')}</p>
<p class="mt-3 text-[13px] leading-relaxed text-black/55">
{t('minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. integrated with 2026 color forecast intelligence and ai palette generator.')}
</p>
</div>
<div class="rise rise-5 mt-8 flex max-w-md flex-wrap items-center justify-between gap-3 text-[10px] uppercase tracking-[0.24em] text-black/40">
<a href={homeUrl} class="inline-flex items-center gap-2 transition-colors hover:text-black">
<span aria-hidden="true">←</span>{backLabel} <span aria-hidden="true">←</span>{backLabel}
</a> </a>
<span class="tracking-[0.28em]">{t('runway · street · archive')}</span>
</div> </div>
</div> </section>
</main>
<aside class="border-t border-black/10 pt-8 md:border-l md:border-t-0 md:pl-8 md:pt-0"> <footer class="flex items-center justify-between border-t border-black/10 px-5 py-4 text-[10px] uppercase tracking-[0.28em] text-black/35 md:px-10">
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">Archive note</p> <p>© 2026 {SITE_NAME}</p>
<p class="text-lg leading-relaxed text-black/80">{t('minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. integrated with 2026 color forecast intelligence and ai palette generator.')}</p> <p>Paris · Milan · Tokyo</p>
<div class="mt-8 border-t border-black/10 pt-5 text-[10px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]"> </footer>
<p class="mb-3">Runway · Street · Archive</p> </div>
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
</div>
</aside>
</div>
</main>
<script> <script>
import { getUser } from "@/lib/auth" import { getUser } from "@/lib/auth"

View File

@ -246,7 +246,7 @@ const headers = {
<slot /> <slot />
</div> </div>
<footer class="relative z-[1] bg-white mt-20 pt-10 border-t-2 border-black text-black w-full font-sans"> <footer class="relative z-[1] mt-20 w-full bg-black font-sans text-white">
<script> <script>
import { getUser } from "@/lib/auth" import { getUser } from "@/lib/auth"
import type { AuthUser } from "@/lib/auth" import type { AuthUser } from "@/lib/auth"
@ -343,58 +343,61 @@ const headers = {
})) }))
}) })
</script> </script>
{/* Top Footer Grid */} {/* 页脚主体:黑底反白,作为整页收尾(沿用全站直角 / 粗线 / 宽字距语言) */}
<div class="grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-8 mb-10 pb-10 border-b border-black/10"> <div class="mx-auto w-full max-w-[1400px] px-5 py-14 md:px-10 md:py-20">
{/* Column 1: Brand & Manifesto */} <div class="grid grid-cols-1 gap-12 md:grid-cols-12 md:gap-8">
<div class="md:col-span-5 space-y-4">
<div class="flex items-center gap-2"> {/* 品牌 */}
<span class="text-base font-black tracking-wider uppercase border-b-2 border-black pb-0.5"> <div class="md:col-span-6">
<p class="text-[10px] uppercase tracking-[0.34em] text-white/35">{t('Archive')}</p>
<p class="mt-5 text-5xl font-black uppercase leading-[0.85] tracking-tight md:text-7xl">
{SITE_NAME} {SITE_NAME}
</span> </p>
<span class="text-[10px] bg-black text-white px-2 py-0.5 font-bold uppercase tracking-widest rounded-full"> <p class="mt-7 max-w-md text-sm leading-relaxed text-white/45">
2026 {t('Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.')}
</span> </p>
<div class="mt-8 flex flex-wrap items-center gap-x-3 gap-y-2 text-[10px] uppercase tracking-[0.3em] text-white/30">
<span>Paris</span><span>·</span><span>Milan</span><span>·</span><span>Tokyo</span><span>·</span><span>New York</span>
</div>
</div> </div>
<p class="text-xs text-[var(--c-gray-800)] leading-relaxed max-w-md"> {/* 导航 */}
{t('Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.')} <div class="md:col-span-3 md:col-start-8">
</p> <h3 class="text-[10px] uppercase tracking-[0.3em] text-white/35">{t('Explore')}</h3>
<ul class="mt-5 space-y-3 text-[11px] uppercase tracking-[0.18em]">
<li><a href={href(locale, ROUTES.runwayLooks)} class="text-white/65 transition-colors hover:text-white">{t('all runway looks')}</a></li>
<li><a href={href(locale, ROUTES.streetSnaps)} class="text-white/65 transition-colors hover:text-white">{t('Street Style')}</a></li>
<li><a href={href(locale, ROUTES.account)} class="text-white/65 transition-colors hover:text-white">{t('my account')}</a></li>
</ul>
</div>
{/* Location Badge */} {/* 站点操作(全部是真实可用的入口,不留死链) */}
<div class="flex flex-wrap gap-3 text-[11px] text-[var(--c-gray-700)] uppercase tracking-widest pt-1"> <div class="md:col-span-3">
<span>Paris</span> <h3 class="text-[10px] uppercase tracking-[0.3em] text-white/35">{t('Site')}</h3>
<span>•</span> <ul class="mt-5 space-y-3 text-[11px] uppercase tracking-[0.18em]">
<span>Milan</span> <li>
<span>•</span> <a href={href(locale === 'cn' ? 'en' : 'cn', ROUTES.home)} class="text-white/65 transition-colors hover:text-white">
<span>Tokyo</span> {locale === 'cn' ? 'English' : '中文'}
<span>•</span> </a>
<span>New York</span> </li>
<li>
<button
type="button"
onclick="window.scrollTo({ top: 0, behavior: 'smooth' })"
class="cursor-pointer text-left uppercase tracking-[0.18em] text-white/65 transition-colors hover:text-white"
>
{t('Back to top ↑')}
</button>
</li>
</ul>
</div> </div>
</div> </div>
{/* Column 2: Explore */} {/* 底部条 */}
<div class="md:col-span-4 md:col-start-7 space-y-3"> <div class="mt-14 flex flex-col gap-4 border-t border-white/10 pt-6 text-[10px] uppercase tracking-[0.26em] text-white/35 sm:flex-row sm:items-center sm:justify-between">
<h3 class="text-[11px] tracking-[0.26em] uppercase font-bold text-black">Explore</h3> <p>© 2026 {SITE_NAME}. {t('all rights reserved.')}</p>
<ul class="space-y-2 text-[11px] uppercase tracking-[0.18em] text-[var(--c-gray-800)]"> <p class="tracking-[0.3em]">{t('Runway · Street · Archive')}</p>
<li><a href={href(locale, ROUTES.runwayLooks)} class="hover:text-black transition-colors">{t('all runway looks')}</a></li>
<li><a href={href(locale, ROUTES.streetSnaps)} class="hover:text-black transition-colors">{t('Street Style')}</a></li>
<li><a href={href(locale, ROUTES.account)} class="hover:text-black transition-colors">{t('my account')}</a></li>
</ul>
</div> </div>
{/* Column 3: Connect */}
<div class="md:col-span-2 space-y-3">
<h3 class="text-[11px] tracking-[0.26em] uppercase font-bold text-black">Connect</h3>
<ul class="space-y-2 text-[11px] uppercase tracking-[0.18em] text-[var(--c-gray-800)]">
<li><a href="#" class="hover:text-black transition-colors">Instagram</a></li>
<li><a href="#" class="hover:text-black transition-colors">Newsletter</a></li>
<li><a href="#" class="hover:text-black transition-colors">Contact</a></li>
</ul>
</div>
</div>
<div class="mt-6 text-[11px] text-[var(--c-gray-600)] uppercase tracking-widest">
© 2026 {SITE_NAME}. {t('all rights reserved.')}
</div> </div>
</footer> </footer>

View File

@ -21,6 +21,29 @@ export const dict: Record<string, TranslationEntry> = {
'loading': { cn: '加载中' }, 'loading': { cn: '加载中' },
// ── 通用文案 / 页脚 / 登录页 ──
// ⚠️ 必须全小写:translate() 用 dict[key.toLowerCase()] 查表,
// 带大写字母的 key 永远命中不了,会静默回退英文。
'page not found': { cn: '页面不存在' },
'coming soon': { cn: '即将上线' },
'archive': { cn: '档案' },
'archive note': { cn: '档案说明' },
'explore': { cn: '探索' },
'site': { cn: '站点' },
'back to top ↑': { cn: '返回顶部 ↑' },
'signing in…': { cn: '登录中…' },
'runway · street · archive': { cn: '秀场 · 街拍 · 档案' },
'paris · milan · tokyo': { cn: '巴黎 · 米兰 · 东京' },
'this module is under construction and not open to the public yet. browse the runway archive first.':
{ cn: '该模块正在筹备中,尚未对外开放。你可以先浏览已上线的走秀档案。' },
'browse runway archive →': { cn: '浏览走秀档案 →' },
'back to home': { cn: '返回首页' },
'enter the archive →': { cn: '进入档案 →' },
'members only': { cn: '仅限会员' },
'fashion archive': { cn: '时尚档案' },
'forgot password?': { cn: '忘记密码?' },
'ask the editor': { cn: '联系管理员' },
// ── 导航 / Navigation ── // ── 导航 / Navigation ──
'home': { cn: '首页' }, 'home': { cn: '首页' },
'login': { cn: '登录' }, 'login': { cn: '登录' },
@ -95,11 +118,11 @@ export const dict: Record<string, TranslationEntry> = {
'no street snap id specified': { cn: '未指定街拍 ID' }, 'no street snap id specified': { cn: '未指定街拍 ID' },
'failed to load street snap. please refresh or try later.': { cn: '加载街拍失败,请刷新或稍后再试' }, 'failed to load street snap. please refresh or try later.': { cn: '加载街拍失败,请刷新或稍后再试' },
'brands': { cn: '品牌' }, 'brands': { cn: '品牌' },
'Year': { cn: '年份' }, 'year': { cn: '年份' },
'Sort': { cn: '排序' }, 'sort': { cn: '排序' },
'Most photos': { cn: '照片最多' }, 'most photos': { cn: '照片最多' },
'Newest year': { cn: '年份最新' }, 'newest year': { cn: '年份最新' },
'Oldest year': { cn: '年份最早' }, 'oldest year': { cn: '年份最早' },
// ── 地区 / Cities(街拍按城市分类,值对齐后端 street_snap.city 列;translate 会小写化 key)── // ── 地区 / Cities(街拍按城市分类,值对齐后端 street_snap.city 列;translate 会小写化 key)──
'city': { cn: '地区' }, 'city': { cn: '地区' },