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;

View File

@@ -1,7 +1,8 @@
<template>
<view :style="componentStyle" data-component-name="diy-image-nav">
<scroll-view :class="['image-nav', value.showStyle == 'fixed' ? 'fixed-layout' : value.showStyle]"
:scroll-x="value.showStyle == 'singleSlide'">
:scroll-x="value.showStyle == 'singleSlide'"
ref="scrollContainer">
<view class="uni-scroll-view-content">
<view v-for="(item, index) in value.list" :key="index" :class="['image-nav-item', value.mode]"
style="margin-right: 28rpx;">
@@ -54,12 +55,33 @@ export default {
return {
pageWidth: '',
indicatorDots: false,
swiperCurrent: 0
swiperCurrent: 0,
// H5 鼠标拖拽状态(singleSlide 模式专用)
slideIsDragging: false,
slideStartX: 0,
slideStartScrollLeft: 0
}
},
created() {
// 组件创建时的逻辑
},
mounted() {
// H5 端:为 singleSlide 模式的 scroll-view 手动绑定 mouse/wheel 事件
// 原因:模板里写 @mousedown/@wheel 在小程序/鸿蒙端会产生"未知事件"警告,
// 所以 H5 专属事件全部在 mounted 中通过 addEventListener 绑定。
// $nextTick 是为了确保 ref 在 DOM 中已就绪(滚动容器总是存在的,无 v-if)。
// #ifdef H5
var self = this;
this.$nextTick(function() {
self.bindSlideMouseEvents();
});
// #endif
},
beforeDestroy() {
// #ifdef H5
this.unbindSlideMouseEvents();
// #endif
},
watch: {
componentRefresh(newValue) {
// 监听组件刷新
@@ -115,6 +137,96 @@ export default {
// 轮播切换
swiperChange(event) {
this.swiperCurrent = event.detail.current;
},
// ========== H5 singleSlide 鼠标拖拽支持 ==========
// 背景:uni-app 的 scroll-view 在 H5 端被编译为 div,虽然 overflow-x: auto 可让
// 浏览器渲染横向滚动条,但 scrollLeft 不响应鼠标拖拽(uni 的滚动逻辑不监听 mouse)。
// 因此 H5 下需要手动绑定 mousedown/mousemove/mouseup,模拟拖拽改变 scrollLeft。
// 同时绑定 wheel,让鼠标滚轮也能横向滚动(浏览器默认滚轮是垂直,需 preventDefault 后手动改 scrollLeft)。
// 仅在 singleSlide 模式下生效;fixed / pageSlide 走原逻辑。
//
// 关键设计:H5 专属事件全部在 mounted 中通过 addEventListener 绑定,而不是写在模板上,
// 避免小程序/鸿蒙端产生"未知事件"编译警告。所有端共用同一份模板。
bindSlideMouseEvents() {
// #ifdef H5
// 防止重复绑定
if (this._slideEventsBound) return;
var el = this.getSlideContainer();
if (!el) return;
// 容器上绑定 mousedown/wheel;mouseup/mousemove 挂在 window,避免鼠标移出容器后丢失
this._slideEl = el;
this._slideEventsBound = true;
el.addEventListener('mousedown', this.onSlideMouseDown);
el.addEventListener('wheel', this.onSlideWheel);
window.addEventListener('mousemove', this.onSlideMouseMove);
window.addEventListener('mouseup', this.onSlideMouseUp);
// #endif
},
unbindSlideMouseEvents() {
// #ifdef H5
if (this._slideEl) {
this._slideEl.removeEventListener('mousedown', this.onSlideMouseDown);
this._slideEl.removeEventListener('wheel', this.onSlideWheel);
this._slideEl = null;
}
window.removeEventListener('mousemove', this.onSlideMouseMove);
window.removeEventListener('mouseup', this.onSlideMouseUp);
this._slideEventsBound = false;
// #endif
},
getSlideContainer() {
// #ifdef H5
var ref = this.$refs.scrollContainer;
if (!ref) return null;
// uni-app 中 $refs 对应组件实例,scroll-view 编译为 div 后,可通过 $el 拿到原生 DOM
var el = ref.$el || ref;
// 兼容:部分版本直接返回 DOM 节点
if (el && el.nodeType === 1) return el;
return null;
// #endif
},
onSlideMouseDown(e) {
// #ifdef H5
if (this.value.showStyle != 'singleSlide') return;
var el = this.getSlideContainer();
if (!el) return;
this.slideIsDragging = true;
this.slideStartX = (e.clientX != null) ? e.clientX : 0;
this.slideStartScrollLeft = el.scrollLeft;
if (e.preventDefault) e.preventDefault();
// #endif
},
onSlideMouseMove(e) {
// #ifdef H5
if (!this.slideIsDragging) return;
var el = this.getSlideContainer();
if (!el) return;
var x = (e.clientX != null) ? e.clientX : 0;
var deltaX = x - this.slideStartX;
// 反向:手指/鼠标往右拖 → 内容向右滚 → scrollLeft 减小
el.scrollLeft = this.slideStartScrollLeft - deltaX;
if (e.preventDefault) e.preventDefault();
// #endif
},
onSlideMouseUp() {
// #ifdef H5
this.slideIsDragging = false;
// #endif
},
onSlideWheel(e) {
// #ifdef H5
if (this.value.showStyle != 'singleSlide') return;
var el = this.getSlideContainer();
if (!el) return;
// 浏览器 wheel 事件的 deltaY 是垂直方向;此处把垂直滚轮映射到横向滚动
var dx = e.deltaX || 0;
var dy = e.deltaY || 0;
// 优先用 deltaX(触控板横向 / Shift+滚轮),否则用 deltaY(普通滚轮 → 转横向)
var step = dx !== 0 ? dx : dy;
el.scrollLeft += step;
if (e.preventDefault) e.preventDefault();
// #endif
}
}
}
@@ -140,6 +252,25 @@ export default {
.image-nav-item {
flex-shrink: 0;
}
// ========== H5 singleSlide 鼠标拖拽支持:横向滚动 ==========
// uni-app 在 H5 端会把 scroll-view 编译为 div,但默认样式不一定是
// overflow-x:auto → 横向内容溢出后无法滚动。需要显式声明。
// #ifdef H5
overflow-x: auto;
overflow-y: hidden;
// 隐藏滚动条,视觉上仍保持单页布局;用户用鼠标拖拽/滚轮即可滚动
scrollbar-width: none; // Firefox
-ms-overflow-style: none; // IE/Edge legacy
&::-webkit-scrollbar {
display: none;
}
// 禁止鼠标选中文字(拖拽时选中会显得很别扭)
user-select: none;
-webkit-user-select: none;
// 拖拽时关闭过渡,避免 Chrome 的惯性滚动与 JS 抢控制权
-webkit-overflow-scrolling: touch;
// #endif
}
&.pageSlide {