Files
frontend_v2/src/pages/article.astro
toom1996 5a8dbf41cb
Some checks failed
deploy / deploy (push) Has been cancelled
update
2026-08-11 22:29:47 +08:00

464 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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")
---
<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>
<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>