重写手机图片预览(不参考旧实现):
- 真凶补刀:lightbox.js 状态模块里一直有 body.style.overflow='hidden'——上次撤销只撤了组件没撤它,撑屏从未消失。现在彻底移除,注释写明绝不可再碰 body inline overflow(会覆盖 overflow-x: clip) - 滚动封锁改由遮罩自身 @wheel/@touchmove.prevent 拦截:事件不落页面,背景滚不动、滚动位置保留 - 图片尺寸全走百分比(遮罩即视口),不再引入 vw/vh/dvh - touch-action:none 手势归遮罩:滑动翻页(位移阈值判定)、屏蔽双击缩放/长按菜单/下拉刷新 - 手机断点:箭头贴边缩小、关闭钮/页码走 safe-area、图片全宽无圆角 - Transition 加 :duration 兜底,防 transitionend 不触发时透明遮罩残留吃点击
This commit is contained in:
@@ -2,41 +2,70 @@
|
||||
import { onBeforeUnmount, watch } from 'vue'
|
||||
import { lightboxState as st, closeLightbox, stepLightbox } from '../lightbox'
|
||||
|
||||
// 图片预览层:App.vue 全局挂一次。铺满全屏的暗底 + 居中大图,
|
||||
// 多图时左右箭头 / 方向键切换,角标显示第几张;Esc / 点空白收起。
|
||||
// 图片预览层(手机重做版):App.vue 全局挂一次。
|
||||
//
|
||||
// 手机上出过两次事故,这次的实现原则:
|
||||
// 1. 不碰 body 的 overflow——之前用 inline overflow 锁背景,把样式表的
|
||||
// overflow-x: clip 覆盖成 hidden,横向从「裁剪」变「可滚动容器」,
|
||||
// 整页被拖宽。现在滚动封锁靠遮罩自己的 @wheel/@touchmove.prevent:
|
||||
// 事件不落到页面里,背景自然滚不动,滚动位置也天然保留。
|
||||
// 2. 图片尺寸全部用百分比(max-width/height: 100%),不引入 vw/vh/dvh——
|
||||
// 这些单位在全面屏 URL 栏收展时各有各的坑,遮罩本身就是视口大小。
|
||||
// 3. 手势全归遮罩:touch-action: none + touchend 位移判滑动翻页,
|
||||
// 双击缩放/长按菜单/下拉刷新这些浏览器手势都不会来捣乱。
|
||||
// 4. 桌面能力不变:Esc / 方向键 / 点空白关闭 / 悬停箭头。
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === 'Escape') closeLightbox()
|
||||
if (e.key === 'ArrowLeft') stepLightbox(-1)
|
||||
if (e.key === 'ArrowRight') stepLightbox(1)
|
||||
}
|
||||
|
||||
// 吸顶栏带 backdrop-filter,真机上其合成层会盖在遮罩之上
|
||||
// (z-index 更高也拦不住)——开预览时整个藏掉
|
||||
watch(
|
||||
() => st.open,
|
||||
(v) => {
|
||||
if (v) {
|
||||
window.addEventListener('keydown', onKey)
|
||||
// 吸顶栏带 backdrop-filter,真机上其合成层会盖在遮罩之上
|
||||
// (遮罩 z-index 更高也拦不住)——开预览时整个藏掉
|
||||
document.body.classList.add('lb-open')
|
||||
} else {
|
||||
window.removeEventListener('keydown', onKey)
|
||||
document.body.classList.remove('lb-open')
|
||||
}
|
||||
if (v) window.addEventListener('keydown', onKey)
|
||||
else window.removeEventListener('keydown', onKey)
|
||||
document.body.classList.toggle('lb-open', v)
|
||||
}
|
||||
)
|
||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onKey)
|
||||
document.body.classList.remove('lb-open')
|
||||
})
|
||||
|
||||
// 滑动翻页:横向位移超 44px 且明显大于纵向才算一次滑动
|
||||
let touchX = 0
|
||||
let touchY = 0
|
||||
function onTouchStart(e) {
|
||||
touchX = e.touches[0].clientX
|
||||
touchY = e.touches[0].clientY
|
||||
}
|
||||
function onTouchEnd(e) {
|
||||
if (st.list.length < 2) return
|
||||
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) {
|
||||
stepLightbox(dx < 0 ? 1 : -1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="lb">
|
||||
<div v-if="st.open" class="lb-overlay" @click="closeLightbox">
|
||||
<img
|
||||
class="lb-img"
|
||||
:src="st.list[st.index]"
|
||||
alt=""
|
||||
@click.stop
|
||||
/>
|
||||
<Transition name="lb" :duration="220">
|
||||
<div
|
||||
v-if="st.open"
|
||||
class="lb-overlay"
|
||||
@click="closeLightbox"
|
||||
@touchstart.passive="onTouchStart"
|
||||
@touchend.passive="onTouchEnd"
|
||||
@wheel.prevent
|
||||
@touchmove.prevent
|
||||
>
|
||||
<img class="lb-img" :src="st.list[st.index]" alt="" @click.stop />
|
||||
<button v-if="st.list.length > 1" class="lb-nav prev" type="button" aria-label="上一张" @click.stop="stepLightbox(-1)">‹</button>
|
||||
<button v-if="st.list.length > 1" class="lb-nav next" type="button" aria-label="下一张" @click.stop="stepLightbox(1)">›</button>
|
||||
<button class="lb-close" type="button" aria-label="关闭预览" @click="closeLightbox">×</button>
|
||||
@@ -51,19 +80,23 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 130;
|
||||
background: rgba(12, 11, 10, 0.88);
|
||||
background: rgba(12, 11, 10, 0.92);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* 遮罩全权处理手势:杜绝双击缩放/长按菜单/下拉刷新来添乱 */
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* 百分比尺寸:遮罩即视口,图片永远不会超出屏幕半像素 */
|
||||
.lb-img {
|
||||
max-width: min(1200px, 92vw);
|
||||
max-height: 88vh;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.6);
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.lb-nav {
|
||||
@@ -130,29 +163,36 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
/* 手机:滑动翻页为主,箭头贴边缩小;位置全走安全区 */
|
||||
@media (pointer: coarse) {
|
||||
.lb-img {
|
||||
max-width: 96vw;
|
||||
max-height: 74vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.lb-nav {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 22px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.lb-nav.prev {
|
||||
left: 10px;
|
||||
left: calc(6px + env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
.lb-nav.next {
|
||||
right: 10px;
|
||||
right: calc(6px + env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
.lb-close {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
top: calc(10px + env(safe-area-inset-top));
|
||||
right: calc(10px + env(safe-area-inset-right));
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.lb-count {
|
||||
bottom: calc(14px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user