update
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
toom1996
2026-08-16 00:56:59 +08:00
parent 31cb7a125a
commit 781242ede0
26 changed files with 1814 additions and 1214 deletions

View File

@ -1,463 +1,8 @@
---
import Layout from "../layouts/Layout.astro"
// 示例数据:字段严格对齐后端 BrandRunway 模型 + 图片集接口
const article = {
id: 1,
title: "Maison Lumen — Paper Light",
description:
"巴黎,2026 秋冬。Maison Lumen 以“Paper Light(纸光)”为题,将薄纱、宣纸与压褶工艺叠合成一种近乎透明的体积感。\n\n系列延续了工作室对“轻”的执念:肩线被刻意放低,廓形在行走中随风摆动,像被光穿透的纸。色彩仅保留骨白、炭灰与一抹雾蓝。\n\n这不是关于廓形的宣言,而是关于 material 的克制——用最少的手段,制造最大的呼吸感。",
created_at: 1711948800,
updated_at: 1711948800,
year: 2026,
source_url: "https://example.com/runway/maison-lumen-paper-light",
brand_id: 12,
brand_name: "Maison Lumen",
images: [
{ id: 1, image: "https://picsum.photos/seed/art1/1200/1500", name: "Look 01", sort_order: 1 },
{ id: 2, image: "https://picsum.photos/seed/art2/1200/1500", name: "Look 02", sort_order: 2 },
{ id: 3, image: "https://picsum.photos/seed/art3/1200/1500", name: "Look 03", sort_order: 3 },
{ id: 4, image: "https://picsum.photos/seed/art4/1200/1500", name: "Look 04", sort_order: 4 },
],
}
const fmt = (ts: number) => {
const d = new Date(ts * 1000)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, "0")
const day = String(d.getDate()).padStart(2, "0")
return `${y}.${m}.${day}`
}
const body = article.description.split("\n\n")
import Article from "../components/pages/article.astro"
---
<Layout title={`${article.title} — Runway Report`}>
<article class="mx-auto max-w-[1100px] px-5 md:px-10 py-12 md:py-16">
<!-- 面包屑 / 品牌 -->
<div class="flex items-center justify-between text-[11px] font-mono uppercase tracking-[0.25em] text-black border-b border-black pb-4">
<a href="/" class="hover:opacity-60 transition-opacity">← Index</a>
<span id="art-brand">Runway · {article.brand_name}</span>
</div>
<!-- 标题 -->
<h1 class="font-serif font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">
{article.title}
</h1>
<!-- 元信息 -->
<div class="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-[12px] font-mono uppercase tracking-widest text-[#666]">
<span id="art-date">{fmt(article.created_at)}</span>
<span id="art-year" class="text-black">{article.year}</span>
<a href={article.source_url} target="_blank" rel="noopener" class="hover:text-black underline underline-offset-4">Source ↗</a>
</div>
<!-- 正文 -->
<div class="mt-12 grid md:grid-cols-12 gap-8">
<div class="md:col-span-2 text-[11px] font-mono uppercase tracking-widest text-[#999] pt-1">
Report
</div>
<div id="art-body" class="md:col-span-10 max-w-[68ch] space-y-5 text-[16px] leading-[1.9] text-[#222]">
{body.map((p) => <p>{p}</p>)}
</div>
</div>
<!-- 图片集 -->
<section class="mt-16">
<div class="flex items-end justify-between border-b border-[#eee] pb-3 mb-6">
<div class="text-[11px] font-mono uppercase tracking-[0.25em] text-[#999]">
Gallery <span id="art-gallery-count" class="ml-2 text-black"></span>
</div>
<button type="button" id="art-viewall" class="text-[11px] font-mono uppercase tracking-widest border border-black px-3 py-1.5 hover:bg-black hover:text-white transition-colors">⤢ 全屏查看</button>
</div>
<!-- 图片网格 -->
<div id="art-gallery" class="grid grid-cols-2 md:grid-cols-3 gap-4">
{article.images.map((im) => (
<figure class="g-item bg-[#f2f2f2] cursor-zoom-in">
<img src={im.image} alt={im.name} loading="lazy" class="w-full aspect-[3/4] object-cover" />
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2">{im.name}</figcaption>
</figure>
))}
</div>
</section>
<div class="mt-16 pt-6 border-t border-black">
<a href="/" class="text-[12px] font-mono uppercase tracking-widest hover:opacity-60 transition-opacity">← Back to Index</a>
</div>
</article>
<!-- 灯箱:左侧竖排缩略图条 + 中央大图(点击放大→宽度最大+纵向滚动) -->
<div id="lb" class="lb" aria-hidden="true">
<div class="lb-counter" id="lb-counter"></div>
<button class="lb-btn lb-close" id="lb-close" aria-label="关闭">✕</button>
<div class="lb-stage" id="lb-stage">
<div class="lb-thumbs" id="lb-thumbs"></div>
<button class="lb-tbtn lb-tup" id="lb-tup" type="button" aria-label="上一张">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 14l6-6 6 6"/></svg>
</button>
<button class="lb-tbtn lb-tdown" id="lb-tdown" type="button" aria-label="下一张">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 10l6 6 6-6"/></svg>
</button>
<button class="lb-btn lb-prev" id="lb-prev" type="button" aria-label="上一张">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>
</button>
<button class="lb-btn lb-next" id="lb-next" type="button" aria-label="下一张">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
</button>
<div class="lb-main" id="lb-main">
<img id="lb-img" class="lb-img" alt="" />
</div>
</div>
<div class="lb-caption" id="lb-caption"></div>
<div class="lb-hint" id="lb-hint">点击图片放大 · 上下滚动查看 · Esc 关闭</div>
</div>
<Layout>
<Article />
</Layout>
<style is:global>
@reference "tailwindcss";
/* 灯箱:左侧竖排缩略图条 + 中央大图;点击放大→宽度最大+纵向滚动 */
.lb {
position: fixed; inset: 0; z-index: 100;
@apply bg-white;
display: none; flex-direction: column;
opacity: 0; transition: opacity .2s ease;
-webkit-tap-highlight-color: transparent;
}
.lb.is-open { display: flex; opacity: 1; }
.lb-stage {
flex: 1 1 auto; min-height: 0;
display: flex; align-items: stretch; justify-content: stretch;
overflow: hidden; position: relative;
animation: lbIn .2s ease both;
}
@keyframes lbIn { from { opacity: 0; } to { opacity: 1; } }
.lb-thumbs {
flex: 0 0 118px; overflow-y: auto; overflow-x: hidden; position: relative; scroll-behavior: smooth;
padding: 38px 18px 38px 20px;
border-right: 1px solid rgba(0,0,0,.08);
background: linear-gradient(180deg, rgba(0,0,0,.02), rgba(0,0,0,0));
scrollbar-width: none; -ms-overflow-style: none;
}
.lb-thumbs::-webkit-scrollbar { width: 0; height: 0; }
.lb-thumb {
position: relative; display: block; width: 100%; aspect-ratio: 3 / 4;
padding: 0; margin: 0 0 8px; border: 0;
@apply bg-[#ebebeb] rounded-[2px];
overflow: hidden; cursor: pointer;
opacity: .42; transition: opacity .2s ease, transform .2s ease;
}
.lb-thumb:last-child { margin-bottom: 0; }
.lb-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lb-thumb::after {
content: ""; position: absolute; inset: 0; border-radius: 2px;
box-shadow: inset 0 0 0 0 transparent; pointer-events: none;
transition: box-shadow .2s ease;
}
.lb-thumb:hover { opacity: .85; }
.lb-thumb.is-active { opacity: 1; transform: scale(1.03); }
.lb-thumb.is-active::after { box-shadow: inset 0 0 0 2px #111; }
.lb-thumb-idx {
position: absolute; left: 6px; top: 6px; z-index: 1;
font: 600 9px/1 ui-monospace, SFMono-Regular, monospace;
@apply tracking-[.12em] text-white px-[5px] py-[3px] rounded-[3px];
background: rgba(0,0,0,.55);
backdrop-filter: blur(4px); pointer-events: none;
}
.lb-thumb.is-active .lb-thumb-idx { @apply bg-[#111] text-white; }
.lb-main {
flex: 1 1 auto; min-width: 0; min-height: 0;
display: flex; align-items: center; justify-content: center;
position: relative; padding: 0 72px;
}
.lb-img {
max-width: 100%; max-height: 100%; display: block;
object-fit: contain; user-select: none; -webkit-user-drag: none;
transition: opacity .2s ease; cursor: zoom-in;
}
/* 放大态:宽度最大化 + 纵向滚动(无滚轮缩放、无拖拽、无特效) */
.lb-main.is-zoomed { align-items: stretch; display: block; overflow-y: auto; overflow-x: hidden; }
.lb-main.is-zoomed .lb-img { width: 100%; max-width: 100%; height: auto; max-height: none; cursor: zoom-out; }
.lb-btn {
position: absolute; z-index: 3;
width: 42px; height: 42px;
@apply rounded-full;
border: 1px solid rgba(0,0,0,.14); background: rgba(255,255,255,.72);
@apply text-[#111];
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background .15s, transform .15s, border-color .15s; backdrop-filter: blur(10px);
}
.lb-btn svg, .lb-tbtn svg {
display: block; fill: none; stroke: currentColor;
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.lb-btn svg { width: 20px; height: 20px; }
.lb-tbtn svg { width: 16px; height: 16px; }
.lb-btn:hover { background: rgba(0,0,0,.06); border-color: rgba(0,0,0,.24); }
.lb-close { top: 18px; right: 18px; }
.lb-prev { left: 114px; top: 50%; transform: translateY(-50%); }
.lb-next { right: 18px; top: 50%; transform: translateY(-50%); }
.lb-prev:hover, .lb-next:hover { transform: translateY(-50%) scale(1.08); }
/* 左栏上下翻页按钮(浮在缩略图条上、禁手动滚动时的翻页入口) */
.lb-tbtn {
position: absolute; z-index: 4;
left: 20px; width: 72px; height: 34px;
border: 1px solid rgba(0,0,0,.12); background: rgba(255,255,255,.82);
@apply rounded-[6px];
cursor: pointer;
display: flex; align-items: center; justify-content: center;
@apply text-[#111];
backdrop-filter: blur(8px);
transition: background .15s, transform .15s, border-color .15s;
}
.lb-tbtn svg { width: 16px; height: 16px; display: block; }
.lb-tbtn:hover { background: rgba(0,0,0,.06); border-color: rgba(0,0,0,.22); }
.lb-tup { top: 8px; }
.lb-tdown { bottom: 8px; }
.lb-counter {
position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 3;
font: 600 12px/1 ui-monospace, monospace;
@apply tracking-[.14em] text-[#111];
background: rgba(255,255,255,.75); border: 1px solid rgba(0,0,0,.1);
@apply px-[11px] py-[7px] rounded-[6px];
backdrop-filter: blur(6px);
}
.lb-caption {
position: absolute; left: 0; right: 0; bottom: 44px; z-index: 3;
text-align: center; color: rgba(0,0,0,.62); pointer-events: none;
font: 11px/1.4 ui-monospace, monospace;
@apply tracking-[.18em] uppercase;
padding: 0 24px; text-shadow: 0 1px 3px rgba(255,255,255,.7);
}
.lb-hint {
position: absolute; left: 0; right: 0; bottom: 16px; z-index: 3;
text-align: center; color: rgba(0,0,0,.45); pointer-events: none;
font: 11px/1 ui-monospace, monospace;
@apply tracking-[.14em];
padding: 0 24px; transition: opacity .2s ease; text-shadow: 0 1px 3px rgba(255,255,255,.7);
}
.lb-hint.is-hidden { opacity: 0; }
@media (max-width: 640px) {
.lb-thumbs { flex: 0 0 76px; padding: 32px 8px 32px 12px; }
.lb-thumb { margin-bottom: 8px; }
.lb-thumb-idx { font-size: 8px; left: 4px; top: 4px; padding: 2px 4px; }
.lb-main { padding: 0 18px; }
.lb-btn { width: 38px; height: 38px; }
.lb-prev { left: 82px; } .lb-next { right: 8px; }
.lb-tbtn { left: 12px; width: 52px; height: 30px; }
.lb-caption { bottom: 40px; }
}
</style>
<script>
// 真实数据接入:按 ?id= 拉取后端接口,失败时保留上方示例布局
const API = import.meta.env.PUBLIC_API_BASE || ""
const toAbs = (url: string) =>
!url ? "" : /^https?:\/\//i.test(url) ? url : API + url
const id = new URLSearchParams(location.search).get("id")
if (id) {
;(async () => {
try {
const res = await fetch(`${API}/api/public/articles/${id}`)
if (!res.ok) return
const d = (await res.json()).data
if (!d) return
const titleEl = document.querySelector("h1")
if (titleEl && d.title) titleEl.textContent = d.title
const bodyEl = document.getElementById("art-body")
if (bodyEl && d.description) {
bodyEl.innerHTML = d.description
.split("\n\n")
.map((p: string) => `<p>${p}</p>`)
.join("")
}
const brandEl = document.getElementById("art-brand")
if (brandEl && d.brand_name) brandEl.textContent = `Runway · ${d.brand_name}`
const yearEl = document.getElementById("art-year")
if (yearEl && d.year) yearEl.textContent = String(d.year)
const dateEl = document.getElementById("art-date")
if (dateEl && d.published_at) {
const dt = new Date(Number(d.published_at) * 1000)
dateEl.textContent = `${dt.getFullYear()}.${String(dt.getMonth() + 1).padStart(2, "0")}.${String(dt.getDate()).padStart(2, "0")}`
}
const imgs = d.images
if (imgs && Array.isArray(imgs)) {
const gal = document.getElementById("art-gallery")
if (gal) {
gal.innerHTML = imgs
.map(
(im: any) =>
`<figure class="bg-[#f2f2f2]"><img src="${toAbs(im.image)}" alt="${im.name || ""}" loading="lazy" class="w-full aspect-[3/4] object-cover" /><figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2">${im.name || ""}</figcaption></figure>`
)
.join("")
}
}
} catch {
/* 后端不可达时,使用示例数据 */
}
})()
}
</script>
<script>
(function () {
const gallery = document.getElementById("art-gallery");
const lb = document.getElementById("lb");
const lbStage = document.getElementById("lb-stage");
const lbMain = document.getElementById("lb-main");
const lbImg = document.getElementById("lb-img");
const lbCounter = document.getElementById("lb-counter");
const lbCaption = document.getElementById("lb-caption");
const lbThumbs = document.getElementById("lb-thumbs");
const lbHint = document.getElementById("lb-hint");
const btnClose = document.getElementById("lb-close");
const btnPrev = document.getElementById("lb-prev");
const btnNext = document.getElementById("lb-next");
const btnTup = document.getElementById("lb-tup");
const btnTdown = document.getElementById("lb-tdown");
const viewall = document.getElementById("art-viewall");
let items = [];
let index = 0;
let zoomed = false;
/* 放大态:宽度最大化 + 纵向滚动(无滚轮缩放 / 无拖拽 / 无特效) */
function zoomIn() {
zoomed = true;
if (lbMain) { lbMain.classList.add("is-zoomed"); lbMain.scrollTop = 0; }
if (lbHint) lbHint.classList.add("is-hidden");
}
function resetZoom() {
zoomed = false;
if (lbMain) { lbMain.classList.remove("is-zoomed"); lbMain.scrollTop = 0; }
if (lbHint) lbHint.classList.remove("is-hidden");
}
function toggleZoom() { zoomed ? resetZoom() : zoomIn(); }
/* 收集画廊图片 */
function collect() {
const list = [];
if (gallery) gallery.querySelectorAll("img").forEach((img) => {
const fig = img.closest("figure");
const cap = fig && fig.querySelector("figcaption");
list.push({ src: img.currentSrc || img.src, name: (cap && cap.textContent || "").trim() });
});
return list;
}
/* 渲染左侧竖排缩略图条 */
function renderStrip() {
if (!lbThumbs) return;
lbThumbs.innerHTML = items
.map((it, i) => {
const n = String(i + 1).padStart(2, "0");
return '<button type="button" class="lb-thumb' + (i === index ? ' is-active' : '') + '" data-i="' + i + '">' +
'<img src="' + it.src + '" alt="" />' +
'<span class="lb-thumb-idx">' + n + '</span>' +
'</button>';
})
.join("");
}
function show(i) {
if (!items.length) return;
index = (i + items.length) % items.length;
const it = items[index];
resetZoom();
lbImg.style.opacity = "0";
const loader = new Image();
loader.onload = function () { lbImg.src = it.src; lbImg.style.opacity = "1"; };
loader.src = it.src;
if (lbCounter) lbCounter.textContent = (index + 1) + " / " + items.length;
if (lbCaption) lbCaption.textContent = it.name || "";
if (lbThumbs) lbThumbs.querySelectorAll(".lb-thumb").forEach((t, k) => {
t.classList.toggle("is-active", k === index);
if (k === index) {
// 自动居中当前张(左栏禁用手动滚动,改用程序化 scrollTo)
const target = t.offsetTop - (lbThumbs.clientHeight - t.clientHeight) / 2;
lbThumbs.scrollTo({ top: Math.max(0, target), behavior: "smooth" });
}
});
}
function open(i) {
items = collect();
if (!items.length) return;
if (lb) { lb.classList.add("is-open"); lb.setAttribute("aria-hidden", "false"); }
document.body.style.overflow = "hidden";
renderStrip();
show(i || 0);
}
function close() {
if (lb) { lb.classList.remove("is-open"); lb.setAttribute("aria-hidden", "true"); }
document.body.style.overflow = "";
}
function next() { show(index + 1); }
function prev() { show(index - 1); }
/* 事件绑定 */
if (gallery) gallery.addEventListener("click", (e) => {
const fig = e.target.closest("figure");
if (!fig) return;
const imgs = Array.prototype.slice.call(gallery.querySelectorAll("img"));
open(Math.max(0, imgs.indexOf(fig.querySelector("img"))));
});
if (btnClose) btnClose.addEventListener("click", close);
if (btnNext) btnNext.addEventListener("click", (e) => { e.stopPropagation(); next(); });
if (btnPrev) btnPrev.addEventListener("click", (e) => { e.stopPropagation(); prev(); });
if (btnTup) btnTup.addEventListener("click", (e) => { e.stopPropagation(); prev(); });
if (btnTdown) btnTdown.addEventListener("click", (e) => { e.stopPropagation(); next(); });
if (lb) lb.addEventListener("click", (e) => {
const t = e.target;
if (t === lb || (t.classList && t.classList.contains("lb-stage"))) close();
});
if (lbThumbs) lbThumbs.addEventListener("click", (e) => {
const t = e.target.closest(".lb-thumb");
if (t) show(Number(t.dataset.i));
});
if (lbImg) lbImg.addEventListener("click", toggleZoom);
document.addEventListener("keydown", (e) => {
if (!lb || !lb.classList.contains("is-open")) return;
if (e.key === "Escape") close();
else if (e.key === "ArrowRight") next();
else if (e.key === "ArrowLeft") prev();
});
if (viewall) viewall.addEventListener("click", () => open(0));
/* 移动端单指滑动切换(仅在未放大时,放大态交给纵向滚动) */
let tx0 = 0, ty0 = 0, two = false;
if (lbStage) {
lbStage.addEventListener("touchstart", (e) => {
if (e.touches.length === 1) { tx0 = e.touches[0].clientX; ty0 = e.touches[0].clientY; }
else if (e.touches.length === 2) two = true;
}, { passive: true });
lbStage.addEventListener("touchend", (e) => {
if (two) { two = false; return; }
if (zoomed) return;
const t = e.changedTouches[0];
const dx = t.clientX - tx0, dy = t.clientY - ty0;
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) (dx < 0 ? next() : prev());
}, { passive: true });
}
/* 计数 + 画廊重渲染后同步 */
function syncCount() {
const c = document.getElementById("art-gallery-count");
const n = gallery ? gallery.querySelectorAll("img").length : 0;
if (c) c.textContent = n ? "· 共 " + n + " 张" : "";
}
syncCount();
if (gallery && "MutationObserver" in window) {
new MutationObserver(syncCount).observe(gallery, { childList: true });
}
})();
</script>