diff --git a/components-diy/diy-article.vue b/components-diy/diy-article.vue index 49b3dba..e856fac 100644 --- a/components-diy/diy-article.vue +++ b/components-diy/diy-article.vue @@ -3,16 +3,15 @@ - + 1 && this.useScroll && this.marqueeMode === 'step') { + if (newLen > 1 && this.useScroll) { var self = this; this.$nextTick(function() { - self.startStepMarquee(); + if (self.marqueeMode === 'step') { + self.startStepMarquee(); + } else { + self.startContinuousMarquee(true); + } }); } }, - // 模式切换(后端修改 stepPause 后):停止旧的步进定时器,按需重启动 + // 模式切换(后端修改 stepPause 后):停止旧的动画,按需重启动 marqueeMode(newMode) { - this.stopStepMarquee(); - if (newMode === 'step' && this.list && this.list.length > 1) { - var self = this; - this.$nextTick(function() { + this.stopAllMarquee(); + if (!this.useScroll || !this.list || this.list.length <= 1) return; + var self = this; + this.$nextTick(function() { + if (newMode === 'step') { self.startStepMarquee(); - }); - } + } else { + self.startContinuousMarquee(true); + } + }); } }, computed: { @@ -393,20 +399,19 @@ export default { return d > 0 ? d : 500; }, // 内层 .marquee-track 的 inline style - // 连贯模式:走 CSS @keyframes diy-article-marquee-up,inline 只指定 duration/delay - // 步进模式:禁用 CSS 动画,用 JS 控制 transform: translateY + transition 实现逐条滚动 + // 统一用 JS 控制 transform: translateY + transition;不再使用 CSS @keyframes, + // 以便用户触摸拖拽时能即时改变滚动位置,并从当前位置恢复自动滚动。 marqueeTrackStyle() { - if (this.marqueeMode === 'step') { - return this.$util.objToStyle({ - 'animation': 'none', - 'transform': 'translateY(-' + this._rpx(this.stepTranslateY) + ')', - 'transition': this.isStepSnapping ? 'none' : ('transform ' + this.stepAnimDuration + 'ms ease-in-out') - }); + var translateY = this.marqueeMode === 'step' ? this.stepTranslateY : this.continuousTranslateY; + // 步进自动滚动时启用 transition 产生动画;拖拽中/snapping 时禁用,保证即时响应/无跳变 + var transition = 'none'; + if (this.marqueeMode === 'step' && !this.isUserDragging && !this.isStepSnapping) { + transition = 'transform ' + this.stepAnimDuration + 'ms ease-in-out'; } - // 连贯模式:走 CSS @keyframes return this.$util.objToStyle({ - 'animation-duration': this.marqueeDuration + 's', - 'animation-delay': '-' + this.marqueeDelay + 's' + 'animation': 'none', + 'transform': 'translateY(-' + this._rpx(translateY) + ')', + 'transition': transition }); }, // list 中最高 item 的自然高度(动态) — swiperHeight + warpWrapStyle CSS 变量都用它 @@ -652,38 +657,136 @@ export default { cover_img: '' }); }, - // ========== 跑马灯触摸控制(连贯模式:CSS animation-play-state; 步进模式:清除/恢复 JS 定时器)========== - // 用户触摸 → 暂停滚动 - onMarqueeTouchStart() { - this.marqueePaused = true; - if (this.marqueeMode === 'step') { - this.stopStepMarquee(); + // ========== 跑马灯触摸控制(连续/步进模式统一:暂停动画,允许用户拖拽手动滚动)========== + // 用户触摸 → 停止自动滚动,记录拖拽起始位置(先不标记为拖拽,避免点击被误识别) + onMarqueeTouchStart(e) { + this.stopAllMarquee(); + this.isUserDragging = false; + this.dragStartY = (e.touches && e.touches[0]) ? e.touches[0].clientY : 0; + this.dragStartTranslateY = this.marqueeMode === 'step' ? this.stepTranslateY : this.continuousTranslateY; + }, + // 用户拖拽 → 当移动距离超过阈值后标记为拖拽,并根据手指位移即时改变 translateY + onMarqueeTouchMove(e) { + var y = (e.touches && e.touches[0]) ? e.touches[0].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; + } + }, + // 用户松手 → 若只是点击则立即恢复,否则 2s 后从当前位置恢复自动滚动 + onMarqueeTouchEnd() { + 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); + }, + // 从当前位置恢复自动滚动(由 touchend/点击触发) + resumeAutoScroll() { + if (!this.useScroll) return; + if (this.marqueeMode === 'step') { + this.resumeStepMarquee(); + } else { + this.startContinuousMarquee(false); + } + }, + // 停止所有自动滚动(连续+步进)及恢复定时器 + stopAllMarquee() { + this.stopContinuousMarquee(); + this.stopStepMarquee(); if (this.marqueeResumeTimer) { clearTimeout(this.marqueeResumeTimer); this.marqueeResumeTimer = null; } }, - // 用户松手 → 2s 后继续滚动(动画会从暂停位置继续,不会跳变) - onMarqueeTouchEnd() { - if (this.marqueeResumeTimer) clearTimeout(this.marqueeResumeTimer); - var self = this; - this.marqueeResumeTimer = setTimeout(function() { - self.marqueePaused = false; - if (self.marqueeMode === 'step') { - // 步进模式:从当前 item 的 pause 阶段继续 - self.stepTimer = setTimeout(function() { - self.stepAdvance(); - }, self.pauseDuration); - } - }, 2000); + // ========== 连续滚动模式:JS 控制 translateY 平滑滚动 ========== + // 启动:根据 marqueeDelay 初始化偏移后开始逐帧滚动 + // resetOffset=true 时从头计算初始偏移(首次启动/模式切换),否则从当前 continuousTranslateY 继续 + startContinuousMarquee(resetOffset) { + this.stopContinuousMarquee(); + if (!this.list || this.list.length <= 1 || this.isUserDragging) return; + var firstH = this.firstListHeight; + if (firstH > 0 && resetOffset) { + // 根据 marqueeDelay 计算初始偏移,与旧 CSS animation-delay 对齐 + var initialOffset = (this.marqueeDelay / this.marqueeDuration) * firstH; + this.continuousTranslateY = initialOffset % firstH; + } + this.continuousLastTime = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now(); + this.continuousRAF = this._requestAnimationFrame(this.continuousFrame); + }, + stopContinuousMarquee() { + if (this.continuousRAF) { + this._cancelAnimationFrame(this.continuousRAF); + this.continuousRAF = null; + } + }, + continuousFrame() { + if (!this.useScroll || this.isUserDragging || this.marqueeMode !== 'continuous') return; + var now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now(); + var delta = now - this.continuousLastTime; + this.continuousLastTime = now; + var firstH = this.firstListHeight; + if (firstH <= 0) { + this.continuousRAF = this._requestAnimationFrame(this.continuousFrame); + return; + } + // 速度: firstListHeight / marqueeDuration (rpx/s),根据时间增量推进 + var speed = firstH / this.marqueeDuration; + this.continuousTranslateY += speed * delta / 1000; + // 无缝循环:滚完第一份 A 后瞬时跳回 0,由于 A+A 内容相同,视觉上无跳变 + if (this.continuousTranslateY >= firstH) { + this.continuousTranslateY = 0; + } + this.continuousRAF = this._requestAnimationFrame(this.continuousFrame); + }, + // 跨端 requestAnimationFrame 兼容:H5 直接用 window,小程序/App 用 uni;极端情况回退 setTimeout + _requestAnimationFrame(callback) { + if (typeof uni !== 'undefined' && uni.requestAnimationFrame) { + return uni.requestAnimationFrame(callback); + } + // #ifdef H5 + if (typeof window !== 'undefined' && window.requestAnimationFrame) { + return window.requestAnimationFrame(callback); + } + // #endif + return setTimeout(callback, 16); + }, + _cancelAnimationFrame(id) { + if (typeof uni !== 'undefined' && uni.cancelAnimationFrame) { + return uni.cancelAnimationFrame(id); + } + // #ifdef H5 + if (typeof window !== 'undefined' && window.cancelAnimationFrame) { + return window.cancelAnimationFrame(id); + } + // #endif + return clearTimeout(id); }, // ========== 步进模式:JS 控制 translateY 逐条滚动 ========== // 启动:从 item 0 开始,先 pause pauseDuration ms 再滚到 item 1 // 注意: 方法名不以 _ 开头,避免 Vue 2 不代理到 this 上 startStepMarquee() { this.stopStepMarquee(); - if (!this.list || this.list.length <= 1) return; + if (!this.list || this.list.length <= 1 || this.isUserDragging) return; this.stepIndex = 0; this.stepTranslateY = 0; this.isStepSnapping = false; @@ -699,6 +802,33 @@ export default { this.stepTimer = null; } }, + // 用户拖拽结束后,从当前位置吸附到最近的 item,然后继续步进 + resumeStepMarquee() { + var offsets = this.itemOffsetArr; + var n = offsets.length; + if (n <= 1) return; + var firstH = this.firstListHeight; + var y = this.stepTranslateY; + // 归一化到 [0, firstH) + y = ((y % firstH) + firstH) % firstH; + // 找到最接近的 offset + var nearestIndex = 0; + var minDiff = Infinity; + for (var i = 0; i < n; i++) { + var diff = Math.abs(offsets[i] - y); + if (diff < minDiff) { + minDiff = diff; + nearestIndex = i; + } + } + this.stepIndex = nearestIndex; + this.stepTranslateY = offsets[nearestIndex]; + this.isStepSnapping = false; + var self = this; + this.stepTimer = setTimeout(function() { + self.stepAdvance(); + }, this.pauseDuration); + }, // 推进到下一条: // - 普通下一条:动画滚到 itemOffsetArr[i+1],停 pauseDuration ms,再推进 // - 最后一条:先动画滚到「第二份 item 0」= translateY(-firstListHeight), @@ -801,14 +931,6 @@ export default { .marquee-track { will-change: transform; - animation-name: diy-article-marquee-up; - animation-timing-function: linear; - animation-iteration-count: infinite; - } - - // 触摸暂停(见 script 中 onMarqueeTouchStart 加 is-marquee-paused 状态) - &.is-marquee-paused .marquee-track { - animation-play-state: paused; } // 关键:loopedList 把 list 复制了一份,最后一条 .item(2N-1)也会被 :last-of-type 命中, @@ -822,18 +944,6 @@ export default { } } - // 关键:marquee-track 内容 = list + list (两份),高度 = 2 × 单份高度 - // translateY(-50%) = 平移一份高度,窗口中显示的内容与起点完全一致 → 循环无跳变 - // 若写 -100%:两份内容都滚走,窗口变空,是错误的。 - @keyframes diy-article-marquee-up { - 0% { - transform: translate3d(0, 0, 0); - } - 100% { - transform: translate3d(0, -50%, 0); - } - } - .item { margin-bottom: var(--article-item-gap, 16rpx); /* 背景 / 圆角通过 CSS 变量从 .article-wrap 继承(由 warpWrapStyle 注入) */