重写手机图片预览(不参考旧实现):
- 真凶补刀: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 { onBeforeUnmount, watch } from 'vue'
|
||||||
import { lightboxState as st, closeLightbox, stepLightbox } from '../lightbox'
|
import { lightboxState as st, closeLightbox, stepLightbox } from '../lightbox'
|
||||||
|
|
||||||
// 图片预览层:App.vue 全局挂一次。铺满全屏的暗底 + 居中大图,
|
// 图片预览层(手机重做版):App.vue 全局挂一次。
|
||||||
// 多图时左右箭头 / 方向键切换,角标显示第几张;Esc / 点空白收起。
|
//
|
||||||
|
// 手机上出过两次事故,这次的实现原则:
|
||||||
|
// 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) {
|
function onKey(e) {
|
||||||
if (e.key === 'Escape') closeLightbox()
|
if (e.key === 'Escape') closeLightbox()
|
||||||
if (e.key === 'ArrowLeft') stepLightbox(-1)
|
if (e.key === 'ArrowLeft') stepLightbox(-1)
|
||||||
if (e.key === 'ArrowRight') stepLightbox(1)
|
if (e.key === 'ArrowRight') stepLightbox(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 吸顶栏带 backdrop-filter,真机上其合成层会盖在遮罩之上
|
||||||
|
// (z-index 更高也拦不住)——开预览时整个藏掉
|
||||||
watch(
|
watch(
|
||||||
() => st.open,
|
() => st.open,
|
||||||
(v) => {
|
(v) => {
|
||||||
if (v) {
|
if (v) window.addEventListener('keydown', onKey)
|
||||||
window.addEventListener('keydown', onKey)
|
else window.removeEventListener('keydown', onKey)
|
||||||
// 吸顶栏带 backdrop-filter,真机上其合成层会盖在遮罩之上
|
document.body.classList.toggle('lb-open', v)
|
||||||
// (遮罩 z-index 更高也拦不住)——开预览时整个藏掉
|
|
||||||
document.body.classList.add('lb-open')
|
|
||||||
} else {
|
|
||||||
window.removeEventListener('keydown', onKey)
|
|
||||||
document.body.classList.remove('lb-open')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="lb">
|
<Transition name="lb" :duration="220">
|
||||||
<div v-if="st.open" class="lb-overlay" @click="closeLightbox">
|
<div
|
||||||
<img
|
v-if="st.open"
|
||||||
class="lb-img"
|
class="lb-overlay"
|
||||||
:src="st.list[st.index]"
|
@click="closeLightbox"
|
||||||
alt=""
|
@touchstart.passive="onTouchStart"
|
||||||
@click.stop
|
@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 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 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>
|
<button class="lb-close" type="button" aria-label="关闭预览" @click="closeLightbox">×</button>
|
||||||
@@ -51,19 +80,23 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 130;
|
z-index: 130;
|
||||||
background: rgba(12, 11, 10, 0.88);
|
background: rgba(12, 11, 10, 0.92);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
/* 遮罩全权处理手势:杜绝双击缩放/长按菜单/下拉刷新来添乱 */
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 百分比尺寸:遮罩即视口,图片永远不会超出屏幕半像素 */
|
||||||
.lb-img {
|
.lb-img {
|
||||||
max-width: min(1200px, 92vw);
|
max-width: 100%;
|
||||||
max-height: 88vh;
|
max-height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.6);
|
box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.6);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lb-nav {
|
.lb-nav {
|
||||||
@@ -130,29 +163,36 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
/* 手机:滑动翻页为主,箭头贴边缩小;位置全走安全区 */
|
||||||
|
@media (pointer: coarse) {
|
||||||
.lb-img {
|
.lb-img {
|
||||||
max-width: 96vw;
|
border-radius: 0;
|
||||||
max-height: 74vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lb-nav {
|
.lb-nav {
|
||||||
width: 38px;
|
width: 40px;
|
||||||
height: 38px;
|
height: 40px;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lb-nav.prev {
|
.lb-nav.prev {
|
||||||
left: 10px;
|
left: calc(6px + env(safe-area-inset-left));
|
||||||
}
|
}
|
||||||
|
|
||||||
.lb-nav.next {
|
.lb-nav.next {
|
||||||
right: 10px;
|
right: calc(6px + env(safe-area-inset-right));
|
||||||
}
|
}
|
||||||
|
|
||||||
.lb-close {
|
.lb-close {
|
||||||
top: 10px;
|
top: calc(10px + env(safe-area-inset-top));
|
||||||
right: 10px;
|
right: calc(10px + env(safe-area-inset-right));
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lb-count {
|
||||||
|
bottom: calc(14px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { reactive } from 'vue'
|
|||||||
// 全站通用的图片预览(lightbox)。任何地方的配图都可以
|
// 全站通用的图片预览(lightbox)。任何地方的配图都可以
|
||||||
// openLightbox(list, index) 打开:list 是同组图片的 URL 数组,index 是
|
// openLightbox(list, index) 打开:list 是同组图片的 URL 数组,index 是
|
||||||
// 点击的那张。Esc / 点空白 / 关闭钮收起,左右键或箭头在同组图片间切换。
|
// 点击的那张。Esc / 点空白 / 关闭钮收起,左右键或箭头在同组图片间切换。
|
||||||
// 打开时锁 body 滚动(和菜单 sheet 同一套做法)。
|
//
|
||||||
|
// 注意:这里绝不能碰 body 的 inline overflow——样式表的 overflow-x: clip
|
||||||
|
// 会被整个 overflow: hidden 覆盖,横向从「裁剪」变「可滚动容器」,
|
||||||
|
// 手机上整页能被拖宽(真实事故)。背景滚动由 ImgLightbox 遮罩自己的
|
||||||
|
// @wheel/@touchmove.prevent 拦截,滚动位置天然保留。
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
list: [],
|
list: [],
|
||||||
index: 0,
|
index: 0,
|
||||||
@@ -17,12 +21,10 @@ export function openLightbox(list, index = 0) {
|
|||||||
state.list = list
|
state.list = list
|
||||||
state.index = Math.max(0, Math.min(index, list.length - 1))
|
state.index = Math.max(0, Math.min(index, list.length - 1))
|
||||||
state.open = true
|
state.open = true
|
||||||
document.body.style.overflow = 'hidden'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeLightbox() {
|
export function closeLightbox() {
|
||||||
state.open = false
|
state.open = false
|
||||||
document.body.style.overflow = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stepLightbox(delta) {
|
export function stepLightbox(delta) {
|
||||||
|
|||||||
Reference in New Issue
Block a user