Compare commits
1 Commits
dev
...
56e3cfcff7
| Author | SHA1 | Date | |
|---|---|---|---|
| 56e3cfcff7 |
@ -1,5 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
# ⚠️ 不要忽略 dist:本地预构建时 Dockerfile.prebuilt 需要 COPY 它
|
dist
|
||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
.gitea
|
.gitea
|
||||||
|
|||||||
8
.gitattributes
vendored
8
.gitattributes
vendored
@ -1,8 +0,0 @@
|
|||||||
# Dockerfile 必须 LF:CRLF 会让续行符 `\` 后面多出一个 \r,被当成指令参数
|
|
||||||
Dockerfile text eol=lf
|
|
||||||
Dockerfile.* text eol=lf
|
|
||||||
|
|
||||||
# 脚本与配置统一 LF
|
|
||||||
*.sh text eol=lf
|
|
||||||
*.yml text eol=lf
|
|
||||||
*.conf text eol=lf
|
|
||||||
@ -1,27 +1,18 @@
|
|||||||
name: deploy
|
name: deploy
|
||||||
on:
|
on:
|
||||||
<<<<<<< HEAD
|
push:
|
||||||
# 手动触发:在 Gitea 仓库 Actions 页面点「Run workflow」才部署,
|
branches: [main, master]
|
||||||
# 避免每次 push 到 main 的半成品代码被自动上线。
|
|
||||||
=======
|
|
||||||
>>>>>>> 29c2d77a8585ff20a7ed193cd45a7a291c63befe
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
steps:
|
steps:
|
||||||
- name: SSH deploy
|
- name: SSH 到服务器执行上线脚本
|
||||||
shell: sh
|
uses: appleboy/ssh-action@v1
|
||||||
env:
|
with:
|
||||||
KEY_B64: ${{ secrets.DEPLOY_KEY_B64 }}
|
host: ${{ secrets.DEPLOY_HOST }}
|
||||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
username: ${{ secrets.DEPLOY_USER }}
|
||||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
key: ${{ secrets.DEPLOY_KEY }}
|
||||||
run: |
|
script: |
|
||||||
set -e
|
cd /opt/fashion/test
|
||||||
apk add --no-cache openssh-client >/dev/null 2>&1 || true
|
./deploy.sh
|
||||||
mkdir -p "$HOME/.ssh"
|
|
||||||
echo "$KEY_B64" | base64 -d > "$HOME/.ssh/id"
|
|
||||||
chmod 600 "$HOME/.ssh/id"
|
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i "$HOME/.ssh/id" \
|
|
||||||
"$DEPLOY_USER@$DEPLOY_HOST" "cd /opt/fashion/backend_v2 && ./deploy.sh web --no-cache"
|
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
# 预构建镜像:dist/ 在本机 build 好(构建期 SSG 数据经 SSH 隧道取自线上),
|
|
||||||
# 服务器只装运行时依赖并打层,不跑 astro build。
|
|
||||||
# 由 backend/scripts/push-build.sh 与 backend/deploy.sh --prebuilt 使用。
|
|
||||||
|
|
||||||
FROM node:22-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
|
||||||
HOST=0.0.0.0 \
|
|
||||||
PORT=3000
|
|
||||||
|
|
||||||
# 运行时依赖层:只在 package-lock 变化时重装,日常发布命中缓存
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --omit=dev && npm cache clean --force
|
|
||||||
|
|
||||||
COPY dist ./dist
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=15s --retries=3 \
|
|
||||||
CMD nc -z 127.0.0.1 3000 || exit 1
|
|
||||||
|
|
||||||
CMD ["node", "./dist/server/entry.mjs"]
|
|
||||||
@ -10,7 +10,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
// 本地预览用的极简代理:模拟生产里 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}`)
|
|
||||||
})
|
|
||||||
@ -23,18 +23,16 @@ const fullLoginLabel = t('go to full login')
|
|||||||
x-show="isOpen"
|
x-show="isOpen"
|
||||||
x-cloak
|
x-cloak
|
||||||
data-auth-fail={failLabel}
|
data-auth-fail={failLabel}
|
||||||
data-auth-modal
|
|
||||||
class="fixed inset-0 z-[200] flex items-center justify-center bg-black/40 px-5"
|
class="fixed inset-0 z-[200] flex items-center justify-center bg-black/40 px-5"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
@click.self="close()"
|
@click.self="close()"
|
||||||
@keydown.escape.window="close()"
|
@keydown.escape.window="close()"
|
||||||
@keydown.tab="trapFocus($event)"
|
|
||||||
>
|
>
|
||||||
<div class="w-full max-w-sm border-2 border-black bg-white p-8">
|
<div class="w-full max-w-sm border-2 border-black bg-white p-8">
|
||||||
<div class="mb-6 flex items-start justify-between border-b-2 border-black pb-4">
|
<div class="mb-6 flex items-start justify-between border-b-2 border-black pb-4">
|
||||||
<h3 class="text-2xl font-semibold uppercase leading-none tracking-display">{title}</h3>
|
<h3 class="text-2xl font-black uppercase leading-none tracking-tight">{title}</h3>
|
||||||
<button type="button" @click="close()" class="-mt-1 text-[11px] uppercase tracking-widest opacity-60 transition-opacity hover:opacity-100">{closeLabel}</button>
|
<button type="button" @click="close()" class="-mt-1 text-[11px] uppercase tracking-widest opacity-60 transition-opacity hover:opacity-100">{closeLabel}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -42,19 +40,19 @@ const fullLoginLabel = t('go to full login')
|
|||||||
|
|
||||||
<form @submit.prevent="submit()" class="space-y-6" novalidate>
|
<form @submit.prevent="submit()" class="space-y-6" novalidate>
|
||||||
<div>
|
<div>
|
||||||
<label for="auth-account" class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{accountLabel}</label>
|
<label class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{accountLabel}</label>
|
||||||
<input id="auth-account" type="text" x-model="account" autocomplete="username" required
|
<input type="text" x-model="account" autocomplete="username" required
|
||||||
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
|
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="auth-password" class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{passwordLabel}</label>
|
<label class="mb-2 block text-[11px] uppercase tracking-widest text-[var(--c-gray-700)]">{passwordLabel}</label>
|
||||||
<input id="auth-password" type="password" x-model="password" autocomplete="current-password" required
|
<input type="password" x-model="password" autocomplete="current-password" required
|
||||||
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
|
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p x-show="error" x-text="error" x-cloak class="text-xs uppercase tracking-wide text-red-600"></p>
|
<p x-show="error" x-text="error" x-cloak class="text-xs uppercase tracking-wide text-red-600"></p>
|
||||||
|
|
||||||
<button type="submit" :disabled="loading" :aria-busy="loading"
|
<button type="submit" :disabled="loading"
|
||||||
class="w-full border border-black bg-black py-3.5 text-[12px] font-bold uppercase tracking-[0.2em] text-white transition-colors hover:bg-white hover:text-black disabled:opacity-40">
|
class="w-full border border-black bg-black py-3.5 text-[12px] font-bold uppercase tracking-[0.2em] text-white transition-colors hover:bg-white hover:text-black disabled:opacity-40">
|
||||||
<span x-show="!loading">{title}</span>
|
<span x-show="!loading">{title}</span>
|
||||||
<span x-show="loading" x-cloak>…</span>
|
<span x-show="loading" x-cloak>…</span>
|
||||||
@ -77,7 +75,6 @@ const fullLoginLabel = t('go to full login')
|
|||||||
interface AuthStore {
|
interface AuthStore {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
pending: HTMLElement | null
|
pending: HTMLElement | null
|
||||||
lastFocused: HTMLElement | null
|
|
||||||
account: string
|
account: string
|
||||||
password: string
|
password: string
|
||||||
error: string
|
error: string
|
||||||
@ -86,7 +83,6 @@ const fullLoginLabel = t('go to full login')
|
|||||||
close(): void
|
close(): void
|
||||||
submit(): Promise<void>
|
submit(): Promise<void>
|
||||||
goFullLogin(): void
|
goFullLogin(): void
|
||||||
trapFocus(e: KeyboardEvent): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
@ -94,7 +90,6 @@ const fullLoginLabel = t('go to full login')
|
|||||||
const authStore: AuthStore = {
|
const authStore: AuthStore = {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
pending: null as HTMLElement | null,
|
pending: null as HTMLElement | null,
|
||||||
lastFocused: null as HTMLElement | null,
|
|
||||||
account: "",
|
account: "",
|
||||||
password: "",
|
password: "",
|
||||||
error: "",
|
error: "",
|
||||||
@ -103,15 +98,9 @@ const fullLoginLabel = t('go to full login')
|
|||||||
request(el: HTMLElement): boolean {
|
request(el: HTMLElement): boolean {
|
||||||
if (getUser()) return true // 已登录:放行,交还原生点击
|
if (getUser()) return true // 已登录:放行,交还原生点击
|
||||||
this.pending = el
|
this.pending = el
|
||||||
this.lastFocused = el
|
|
||||||
this.error = ""
|
this.error = ""
|
||||||
this.isOpen = true
|
this.isOpen = true
|
||||||
document.body.style.overflow = "hidden" // 锁背景滚动
|
document.body.style.overflow = "hidden" // 锁背景滚动
|
||||||
// 焦点移入弹窗:屏幕阅读器立即播报 aria-label,键盘用户不必从页面顶端重新 Tab
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const first = document.querySelector<HTMLElement>("[data-auth-modal] input")
|
|
||||||
first?.focus()
|
|
||||||
})
|
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -123,11 +112,6 @@ const fullLoginLabel = t('go to full login')
|
|||||||
this.pending = null
|
this.pending = null
|
||||||
this.error = ""
|
this.error = ""
|
||||||
document.body.style.overflow = ""
|
document.body.style.overflow = ""
|
||||||
// 关闭后把焦点还给触发它的元素。否则焦点会掉回 <body>,
|
|
||||||
// 键盘用户等于被「传送」回页面顶部,需要重新 Tab 十几下。
|
|
||||||
const back = this.lastFocused
|
|
||||||
this.lastFocused = null
|
|
||||||
if (back && document.contains(back)) back.focus?.()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
@ -158,29 +142,6 @@ const fullLoginLabel = t('go to full login')
|
|||||||
this.close()
|
this.close()
|
||||||
window.location.assign(`/${loc}/login?redirect=${encodeURIComponent(p)}`)
|
window.location.assign(`/${loc}/login?redirect=${encodeURIComponent(p)}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 焦点陷阱:aria-modal="true" 只是「声明」,不会自动把 Tab 关在弹窗里。
|
|
||||||
// 不实现这个,用户 Tab 几下就会跑到背后被遮住的页面元素上。
|
|
||||||
trapFocus(e: KeyboardEvent) {
|
|
||||||
const root = document.querySelector<HTMLElement>("[data-auth-modal]")
|
|
||||||
if (!root) return
|
|
||||||
const items = Array.from(
|
|
||||||
root.querySelectorAll<HTMLElement>(
|
|
||||||
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
||||||
)
|
|
||||||
).filter((el) => el.offsetParent !== null)
|
|
||||||
if (items.length === 0) return
|
|
||||||
const first = items[0]
|
|
||||||
const last = items[items.length - 1]
|
|
||||||
if (!first || !last) return
|
|
||||||
if (e.shiftKey && document.activeElement === first) {
|
|
||||||
e.preventDefault()
|
|
||||||
last.focus()
|
|
||||||
} else if (!e.shiftKey && document.activeElement === last) {
|
|
||||||
e.preventDefault()
|
|
||||||
first.focus()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Alpine.store("auth", authStore)
|
Alpine.store("auth", authStore)
|
||||||
|
|||||||
@ -7,27 +7,23 @@ interface Props {
|
|||||||
moduleZh?: string
|
moduleZh?: string
|
||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
const { module = "", moduleZh = "", note = "" } = Astro.props
|
const { module = "This module", moduleZh = "", note = "" } = Astro.props
|
||||||
const { locale, t } = getI18n(Astro)
|
const { locale } = 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>{t('Coming Soon')}</span>
|
<span>Coming Soon</span>
|
||||||
<span>{t('Archive')}</span>
|
<span>暂未上线</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="cs-title">{t('Coming Soon')}</h1>
|
<h1 class="cs-title">暂未上线</h1>
|
||||||
<p class="cs-mod">{moduleLabel}</p>
|
<p class="cs-mod">{module}{moduleZh && ` · ${moduleZh}`}</p>
|
||||||
<p class="cs-desc">
|
<p class="cs-desc">
|
||||||
{note || t('This module is under construction and not open to the public yet. Browse the runway archive first.')}
|
{note || "该模块正在筹备中,尚未对外开放。站点正按模块逐个完善,你可以先浏览已上线的走秀档案。"}
|
||||||
</p>
|
</p>
|
||||||
<div class="cs-actions">
|
<div class="cs-actions">
|
||||||
<a class="cs-btn" href={href(locale, ROUTES.runwayLooks)}>{t('Browse runway archive →')}</a>
|
<a class="cs-btn" href={href(locale, ROUTES.runwayLooks)}>浏览走秀档案 →</a>
|
||||||
<a class="cs-btn-ghost" href={href(locale, ROUTES.home)}>{t('Back to home')}</a>
|
<a class="cs-btn-ghost" href={href(locale, ROUTES.home)}>返回首页</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -48,7 +44,7 @@ const moduleLabel = locale === 'cn' ? moduleZh || module : module || moduleZh
|
|||||||
@apply text-[11px] tracking-[0.25em] uppercase text-[var(--c-gray-800)];
|
@apply text-[11px] tracking-[0.25em] uppercase text-[var(--c-gray-800)];
|
||||||
}
|
}
|
||||||
.cs-title {
|
.cs-title {
|
||||||
@apply font-bold tracking-[-0.02em];
|
@apply font-black tracking-[-0.02em];
|
||||||
line-height: 0.9;
|
line-height: 0.9;
|
||||||
font-size: clamp(56px, 13vw, 128px);
|
font-size: clamp(56px, 13vw, 128px);
|
||||||
margin: 0.16em 0 0.3em;
|
margin: 0.16em 0 0.3em;
|
||||||
|
|||||||
@ -132,7 +132,7 @@ const hasImages = previewImages.length > 0
|
|||||||
{
|
{
|
||||||
hasItem ? (
|
hasItem ? (
|
||||||
<article class="mx-auto max-w-[1600px] px-5 md:px-10 py-12 md:py-16">
|
<article class="mx-auto max-w-[1600px] px-5 md:px-10 py-12 md:py-16">
|
||||||
<h1 class="font-bold leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">
|
<h1 class="font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const browseLabel = t("browse the archive")
|
|||||||
<!-- ===== 未登录:整版引导 ===== -->
|
<!-- ===== 未登录:整版引导 ===== -->
|
||||||
<div x-show="!user" x-cloak class="flex flex-col items-center justify-center border border-black/15 px-6 py-32 text-center">
|
<div x-show="!user" x-cloak class="flex flex-col items-center justify-center border border-black/15 px-6 py-32 text-center">
|
||||||
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{pleaseLabel}</p>
|
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{pleaseLabel}</p>
|
||||||
<p class="mb-12 text-5xl font-bold uppercase leading-none tracking-tight md:text-7xl">{titleLabel}</p>
|
<p class="mb-12 text-5xl font-black uppercase leading-none tracking-tight md:text-7xl">{titleLabel}</p>
|
||||||
<a href={loginUrl} class="inline-flex items-center justify-center border border-black bg-black px-10 py-4 text-[12px] font-bold uppercase tracking-[0.22em] text-white transition-colors hover:bg-white hover:text-black">{loginLabel}</a>
|
<a href={loginUrl} class="inline-flex items-center justify-center border border-black bg-black px-10 py-4 text-[12px] font-bold uppercase tracking-[0.22em] text-white transition-colors hover:bg-white hover:text-black">{loginLabel}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ const browseLabel = t("browse the archive")
|
|||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{t("member profile")}</p>
|
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{t("member profile")}</p>
|
||||||
<div class="flex flex-wrap items-end gap-x-4">
|
<div class="flex flex-wrap items-end gap-x-4">
|
||||||
<h1 class="text-6xl font-bold uppercase leading-[0.88] tracking-tight md:text-8xl" x-text="(user?.username || '')"></h1>
|
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl" x-text="(user?.username || '')"></h1>
|
||||||
<!-- VIP 徽标:仅高级用户(图集详情页可见高清原图)显示 -->
|
<!-- VIP 徽标:仅高级用户(图集详情页可见高清原图)显示 -->
|
||||||
<span x-show="user?.tier === 'vip'" x-cloak class="mb-2 inline-block border border-black bg-black px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.3em] text-white">{t("vip")}</span>
|
<span x-show="user?.tier === 'vip'" x-cloak class="mb-2 inline-block border border-black bg-black px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.3em] text-white">{t("vip")}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -85,7 +85,7 @@ const browseLabel = t("browse the archive")
|
|||||||
<div x-show="tab==='saved'" x-cloak>
|
<div x-show="tab==='saved'" x-cloak>
|
||||||
|
|
||||||
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
|
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
|
||||||
<h2 class="text-4xl font-bold uppercase leading-none tracking-tight md:text-6xl">{savedLooksLabel}</h2>
|
<h2 class="text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{savedLooksLabel}</h2>
|
||||||
<p class="pb-1 text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p>
|
<p class="pb-1 text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ const browseLabel = t("browse the archive")
|
|||||||
|
|
||||||
<!-- 空态 -->
|
<!-- 空态 -->
|
||||||
<div x-show="favorites.length === 0" class="flex flex-col items-center justify-center border-y border-black/10 py-28 text-center">
|
<div x-show="favorites.length === 0" class="flex flex-col items-center justify-center border-y border-black/10 py-28 text-center">
|
||||||
<p class="mb-10 text-3xl font-bold uppercase tracking-tight text-black/80 md:text-4xl">{noSavedLabel}</p>
|
<p class="mb-10 text-3xl font-black uppercase tracking-tight text-black/80 md:text-4xl">{noSavedLabel}</p>
|
||||||
<a href={archiveUrl} class="inline-flex items-center justify-center border border-black bg-white px-8 py-3.5 text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
|
<a href={archiveUrl} class="inline-flex items-center justify-center border border-black bg-white px-8 py-3.5 text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ const browseLabel = t("browse the archive")
|
|||||||
<div x-show="tab==='history'" x-cloak>
|
<div x-show="tab==='history'" x-cloak>
|
||||||
|
|
||||||
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
|
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
|
||||||
<h2 class="text-4xl font-bold uppercase leading-none tracking-tight md:text-6xl">{historyLabel}</h2>
|
<h2 class="text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{historyLabel}</h2>
|
||||||
<div class="flex items-baseline gap-8 pb-1">
|
<div class="flex items-baseline gap-8 pb-1">
|
||||||
<p class="text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="histTotal"></span> {itemsLabel}</p>
|
<p class="text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]"><span class="font-bold text-black" x-text="histTotal"></span> {itemsLabel}</p>
|
||||||
<button x-show="histTotal > 0" @click="clearAllHistory()" class="text-[11px] uppercase tracking-[0.2em] text-[var(--c-gray-600)] underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button>
|
<button x-show="histTotal > 0" @click="clearAllHistory()" class="text-[11px] uppercase tracking-[0.2em] text-[var(--c-gray-600)] underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button>
|
||||||
@ -145,7 +145,7 @@ const browseLabel = t("browse the archive")
|
|||||||
|
|
||||||
<!-- 空态 -->
|
<!-- 空态 -->
|
||||||
<div x-show="histTotal === 0" class="flex flex-col items-center justify-center border-y border-black/10 py-28 text-center">
|
<div x-show="histTotal === 0" class="flex flex-col items-center justify-center border-y border-black/10 py-28 text-center">
|
||||||
<p class="mb-10 text-3xl font-bold uppercase tracking-tight text-black/80 md:text-4xl">{noHistoryLabel}</p>
|
<p class="mb-10 text-3xl font-black uppercase tracking-tight text-black/80 md:text-4xl">{noHistoryLabel}</p>
|
||||||
<a href={archiveUrl} class="inline-flex items-center justify-center border border-black bg-white px-8 py-3.5 text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
|
<a href={archiveUrl} class="inline-flex items-center justify-center border border-black bg-white px-8 py-3.5 text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -46,10 +46,10 @@ const streetItems: {
|
|||||||
<div x-data="{ activeTab: 'backstage' }" class="flex flex-col min-h-screen">
|
<div x-data="{ activeTab: 'backstage' }" class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
<!-- 二级分类标签页 -->
|
<!-- 二级分类标签页 -->
|
||||||
<div id="tabbar" class="mt-20 md:mt-28">
|
<div id="tabbar" class="mt-16 md:mt-24">
|
||||||
<ul class="flex items-center gap-7 md:gap-9 list-none p-0 m-0 overflow-x-auto no-scrollbar">
|
<ul class="flex items-center gap-6 list-none p-0 m-0 overflow-x-auto">
|
||||||
<li
|
<li
|
||||||
class="tab text-[clamp(1.875rem,4.6vw,3.25rem)] font-bold tracking-display leading-[0.95] cursor-pointer whitespace-nowrap"
|
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap"
|
||||||
:class="{ 'is-active': activeTab === 'backstage' }"
|
:class="{ 'is-active': activeTab === 'backstage' }"
|
||||||
@click="activeTab = 'backstage'"
|
@click="activeTab = 'backstage'"
|
||||||
>
|
>
|
||||||
@ -57,7 +57,7 @@ const streetItems: {
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class="tab text-[clamp(1.875rem,4.6vw,3.25rem)] font-bold tracking-display leading-[0.95] cursor-pointer whitespace-nowrap"
|
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap"
|
||||||
:class="{ 'is-active': activeTab === 'street' }"
|
:class="{ 'is-active': activeTab === 'street' }"
|
||||||
@click="activeTab = 'street'"
|
@click="activeTab = 'street'"
|
||||||
>
|
>
|
||||||
@ -65,7 +65,7 @@ const streetItems: {
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class="tab text-[clamp(1.875rem,4.6vw,3.25rem)] font-bold tracking-display leading-[0.95] cursor-pointer whitespace-nowrap"
|
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap"
|
||||||
:class="{ 'is-active': activeTab === 'events' }"
|
:class="{ 'is-active': activeTab === 'events' }"
|
||||||
@click="activeTab = 'events'"
|
@click="activeTab = 'events'"
|
||||||
>
|
>
|
||||||
@ -75,11 +75,11 @@ const streetItems: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 中间内容展示区 -->
|
<!-- 中间内容展示区 -->
|
||||||
<main class="grow min-h-[400px] mt-14">
|
<main class="grow min-h-[400px] mt-10">
|
||||||
|
|
||||||
<!-- Runway -->
|
<!-- Runway -->
|
||||||
<div class="panel" x-show="activeTab === 'backstage'" x-transition.opacity.duration.300ms>
|
<div class="panel" x-show="activeTab === 'backstage'" x-transition.opacity.duration.300ms>
|
||||||
<div class="text-[18px] uppercase tracking-label font-semibold mb-6 text-[var(--c-gray-800)]">
|
<div class="text-2xl font-medium mb-4">
|
||||||
Hot Brand
|
Hot Brand
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ const streetItems: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hs-controls flex justify-between items-center text-[13px] font-medium">
|
<div class="hs-controls flex justify-between items-center text-[13px] font-bold">
|
||||||
<div
|
<div
|
||||||
class="text-[16px] hs-arrow hs-prev cursor-pointer hover:opacity-60 transition-opacity duration-200"
|
class="text-[16px] hs-arrow hs-prev cursor-pointer hover:opacity-60 transition-opacity duration-200"
|
||||||
role="button"
|
role="button"
|
||||||
@ -209,15 +209,14 @@ const streetItems: {
|
|||||||
|
|
||||||
/* 标签 */
|
/* 标签 */
|
||||||
.tab {
|
.tab {
|
||||||
@apply relative pb-[6px] text-[var(--c-gray-500)] transition-colors duration-[240ms] no-underline;
|
@apply relative pb-[6px] text-[var(--c-gray-600)] transition-colors duration-200 no-underline;
|
||||||
}
|
}
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
@apply text-[var(--c-gray-800)];
|
@apply text-[var(--c-gray-800)];
|
||||||
}
|
}
|
||||||
.tab::after {
|
.tab::after {
|
||||||
content: "";
|
content: "";
|
||||||
@apply absolute left-0 right-0 bottom-0 h-[1px] bg-black scale-x-0 origin-left transition-transform duration-[240ms];
|
@apply absolute left-0 right-0 bottom-0 h-[2px] bg-black scale-x-0 origin-left transition-transform duration-[250ms] ease-in-out;
|
||||||
transition-timing-function: var(--ease-editorial, ease);
|
|
||||||
}
|
}
|
||||||
.tab.is-active {
|
.tab.is-active {
|
||||||
@apply text-black;
|
@apply text-black;
|
||||||
@ -368,18 +367,18 @@ const streetItems: {
|
|||||||
.hs-cap-brand {
|
.hs-cap-brand {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
font-size: 21px;
|
font-size: 21px;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.02em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
|
||||||
}
|
}
|
||||||
.hs-cap-meta {
|
.hs-cap-meta {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
opacity: 0.85;
|
opacity: 0.9;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.04em;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
@ -406,12 +405,8 @@ const streetItems: {
|
|||||||
@apply mb-9;
|
@apply mb-9;
|
||||||
}
|
}
|
||||||
.ss-grid {
|
.ss-grid {
|
||||||
@apply grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 md:gap-5;
|
@apply grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-[14px] md:gap-[18px];
|
||||||
}
|
}
|
||||||
/* 刻意不做逐项入场动画。
|
|
||||||
frontend-design skill 把它列为「AI 生成的识别特征」:
|
|
||||||
每个 section 淡入上滑、每张卡片都错落出现,是通用水印,不承载信息。
|
|
||||||
这里只保留图片加载的 blur/opacity 淡入(那是真实的状态变化)。 */
|
|
||||||
.ss-card {
|
.ss-card {
|
||||||
@apply relative block w-full overflow-hidden bg-[var(--c-gray-100)] no-underline text-inherit;
|
@apply relative block w-full overflow-hidden bg-[var(--c-gray-100)] no-underline text-inherit;
|
||||||
aspect-ratio: 3 / 4;
|
aspect-ratio: 3 / 4;
|
||||||
@ -488,17 +483,17 @@ const streetItems: {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.ss-cap-loc {
|
.ss-cap-loc {
|
||||||
font-size: clamp(1.125rem, 2vw, 1.625rem);
|
font-size: clamp(18px, 2vw, 26px);
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
|
||||||
}
|
}
|
||||||
.ss-cap-event {
|
.ss-cap-event {
|
||||||
font-size: clamp(0.625rem, 0.8vw, 0.75rem);
|
font-size: clamp(10px, 0.8vw, 12px);
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.16em;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||||
|
|||||||
@ -1,17 +1,12 @@
|
|||||||
---
|
---
|
||||||
// 登录页的 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';
|
||||||
|
|
||||||
@ -29,6 +24,7 @@ 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"
|
||||||
@ -39,19 +35,7 @@ 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>
|
||||||
@ -61,64 +45,25 @@ const sheet = [...new Set([...showCovers, ...snapCovers])].slice(0, 6)
|
|||||||
<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">
|
||||||
<div class="mx-auto flex min-h-screen w-full max-w-[1700px] flex-col">
|
<main class="mx-auto w-full max-w-[1400px] px-5 py-10 md:px-10 md:py-14 font-sans">
|
||||||
|
|
||||||
<!-- 顶栏:返回 / 期号 / 语言 -->
|
<!-- 顶部工具行:返回档案 + 语言切换 -->
|
||||||
<header class="flex items-center justify-between gap-4 border-b border-black/15 px-5 py-4 md:px-10">
|
<header class="mb-12 flex items-center justify-between gap-6">
|
||||||
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.22em] text-black/55 transition-colors hover:text-black">
|
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.22em] text-black/50 transition-colors hover:text-black">
|
||||||
<span aria-hidden="true">←</span>{backLabel}
|
<span aria-hidden="true">←</span>{backLabel}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="flex items-center gap-5">
|
|
||||||
<span class="hidden text-[10px] uppercase tracking-[0.3em] text-black/35 sm:inline">N° 04 / 2026</span>
|
|
||||||
|
|
||||||
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false" @click.outside="open = false">
|
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false" @click.outside="open = false">
|
||||||
<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">
|
<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>{LOCALE_NAMES[locale].nativeName}</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">
|
<svg viewBox="0 0 12 12" width="11" height="11" class="transition-transform duration-200" :class="open ? 'rotate-180' : ''" aria-hidden="true">
|
||||||
<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"/>
|
<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"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<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">
|
<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">
|
||||||
{languages.map((l) => (
|
{languages.map((l) => (
|
||||||
<li>
|
<li>
|
||||||
<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>
|
<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>
|
||||||
@ -126,122 +71,70 @@ const sheet = [...new Set([...showCovers, ...snapCovers])].slice(0, 6)
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 主体:左封面 / 右表单 -->
|
<!-- 报头:与账户页同一套 masthead(宽字距眉标 + 粗黑巨标题 + 档案标签 + 2px 粗黑线) -->
|
||||||
<main class="grid flex-1 md:grid-cols-[1.08fr_1fr]">
|
<section class="border-b-2 border-black pb-10">
|
||||||
|
<div class="flex flex-wrap items-end justify-between gap-x-16 gap-y-8">
|
||||||
<!-- 左:整幅封面 -->
|
<div>
|
||||||
<aside class="relative hidden overflow-hidden bg-black md:block">
|
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">{membersLabel}</p>
|
||||||
{sheet.length > 0 ? (
|
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl">{loginLabel}</h1>
|
||||||
<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="pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-[var(--c-gray-600)]">
|
||||||
</div>
|
<p>{SITE_NAME}</p>
|
||||||
) : (
|
<p class="flex items-center justify-end gap-2 text-[var(--c-gray-700)]">Archive
|
||||||
<div class="h-full w-full bg-black"></div>
|
<span class="bg-black px-1.5 py-0.5 text-[10px] font-bold tracking-widest text-white">2026</span>
|
||||||
)}
|
|
||||||
|
|
||||||
<!-- 压暗 + 颗粒 -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 四角套准标记 -->
|
|
||||||
<span class="absolute left-5 top-5 h-3 w-3 border-l border-t border-white/40"></span>
|
|
||||||
<span class="absolute right-5 top-5 h-3 w-3 border-r border-t border-white/40"></span>
|
|
||||||
<span class="absolute bottom-5 left-5 h-3 w-3 border-b border-l border-white/40"></span>
|
|
||||||
<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-bold 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>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- 右:表单 -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<p class="rise rise-1 flex items-center gap-2 text-[10px] uppercase tracking-[0.36em] text-black/45">
|
|
||||||
<span class="inline-block h-[7px] w-[7px] bg-black"></span>{t('members only')}
|
|
||||||
</p>
|
</p>
|
||||||
|
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
|
||||||
<h1 class="rise rise-2 mt-5 text-[clamp(52px,8vw,104px)] font-bold uppercase leading-[0.82] tracking-[-0.045em]">
|
|
||||||
{loginLabel}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<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>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="mt-9 flex items-baseline gap-4">
|
<!-- 主体:左表单 / 右档案说明(mobile 单栏堆叠) -->
|
||||||
<span class="w-6 shrink-0 text-[10px] font-bold tracking-[0.2em] text-black/30">02</span>
|
<div class="grid gap-12 pt-12 md:grid-cols-[minmax(0,1fr)_320px] md:gap-16 md:pt-16">
|
||||||
<div class="flex-1">
|
|
||||||
<label for="lg-password" class="mb-1 block text-[10px] uppercase tracking-[0.28em] text-black/45">{passwordLabel}</label>
|
<div class="w-full max-w-md">
|
||||||
<input id="lg-password" type="password" x-model="password" autocomplete="current-password" required
|
<p class="mb-10 text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-600)]">{promptLabel}</p>
|
||||||
class="inkinput w-full py-3 text-base text-black md:text-lg" />
|
|
||||||
</div>
|
<form x-data="loginForm" data-locale={locale} data-fail-label={failLabel} @submit.prevent="submit()" class="space-y-8" novalidate>
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-700)]">{accountLabel}</label>
|
||||||
|
<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" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p x-show="error" x-text="error" x-cloak
|
<div>
|
||||||
class="mt-7 border-l-4 border-black bg-black px-4 py-3 text-[11px] uppercase tracking-[0.18em] text-white"></p>
|
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-[var(--c-gray-700)]">{passwordLabel}</label>
|
||||||
|
<input type="password" x-model="password" autocomplete="current-password" required
|
||||||
|
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" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p x-show="error" x-text="error" x-cloak class="text-[11px] uppercase tracking-[0.2em] text-red-600"></p>
|
||||||
|
|
||||||
<button type="submit" :disabled="loading"
|
<button type="submit" :disabled="loading"
|
||||||
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">
|
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">
|
||||||
<span x-show="!loading">{t('enter the archive →')}</span>
|
<span x-show="!loading">{loginLabel}</span>
|
||||||
<span x-show="loading" x-cloak>{t('signing in…')}</span>
|
<span x-show="loading" x-cloak>…</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">
|
||||||
<div class="rise rise-4 mt-12 max-w-md border-t border-black/10 pt-6">
|
<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">
|
||||||
<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>
|
||||||
</section>
|
</div>
|
||||||
</main>
|
|
||||||
|
|
||||||
<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">
|
<aside class="border-t border-black/10 pt-8 md:border-l md:border-t-0 md:pl-8 md:pt-0">
|
||||||
<p>© 2026 {SITE_NAME}</p>
|
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">Archive note</p>
|
||||||
<p>Paris · Milan · Tokyo</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>
|
||||||
</footer>
|
<div class="mt-8 border-t border-black/10 pt-5 text-[10px] uppercase tracking-[0.3em] text-[var(--c-gray-600)]">
|
||||||
|
<p class="mb-3">Runway · Street · Archive</p>
|
||||||
|
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
|
||||||
</div>
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getUser } from "@/lib/auth"
|
import { getUser } from "@/lib/auth"
|
||||||
|
|||||||
@ -62,11 +62,6 @@ const pathWithoutLocale = locales.includes(pathName.split('/')[1])
|
|||||||
? pathName.replace(`/${locale}`, '')
|
? pathName.replace(`/${locale}`, '')
|
||||||
: pathName;
|
: pathName;
|
||||||
|
|
||||||
// 导航当前项:给对应链接加 aria-current="page",屏幕阅读器据此播报「当前页面」。
|
|
||||||
// 只比较去尾斜杠后的路径,避免 /runway-looks 与 /runway-looks/ 被判成两项。
|
|
||||||
const normPath = (p: string) => p.replace(/\/+$/, '') || '/';
|
|
||||||
const isCurrent = (target: string) => normPath(pathWithoutLocale) === normPath(target);
|
|
||||||
|
|
||||||
// 顶部导航数据模型(替代原 Alpine x-data 的 menu/state)。
|
// 顶部导航数据模型(替代原 Alpine x-data 的 menu/state)。
|
||||||
// 下拉菜单与语言列表均由此驱动渲染,逻辑用 Astro 原生 script 控制开合。
|
// 下拉菜单与语言列表均由此驱动渲染,逻辑用 Astro 原生 script 控制开合。
|
||||||
const headers = {
|
const headers = {
|
||||||
@ -141,18 +136,11 @@ const headers = {
|
|||||||
<!-- 加载动画屏:独立组件,标记/样式/隐藏逻辑封装在 LoadingOverlay.astro -->
|
<!-- 加载动画屏:独立组件,标记/样式/隐藏逻辑封装在 LoadingOverlay.astro -->
|
||||||
<LoadingOverlay />
|
<LoadingOverlay />
|
||||||
|
|
||||||
<!-- 键盘用户跳过导航:平时视觉隐藏,Tab 聚焦时出现在左上角。
|
|
||||||
没有它,键盘用户每进一页都要按十几次 Tab 才能摸到正文。 -->
|
|
||||||
<a
|
|
||||||
href="#main"
|
|
||||||
class="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[100] focus:bg-black focus:px-4 focus:py-2 focus:text-[12px] focus:uppercase focus:tracking-label focus:text-white"
|
|
||||||
>跳到主内容</a>
|
|
||||||
|
|
||||||
<!-- 顶部主导航栏 -->
|
<!-- 顶部主导航栏 -->
|
||||||
<nav class="flex justify-between items-center text-[13px] font-semibold tracking-label uppercase">
|
<nav class="flex justify-between items-center text-[13px] font-bold tracking-[0.5px] uppercase">
|
||||||
<div class="flex flex-wrap items-center gap-6 md:gap-8">
|
<div class="flex flex-wrap items-center gap-6 md:gap-8">
|
||||||
<a href={href(locale, ROUTES.home)} class="nav-link cursor-pointer" aria-current={isCurrent(ROUTES.home) ? 'page' : undefined}>{t('Home')}</a>
|
<a href={href(locale, ROUTES.home)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Home')}</a>
|
||||||
<a href={href(locale, ROUTES.home)} class="nav-link cursor-pointer flex items-center gap-1.5 opacity-70">
|
<a href={href(locale, ROUTES.home)} class="relative pb-[2px] cursor-pointer transition-opacity duration-200 flex items-center gap-1.5 hover:opacity-60 opacity-80 border-b-2 border-transparent">
|
||||||
{t('Portfolio')}
|
{t('Portfolio')}
|
||||||
<span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full tracking-tight leading-none">
|
<span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full tracking-tight leading-none">
|
||||||
2026
|
2026
|
||||||
@ -161,7 +149,7 @@ const headers = {
|
|||||||
<!-- 全部的 runway:菜单项用 Astro 渲染(headers.runways.map),开合逻辑用 Alpine.js -->
|
<!-- 全部的 runway:菜单项用 Astro 渲染(headers.runways.map),开合逻辑用 Alpine.js -->
|
||||||
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<a href={href(locale, ROUTES.runwayLooks)} class="nav-link cursor-pointer" aria-current={isCurrent(ROUTES.runwayLooks) ? 'page' : undefined}>{t('all runway looks')}</a>
|
<a href={href(locale, ROUTES.runwayLooks)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('all runway looks')}</a>
|
||||||
<button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 runway 列表" aria-haspopup="true" :aria-expanded="open">
|
<button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 runway 列表" aria-haspopup="true" :aria-expanded="open">
|
||||||
<svg class="w-2.5 h-2.5 transition-transform duration-200" :class="open ? 'rotate-180' : ''" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
<svg class="w-2.5 h-2.5 transition-transform duration-200" :class="open ? 'rotate-180' : ''" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@ -179,7 +167,7 @@ const headers = {
|
|||||||
<!-- 街拍:菜单项用 Astro 渲染(headers.streets.map),开合逻辑用 Alpine.js -->
|
<!-- 街拍:菜单项用 Astro 渲染(headers.streets.map),开合逻辑用 Alpine.js -->
|
||||||
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<a href={href(locale, ROUTES.streetSnaps)} class="nav-link cursor-pointer" aria-current={isCurrent(ROUTES.streetSnaps) ? 'page' : undefined}>{t('Street Style')}</a>
|
<a href={href(locale, ROUTES.streetSnaps)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Street Style')}</a>
|
||||||
<button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 street style 列表" aria-haspopup="true" :aria-expanded="open">
|
<button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 street style 列表" aria-haspopup="true" :aria-expanded="open">
|
||||||
<svg viewBox="0 0 12 12" width="10" height="10"><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"/></svg>
|
<svg viewBox="0 0 12 12" width="10" height="10"><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"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@ -219,7 +207,7 @@ const headers = {
|
|||||||
@click={`open = false; pick('${l.code}')`}
|
@click={`open = false; pick('${l.code}')`}
|
||||||
class:list={[
|
class:list={[
|
||||||
'block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
|
'block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
|
||||||
l.code === locale ? 'font-medium' : 'opacity-60',
|
l.code === locale ? 'font-bold' : 'opacity-60',
|
||||||
]}
|
]}
|
||||||
>{l.label}</a>
|
>{l.label}</a>
|
||||||
</li>
|
</li>
|
||||||
@ -230,7 +218,7 @@ const headers = {
|
|||||||
<!-- 未登录:Login 链接(Alpine 登录后置为隐藏)。
|
<!-- 未登录:Login 链接(Alpine 登录后置为隐藏)。
|
||||||
login-redirect:交给 AuthModal 的全局门禁捕获,带上「当前页」跳登录页,
|
login-redirect:交给 AuthModal 的全局门禁捕获,带上「当前页」跳登录页,
|
||||||
登录成功后回到原页,而不是一律落到个人中心(无 JS 时回落 href 到 /login)。 -->
|
登录成功后回到原页,而不是一律落到个人中心(无 JS 时回落 href 到 /login)。 -->
|
||||||
<a x-show="!user" x-cloak login-redirect href={href(locale, ROUTES.login)} class="nav-link uppercase tracking-label text-[13px]">{t('Login')}</a>
|
<a x-show="!user" x-cloak login-redirect href={href(locale, ROUTES.login)} class="hover:opacity-60 transition-opacity duration-200 uppercase tracking-[0.5px] text-[13px]">{t('Login')}</a>
|
||||||
|
|
||||||
<!-- 已登录:账号下拉(hover 展开,与语言切换器同款交互) -->
|
<!-- 已登录:账号下拉(hover 展开,与语言切换器同款交互) -->
|
||||||
<div x-show="user" x-cloak class="relative" @mouseenter="open = true" @mouseleave="open = false">
|
<div x-show="user" x-cloak class="relative" @mouseenter="open = true" @mouseleave="open = false">
|
||||||
@ -254,29 +242,11 @@ const headers = {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- 公告条:放在导航之下,作为头部的收边。
|
<div class="flex-1 flex flex-col">
|
||||||
纯 CSS 循环(内容重复两遍,轨道左移 50% 即为一个周期),零 JS。 -->
|
|
||||||
<div class="marquee -mx-5 md:-mx-[50px] mt-6 border-b border-black/10 py-2 text-[11px] uppercase tracking-wide text-[var(--c-gray-800)]">
|
|
||||||
<div class="marquee__track">
|
|
||||||
{Array.from({ length: 2 }).map(() => (
|
|
||||||
<Fragment>
|
|
||||||
<span>✦ 2026 秋冬时装周档案</span>
|
|
||||||
<span>✦ Runway · Street · Archive</span>
|
|
||||||
<span>✦ 全球品牌持续收录</span>
|
|
||||||
<span>✦ New Season</span>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="main" tabindex="-1" class="flex-1 flex flex-col">
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 黑色页脚要贴到视口左右与底部边缘:body 上有 px-5 py-5 md:px-[50px] md:py-[30px],
|
<footer class="relative z-[1] bg-white mt-20 pt-10 border-t-2 border-black text-black w-full font-sans">
|
||||||
子元素默认会缩进这层 padding;这里用等量负外边距抵消(与上方 marquee 同一手法)。
|
|
||||||
注意不能加 w-full:width:100% + 负 margin-left 会让右边少 20px,只能靠 auto 宽度撑满。 */}
|
|
||||||
<footer class="relative z-[1] mt-20 -mx-5 -mb-5 bg-black font-sans text-white md:-mx-[50px] md:-mb-[30px]">
|
|
||||||
<script>
|
<script>
|
||||||
import { getUser } from "@/lib/auth"
|
import { getUser } from "@/lib/auth"
|
||||||
import type { AuthUser } from "@/lib/auth"
|
import type { AuthUser } from "@/lib/auth"
|
||||||
@ -373,64 +343,58 @@ 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">
|
||||||
移动端 body 20 + 原 footer 20 = 40 → px-10
|
{/* Column 1: Brand & Manifesto */}
|
||||||
桌面 body 50 + 原 footer 40 = 90 → md:px-[90px] */}
|
<div class="md:col-span-5 space-y-4">
|
||||||
<div class="mx-auto w-full max-w-[1400px] px-10 py-14 md:px-[90px] md:py-20">
|
<div class="flex items-center gap-2">
|
||||||
<div class="grid grid-cols-1 gap-12 md:grid-cols-12 md:gap-8">
|
<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-wide text-white/35">{t('Archive')}</p>
|
|
||||||
<p class="mt-5 text-5xl font-bold uppercase leading-[0.85] tracking-display md:text-7xl">
|
|
||||||
{SITE_NAME}
|
{SITE_NAME}
|
||||||
</p>
|
</span>
|
||||||
<p class="mt-7 max-w-md text-sm leading-relaxed text-white/45">
|
<span class="text-[10px] bg-black text-white px-2 py-0.5 font-bold uppercase tracking-widest rounded-full">
|
||||||
|
2026
|
||||||
|
</span>
|
||||||
|
</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.')}
|
{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>
|
||||||
<div class="mt-8 flex flex-wrap items-center gap-x-3 gap-y-2 text-[10px] uppercase tracking-wide text-white/30">
|
|
||||||
<span>Paris</span><span>·</span><span>Milan</span><span>·</span><span>Tokyo</span><span>·</span><span>New York</span>
|
{/* Location Badge */}
|
||||||
|
<div class="flex flex-wrap gap-3 text-[11px] text-[var(--c-gray-700)] uppercase tracking-widest pt-1">
|
||||||
|
<span>Paris</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Milan</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Tokyo</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>New York</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 导航 */}
|
{/* Column 2: Explore */}
|
||||||
<div class="md:col-span-3 md:col-start-8">
|
<div class="md:col-span-4 md:col-start-7 space-y-3">
|
||||||
<h3 class="text-[10px] uppercase tracking-wide text-white/35">{t('Explore')}</h3>
|
<h3 class="text-[11px] tracking-[0.26em] uppercase font-bold text-black">Explore</h3>
|
||||||
<ul class="mt-5 space-y-3 text-[11px] uppercase tracking-[0.18em]">
|
<ul class="space-y-2 text-[11px] uppercase tracking-[0.18em] text-[var(--c-gray-800)]">
|
||||||
<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.runwayLooks)} class="hover:text-black transition-colors">{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.streetSnaps)} class="hover:text-black transition-colors">{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>
|
<li><a href={href(locale, ROUTES.account)} class="hover:text-black transition-colors">{t('my account')}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 站点操作(全部是真实可用的入口,不留死链) */}
|
{/* Column 3: Connect */}
|
||||||
<div class="md:col-span-3">
|
<div class="md:col-span-2 space-y-3">
|
||||||
<h3 class="text-[10px] uppercase tracking-wide text-white/35">{t('Site')}</h3>
|
<h3 class="text-[11px] tracking-[0.26em] uppercase font-bold text-black">Connect</h3>
|
||||||
<ul class="mt-5 space-y-3 text-[11px] uppercase tracking-[0.18em]">
|
<ul class="space-y-2 text-[11px] uppercase tracking-[0.18em] text-[var(--c-gray-800)]">
|
||||||
<li>
|
<li><a href="#" class="hover:text-black transition-colors">Instagram</a></li>
|
||||||
<a href={href(locale === 'cn' ? 'en' : 'cn', ROUTES.home)} class="text-white/65 transition-colors hover:text-white">
|
<li><a href="#" class="hover:text-black transition-colors">Newsletter</a></li>
|
||||||
{locale === 'cn' ? 'English' : '中文'}
|
<li><a href="#" class="hover:text-black transition-colors">Contact</a></li>
|
||||||
</a>
|
|
||||||
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部条 */}
|
<div class="mt-6 text-[11px] text-[var(--c-gray-600)] uppercase tracking-widest">
|
||||||
<div class="mt-14 flex flex-col gap-4 border-t border-white/10 pt-6 text-[10px] uppercase tracking-label text-white/35 sm:flex-row sm:items-center sm:justify-between">
|
© 2026 {SITE_NAME}. {t('all rights reserved.')}
|
||||||
<p>© 2026 {SITE_NAME}. {t('all rights reserved.')}</p>
|
|
||||||
<p class="tracking-wide">{t('Runway · Street · Archive')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@ -21,29 +21,6 @@ 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: '登录' },
|
||||||
@ -118,11 +95,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: '地区' },
|
||||||
|
|||||||
@ -10,7 +10,7 @@ Astro.response.status = 404
|
|||||||
|
|
||||||
<Layout title={t('Page not found')}>
|
<Layout title={t('Page not found')}>
|
||||||
<div class="flex flex-1 flex-col items-center justify-center min-h-[50vh] text-center gap-6">
|
<div class="flex flex-1 flex-col items-center justify-center min-h-[50vh] text-center gap-6">
|
||||||
<h1 class="text-6xl font-medium tracking-tight">404</h1>
|
<h1 class="text-6xl font-black tracking-tight">404</h1>
|
||||||
<p class="text-sm text-[var(--c-gray-700)] uppercase tracking-[0.2em]">{t('Page not found')}</p>
|
<p class="text-sm text-[var(--c-gray-700)] uppercase tracking-[0.2em]">{t('Page not found')}</p>
|
||||||
<a
|
<a
|
||||||
href={href(locale, ROUTES.home)}
|
href={href(locale, ROUTES.home)}
|
||||||
|
|||||||
@ -1,57 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* ============================================================
|
/* 中性灰阶令牌:统一全站散落的 #f1f1f1/#efefef/#e6e6e6/#ccc/#bdbdbd/#999/#888/#666 等 */
|
||||||
设计令牌(Tailwind v4 @theme)
|
|
||||||
|
|
||||||
参考规律(NABOUN 实站抓取 + ui-ux-pro-max 方法论):
|
|
||||||
1. 字重只用 400 / 500,高级感来自字距与留白,不来自粗体
|
|
||||||
2. 全大写 + 0.12em 字距 + 11~13px 是标签的统一签名
|
|
||||||
3. 1px 发丝线分块,不用色块;全站只保留黑白灰三层明度
|
|
||||||
4. 图片统一竖版,卡片无圆角无遮罩
|
|
||||||
|
|
||||||
约束:不加载任何 webfont(体积与首屏优先),层级靠字重 + 字距 + 尺度建立。
|
|
||||||
============================================================ */
|
|
||||||
@theme {
|
|
||||||
--font-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
||||||
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC",
|
|
||||||
"Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
|
|
||||||
|
|
||||||
/* 字距:全站只允许这四档。
|
|
||||||
历史上散落的 0.5px / 0.04em / 0.14em / 0.22em / 0.26em 全部收敛到这里。 */
|
|
||||||
--tracking-display: -0.03em;
|
|
||||||
--tracking-label: 0.12em;
|
|
||||||
--tracking-wide: 0.18em;
|
|
||||||
--tracking-meta: 0.02em;
|
|
||||||
|
|
||||||
/* 动效:统一缓动 + 三档时长,替代 150/200/250/300/400/450ms 混用。
|
|
||||||
0.22,1,0.36,1 是「快出慢停」曲线,进入视口时更稳,不弹。 */
|
|
||||||
--ease-editorial: cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
|
|
||||||
/* 字号阶梯:全部用 rem,而不是 px。
|
|
||||||
默认根字号 16px 下数值与原来的 px 完全等价,视觉零变化;
|
|
||||||
但用户把浏览器「字体大小」调大时,rem 会跟着放大,px 不会(Vercel #1.3)。
|
|
||||||
括号里是 16px 基准下的等价像素。 */
|
|
||||||
--text-micro: 0.625rem; /* 10px */
|
|
||||||
--text-micro--line-height: 1.4;
|
|
||||||
--text-label: 0.6875rem; /* 11px */
|
|
||||||
--text-label--line-height: 1.2;
|
|
||||||
--text-meta: 0.8125rem; /* 13px */
|
|
||||||
--text-meta--line-height: 1.55;
|
|
||||||
--text-body: 0.9375rem; /* 15px */
|
|
||||||
--text-body--line-height: 1.75;
|
|
||||||
--text-lead: 1.0625rem; /* 17px */
|
|
||||||
--text-lead--line-height: 1.55;
|
|
||||||
--text-h3: 1.375rem; /* 22px */
|
|
||||||
--text-h3--line-height: 1.25;
|
|
||||||
--text-h2: clamp(1.625rem, 3vw, 2.125rem); /* 26 → 34px */
|
|
||||||
--text-h2--line-height: 1.15;
|
|
||||||
--text-h1: clamp(2.125rem, 5.5vw, 4rem); /* 34 → 64px */
|
|
||||||
--text-h1--line-height: 0.95;
|
|
||||||
--text-display: clamp(2.625rem, 7vw, 5.25rem); /* 42 → 84px */
|
|
||||||
--text-display--line-height: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 中性灰阶 + 发丝线令牌:统一全站散落的 #f1f1f1/#efefef/#e6e6e6/#ccc/#bdbdbd/#999/#888/#666 等 */
|
|
||||||
:root {
|
:root {
|
||||||
--c-gray-50: #f6f6f6;
|
--c-gray-50: #f6f6f6;
|
||||||
--c-gray-100: #f1f1f1;
|
--c-gray-100: #f1f1f1;
|
||||||
@ -62,20 +11,6 @@
|
|||||||
--c-gray-600: #999;
|
--c-gray-600: #999;
|
||||||
--c-gray-700: #888;
|
--c-gray-700: #888;
|
||||||
--c-gray-800: #666;
|
--c-gray-800: #666;
|
||||||
|
|
||||||
/* 发丝线:现代极简电商的主要分隔手段。1px + 12% 黑,比纯黑细框更透气。 */
|
|
||||||
--line: rgba(0, 0, 0, 0.12);
|
|
||||||
--line-strong: rgba(0, 0, 0, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 英文站字距单独收紧一档。
|
|
||||||
原因:英文标签是全大写 + 宽字距,大写字母本身等高、无升降部,
|
|
||||||
加上 0.12em 字间空隙,整行密度明显低于中文,视觉上会「变轻」。
|
|
||||||
收窄到 0.09em 让单词重新聚拢,重量感就回来了。
|
|
||||||
中文站保持原值(方块字之间需要更多呼吸,收太紧会糊)。 */
|
|
||||||
html[lang="en"] {
|
|
||||||
--tracking-label: 0.09em;
|
|
||||||
--tracking-wide: 0.14em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
@ -114,109 +49,4 @@ select:focus-visible,
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
全局排版细节
|
|
||||||
============================================================ */
|
|
||||||
|
|
||||||
/* 数字等宽:页码 / 年份 / 计数在内容切换时不会左右跳动 */
|
|
||||||
body {
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
/* 翻页 / 筛选导致内容高度变化时,避免滚动条出现消失引起的整页横向抖动。
|
|
||||||
来自 Vercel web-design-guidelines #3.6 */
|
|
||||||
scrollbar-gutter: stable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 长标题折行更均衡,避免最后一行只剩一个词(#1.5)。
|
|
||||||
目前只作用于大标题,正文不设,避免影响折行节奏。 */
|
|
||||||
h1,
|
|
||||||
.display-title {
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: #111;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 导航链接:hover 时 1px 黑线从左滑入。
|
|
||||||
比原来的整体 opacity 变化更克制,也更像编辑类网站的处理。 */
|
|
||||||
.nav-link {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.nav-link::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: -3px;
|
|
||||||
height: 1px;
|
|
||||||
background: currentColor;
|
|
||||||
transform: scaleX(0);
|
|
||||||
transform-origin: left center;
|
|
||||||
transition: transform 240ms var(--ease-editorial, ease);
|
|
||||||
}
|
|
||||||
.nav-link:hover::after {
|
|
||||||
transform: scaleX(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 触屏(粗指针)上把点击热区撑到 ≥44px 高,但不动布局。
|
|
||||||
13px 的导航文字在手机上可点区域只有 ~16px 高,拇指很容易点空(Vercel #3.3)。
|
|
||||||
用伪元素扩展命中区,而不是加 padding —— 加 padding 会把下划线一起推下去。 */
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
.nav-link::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: -0.75rem -0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
顶部公告条(marquee)
|
|
||||||
NABOUN 式的循环滚动条:零 JS、纯 CSS、无缝衔接。
|
|
||||||
用法:内容在容器里重复两遍,轨道整体左移 50% 即为一个周期。
|
|
||||||
============================================================ */
|
|
||||||
.marquee {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.marquee__track {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2.5rem;
|
|
||||||
padding-left: 2.5rem;
|
|
||||||
animation: marquee-scroll 32s linear infinite;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
.marquee:hover .marquee__track {
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
@keyframes marquee-scroll {
|
|
||||||
from { transform: translateX(0); }
|
|
||||||
to { transform: translateX(-50%); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 列表入场:逐项错落 40ms,避免整屏同时出现造成的「跳一下」 */
|
|
||||||
@keyframes rise-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
animation-iteration-count: 1 !important;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
scroll-behavior: auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -23,12 +23,10 @@
|
|||||||
@apply flex justify-between items-end gap-6 py-5 pb-[22px];
|
@apply flex justify-between items-end gap-6 py-5 pb-[22px];
|
||||||
}
|
}
|
||||||
.showsh1 {
|
.showsh1 {
|
||||||
@apply text-[clamp(2.5rem,6vw,4.5rem)] leading-[0.9] font-bold my-0 mb-[10px];
|
@apply text-[clamp(40px,6vw,72px)] leading-[0.9] font-black tracking-[-0.02em] my-0 mb-[10px];
|
||||||
letter-spacing: var(--tracking-display, -0.03em);
|
|
||||||
}
|
}
|
||||||
.snapsh1 {
|
.snapsh1 {
|
||||||
@apply text-[clamp(2.125rem,5.5vw,4rem)] leading-[0.95] font-bold my-0 mb-[8px];
|
@apply text-[clamp(34px,5.5vw,64px)] leading-[0.95] font-black tracking-[-0.02em] my-0 mb-[8px];
|
||||||
letter-spacing: var(--tracking-display, -0.03em);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 已激活筛选 pills */
|
/* 已激活筛选 pills */
|
||||||
@ -36,8 +34,7 @@
|
|||||||
.snapspills { @apply flex flex-wrap gap-2 mt-4; }
|
.snapspills { @apply flex flex-wrap gap-2 mt-4; }
|
||||||
.showspill,
|
.showspill,
|
||||||
.snapspill {
|
.snapspill {
|
||||||
@apply inline-flex items-center gap-2 border border-black p-[6px_10px] text-[10px] uppercase text-black bg-white;
|
@apply inline-flex items-center gap-2 border-[1.5px] border-black p-[6px_10px] text-[10.5px] uppercase tracking-[0.14em] text-black bg-white;
|
||||||
letter-spacing: var(--tracking-label, 0.12em);
|
|
||||||
}
|
}
|
||||||
.showspill .x,
|
.showspill .x,
|
||||||
.snapspill .x {
|
.snapspill .x {
|
||||||
@ -59,14 +56,11 @@
|
|||||||
}
|
}
|
||||||
.showsaside-head,
|
.showsaside-head,
|
||||||
.snapsaside-head {
|
.snapsaside-head {
|
||||||
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black/15 shrink-0;
|
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black shrink-0;
|
||||||
}
|
}
|
||||||
.showsaside-h,
|
.showsaside-h,
|
||||||
.snapsaside-h {
|
.snapsaside-h {
|
||||||
@apply text-[11px] uppercase font-medium m-0 leading-none transition-opacity duration-[240ms];
|
@apply text-[11px] tracking-[0.26em] uppercase font-bold m-0 leading-none transition-opacity duration-200;
|
||||||
/* 不用 @apply tracking-*:本文件 @reference 的是 tailwind 默认主题,
|
|
||||||
取不到 global.css 里 @theme 定义的值,只能走运行时 CSS 变量。 */
|
|
||||||
letter-spacing: var(--tracking-wide, 0.18em);
|
|
||||||
}
|
}
|
||||||
.showsaside-body,
|
.showsaside-body,
|
||||||
.snapsaside-body {
|
.snapsaside-body {
|
||||||
@ -80,8 +74,7 @@
|
|||||||
}
|
}
|
||||||
.showsg-h,
|
.showsg-h,
|
||||||
.snapsg-h {
|
.snapsg-h {
|
||||||
@apply flex justify-between items-center text-[11px] uppercase font-medium text-black mb-2 cursor-pointer select-none;
|
@apply flex justify-between items-center text-[11px] tracking-[0.22em] uppercase font-bold text-black mb-2 cursor-pointer select-none;
|
||||||
letter-spacing: var(--tracking-wide, 0.18em);
|
|
||||||
}
|
}
|
||||||
.showsg-right {
|
.showsg-right {
|
||||||
@apply inline-flex items-center gap-2.5;
|
@apply inline-flex items-center gap-2.5;
|
||||||
@ -120,8 +113,7 @@
|
|||||||
.snapschecks--flat li label { @apply inline-flex items-center gap-2 cursor-pointer w-full; }
|
.snapschecks--flat li label { @apply inline-flex items-center gap-2 cursor-pointer w-full; }
|
||||||
|
|
||||||
.showsmore {
|
.showsmore {
|
||||||
@apply block w-full mt-[12px] text-center text-[11px] uppercase text-black font-medium cursor-pointer;
|
@apply block w-full mt-[12px] text-center text-[11px] uppercase tracking-[0.22em] text-black font-bold cursor-pointer;
|
||||||
letter-spacing: var(--tracking-wide, 0.18em);
|
|
||||||
}
|
}
|
||||||
.showsclear,
|
.showsclear,
|
||||||
.snapsclear {
|
.snapsclear {
|
||||||
@ -134,7 +126,7 @@
|
|||||||
|
|
||||||
.showsgrid,
|
.showsgrid,
|
||||||
.snapsgrid {
|
.snapsgrid {
|
||||||
@apply grid grid-cols-2 gap-6 relative max-sm:grid-cols-1;
|
@apply grid grid-cols-2 gap-[22px] relative max-sm:grid-cols-1;
|
||||||
}
|
}
|
||||||
.showsgrid.is-loading::before,
|
.showsgrid.is-loading::before,
|
||||||
.snapsgrid.is-loading::before {
|
.snapsgrid.is-loading::before {
|
||||||
@ -145,17 +137,7 @@
|
|||||||
|
|
||||||
.showscard,
|
.showscard,
|
||||||
.snapscard {
|
.snapscard {
|
||||||
@apply relative flex gap-[14px] no-underline text-inherit bg-white p-3 border border-transparent transition-[color,background-color,border-color,transform] duration-[240ms] hover:border-black/20 max-md:flex-col max-md:gap-[10px];
|
@apply relative flex gap-[14px] no-underline text-inherit bg-white p-3 transition-colors duration-200 hover:border-black max-md:flex-col max-md:gap-[10px];
|
||||||
transition-timing-function: var(--ease-editorial, ease);
|
|
||||||
}
|
|
||||||
/* hover 时整体上浮 2px:比单纯变色更能表达「可点」,且不触发重排。
|
|
||||||
必须包在 (hover: hover) 里:触屏浏览器会「粘滞 hover」,
|
|
||||||
手指离开后卡片停在上浮状态,看起来像坏了。 */
|
|
||||||
@media (hover: hover) {
|
|
||||||
.showscard:hover,
|
|
||||||
.snapscard:hover {
|
|
||||||
@apply -translate-y-[2px];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.showscard:hover .showscard-img img,
|
.showscard:hover .showscard-img img,
|
||||||
.snapscard:hover .snapscard-img img { @apply scale-[1.04]; }
|
.snapscard:hover .snapscard-img img { @apply scale-[1.04]; }
|
||||||
@ -194,13 +176,11 @@
|
|||||||
}
|
}
|
||||||
.showscard-title,
|
.showscard-title,
|
||||||
.snapscard-title {
|
.snapscard-title {
|
||||||
@apply text-[16px] font-bold leading-[1.32] my-0 mb-[10px] text-[#111] line-clamp-2 h-[42px] overflow-hidden;
|
@apply text-[15.5px] font-bold leading-[1.32] my-0 mb-[10px] text-[#111] tracking-[-0.01em] line-clamp-2 h-[41px] overflow-hidden;
|
||||||
letter-spacing: var(--tracking-display, -0.03em);
|
|
||||||
}
|
}
|
||||||
.showscard-meta,
|
.showscard-meta,
|
||||||
.snapscard-meta {
|
.snapscard-meta {
|
||||||
@apply text-[10px] text-[var(--c-gray-700)] my-0 mb-[14px] leading-[1.55] uppercase line-clamp-2 h-[31px] overflow-hidden;
|
@apply text-[9.5px] text-[var(--c-gray-700)] my-0 mb-[14px] leading-[1.55] tracking-[0.04em] uppercase line-clamp-2 h-[29px] overflow-hidden;
|
||||||
letter-spacing: var(--tracking-meta, 0.02em);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.showscard-strip,
|
.showscard-strip,
|
||||||
@ -220,8 +200,7 @@
|
|||||||
.showscard-thumb.placeholder,
|
.showscard-thumb.placeholder,
|
||||||
.snapscard-thumb.placeholder { @apply bg-[var(--c-gray-50)]; }
|
.snapscard-thumb.placeholder { @apply bg-[var(--c-gray-50)]; }
|
||||||
.showscard-thumb-more {
|
.showscard-thumb-more {
|
||||||
@apply absolute inset-0 bg-black/55 text-white text-[11px] font-medium flex items-center justify-center;
|
@apply absolute inset-0 bg-black/55 text-white text-[11px] font-bold tracking-[0.04em] flex items-center justify-center;
|
||||||
letter-spacing: var(--tracking-meta, 0.02em);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 骨架屏 */
|
/* 骨架屏 */
|
||||||
@ -239,10 +218,7 @@
|
|||||||
.showspager,
|
.showspager,
|
||||||
.snapspager { @apply flex flex-wrap items-center justify-center gap-2 mt-[38px]; }
|
.snapspager { @apply flex flex-wrap items-center justify-center gap-2 mt-[38px]; }
|
||||||
.pg {
|
.pg {
|
||||||
/* 边框不透明度提到 50%:UI 组件的边界对比度需 ≥3:1(WCAG 2.2 / Vercel #2.3),
|
@apply min-w-[40px] h-[40px] px-3 border-[1.5px] border-black bg-white text-black text-[11px] uppercase tracking-[0.12em] font-bold cursor-pointer rounded-none transition-colors duration-200;
|
||||||
black/25 只有约 1.8:1,弱视用户几乎看不见分页框。 */
|
|
||||||
@apply min-w-[40px] h-[40px] px-3 border border-black/50 bg-white text-black text-[11px] uppercase font-medium cursor-pointer rounded-none transition-colors duration-[240ms];
|
|
||||||
letter-spacing: var(--tracking-label, 0.12em);
|
|
||||||
}
|
}
|
||||||
.pg.is-current { @apply bg-black text-white cursor-default; }
|
.pg.is-current { @apply bg-black text-white cursor-default; }
|
||||||
.pg:disabled { @apply opacity-30 cursor-default; }
|
.pg:disabled { @apply opacity-30 cursor-default; }
|
||||||
|
|||||||
Reference in New Issue
Block a user