From 7a76824fcf3513f70a1d6a97c3f69fce2027a66f Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Sat, 11 Jul 2026 10:03:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(diy-article,=20diy-image-nav):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DH5=E7=AB=AF=E6=8B=96=E6=8B=BD=E5=90=8E=E8=AF=AF?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E8=B7=B3=E8=BD=AC=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重构两个组件的H5端鼠标事件绑定逻辑,改用document级别capture监听解决uni-app自定义元素事件拦截问题 2. 添加拖拽阈值检测,避免点击误触发滚动 3. 新增click拦截逻辑,拖拽结束后自动屏蔽后续一次click事件防止误跳转 4. 优化滚动容器获取方式,兼容uni-app编译后的嵌套DOM结构 --- components-diy/diy-article.vue | 91 +++++++++++++++++++++---- components-diy/diy-image-nav.vue | 112 +++++++++++++++++++++++++------ 2 files changed, 167 insertions(+), 36 deletions(-) diff --git a/components-diy/diy-article.vue b/components-diy/diy-article.vue index 1f3b32f..94256f8 100644 --- a/components-diy/diy-article.vue +++ b/components-diy/diy-article.vue @@ -10,7 +10,6 @@ @@ -130,9 +129,15 @@ export default { // ========== 跑马灯状态(JS 驱动 transform,同时支持用户手动拖拽)========== // marqueeResumeTimer: touchend 后延迟恢复自动滚动 marqueeResumeTimer: null, - // 用户拖拽状态 + // user 拖拽状态 isUserDragging: false, dragStartY: 0, + // H5 端:标记 mousedown 已成功触发(mousemove 在没有这个标记时忽略) + _mouseDownActive: false, + // H5 端:标记 mousedown 后是否真的拖拽超过 5px 阈值 + // 拖拽结束后用来判断 mouseup 后是否需要屏蔽 click(防止 uni-app @tap.stop 误触发跳转) + _mouseDidDrag: false, + _suppressNextClick: false, dragStartTranslateY: 0, // ========== 连续滚动模式字段(marqueeMode === 'continuous')========== // 驱动 transform: translateY 在 A+A 双份内容的 track 上连续滚动 @@ -737,14 +742,31 @@ export default { // #ifdef H5 // 防止重复绑定(mounted + list.length watcher 都会调用) if (this._marqueeEventsBound) return; + // 关键:list.length 变化触发 watcher 时,vue 已把 list 改了但 v-if 还没渲染到 DOM, + // $nextTick 也只保证本组件渲染完毕,但 v-if 子树内的 DOM 还在等父级渲染; + // 经验上需要再等一段时间(测试 1000ms 后能找到,vue 异步 + skeleton 内部)才能 querySelector。 + var self = this; + setTimeout(function() { + self._doBindMarqueeMouseEvents(); + }, 1000); + // #endif + }, + _doBindMarqueeMouseEvents() { + // #ifdef H5 + if (this._marqueeEventsBound) return; var el = this._getMarqueeEl(); if (!el) return; this._marqueeEl = el; this._marqueeEventsBound = true; - el.addEventListener('mousedown', this.onMarqueeMouseDown); - el.addEventListener('wheel', this.onMarqueeWheel); + // 与 diy-image-nav 同款经验:uni-app 的自定义元素(uni-view)在 H5 下 + // 也会拦截/重定向 mousedown 到 HTML,导致绑到 .auto-scroll-list 上的监听器收不到。 + // 解决:document 级别 capture + 用 clientX/clientY 判断是否落在容器内。 + document.addEventListener('mousedown', this.onMarqueeMouseDown, true); + document.addEventListener('wheel', this.onMarqueeWheel, { passive: false, capture: true }); window.addEventListener('mousemove', this.onMarqueeMouseMove); window.addEventListener('mouseup', this.onMarqueeMouseUp); + // click 拦截:拖拽后屏蔽接下来的 click(防止 uni-app @tap.stop 误触发跳转) + document.addEventListener('click', this.onMarqueeMaybeClick, true); // 全局禁用 selectstart,防止拖拽时选中文字 this._suppressSelect = function(ev) { if (ev && ev.preventDefault) ev.preventDefault(); @@ -754,25 +776,26 @@ export default { }, unbindMarqueeMouseEvents() { // #ifdef H5 - if (this._marqueeEl) { - this._marqueeEl.removeEventListener('mousedown', this.onMarqueeMouseDown); - this._marqueeEl.removeEventListener('wheel', this.onMarqueeWheel); - this._marqueeEl = null; - } + document.removeEventListener('mousedown', this.onMarqueeMouseDown, true); + document.removeEventListener('wheel', this.onMarqueeWheel, true); window.removeEventListener('mousemove', this.onMarqueeMouseMove); window.removeEventListener('mouseup', this.onMarqueeMouseUp); + document.removeEventListener('click', this.onMarqueeMaybeClick, true); if (this._suppressSelect) { document.removeEventListener('selectstart', this._suppressSelect); this._suppressSelect = null; } + this._marqueeEl = null; this._marqueeEventsBound = false; // #endif }, _getMarqueeEl() { // #ifdef H5 - var ref = this.$refs.scrollContainer; - if (!ref) return null; - var el = ref.$el || ref; + // .auto-scroll-list 是 v-if 渲染的,Vue $refs 不收集。 + // x-skeleton 是 web component,querySelector 不能穿透其 shadow DOM, + // 但 .auto-scroll-list 直接在 document 下可被查找到 — 而本组件只有这一个 .auto-scroll-list, + // 不会与其他组件冲突。 + var el = document.querySelector('.auto-scroll-list'); if (el && el.nodeType === 1) return el; return null; // #endif @@ -780,21 +803,35 @@ export default { onMarqueeMouseDown(e) { // #ifdef H5 if (!this.useScroll) return; + // document 级别监听,用 bounding box 判断是否落在容器内 + if (!this._marqueeEl) return; + var rect = this._marqueeEl.getBoundingClientRect(); + if (!rect) return; + var x = e.clientX, y = e.clientY; + if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return; this.stopAllMarquee(); - this.isUserDragging = false; - this.dragStartY = (e.clientY != null) ? e.clientY : 0; + this._mouseDownActive = true; // 标记 mousedown 成功触发,mousemove 用此开关 + this._mouseDidDrag = false; // mousedown 时清零,5px 阈值后才置 true + this._suppressNextClick = false; + this.isUserDragging = false; // 5px 阈值后才进入真正拖动状态 + this.dragStartY = y; this.dragStartTranslateY = this.marqueeMode === 'step' ? this.stepTranslateY : this.continuousTranslateY; if (e.preventDefault) e.preventDefault(); + if (e.stopPropagation) e.stopPropagation(); // #endif }, onMarqueeMouseMove(e) { // #ifdef H5 + // 关键:必须在 mousedown 成功触发后才响应。 + // 否则:hover 状态下的鼠标随意移动,deltaY 累积超过 5px 阈值后会误触发滚动。 + if (!this._mouseDownActive) return; var y = (e.clientY != null) ? e.clientY : 0; var deltaY = y - this.dragStartY; if (!this.isUserDragging) { // 5px 阈值:只有真正滑动时才进入手动滚动,避免点击触发 if (Math.abs(deltaY) < 5) return; this.isUserDragging = true; + this._mouseDidDrag = true; } var newTranslateY = this.dragStartTranslateY - deltaY; var firstH = this.firstListHeight; @@ -816,6 +853,11 @@ export default { document.removeEventListener('selectstart', this._suppressSelect); this._suppressSelect = null; } + this._mouseDownActive = false; + // 如果确实拖拽过,标记接下来一次 click 应当被拦截(防止拖拽结束后误触发跳转) + if (this._mouseDidDrag) { + this._suppressNextClick = true; + } // 复用 touchend 的松手逻辑:纯点击立即恢复,拖拽后 2s 恢复 if (this.marqueeResumeTimer) clearTimeout(this.marqueeResumeTimer); if (!this.isUserDragging) { @@ -829,11 +871,32 @@ export default { }, 2000); // #endif }, + // click 拦截器:如果 mouseup 设置了 _suppressNextClick,拦截这一次 click 并清除标记。 + // 即使 uni-app 把 @tap.stop 编译成 inline onclick,我们的 capture 阶段也能拦截到。 + onMarqueeMaybeClick(e) { + // #ifdef H5 + if (!this._suppressNextClick) return; + if (!e.target) return; + var el = this._marqueeEl; + if (!el || !el.contains(e.target)) return; + this._suppressNextClick = false; + if (e.preventDefault) e.preventDefault(); + if (e.stopPropagation) e.stopPropagation(); + if (e.stopImmediatePropagation) e.stopImmediatePropagation(); + // #endif + }, // 鼠标滚轮支持:H5 用户在 PC 浏览器滚动鼠标滚轮时,把 deltaY 转译为 translateY 推动 onMarqueeWheel(e) { // #ifdef H5 if (!this.useScroll) return; + // document 级别监听,用 bounding box 判断 + if (!this._marqueeEl) return; + var rect = this._marqueeEl.getBoundingClientRect(); + if (!rect) return; + var x = e.clientX, y = e.clientY; + if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return; if (e.preventDefault) e.preventDefault(); + if (e.stopPropagation) e.stopPropagation(); var dy = (e.deltaY != null) ? e.deltaY : 0; if (dy === 0) return; // 滚轮向下 → 内容向上滚 → translateY 增加 diff --git a/components-diy/diy-image-nav.vue b/components-diy/diy-image-nav.vue index acf407a..e36e4d8 100644 --- a/components-diy/diy-image-nav.vue +++ b/components-diy/diy-image-nav.vue @@ -1,8 +1,7 @@