diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 54830c6..2eae261 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,9 +7,26 @@ import ImgLightbox from './components/ImgLightbox.vue' import YohakuTop from './ui/yohaku/YohakuTop.vue' import YohakuFoot from './ui/yohaku/YohakuFoot.vue' import { site, setAdminScope } from './site' +import { openLightbox } from './lightbox' const route = useRoute() +// 正文图片点击预览(capture 阶段):时间线/详情页里 v-html 的正文图 +// 都没有自己的点击入口,这里统一接管。capture + stopPropagation 是为了 +// 压住整卡点击(时间线点图不该跳详情);配图网格有自己的 handler 不在 +// .prose 内,天然不受影响。lightbox 用 data-fullsrc 原图(thumbify 保留)。 +function onProseImgClick(e) { + const img = e.target + if (!img || img.tagName !== 'IMG') return + const scope = img.closest('.prose') + if (!scope || img.closest('.post-imgs') || img.closest('a')) return + const list = [...scope.querySelectorAll('img')].map((i) => i.dataset.fullsrc || i.currentSrc || i.src) + const cur = img.dataset.fullsrc || img.currentSrc || img.src + const idx = Math.max(0, list.indexOf(cur)) + e.stopPropagation() + openLightbox(list, idx) +} + // 正文代码块复制按钮(事件委托:复制按钮是 enhanceProse 注入的静态 HTML) function onCopyClick(e) { const btn = e.target.closest?.('.pre-copy') @@ -31,8 +48,14 @@ function onCopyClick(e) { setTimeout(() => (btn.textContent = '复制'), 1600) }) } -onMounted(() => document.addEventListener('click', onCopyClick)) -onBeforeUnmount(() => document.removeEventListener('click', onCopyClick)) +onMounted(() => { + document.addEventListener('click', onCopyClick) + document.addEventListener('click', onProseImgClick, true) +}) +onBeforeUnmount(() => { + document.removeEventListener('click', onCopyClick) + document.removeEventListener('click', onProseImgClick, true) +}) const isAdmin = computed(() => route.path.startsWith('/admin')) // vivid 只在公开前台生效:后台永远走 classic 那套(--admin-* token + 原 data-ui/data-theme), diff --git a/frontend/src/components/ImgLightbox.vue b/frontend/src/components/ImgLightbox.vue index 60b70ef..0755673 100644 --- a/frontend/src/components/ImgLightbox.vue +++ b/frontend/src/components/ImgLightbox.vue @@ -3,27 +3,54 @@ import { onBeforeUnmount, watch } from 'vue' import { lightboxState as st, closeLightbox, stepLightbox } from '../lightbox' // 图片预览层:App.vue 全局挂一次。铺满全屏的暗底 + 居中大图, -// 多图时左右箭头 / 方向键切换,角标显示第几张;Esc / 点空白收起。 +// 多图时左右箭头 / 方向键 / 手机滑动切换,角标显示第几张;Esc / 点空白收起。 function onKey(e) { if (e.key === 'Escape') closeLightbox() if (e.key === 'ArrowLeft') stepLightbox(-1) if (e.key === 'ArrowRight') stepLightbox(1) } + +// 打开时锁住背景滚动(手机上尤其明显:否则手指滑动会带着页面一起动) +let prevOverflow = '' watch( () => st.open, (v) => { - if (v) window.addEventListener('keydown', onKey) - else window.removeEventListener('keydown', onKey) + if (v) { + window.addEventListener('keydown', onKey) + prevOverflow = document.body.style.overflow + document.body.style.overflow = 'hidden' + } else { + window.removeEventListener('keydown', onKey) + document.body.style.overflow = prevOverflow + } } ) -onBeforeUnmount(() => window.removeEventListener('keydown', onKey)) +onBeforeUnmount(() => { + window.removeEventListener('keydown', onKey) + document.body.style.overflow = prevOverflow +}) + +// 手机滑动翻页:横向位移超 44px 且大于纵向位移才算一次滑动 +let touchX = 0 +let touchY = 0 +function onTouchStart(e) { + touchX = e.touches[0].clientX + touchY = e.touches[0].clientY +} +function onTouchEnd(e) { + const dx = e.changedTouches[0].clientX - touchX + const dy = e.changedTouches[0].clientY - touchY + if (Math.abs(dx) > 44 && Math.abs(dx) > Math.abs(dy) * 1.4 && st.list.length > 1) { + stepLightbox(dx < 0 ? 1 : -1) + } +}