From 408564269522acda731c7592244841cf2b52a0b0 Mon Sep 17 00:00:00 2001 From: mole404 <248403041+mole404@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:49:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(viewer):=20=E5=88=87=E5=9B=BE=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E8=B5=B7=E7=82=B9=E5=9B=BA=E5=AE=9A=E4=B8=BA=E5=B1=8F?= =?UTF-8?q?=E5=B9=95=E4=B8=AD=E5=BF=83=EF=BC=88=E5=BC=BA=E5=88=B6=E5=9B=9E?= =?UTF-8?q?=E6=B5=81=EF=BC=89=EF=BC=8C=E5=B9=B6=E5=88=A0=E6=8E=89=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=E7=89=88=E6=97=A0=E6=95=88=E7=9A=84=E6=91=98=E7=B1=BB?= =?UTF-8?q?=E8=A1=A5=E4=B8=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 老师反馈上一版没修好(附真机截图)。按 Claude 的静态分析复核后确认,根因是 view() 的时序竞态: · view() 每次都新建一个 :先写成"屏幕中心的点"(宽高 0 + margin 居中),读自然尺寸后 再加 viewer-transition 写最终尺寸,靠 CSS 过渡补间。 · CSS 过渡的起点 = 浏览器"上一次真正渲染过的样式"。第 1 步默认样式是"满宽贴顶",第 2 步是 "中心点",第 3 步是最终值。哪一帧被渲染过决定动画从哪开始:第 2、3 步之间有帧 ⇒ 从中心 长出来;只渲染到第 1 步 ⇒ 从屏幕上方飞入;一帧都没渲染 ⇒ 无动画直接出现。取决于图片是否 缓存、网络快慢、主线程是否空闲 —— 这就是"没规律"的来源。 · 上一版在 pointerup 里 viewer.image.classList.remove('viewer-transition') 无效:view() 新建 的是另一个 ,且 load() 会按 options.transition 给新图重新加类。已删除该段及错误注释 (原注释"从上一张图 margin 滑过来"也不成立)。 修法(Claude 方案 A):包一层 viewer.initImage,在它读自然尺寸之前强制回流一次 (void viewer.image.offsetWidth),把"中心点"提交为已渲染样式 ⇒ 起点恒为屏幕中心。 覆盖手指滑动、缩略图条、键盘、底部按钮等所有切图路径(都在 view() 内)。 本机实测(延迟图片响应 + 限速,模拟"未缓存的大图",即 Claude 给的复现法): 切换瞬间 top 与稳定后 top 的差值:A 组 0px / 0px,C 组重跑 0px / 0px ✓ 对照组(剥掉补丁)未取得有效数据(生成对照版本时脚本把页面切坏),"无补丁会飞"留待真机确认。 本机 9 套 jsdom 测试全绿。 --- Dockerfile | 4 ++-- patches/ios-longpress.patch | 2 +- src/resources/views/user/images.blade.php | 25 ++++++++++++++++------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1eb71b4..5d2c7d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -165,7 +165,7 @@ RUN php -r "file_exists('.env') || copy('.env.example', '.env');" \ RUN printf '%s\n' \ 'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./public/js/context-js/context-js.js' \ 'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./resources/js/context-js.js' \ - '69e2e947c24af2795bee2e6323723145 ./resources/views/user/images.blade.php' \ + '15878ee87c65d46137249dde8e40931f ./resources/views/user/images.blade.php' \ | md5sum -c - \ && grep -q "assetVersion('js/context-js/context-js.js')" ./resources/views/user/images.blade.php \ && grep -q 'isIOSWebKit' ./public/js/context-js/context-js.js \ @@ -208,7 +208,7 @@ RUN APP_SRC_MD5=$(find app config routes -type f -print0 2>/dev/null | sort -z | "fork_sha=${FORK_SHA}" \ "lsky_commit=${LSKY_COMMIT}" \ "context_js_md5=c0e513ec8e93fd81b34b3c6de5cf5eb8" \ - "images_blade_md5=69e2e947c24af2795bee2e6323723145" \ + "images_blade_md5=15878ee87c65d46137249dde8e40931f" \ "app_src_md5=${APP_SRC_MD5}" \ "app_js_md5=${APP_JS_MD5}" \ "app_css_md5=${APP_CSS_MD5}" \ diff --git a/patches/ios-longpress.patch b/patches/ios-longpress.patch index 604ff46..32158a2 100644 --- a/patches/ios-longpress.patch +++ b/patches/ios-longpress.patch @@ -52,7 +52,7 @@ # 三个文件各自的 md5(与 Dockerfile「自证 2」里的期望值一致): # c0e513ec8e93fd81b34b3c6de5cf5eb8 src/resources/js/context-js.js # c0e513ec8e93fd81b34b3c6de5cf5eb8 src/public/js/context-js/context-js.js -# 69e2e947c24af2795bee2e6323723145 src/resources/views/user/images.blade.php +# 15878ee87c65d46137249dde8e40931f src/resources/views/user/images.blade.php # # 上游原文件 md5(改动前,供比对): # c8e57f6232848ca8277341ddf1a3a7a6 resources/js/context-js.js diff --git a/src/resources/views/user/images.blade.php b/src/resources/views/user/images.blade.php index e70156b..bca9ad6 100644 --- a/src/resources/views/user/images.blade.php +++ b/src/resources/views/user/images.blade.php @@ -644,7 +644,24 @@ class="relative inline-flex h-4 w-7 shrink-0 items-center rounded-full bg-brand document.addEventListener('touchcancel', () => { navDown = false; }, true); })(); - /* 触摸端切图判定(接管 slideOnTouch 后的配套逻辑): + /* 切图动画起点固定为"屏幕中心"(Claude 方案 A)。 + * 库每切一张都新建 :先写成屏幕中心的点(宽高 0 + margin 居中),读完自然尺寸 + * 再加 viewer-transition 写最终宽高与 margin,靠 CSS 过渡补间。CSS 过渡的起点是 + * 浏览器"上一次真正渲染过的样式" —— 中间那帧有没有渲染出来取决于图片缓存/网速/ + * 主线程忙不忙,于是表现为"有时从中间长出来、有时从屏幕上方飞进来"。 + * 这里在 initImage 之前强制回流一次,把"中心点"提交为已渲染样式 ⇒ 起点永远是中心。 + * 覆盖手指滑动、缩略图条、键盘与底部按钮等所有切图路径(都在 view() 里)。 + * 注:早先那版在 pointerup 里摘旧图的 viewer-transition 是无效的 —— view() 新建的是 + * 另一个 ,且 load() 会按 options.transition 给新图重新加类。 */ + if (viewer && typeof viewer.initImage === 'function') { + const rawInitImage = viewer.initImage.bind(viewer); + viewer.initImage = function (cb) { + if (viewer.image) void viewer.image.offsetWidth; // 强制回流,提交"中心点" + return rawInitImage(cb); + }; + } + + /* 触摸端切图判定(接管 slideOnTouch 后的配套逻辑): * 库里 slideOnTouch:false ⇒ 单指动作永远是 'move',库自己的平移就是跟手拖动, * 不再出现 'switch' 动作 ⇒ 1px 判据与"闩锁"都不存在。切图还是回弹由松手时决定。 * 为什么不拦事件:库用 HAS_POINTER_EVENT ? 'pointermove' : 'touchmove' 选通道, @@ -680,12 +697,6 @@ class="relative inline-flex h-4 w-7 shrink-0 items-center rounded-full bg-brand const far = Math.abs(dx) > Math.max(FAR_MIN, window.innerWidth * FAR_RATIO); const fast = Math.abs(dx) / dt > FAST_SPEED && Math.abs(dx) > 30; if ((far || fast) && Math.abs(dx) > Math.abs(dy) * 1.5) { - /* 切图不要"飞入":库的过渡是"从上一张图的 margin 滑到新图的 margin", - * 两张图高度不同(横图↔竖图)时,新图就会从偏上/偏下的位置滑进来。 - * 这一段临时摘掉过渡类 ⇒ 新图直接出现在它该在的位置(= "从中间出来")。 - * 过渡类是库自己在管的(action 为 move/zoom 时它自己也会摘),不属于 - * 那个碰不得的 action 状态机,且同一帧即由 renderImage 恢复。 */ - viewer.image.classList.remove('viewer-transition'); if (dx < 0) viewer.next(false); else viewer.prev(false); guardClick = Date.now(); // 标记:刚刚切过图 } else if (dx !== 0 || dy !== 0) {