feat(diy-article): add step-by-step marquee scroll mode
新增逐条步进式跑马灯滚动模式,适配后端配置的stepPause参数,实现单条内容间暂停的滚动效果,优化了触摸控制逻辑与定时器清理逻辑,修复了Vue2代理问题。
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user