feat(components-diy): 为两个组件添加H5端鼠标拖拽滚动支持

1.  给diy-image-nav组件新增singleSlide模式下H5端鼠标拖拽和滚轮横向滚动功能,修复uni-app scroll-view在H5端不支持鼠标拖拽的问题,同时添加对应样式优化
2.  给diy-article组件的跑马灯列表新增H5端鼠标拖拽和滚轮滚动支持,复用原有触摸事件逻辑适配鼠标操作,优化拖拽交互体验
3.  统一采用mounted绑定H5专属事件的方式,避免小程序/鸿蒙端出现未知事件编译警告
This commit is contained in:
2026-07-11 08:26:14 +08:00
parent 203c47e437
commit 75bc8876b5
2 changed files with 310 additions and 6 deletions

View File

@@ -10,6 +10,7 @@
<view v-if="list.length > 0 && useScroll"
class="auto-scroll-list"
:style="autoScrollListStyle"
ref="scrollContainer"
@touchstart="onMarqueeTouchStart"
@touchmove.stop.prevent="onMarqueeTouchMove"
@touchend="onMarqueeTouchEnd">
@@ -150,11 +151,28 @@ export default {
created() {
this.getList();
},
beforeDestroy() {
// 直接清理所有定时器/动画帧,避免在某些场景(如 HMR)下调用以 _ 开头方法时出现
// "is not a function" 的问题
this.stopAllMarquee();
mounted() {
// H5 端:为 .auto-scroll-list 容器手动绑定 mousedown/wheel 事件
// 原因:模板上写 @mousedown/@wheel 在小程序/鸿蒙端会产生"未知事件"编译警告。
// 所有端公用同一份模板,H5 专属事件通过 mounted 中的 addEventListener 绑定。
// 注意:.auto-scroll-list 是 v-if 渲染的(list 加载完后才出现),
// 实际绑定在 'list.length' watcher 的 nextTick 中。这里调用一次
// 只是为了兼容首屏 mounted 时 list 已有数据的场景(HMR / SSR 缓存)。
// #ifdef H5
var self = this;
this.$nextTick(function() {
self.bindMarqueeMouseEvents();
});
// #endif
},
beforeDestroy() {
// 直接清理所有定时器/动画帧,避免在某些场景(如 HMR)下调用以 _ 开头方法时出现
// "is not a function" 的问题
this.stopAllMarquee();
// #ifdef H5
this.unbindMarqueeMouseEvents();
// #endif
},
watch: {
// 组件刷新监听
componentRefresh() {
@@ -170,6 +188,11 @@ export default {
} else {
self.startContinuousMarquee(true);
}
// H5 端:.auto-scroll-list 是 v-if 渲染的,首屏 mounted 时可能 list 还是空,
// 所以要在 list 加载完后才真正绑定鼠标事件(避免 refs 拿不到)
// #ifdef H5
self.bindMarqueeMouseEvents();
// #endif
});
}
},
@@ -700,6 +723,146 @@ export default {
self.resumeAutoScroll();
}, 2000);
},
// ========== H5 鼠标拖拽支持 ==========
// 背景:H5 环境下 touchstart/touchmove/touchend 不会触发(浏览器鼠标用户用 mouse);
// 因此需要补 mousedown/mousemove/mouseup + wheel 处理。
// 策略:复用 onMarqueeTouchStart/Move/End 的核心逻辑,把 e.clientY 当作手指 Y 坐标处理。
// 关键:① mousemove/mouseup 在 mounted 时挂到 window,避免鼠标移出容器后丢失事件;
// ② 全局禁用 selectstart,防止拖拽时选中文字;
// ③ wheel 事件按 deltaY 反向推动 translateY。
//
// 设计:H5 专属事件全部在 mounted 中通过 addEventListener 绑定,而不是写在模板上,
// 避免小程序/鸿蒙端产生"未知事件"编译警告。所有端共用同一份模板。
bindMarqueeMouseEvents() {
// #ifdef H5
// 防止重复绑定(mounted + list.length watcher 都会调用)
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);
window.addEventListener('mousemove', this.onMarqueeMouseMove);
window.addEventListener('mouseup', this.onMarqueeMouseUp);
// 全局禁用 selectstart,防止拖拽时选中文字
this._suppressSelect = function(ev) {
if (ev && ev.preventDefault) ev.preventDefault();
};
document.addEventListener('selectstart', this._suppressSelect);
// #endif
},
unbindMarqueeMouseEvents() {
// #ifdef H5
if (this._marqueeEl) {
this._marqueeEl.removeEventListener('mousedown', this.onMarqueeMouseDown);
this._marqueeEl.removeEventListener('wheel', this.onMarqueeWheel);
this._marqueeEl = null;
}
window.removeEventListener('mousemove', this.onMarqueeMouseMove);
window.removeEventListener('mouseup', this.onMarqueeMouseUp);
if (this._suppressSelect) {
document.removeEventListener('selectstart', this._suppressSelect);
this._suppressSelect = null;
}
this._marqueeEventsBound = false;
// #endif
},
_getMarqueeEl() {
// #ifdef H5
var ref = this.$refs.scrollContainer;
if (!ref) return null;
var el = ref.$el || ref;
if (el && el.nodeType === 1) return el;
return null;
// #endif
},
onMarqueeMouseDown(e) {
// #ifdef H5
if (!this.useScroll) return;
this.stopAllMarquee();
this.isUserDragging = false;
this.dragStartY = (e.clientY != null) ? e.clientY : 0;
this.dragStartTranslateY = this.marqueeMode === 'step' ? this.stepTranslateY : this.continuousTranslateY;
if (e.preventDefault) e.preventDefault();
// #endif
},
onMarqueeMouseMove(e) {
// #ifdef H5
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;
}
var newTranslateY = this.dragStartTranslateY - deltaY;
var firstH = this.firstListHeight;
if (firstH > 0) {
// 限制在 [0, firstListHeight) 循环区间内
newTranslateY = ((newTranslateY % firstH) + firstH) % firstH;
}
if (this.marqueeMode === 'step') {
this.stepTranslateY = newTranslateY;
} else {
this.continuousTranslateY = newTranslateY;
}
// #endif
},
onMarqueeMouseUp() {
// #ifdef H5
// 解除 selectstart 抑制(mousemove/mouseup 由 unbindMarqueeMouseEvents 统一管理,无需此处解绑)
if (this._suppressSelect) {
document.removeEventListener('selectstart', this._suppressSelect);
this._suppressSelect = null;
}
// 复用 touchend 的松手逻辑:纯点击立即恢复,拖拽后 2s 恢复
if (this.marqueeResumeTimer) clearTimeout(this.marqueeResumeTimer);
if (!this.isUserDragging) {
this.resumeAutoScroll();
return;
}
this.isUserDragging = false;
var self = this;
this.marqueeResumeTimer = setTimeout(function() {
self.resumeAutoScroll();
}, 2000);
// #endif
},
// 鼠标滚轮支持:H5 用户在 PC 浏览器滚动鼠标滚轮时,把 deltaY 转译为 translateY 推动
onMarqueeWheel(e) {
// #ifdef H5
if (!this.useScroll) return;
if (e.preventDefault) e.preventDefault();
var dy = (e.deltaY != null) ? e.deltaY : 0;
if (dy === 0) return;
// 滚轮向下 → 内容向上滚 → translateY 增加
var newTranslateY;
if (this.marqueeMode === 'step') {
newTranslateY = this.stepTranslateY + dy;
} else {
newTranslateY = this.continuousTranslateY + dy;
}
var firstH = this.firstListHeight;
if (firstH > 0) {
// 限制在 [0, firstListHeight) 循环区间内
newTranslateY = ((newTranslateY % firstH) + firstH) % firstH;
}
if (this.marqueeMode === 'step') {
this.stepTranslateY = newTranslateY;
} else {
this.continuousTranslateY = newTranslateY;
}
// 滚轮滚动时停止自动滚动,模拟手指拖拽行为
this.stopAllMarquee();
// 2s 后从当前位置恢复
var self = this;
if (this.marqueeResumeTimer) clearTimeout(this.marqueeResumeTimer);
this.marqueeResumeTimer = setTimeout(function() {
self.resumeAutoScroll();
}, 2000);
// #endif
},
// 从当前位置恢复自动滚动(由 touchend/点击触发)
resumeAutoScroll() {
if (!this.useScroll) return;
@@ -928,6 +1091,16 @@ export default {
width: 100%;
overflow: hidden;
position: relative;
// ========== H5 鼠标拖拽支持 ==========
// 禁用鼠标选中文字(避免拖拽时高亮一片文字),并保留 cursor 提示可拖拽。
// #ifdef H5
user-select: none;
-webkit-user-select: none;
cursor: grab;
&:active {
cursor: grabbing;
}
// #endif
.marquee-track {
will-change: transform;