update
This commit is contained in:
@ -32,20 +32,20 @@ const fullLoginLabel = t('go to full login')
|
||||
>
|
||||
<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">
|
||||
<h3 class="font-serif text-2xl font-black uppercase leading-none tracking-tight">{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>
|
||||
</div>
|
||||
|
||||
<p class="mb-7 font-mono text-[11px] uppercase tracking-widest text-zinc-400">{prompt}</p>
|
||||
<p class="mb-7 text-[11px] uppercase tracking-widest text-zinc-400">{prompt}</p>
|
||||
|
||||
<form @submit.prevent="submit()" class="space-y-6" novalidate>
|
||||
<div>
|
||||
<label class="mb-2 block font-mono text-[11px] uppercase tracking-widest text-zinc-500">{accountLabel}</label>
|
||||
<label class="mb-2 block text-[11px] uppercase tracking-widest text-zinc-500">{accountLabel}</label>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block font-mono text-[11px] uppercase tracking-widest text-zinc-500">{passwordLabel}</label>
|
||||
<label class="mb-2 block text-[11px] uppercase tracking-widest text-zinc-500">{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-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
|
||||
</div>
|
||||
@ -53,14 +53,14 @@ const fullLoginLabel = t('go to full login')
|
||||
<p x-show="error" x-text="error" x-cloak class="text-xs uppercase tracking-wide text-red-600"></p>
|
||||
|
||||
<button type="submit" :disabled="loading"
|
||||
class="w-full border border-black bg-black py-3.5 font-mono 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" x-cloak>…</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 border-t border-black/10 pt-4 text-center">
|
||||
<button type="button" @click="goFullLogin()" class="font-mono text-[10px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">{fullLoginLabel}</button>
|
||||
<button type="button" @click="goFullLogin()" class="text-[10px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">{fullLoginLabel}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -102,13 +102,13 @@
|
||||
@apply w-full border border-black/15 px-3 py-2 text-[13px] outline-none bg-[#fafafa] focus:bg-white focus:border-black;
|
||||
}
|
||||
.showsmodal-tip {
|
||||
@apply mt-2 text-[11px] leading-snug text-black/45 font-mono;
|
||||
@apply mt-2 text-[11px] leading-snug text-black/45;
|
||||
}
|
||||
.showsmodal-az {
|
||||
@apply flex flex-wrap gap-1 px-4 pb-3 shrink-0;
|
||||
}
|
||||
.showsmodal-az .az {
|
||||
@apply appearance-none bg-white border border-black/10 w-6 h-6 text-[10px] cursor-pointer leading-none font-mono transition-colors hover:bg-black/5;
|
||||
@apply appearance-none bg-white border border-black/10 w-6 h-6 text-[10px] cursor-pointer leading-none transition-colors hover:bg-black/5;
|
||||
}
|
||||
.showsmodal-az .az.active {
|
||||
@apply bg-black text-white border-black;
|
||||
|
||||
@ -34,7 +34,7 @@ const { locale } = getI18n(Astro)
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 4px 60px;
|
||||
@apply font-mono text-black;
|
||||
@apply text-black;
|
||||
}
|
||||
.cs-kicker {
|
||||
display: flex;
|
||||
@ -44,7 +44,7 @@ const { locale } = getI18n(Astro)
|
||||
@apply text-[11px] tracking-[0.25em] uppercase text-[#666];
|
||||
}
|
||||
.cs-title {
|
||||
@apply font-serif font-black tracking-[-0.02em];
|
||||
@apply font-black tracking-[-0.02em];
|
||||
line-height: 0.9;
|
||||
font-size: clamp(56px, 13vw, 128px);
|
||||
margin: 0.16em 0 0.3em;
|
||||
|
||||
@ -31,7 +31,7 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
|
||||
>
|
||||
{/* 计数:mono + 0.22em 字距(全站「小标与计数」档),直角细框,零填充 */}
|
||||
<div
|
||||
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 font-mono text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
|
||||
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
|
||||
x-text="`${String(lbIndex + 1).padStart(2, '0')} / ${String(imageCount).padStart(2, '0')}`"
|
||||
></div>
|
||||
|
||||
@ -75,7 +75,7 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
|
||||
x-bind:class="lbIndex === Number($el.dataset.lbIndex) ? 'opacity-100 ring-1 ring-black' : ''"
|
||||
aria-label={`${i + 1}`}
|
||||
>
|
||||
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 font-mono text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm">
|
||||
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm">
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
</button>
|
||||
@ -91,7 +91,7 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
|
||||
x-bind:class="lbIndex === Number($el.dataset.lbIndex) ? 'opacity-100 ring-1 ring-black' : ''"
|
||||
:aria-label="String(5 + i + 1)"
|
||||
>
|
||||
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 font-mono text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm" x-text="String(5 + i + 1).padStart(2, '0')"></span>
|
||||
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm" x-text="String(5 + i + 1).padStart(2, '0')"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
// 加载屏:首屏/整页切换时遮盖未渲染内容,避免 FOUC。
|
||||
// 轻量原则(用户要求"朴实流畅"):
|
||||
// - 只用静态大字 + mono 小标 + 一条 2px 细线进度(transform/opacity 动画,最便宜);
|
||||
// - 只用静态大字 + 小标 + 一条 2px 细线进度(transform/opacity 动画,最便宜);
|
||||
// - 去掉昂贵的多层 text-shadow 位移动画与外部字体;最短展示时间压缩到 ~350ms,跟手不拖沓。
|
||||
|
||||
import { config } from '@/lib/config';
|
||||
@ -37,7 +37,6 @@ const { t } = getI18n(Astro)
|
||||
}
|
||||
#loading-screen h1 {
|
||||
margin: 0;
|
||||
font-family: ui-serif, Georgia, Cambria, 'Times New Roman', serif;
|
||||
font-size: clamp(2.2rem, 6vw, 4rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
@ -46,7 +45,6 @@ const { t } = getI18n(Astro)
|
||||
}
|
||||
#loading-screen .ls-sub {
|
||||
margin: 1.1em 0 0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.34em;
|
||||
text-transform: uppercase;
|
||||
|
||||
@ -15,8 +15,8 @@ interface Props {
|
||||
const { article, error = null, preview = false, imageTotal = 0 } = Astro.props
|
||||
const { locale, t } = getI18n(Astro)
|
||||
|
||||
// 改造后:后端详情接口 images 仅返回主图(look 图),细节图在 details 分组,需经「细节入口」展开。
|
||||
// 故此处 images 即为主图集合,默认只渲染它们;每张主图带 look_index 用于关联其细节图。
|
||||
// 后端详情接口 images 仅返回主图(look 图),每张主图的细节图嵌套在它自己的 detail 子数组下。
|
||||
// 故此处 images 即为主图集合,默认只渲染它们;detail 随主图一起带上,供「细节入口」展开。
|
||||
const images = (article?.images ?? [])
|
||||
.map((im) => ({
|
||||
id: im.id,
|
||||
@ -27,17 +27,28 @@ const images = (article?.images ?? [])
|
||||
raw: im.image || "", // 相对路径:收藏快照存相对地址,不把宿主/端口写死进库
|
||||
name: im.name || "",
|
||||
lookIndex: Number(im.look_index ?? 0),
|
||||
// 该主图下的细节图:与主图同一套字段映射(递归同构)
|
||||
detail: (im.detail ?? []).map((d) => ({
|
||||
id: d.id || "",
|
||||
url: ssrAbs(d.image || ""),
|
||||
thumb: ssrAbs(d.thumb || d.image || ""),
|
||||
raw: d.image || "",
|
||||
name: d.name || "",
|
||||
})),
|
||||
}))
|
||||
.filter((im) => im.url)
|
||||
|
||||
// 每个主图(look_index)对应的细节图数量,用于网格卡片左下角标「该主图有几张细节图」。
|
||||
// 来源与灯箱右下角细节簇一致:后端详情接口的 details 分组(按 look_index 聚合)。
|
||||
// SSR 首屏图直接查这张表;登录后追加的 extraImages 由 Alpine 的 detailCount() 读同一份数据。
|
||||
const detailsByLook = new Map<number, number>()
|
||||
for (const d of (article?.details ?? []) as { look_index?: number; images?: unknown[] }[]) {
|
||||
const li = Number(d.look_index ?? 0)
|
||||
if (li) detailsByLook.set(li, (d.images ?? []).length)
|
||||
}
|
||||
// 网格左下角的「细节图张数」角标交给 Alpine 的 detailCount() 响应式控制(而非 SSR 编译期条件):
|
||||
// 首屏前 PREVIEW_LIMIT 张由下面的 data-details 载荷构建映射,登录后由 gallery:full 用完整图集重建,
|
||||
// 未登录 / 已登录共用同一条渲染路径。
|
||||
|
||||
// 递给浏览器端 Alpine 的细节图数据:只序列化「带细节图的主图」投影(不带主图自身 URL,
|
||||
// 避免把整份 images 重复塞进 HTML)。元素形状与 images 元素一致(look_index + detail),
|
||||
// 故 Alpine 的 buildDetailMapFrom() 能与「登录后拿回的完整 images」共用同一套解析逻辑。
|
||||
// 未登录时后端只返回前 PREVIEW_LIMIT 张主图(细节图随所属主图一起返回),此处即为这几张的细节子树。
|
||||
const detailPayload = (article?.images ?? [])
|
||||
.filter((im) => (im.detail?.length ?? 0) > 0)
|
||||
.map((im) => ({ look_index: Number(im.look_index ?? 0), detail: im.detail }))
|
||||
|
||||
// 未登录预览上限:详情页(图集)未登录最多展示前 PREVIEW_LIMIT 张图片;其余由登录门禁提示,
|
||||
// 已登录(客户端 loadAuthedState 拉全量)或图片本就不超上限时全部展示。前 PREVIEW_LIMIT 张服务端已渲染(SEO/无 JS 友好)。
|
||||
@ -64,7 +75,7 @@ const homeHref = href(locale, ROUTES.home)
|
||||
data-gallery-title={article?.title ?? ""}
|
||||
data-gallery-brand={article?.brand_name ?? ""}
|
||||
data-preview={preview ? "true" : "false"}
|
||||
data-details={JSON.stringify(article?.details ?? [])}
|
||||
data-details={JSON.stringify(detailPayload)}
|
||||
class="min-h-screen bg-white text-black font-sans selection:bg-black selection:text-white"
|
||||
>
|
||||
{
|
||||
@ -78,11 +89,11 @@ const homeHref = href(locale, ROUTES.home)
|
||||
<section class="mt-16">
|
||||
{/* 每行张数切换(应对图集图片很多的情况):2/3/4/5 列,移动端固定 2 列 */}
|
||||
<div class="flex items-center justify-between mb-5 border-b border-black/10 pb-3">
|
||||
<span class="text-[11px] font-mono uppercase tracking-[0.22em] text-[#999]">
|
||||
<span class="text-[11px] uppercase tracking-[0.22em] text-[#999]">
|
||||
{(imageTotal || images.length)} LOOKS
|
||||
</span>
|
||||
<div class="flex items-center gap-1" role="group" aria-label="Images per row">
|
||||
<span class="text-[11px] font-mono uppercase tracking-[0.18em] text-[#999] mr-2 hidden sm:inline">PER ROW</span>
|
||||
<span class="text-[11px] uppercase tracking-[0.18em] text-[#999] mr-2 hidden sm:inline">PER ROW</span>
|
||||
{/* 每行张数切换:只有「3 张 / 5 张」两档,用竖条图标直观表达列数(避免文字数字按钮)。
|
||||
density 只影响 md+;移动端固定 2 列(5 列在手机上不可用)。 */}
|
||||
<button
|
||||
@ -125,7 +136,6 @@ const homeHref = href(locale, ROUTES.home)
|
||||
:class="gridClass()"
|
||||
>
|
||||
{previewImages.map((im, i) => {
|
||||
const dc = detailsByLook.get(im.lookIndex) || 0
|
||||
return (
|
||||
<figure
|
||||
data-image-index={i}
|
||||
@ -162,22 +172,24 @@ const homeHref = href(locale, ROUTES.home)
|
||||
</button>
|
||||
|
||||
{/* 细节图数量角标:有细节图的主图在左下角标注「张数」(图层图标 + 数字),
|
||||
点它直接开细节灯箱。纯主图(无细节图)不显示任何角标,保持网格干净。 */}
|
||||
{dc > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
@click.stop={`openDetailLb(${im.lookIndex})`}
|
||||
class="absolute left-2 bottom-2 z-10 flex items-center gap-1 px-1.5 py-1 bg-white/85 backdrop-blur-sm border border-black/15 text-[#111] text-[10px] font-mono uppercase tracking-[0.14em] cursor-pointer hover:bg-black hover:text-white transition-colors"
|
||||
title={`${dc} detail${dc > 1 ? "s" : ""}`}
|
||||
aria-label={`${dc} detail${dc > 1 ? "s" : ""}`}
|
||||
>
|
||||
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="3" y="3" width="13" height="13" rx="1.5" />
|
||||
<path d="M8 21h11a2 2 0 0 0 2-2V8" />
|
||||
</svg>
|
||||
{dc}
|
||||
</button>
|
||||
)}
|
||||
点它直接开细节灯箱。纯主图(无细节图)不显示任何角标,保持网格干净。
|
||||
数量走 Alpine 的 detailCount()(响应式):未登录取前 PREVIEW_LIMIT 张的细节子树,
|
||||
登录后由 gallery:full 重建映射,两态共用同一条渲染路径。 */}
|
||||
<button
|
||||
type="button"
|
||||
x-show={`detailCount(${im.lookIndex}) > 0`}
|
||||
x-cloak
|
||||
@click.stop={`openDetailLb(${im.lookIndex})`}
|
||||
:title={`detailCount(${im.lookIndex}) + ' detail' + (detailCount(${im.lookIndex}) > 1 ? 's' : '')`}
|
||||
:aria-label={`detailCount(${im.lookIndex}) + ' detail' + (detailCount(${im.lookIndex}) > 1 ? 's' : '')`}
|
||||
class="absolute left-2 bottom-2 z-10 flex items-center gap-1 px-1.5 py-1 bg-white/85 backdrop-blur-sm border border-black/15 text-[#111] text-[10px] uppercase tracking-[0.14em] cursor-pointer hover:bg-black hover:text-white transition-colors"
|
||||
>
|
||||
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="3" y="3" width="13" height="13" rx="1.5" />
|
||||
<path d="M8 21h11a2 2 0 0 0 2-2V8" />
|
||||
</svg>
|
||||
<span x-text={`detailCount(${im.lookIndex})`}></span>
|
||||
</button>
|
||||
</figure>
|
||||
)
|
||||
})}
|
||||
@ -193,18 +205,18 @@ const homeHref = href(locale, ROUTES.home)
|
||||
class="relative bg-[#f2f2f2] cursor-pointer group overflow-hidden"
|
||||
>
|
||||
{/* 与真实图片等高的 3:4 图位:锁图标 + 提示,点击跳登录 */}
|
||||
<div class="w-full aspect-[2/3] flex items-center justify-center text-[#c4c4c4] group-hover:text-[#9a9a9a] transition-colors pointer-events-none">
|
||||
<div class="w-full aspect-[2/3] flex items-center justify-center text-black group-hover:text-[#9a9a9a] transition-colors pointer-events-none">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg class="w-7 h-7" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4.5" y="10.5" width="15" height="10" rx="2" />
|
||||
<path d="M8 10.5V7a4 4 0 0 1 8 0v3.5" />
|
||||
</svg>
|
||||
<span class="text-[10px] font-mono uppercase tracking-[0.18em]">{t('login to view')}</span>
|
||||
<span class="text-[10px] uppercase tracking-[0.18em]">{t('login to view')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 真实图已无 look 序号 caption,占位格也改为绝对定位的 +N photos,
|
||||
不撑高格子,保证遮罩格与真实图片格总高一致 */}
|
||||
<span class="absolute left-2 bottom-2 z-10 text-[10px] font-mono uppercase tracking-[0.14em] text-[#b8b8b8] pointer-events-none">+{lockedCount} {t('photos')}</span>
|
||||
{/* <span class="absolute left-2 bottom-2 z-10 text-[10px] uppercase tracking-[0.14em] text-[#b8b8b8] pointer-events-none">+{lockedCount} {t('photos')}</span> */}
|
||||
</figure>
|
||||
))
|
||||
}
|
||||
@ -248,7 +260,7 @@ const homeHref = href(locale, ROUTES.home)
|
||||
type="button"
|
||||
x-show="detailCount(im.lookIndex) > 0"
|
||||
@click.stop="openDetailLb(im.lookIndex)"
|
||||
class="absolute left-2 bottom-2 z-10 flex items-center gap-1 px-1.5 py-1 bg-white/85 backdrop-blur-sm border border-black/15 text-[#111] text-[10px] font-mono uppercase tracking-[0.14em] cursor-pointer hover:bg-black hover:text-white transition-colors"
|
||||
class="absolute left-2 bottom-2 z-10 flex items-center gap-1 px-1.5 py-1 bg-white/85 backdrop-blur-sm border border-black/15 text-[#111] text-[10px] uppercase tracking-[0.14em] cursor-pointer hover:bg-black hover:text-white transition-colors"
|
||||
:title="`${detailCount(im.lookIndex)} detail${detailCount(im.lookIndex) > 1 ? 's' : ''}`"
|
||||
>
|
||||
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
@ -273,13 +285,13 @@ const homeHref = href(locale, ROUTES.home)
|
||||
</article>
|
||||
) : (
|
||||
<div class="flex flex-col items-center justify-center min-h-[60vh] px-5 text-center">
|
||||
<p class="font-mono text-sm text-[#888] uppercase tracking-widest mb-4">
|
||||
<p class="text-sm text-[#888] uppercase tracking-widest mb-4">
|
||||
{error === "missing" ? t('No article ID specified') : t('Failed to load article. Please refresh or try later.')}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href={homeHref}
|
||||
class="text-[12px] font-mono uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors"
|
||||
class="text-[12px] uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors"
|
||||
>
|
||||
← {t('Back to Index')}
|
||||
</a>
|
||||
@ -287,8 +299,7 @@ const homeHref = href(locale, ROUTES.home)
|
||||
)
|
||||
}
|
||||
|
||||
{/* 大图灯箱:与街拍详情共用 components/GalleryLightbox.astro(标记)+ lib/gallery.ts(交互)。
|
||||
走秀独有的「当前 look 的细节图簇」经 details 插槽注入。 */}
|
||||
{/* 大图灯箱:与街拍详情共用 components/GalleryLightbox.astro(标记)+ lib/gallery.ts(交互)。 走秀独有的「当前 look 的细节图簇」经 details 插槽注入。 */}
|
||||
{article && previewImages.length > 0 && (
|
||||
<GalleryLightbox images={previewImages} aspect="2/3">
|
||||
{/* 当前 look 的细节图:右下角一小簇缩略图(竖版主图右下角本就是空白,不遮挡主图)。
|
||||
@ -301,7 +312,7 @@ const homeHref = href(locale, ROUTES.home)
|
||||
x-cloak
|
||||
class="absolute right-4 sm:right-6 bottom-[72px] z-30 flex flex-col items-end gap-2"
|
||||
>
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.22em] text-[#999]">Details</span>
|
||||
<span class="text-[10px] uppercase tracking-[0.22em] text-[#999]">Details</span>
|
||||
<div class="flex max-w-[min(70vw,440px)] gap-2 overflow-x-auto no-scrollbar">
|
||||
<template x-for="(d, i) in currentDetailList" :key="d.url">
|
||||
<button
|
||||
@ -330,7 +341,7 @@ const homeHref = href(locale, ROUTES.home)
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 font-mono text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
|
||||
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
|
||||
x-text="`${String(detailLbIndex + 1).padStart(2, '0')} / ${String(detailImages.length).padStart(2, '0')}`"
|
||||
></div>
|
||||
|
||||
@ -373,7 +384,7 @@ const homeHref = href(locale, ROUTES.home)
|
||||
x-bind:class="detailLbIndex === i ? 'opacity-100 ring-1 ring-black' : ''"
|
||||
:aria-label="String(i + 1)"
|
||||
>
|
||||
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 font-mono text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm" x-text="String(i + 1).padStart(2, '0')"></span>
|
||||
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm" x-text="String(i + 1).padStart(2, '0')"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
@ -423,7 +434,7 @@ const homeHref = href(locale, ROUTES.home)
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute left-0 right-0 bottom-4 z-20 px-6 text-center font-mono text-[11px] uppercase tracking-[0.2em] text-black/60 pointer-events-none"
|
||||
class="absolute left-0 right-0 bottom-4 z-20 px-6 text-center text-[11px] uppercase tracking-[0.2em] text-black/60 pointer-events-none"
|
||||
x-text="currentDetailName"
|
||||
></div>
|
||||
</div>
|
||||
@ -464,6 +475,7 @@ const homeHref = href(locale, ROUTES.home)
|
||||
getImageLookIndex(index: number): number
|
||||
currentDetailList: DetailImage[]
|
||||
openDetailLbAt(idx: number): void
|
||||
buildDetailMapFrom(images: any[]): void
|
||||
buildDetailMap(): void
|
||||
detailCount(li: number): number
|
||||
openDetailLb(li: number, startIndex?: number, fromLb?: boolean): void
|
||||
@ -511,8 +523,15 @@ const homeHref = href(locale, ROUTES.home)
|
||||
lbReturnIndex: 0,
|
||||
|
||||
init() {
|
||||
// 本组件独有:解析后端 details 分组(按 look_index 聚合细节图),供「细节入口」展开灯箱。
|
||||
// 本组件独有:从 SSR 载荷构建「细节入口」用的 look_index -> 细节图 映射。
|
||||
this.buildDetailMap()
|
||||
// 未登录时后端只返回前 PREVIEW_LIMIT 张主图的细节子树;登录后 loadAuthedState 拿回完整图集,
|
||||
// 需据其重建映射,否则第 6 张起的角标 / 细节灯箱仍为空(详见 lib/gallery.ts 的 gallery:full)。
|
||||
// 先挂监听再初始化:galleryInit() 内部可能立刻触发一次登录态取数。
|
||||
window.addEventListener("gallery:full", ((e: Event) => {
|
||||
const imgs = (e as CustomEvent).detail?.images
|
||||
this.buildDetailMapFrom(Array.isArray(imgs) ? imgs : [])
|
||||
}) as EventListener)
|
||||
// 共享初始化(登录态、图片计数、缩略图懒加载、收藏态、深链定位、登录后补全量)
|
||||
this.galleryInit()
|
||||
},
|
||||
@ -551,24 +570,30 @@ const homeHref = href(locale, ROUTES.home)
|
||||
},
|
||||
|
||||
// ===== 细节图分组 =====
|
||||
// 由根节点 data-details(后端按 look_index 聚合的细节图 JSON)构建 look_index -> 图片数组映射。
|
||||
// 由「主图数组」构建 look_index -> 细节图数组 映射。两个来源同构,共用本函数:
|
||||
// ① SSR:根节点 data-details(只含带细节图的主图投影 [{ look_index, detail }])
|
||||
// ② 登录后:详情接口返回的完整 images(细节图已嵌套在每张主图的 detail 下)
|
||||
buildDetailMapFrom(images: any[]) {
|
||||
const map: Record<number, { id: string; url: string; thumb: string; raw: string; name: string }[]> = {}
|
||||
for (const im of images || []) {
|
||||
const li = Number(im?.look_index ?? 0)
|
||||
const kids: any[] = Array.isArray(im?.detail) ? im.detail : []
|
||||
if (!li || !kids.length) continue
|
||||
map[li] = kids.map((d: any) => ({
|
||||
id: d.id || "",
|
||||
url: toAbs(d.image || ""),
|
||||
thumb: toAbs(d.thumb || d.image || ""),
|
||||
raw: d.image || "",
|
||||
name: d.name || "",
|
||||
}))
|
||||
}
|
||||
this.detailImagesByLook = map
|
||||
},
|
||||
|
||||
buildDetailMap() {
|
||||
try {
|
||||
const raw = this.root()?.dataset.details
|
||||
const arr: any[] = raw ? JSON.parse(raw) : []
|
||||
const map: Record<number, { id: string; url: string; thumb: string; raw: string; name: string }[]> = {}
|
||||
for (const d of arr) {
|
||||
const li = Number(d.look_index ?? 0)
|
||||
if (!li) continue
|
||||
map[li] = (d.images || []).map((im: any) => ({
|
||||
id: im.id || "",
|
||||
url: toAbs(im.image || ""),
|
||||
thumb: toAbs(im.thumb || im.image || ""),
|
||||
raw: im.image || "",
|
||||
name: im.name || "",
|
||||
}))
|
||||
}
|
||||
this.detailImagesByLook = map
|
||||
this.buildDetailMapFrom(raw ? JSON.parse(raw) : [])
|
||||
} catch {
|
||||
this.detailImagesByLook = {}
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
|
||||
</button>
|
||||
|
||||
{im.name && (
|
||||
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2">
|
||||
<figcaption class="text-[11px] uppercase tracking-widest text-[#999] mt-2">
|
||||
{im.name}
|
||||
</figcaption>
|
||||
)}
|
||||
@ -123,12 +123,12 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
|
||||
<rect x="4.5" y="10.5" width="15" height="10" rx="2" />
|
||||
<path d="M8 10.5V7a4 4 0 0 1 8 0v3.5" />
|
||||
</svg>
|
||||
<span class="text-[10px] font-mono uppercase tracking-[0.18em]">{t('login to view')}</span>
|
||||
<span class="text-[10px] uppercase tracking-[0.18em]">{t('login to view')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 与真实图 figcaption 同高的 caption 行(正常图显示 img01 等小字):
|
||||
占位显示 +N photos,保证遮罩格子与真实图片格子总高一致 */}
|
||||
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#cfcfcf] mt-2">+{lockedCount} {t('photos')}</figcaption>
|
||||
<figcaption class="text-[11px] uppercase tracking-widest text-[#cfcfcf] mt-2">+{lockedCount} {t('photos')}</figcaption>
|
||||
</figure>
|
||||
))
|
||||
}
|
||||
@ -166,7 +166,7 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
|
||||
</svg>
|
||||
</button>
|
||||
<template x-if="im.name">
|
||||
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2" x-text="im.name"></figcaption>
|
||||
<figcaption class="text-[11px] uppercase tracking-widest text-[#999] mt-2" x-text="im.name"></figcaption>
|
||||
</template>
|
||||
</figure>
|
||||
</template>
|
||||
@ -177,14 +177,14 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
|
||||
</article>
|
||||
) : (
|
||||
<div class="flex flex-col items-center justify-center min-h-[60vh] px-5 text-center">
|
||||
<p class="font-mono text-sm text-[#888] uppercase tracking-widest mb-4">
|
||||
<p class="text-sm text-[#888] uppercase tracking-widest mb-4">
|
||||
{error === "missing" ? t('No street snap ID specified') : t('Failed to load street snap. Please refresh or try later.')}
|
||||
</p>
|
||||
|
||||
|
||||
<a
|
||||
href={homeHref}
|
||||
class="text-[12px] font-mono uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors"
|
||||
class="text-[12px] uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors"
|
||||
>
|
||||
← {t('Back to Index')}
|
||||
</a>
|
||||
|
||||
@ -37,9 +37,9 @@ 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">
|
||||
<p class="mb-5 font-mono text-[11px] uppercase tracking-[0.3em] text-zinc-400">{pleaseLabel}</p>
|
||||
<p class="mb-12 font-serif 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 font-mono text-[12px] font-bold uppercase tracking-[0.22em] text-white transition-colors hover:bg-white hover:text-black">{loginLabel}</a>
|
||||
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{pleaseLabel}</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>
|
||||
</div>
|
||||
|
||||
<!-- ===== 已登录 ===== -->
|
||||
@ -48,14 +48,14 @@ const browseLabel = t("browse the archive")
|
||||
<!-- 报头:衬线大标题会员名 + 档案标签 -->
|
||||
<header class="flex flex-wrap items-end justify-between gap-x-16 gap-y-8 border-b-2 border-black pb-10 pt-2">
|
||||
<div class="min-w-0">
|
||||
<p class="mb-4 font-mono text-[11px] uppercase tracking-[0.3em] text-zinc-400">{t("member profile")}</p>
|
||||
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{t("member profile")}</p>
|
||||
<div class="flex flex-wrap items-end gap-x-4">
|
||||
<h1 class="font-serif text-6xl font-black 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 徽标:仅高级用户(图集详情页可见高清原图)显示 -->
|
||||
<span x-show="user?.tier === 'vip'" x-cloak class="mb-2 inline-block border border-black bg-black px-2.5 py-1 font-mono 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>
|
||||
<div class="shrink-0 pb-1 text-right font-mono text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
|
||||
<div class="shrink-0 pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
|
||||
<p>Portfolio Studio</p>
|
||||
<p class="flex items-center justify-end gap-2 text-zinc-500">Archive
|
||||
<span class="bg-black px-1.5 py-0.5 text-[10px] font-bold tracking-widest text-white">2026</span>
|
||||
@ -69,8 +69,8 @@ const browseLabel = t("browse the archive")
|
||||
|
||||
<!-- 左:编号目录式垂直导航(移动端折成横向条) -->
|
||||
<nav class="lg:sticky lg:top-12 lg:self-start" aria-label="Account sections">
|
||||
<p class="mb-3 hidden font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-400 lg:block">Index</p>
|
||||
<div class="flex gap-10 overflow-x-auto border-b border-black/15 font-mono text-[12px] uppercase tracking-[0.2em] lg:flex-col lg:gap-0 lg:overflow-visible lg:border-b-0 lg:border-t lg:border-black/10">
|
||||
<p class="mb-3 hidden text-[10px] uppercase tracking-[0.3em] text-zinc-400 lg:block">Index</p>
|
||||
<div class="flex gap-10 overflow-x-auto border-b border-black/15 text-[12px] uppercase tracking-[0.2em] lg:flex-col lg:gap-0 lg:overflow-visible lg:border-b-0 lg:border-t lg:border-black/10">
|
||||
<button type="button" @click="tab='saved'" :class="tab==='saved' ? 'border-black text-black' : 'border-transparent text-black/40 hover:text-black'" class="-mb-px whitespace-nowrap border-b-2 px-0.5 pb-4 pt-1 text-left transition-colors lg:mb-0 lg:w-full lg:border-b-0 lg:border-l-2 lg:px-5 lg:py-4">
|
||||
<span class="mr-3 text-[10px] opacity-60">01</span>{savedLooksLabel}
|
||||
</button>
|
||||
@ -85,20 +85,20 @@ const browseLabel = t("browse the archive")
|
||||
<div x-show="tab==='saved'" x-cloak>
|
||||
|
||||
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
|
||||
<h2 class="font-serif text-4xl font-black uppercase leading-none tracking-tight md:text-6xl">{savedLooksLabel}</h2>
|
||||
<p class="pb-1 font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-400"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p>
|
||||
<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-zinc-400"><span class="font-bold text-black" x-text="favoritesCount"></span> {itemsLabel}</p>
|
||||
</div>
|
||||
|
||||
<!-- 粒度切换:图集 / 单图(服务端 kind 过滤分页) -->
|
||||
<div class="mb-10 flex items-center gap-8 border-b border-black/15 font-mono text-[11px] uppercase tracking-[0.2em]">
|
||||
<div class="mb-10 flex items-center gap-8 border-b border-black/15 text-[11px] uppercase tracking-[0.2em]">
|
||||
<button type="button" @click="setFavKind('gallery')" :class="favKind==='gallery' ? 'border-black text-black' : 'border-transparent text-black/45 hover:text-black'" class="-mb-px border-b-2 py-4 transition-colors">{t('galleries')}</button>
|
||||
<button type="button" @click="setFavKind('image')" :class="favKind==='image' ? 'border-black text-black' : 'border-transparent text-black/45 hover:text-black'" class="-mb-px border-b-2 py-4 transition-colors">{t('images')}</button>
|
||||
</div>
|
||||
|
||||
<!-- 空态 -->
|
||||
<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 font-serif 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 font-mono text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 收藏网格 -->
|
||||
@ -108,19 +108,19 @@ const browseLabel = t("browse the archive")
|
||||
<a :href="itemHref(f)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
|
||||
<img :src="coverUrl(f)" :alt="f.title" loading="lazy" class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.04]" />
|
||||
</a>
|
||||
<button @click="removeFav(f.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 font-mono text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
|
||||
<button @click="removeFav(f.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
|
||||
<div class="mt-3 min-w-0">
|
||||
<a :href="itemHref(f)" target="_blank" rel="noopener" class="block">
|
||||
<p class="truncate text-[13px] font-medium leading-snug" x-text="f.title"></p>
|
||||
</a>
|
||||
<p class="mt-1.5 truncate font-mono text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="f.brand || '—'"></p>
|
||||
<p class="mt-1.5 truncate text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="f.brand || '—'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 收藏分页(服务端分页,与收藏总量解耦) -->
|
||||
<nav class="mt-14 flex flex-wrap items-center justify-center gap-2 font-mono" x-show="favLastPage > 1" aria-label="Saved pagination">
|
||||
<nav class="mt-14 flex flex-wrap items-center justify-center gap-2" x-show="favLastPage > 1" aria-label="Saved pagination">
|
||||
<button type="button" class="border border-black/20 px-4 py-2 text-[11px] uppercase tracking-widest transition-colors hover:bg-black hover:text-white disabled:opacity-30" :disabled="favPage <= 1" @click="goFavPage(favPage - 1)">‹ Prev</button>
|
||||
<template x-for="(p, i) in favPageList" :key="i">
|
||||
<span>
|
||||
@ -136,17 +136,17 @@ const browseLabel = t("browse the archive")
|
||||
<div x-show="tab==='history'" x-cloak>
|
||||
|
||||
<div class="mb-10 flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
|
||||
<h2 class="font-serif text-4xl font-black 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">
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-400"><span class="font-bold text-black" x-text="histTotal"></span> {itemsLabel}</p>
|
||||
<button x-show="histTotal > 0" @click="clearAllHistory()" class="font-mono text-[11px] uppercase tracking-[0.2em] text-zinc-400 underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button>
|
||||
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-400"><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-zinc-400 underline-offset-4 transition-colors hover:text-black hover:underline">{clearHistoryLabel}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空态 -->
|
||||
<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 font-serif 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 font-mono text-[11px] font-bold uppercase tracking-[0.2em] text-black transition-colors hover:bg-black hover:text-white">{browseLabel}</a>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 历史网格 -->
|
||||
@ -156,19 +156,19 @@ const browseLabel = t("browse the archive")
|
||||
<a :href="itemHref(h)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
|
||||
<img :src="coverUrl(h)" :alt="h.data?.title || ''" loading="lazy" class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.04]" />
|
||||
</a>
|
||||
<button @click="removeHist(h.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 font-mono text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
|
||||
<button @click="removeHist(h.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
|
||||
<div class="mt-3 min-w-0">
|
||||
<a :href="itemHref(h)" target="_blank" rel="noopener" class="block">
|
||||
<p class="truncate text-[13px] font-medium leading-snug" x-text="h.data?.title || '…'"></p>
|
||||
</a>
|
||||
<p class="mt-1.5 truncate font-mono text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="h.data?.brand || '—'"></p>
|
||||
<p class="mt-1.5 truncate text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="h.data?.brand || '—'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 历史分页 -->
|
||||
<nav class="mt-14 flex flex-wrap items-center justify-center gap-2 font-mono" x-show="histLastPage > 1" aria-label="History pagination">
|
||||
<nav class="mt-14 flex flex-wrap items-center justify-center gap-2" x-show="histLastPage > 1" aria-label="History pagination">
|
||||
<button type="button" class="border border-black/20 px-4 py-2 text-[11px] uppercase tracking-widest transition-colors hover:bg-black hover:text-white disabled:opacity-30" :disabled="histPage <= 1" @click="goHistPage(histPage - 1)">‹ Prev</button>
|
||||
<template x-for="(p, i) in histPageList" :key="i">
|
||||
<span>
|
||||
@ -203,7 +203,7 @@ const browseLabel = t("browse the archive")
|
||||
const PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="300" height="400">' +
|
||||
'<rect width="300" height="400" fill="#f1f1f1"/>' +
|
||||
'<text x="150" y="200" font-family="monospace" font-size="16" letter-spacing="3" fill="#9ca3af" text-anchor="middle">LOOK</text>' +
|
||||
'<text x="150" y="200" font-family="sans-serif" font-size="16" letter-spacing="3" fill="#9ca3af" text-anchor="middle">LOOK</text>' +
|
||||
'</svg>'
|
||||
)
|
||||
|
||||
|
||||
@ -52,12 +52,12 @@ const loginKeywords = locale === 'cn'
|
||||
|
||||
<!-- 顶部工具行:返回档案 + 语言切换 -->
|
||||
<header class="mb-12 flex items-center justify-between gap-6">
|
||||
<a href={homeUrl} class="inline-flex items-center gap-2 font-mono 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/50 transition-colors hover:text-black">
|
||||
<span aria-hidden="true">←</span>{backLabel}
|
||||
</a>
|
||||
|
||||
<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/15 px-3 py-2 font-mono 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>
|
||||
<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"/>
|
||||
@ -66,7 +66,7 @@ const loginKeywords = locale === 'cn'
|
||||
<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) => (
|
||||
<li>
|
||||
<a href={getRelativeLocaleUrl(l.code, pathWithoutLocale)} @click="open = false" class:list={['block px-4 py-2 font-mono 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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -77,10 +77,10 @@ const loginKeywords = locale === 'cn'
|
||||
<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>
|
||||
<p class="mb-4 font-mono text-[11px] uppercase tracking-[0.3em] text-zinc-400">{membersLabel}</p>
|
||||
<h1 class="font-serif text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl">{loginLabel}</h1>
|
||||
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{membersLabel}</p>
|
||||
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl">{loginLabel}</h1>
|
||||
</div>
|
||||
<div class="pb-1 text-right font-mono text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
|
||||
<div class="pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
|
||||
<p>Portfolio Studio</p>
|
||||
<p class="flex items-center justify-end gap-2 text-zinc-500">Archive
|
||||
<span class="bg-black px-1.5 py-0.5 text-[10px] font-bold tracking-widest text-white">2026</span>
|
||||
@ -94,41 +94,41 @@ const loginKeywords = locale === 'cn'
|
||||
<div class="grid gap-12 pt-12 md:grid-cols-[minmax(0,1fr)_320px] md:gap-16 md:pt-16">
|
||||
|
||||
<div class="w-full max-w-md">
|
||||
<p class="mb-10 font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-400">{promptLabel}</p>
|
||||
<p class="mb-10 text-[11px] uppercase tracking-[0.22em] text-zinc-400">{promptLabel}</p>
|
||||
|
||||
<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 font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-500">{accountLabel}</label>
|
||||
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-zinc-500">{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>
|
||||
<label class="mb-2 block font-mono text-[11px] uppercase tracking-[0.22em] text-zinc-500">{passwordLabel}</label>
|
||||
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-zinc-500">{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="font-mono text-[11px] uppercase tracking-[0.2em] text-red-600"></p>
|
||||
<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"
|
||||
class="w-full border border-black bg-black py-4 font-mono 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="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">{loginLabel}</span>
|
||||
<span x-show="loading" x-cloak>…</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-10 border-t border-black/15 pt-5">
|
||||
<a href={homeUrl} class="inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">
|
||||
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">
|
||||
<span aria-hidden="true">←</span>{backLabel}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="border-t border-black/10 pt-8 md:border-l md:border-t-0 md:pl-8 md:pt-0">
|
||||
<p class="mb-5 font-mono text-[11px] uppercase tracking-[0.3em] text-zinc-400">Archive note</p>
|
||||
<p class="font-serif 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>
|
||||
<div class="mt-8 border-t border-black/10 pt-5 font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-400">
|
||||
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-zinc-400">Archive note</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>
|
||||
<div class="mt-8 border-t border-black/10 pt-5 text-[10px] uppercase tracking-[0.3em] text-zinc-400">
|
||||
<p class="mb-3">Runway · Street · Archive</p>
|
||||
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user