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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user