feat(diy-article): add step-by-step marquee scroll mode

新增逐条步进式跑马灯滚动模式,适配后端配置的stepPause参数,实现单条内容间暂停的滚动效果,优化了触摸控制逻辑与定时器清理逻辑,修复了Vue2代理问题。
This commit is contained in:
2026-06-15 16:12:19 +08:00
parent d01b0d46e4
commit 7b83636c99

View File

@@ -131,13 +131,26 @@ export default {
// marqueePaused: true 时给外层加 is-marquee-paused class → animation-play-state: paused
// marqueeResumeTimer: touchend 后延迟恢复
marqueePaused: false,
marqueeResumeTimer: null
marqueeResumeTimer: null,
// ========== 步进模式字段(stepPause=true 时启用)==========
// 驱动 transform: translateY 在 A+A 双份内容的 track 上做 item-by-item 滚动
// 注意: 自定义字段不以 _ 开头,避免 Vue 2 不代理到 this 上导致 "is not a function"
stepIndex: 0,
stepTimer: null,
stepTranslateY: 0,
isStepSnapping: false
};
},
created() {
this.getList();
},
beforeDestroy() {
// 直接清理所有定时器,避免在某些场景(如 HMR)下调用以 _ 开头方法时出现
// "is not a function" 的问题
if (this.stepTimer) {
clearTimeout(this.stepTimer);
this.stepTimer = null;
}
if (this.marqueeResumeTimer) {
clearTimeout(this.marqueeResumeTimer);
this.marqueeResumeTimer = null;
@@ -147,6 +160,25 @@ export default {
// 组件刷新监听
componentRefresh() {
this.getList();
},
// list 加载完后,根据当前模式启动滚动
'list.length'(newLen) {
if (newLen > 1 && this.useScroll && this.marqueeMode === 'step') {
var self = this;
this.$nextTick(function() {
self.startStepMarquee();
});
}
},
// 模式切换(后端修改 stepPause 后):停止旧的步进定时器,按需重启动
marqueeMode(newMode) {
this.stopStepMarquee();
if (newMode === 'step' && this.list && this.list.length > 1) {
var self = this;
this.$nextTick(function() {
self.startStepMarquee();
});
}
}
},
computed: {
@@ -306,10 +338,12 @@ export default {
// ========== 滚动配置(兼容老数据 swiperConfig;现在用 CSS 跑马灯实现)==========
// scrollMode: 'none' 关闭滚动,显示普通列表;其他值均走 CSS 跑马灯
swiperConfig() {
return this.value.swiperConfig || {};
const v = this.value || {};
return v.swiperConfig || {};
},
scrollMode() {
return this.value.scrollMode || 'vertical';
const v = this.value || {};
return v.scrollMode || 'vertical';
},
// 是否使用滚动模式
useScroll() {
@@ -341,9 +375,35 @@ export default {
if (!isNaN(explicit) && explicit >= 0) return explicit;
return 1;
},
// 内层 .marquee-track 的 inline style(动画时长 + 起点延迟)
// 其余动画属性走 CSS: name / timing-function / iteration-count
// ========== 步进模式配置(与后端 design.js nestedDefaults 对齐)==========
// stepPause=true 时,一条一条滚动,每条之间停 pauseDuration 毫秒;
// 每条滚动动画耗时 stepAnimDuration 毫秒。
marqueeMode() {
return this.stepPauseEnabled ? 'step' : 'continuous';
},
stepPauseEnabled() {
return !!this.swiperConfig.stepPause;
},
pauseDuration() {
var d = parseInt(this.swiperConfig.pauseDuration);
return d > 0 ? d : 1500;
},
stepAnimDuration() {
var d = parseInt(this.swiperConfig.stepAnimDuration);
return d > 0 ? d : 500;
},
// 内层 .marquee-track 的 inline style
// 连贯模式:走 CSS @keyframes diy-article-marquee-up,inline 只指定 duration/delay
// 步进模式:禁用 CSS 动画,用 JS 控制 transform: translateY + transition 实现逐条滚动
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')
});
}
// 连贯模式:走 CSS @keyframes
return this.$util.objToStyle({
'animation-duration': this.marqueeDuration + 's',
'animation-delay': '-' + this.marqueeDelay + 's'
@@ -377,9 +437,10 @@ export default {
return sum + (this.list.length - 1) * gap;
},
// item 索引 i 的累计顶部偏移(rpx)= sum(items[0..i-1].height) + i × gap
// 步进模式用:把 scrollTop 推到 _itemOffset(i) 即「item i 顶部贴窗口顶部」
// loopedList[i+N] 位置 = firstListHeight + _itemOffset(i),即「第一份末尾之后接续」
_itemOffsetArr() {
// 步进模式用:把 scrollTop 推到 itemOffset(i) 即「item i 顶部贴窗口顶部」
// loopedList[i+N] 位置 = firstListHeight + itemOffset(i),即「第一份末尾之后接续」
// 注意: computed 名不以 _ 开头,避免 Vue 2 不代理到 this 上
itemOffsetArr() {
if (!this.list || this.list.length === 0) return [];
var gap = this._num(this.value.itemGap, 16);
var arr = [];
@@ -591,22 +652,97 @@ export default {
cover_img: ''
});
},
// ========== 跑马灯触摸控制(CSS animation-play-state)==========
// 用户触摸 → animation-play-state: paused(给外层加 is-marquee-paused class)
// ========== 跑马灯触摸控制(连贯模式:CSS animation-play-state; 步进模式:清除/恢复 JS 定时器)==========
// 用户触摸 → 暂停滚动
onMarqueeTouchStart() {
this.marqueePaused = true;
if (this.marqueeMode === 'step') {
this.stopStepMarquee();
}
if (this.marqueeResumeTimer) {
clearTimeout(this.marqueeResumeTimer);
this.marqueeResumeTimer = null;
}
},
// 用户松手 → 2s 后继续滚动(给用户阅读时间; 动画会从暂停位置继续,不会跳变)
// 用户松手 → 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 逐条滚动 ==========
// 启动:从 item 0 开始,先 pause pauseDuration ms 再滚到 item 1
// 注意: 方法名不以 _ 开头,避免 Vue 2 不代理到 this 上
startStepMarquee() {
this.stopStepMarquee();
if (!this.list || this.list.length <= 1) return;
this.stepIndex = 0;
this.stepTranslateY = 0;
this.isStepSnapping = false;
var self = this;
this.stepTimer = setTimeout(function() {
self.stepAdvance();
}, this.pauseDuration);
},
// 停止:清除所有 setTimeout,避免组件销毁/暂停后仍然推进
stopStepMarquee() {
if (this.stepTimer) {
clearTimeout(this.stepTimer);
this.stepTimer = null;
}
},
// 推进到下一条:
// - 普通下一条:动画滚到 itemOffsetArr[i+1],停 pauseDuration ms,再推进
// - 最后一条:先动画滚到「第二份 item 0」= translateY(-firstListHeight),
// 动画结束后瞬时跳回 translateY(0)(无过渡,视觉上无感),进入 pause → 再从 item 1 继续
stepAdvance() {
var offsets = this.itemOffsetArr;
var n = offsets.length;
if (n <= 1) return;
var firstH = this.firstListHeight;
this.stepIndex++;
var self = this;
if (this.stepIndex >= n) {
// 最后一条的下一条:先到第二份的 item 0,再跳回
this.stepIndex = 0;
this.isStepSnapping = false;
this.stepTranslateY = firstH;
// stepAnimDuration 后动画完成,瞬时跳回第一份 item 0
this.stepTimer = setTimeout(function() {
self.isStepSnapping = true;
self.stepTranslateY = 0;
// 等一个 tick 让 transition:none 生效,再进入 pause
self.stepTimer = setTimeout(function() {
self.isStepSnapping = false;
// pauseDuration 后再滚到 item 1
self.stepTimer = setTimeout(function() {
self.stepAdvance();
}, self.pauseDuration);
}, 50);
}, this.stepAnimDuration);
} else {
// 正常推进:滚到 itemOffsetArr[stepIndex]
this.isStepSnapping = false;
this.stepTranslateY = offsets[this.stepIndex];
// 动画完 → pause → 再推进
this.stepTimer = setTimeout(function() {
self.stepTimer = setTimeout(function() {
self.stepAdvance();
}, self.pauseDuration);
}, this.stepAnimDuration);
}
}
}
};