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