feat: 文章正文图片支持灯箱;删除未使用的 GitHub Card 插件

灯箱
- 相册页原本已有一套无依赖灯箱(事件委托 + ClientRouter 友好),
  但它写在 gallery/[album].astro 里,只有相册页能用。
  现抽成 src/components/ImageLightbox.astro,挂到 BaseLayout,全站可用。
- 新增 src/plugins/rehype-image-lightbox.mjs:给正文里的 <img> 注入
  data-lightbox-src。跳过链接内的图片(点击应跳转)和显式标记
  data-lightbox-ignore 的图片。
- 顺带让灯箱大图继承原图的 alt 文本(原先固定为空)。
- 正文图片加 cursor: zoom-in 提示。

清理
- 删除 src/plugins/rehype-component-github-card.mjs(165 行)及其在
  rehypeComponents 里的 github 映射:内容里从未使用过 ::github 指令。

验证:构建 41 页通过;文章页正文图片 4/4 带上 data-lightbox-src,
相册页 5 个触发元素不受影响;灯箱脚本按页内联(2.8KB 压缩后);
正文渲染文本与线上基线一致,无回归。
This commit is contained in:
2026-09-17 17:29:23 +08:00
parent 738fe3175e
commit c1643ae95d
6 changed files with 181 additions and 279 deletions
-112
View File
@@ -122,115 +122,3 @@ const description = album.description ?? "相册"
break-inside: avoid;
}
</style>
<script>
/**
* 无依赖灯箱。
* 用事件委托 + 每次打开时重新收集当前页的照片,因此 ClientRouter 切页后依然有效。
*/
const LB_ID = "vv-lightbox"
const ensureOverlay = (): HTMLElement => {
let overlay = document.getElementById(LB_ID)
if (overlay) return overlay
overlay = document.createElement("div")
overlay.id = LB_ID
overlay.setAttribute("role", "dialog")
overlay.setAttribute("aria-modal", "true")
overlay.className =
"fixed inset-0 z-[100] hidden items-center justify-center bg-black/90 backdrop-blur-sm"
overlay.innerHTML = `
<button type="button" data-lb-close aria-label="关闭"
class="absolute right-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20">×</button>
<button type="button" data-lb-prev aria-label="上一张"
class="absolute left-3 z-10 flex h-11 w-11 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20 sm:left-6">‹</button>
<button type="button" data-lb-next aria-label="下一张"
class="absolute right-3 z-10 flex h-11 w-11 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20 sm:right-6">›</button>
<img data-lb-img src="" alt="" class="max-h-[88vh] max-w-[92vw] object-contain" />
<div data-lb-count
class="absolute bottom-5 left-1/2 -translate-x-1/2 rounded-full bg-white/10 px-3 py-1 font-mono text-xs text-white/80"></div>
`
document.body.appendChild(overlay)
return overlay
}
let sources: string[] = []
let current = 0
const render = (overlay: HTMLElement) => {
const img = overlay.querySelector<HTMLImageElement>("[data-lb-img]")
const counter = overlay.querySelector<HTMLElement>("[data-lb-count]")
if (img) img.src = sources[current] ?? ""
if (counter) counter.textContent = `${current + 1} / ${sources.length}`
const single = sources.length < 2
for (const sel of ["[data-lb-prev]", "[data-lb-next]"]) {
const el = overlay.querySelector<HTMLElement>(sel)
if (el) el.style.display = single ? "none" : ""
}
}
const openAt = (index: number) => {
const overlay = ensureOverlay()
current = index
render(overlay)
overlay.classList.remove("hidden")
overlay.classList.add("flex")
document.body.style.overflow = "hidden"
}
const close = () => {
const overlay = document.getElementById(LB_ID)
if (!overlay) return
overlay.classList.add("hidden")
overlay.classList.remove("flex")
document.body.style.overflow = ""
}
const step = (delta: number) => {
if (sources.length < 2) return
current = (current + delta + sources.length) % sources.length
const overlay = document.getElementById(LB_ID)
if (overlay) render(overlay)
}
if (!(window as unknown as { __vvLightboxBound?: boolean }).__vvLightboxBound) {
;(window as unknown as { __vvLightboxBound?: boolean }).__vvLightboxBound = true
document.addEventListener("click", (event) => {
const target = event.target as Element | null
const trigger = target?.closest<HTMLElement>("[data-lightbox-src]")
if (trigger) {
// 每次打开都重新收集当前页的照片
const all = Array.from(document.querySelectorAll<HTMLElement>("[data-lightbox-src]"))
sources = all.map((el) => el.dataset.lightboxSrc ?? "")
openAt(Number(trigger.dataset.lightboxIndex ?? all.indexOf(trigger)))
return
}
if (target?.closest("[data-lb-close]")) return close()
if (target?.closest("[data-lb-prev]")) return step(-1)
if (target?.closest("[data-lb-next]")) return step(1)
// 点背景关闭(点图片本身不关)
const overlay = document.getElementById(LB_ID)
if (overlay && target === overlay) close()
})
document.addEventListener("keydown", (event) => {
const overlay = document.getElementById(LB_ID)
if (!overlay || overlay.classList.contains("hidden")) return
if (event.key === "Escape") close()
if (event.key === "ArrowLeft") step(-1)
if (event.key === "ArrowRight") step(1)
})
// 切页时收拾干净,避免残留遮罩锁住滚动
document.addEventListener("astro:page-load", () => {
const overlay = document.getElementById(LB_ID)
if (overlay) overlay.remove()
document.body.style.overflow = ""
})
}
</script>