refactor(diy-article): 替换scroll-view滚动为CSS跑马灯实现
移除原scroll-view自动滚动逻辑,改用CSS animation实现无缝跑马灯效果,解决跨端滚动跳变问题,移除JS定时器驱动逻辑,提升动画流畅度和跨端一致性,优化触摸暂停恢复逻辑。
This commit is contained in:
@@ -3,21 +3,18 @@
|
|||||||
<view class="article-wrap" :style="warpWrapStyle">
|
<view class="article-wrap" :style="warpWrapStyle">
|
||||||
<!-- 注: :class 用内联数组,与 ns-newform 对齐;computed listClass/itemClass 已移除(避免数组 toString 用逗号 join 的问题) -->
|
<!-- 注: :class 用内联数组,与 ns-newform 对齐;computed listClass/itemClass 已移除(避免数组 toString 用逗号 join 的问题) -->
|
||||||
<view :class="['list-wrap', value.style || 'style-1']">
|
<view :class="['list-wrap', value.style || 'style-1']">
|
||||||
<!-- 自动垂直滚动(scroll-view 实现,默认开启,兼容老数据:无 scrollMode/swiperConfig 字段也走这里)
|
<!-- 自动垂直滚动(CSS animation 跑马灯实现,默认开启)
|
||||||
设计:item 自然高度 + 固定 itemGap,scrollTop 累加 + loopedList 复制实现无缝循环
|
设计:loopedList 把 list 复制成 A+A 两份;外层容器固定高度(窗口)overflow:hidden,
|
||||||
vs 旧 swiper 方案:旧方案用 min-height 强制等高 → 短 item 下方空一大块,不符合"紧凑"诉求
|
内层容器用 @keyframes translateY 从 0 → -第一份高度,动画循环无缝接续。
|
||||||
新方案:item 完全用内容撑起,每条 item 后固定 itemGap 空白,视觉间距天然恒等 -->
|
优势:① 无 JS setInterval 驱动 scrollTop,避免 uni-app scroll-view 在 H5/MP 下单位不一致
|
||||||
<scroll-view v-if="list.length > 0 && useScroll" class="auto-scroll-list"
|
导致"滚到底后跳变"问题;② 浏览器原生动画,60fps 平滑,跨端一致;
|
||||||
|
③ 触摸(touchstart)时 animation-play-state=paused,touchend 2s 后恢复。 -->
|
||||||
|
<view v-if="list.length > 0 && useScroll"
|
||||||
|
:class="['auto-scroll-list', { 'is-marquee-paused': marqueePaused }]"
|
||||||
:style="autoScrollListStyle"
|
:style="autoScrollListStyle"
|
||||||
scroll-y="true"
|
@touchstart="onMarqueeTouchStart"
|
||||||
:scroll-top="scrollTop"
|
@touchend="onMarqueeTouchEnd">
|
||||||
:scroll-with-animation="scrollWithAnimation"
|
<view class="marquee-track" :style="marqueeTrackStyle">
|
||||||
:show-scrollbar="false"
|
|
||||||
:enhanced="false"
|
|
||||||
:bounces="false"
|
|
||||||
:throttle="false"
|
|
||||||
@touchstart="onAutoScrollTouchStart"
|
|
||||||
@touchend="onAutoScrollTouchEnd">
|
|
||||||
<view v-for="(item, index) in loopedList" :key="index"
|
<view v-for="(item, index) in loopedList" :key="index"
|
||||||
:class="['item', value.ornament && value.ornament.type, !item.cover_img && value.style !== 'style-4' ? 'no-image' : '']"
|
:class="['item', value.ornament && value.ornament.type, !item.cover_img && value.style !== 'style-4' ? 'no-image' : '']"
|
||||||
:style="itemStyle"
|
:style="itemStyle"
|
||||||
@@ -54,7 +51,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</view>
|
||||||
|
</view>
|
||||||
<!-- 非滚动模式(scrollMode==='none')时的普通列表 -->
|
<!-- 非滚动模式(scrollMode==='none')时的普通列表 -->
|
||||||
<template v-if="!useScroll">
|
<template v-if="!useScroll">
|
||||||
<view v-for="(item, index) in list" :key="index" :class="['item', value.ornament && value.ornament.type, !item.cover_img && value.style !== 'style-4' ? 'no-image' : '']" :style="itemStyle"
|
<view v-for="(item, index) in list" :key="index" :class="['item', value.ornament && value.ornament.type, !item.cover_img && value.style !== 'style-4' ? 'no-image' : '']" :style="itemStyle"
|
||||||
@@ -129,45 +127,26 @@ export default {
|
|||||||
headHeight: '160rpx',
|
headHeight: '160rpx',
|
||||||
textRows: 2
|
textRows: 2
|
||||||
},
|
},
|
||||||
// ========== 滚动模式(scroll-view 自动滚动)状态 ==========
|
// ========== 跑马灯状态(CSS animation 驱动,替代 scroll-view + scrollTop)==========
|
||||||
// scrollTop 累加驱动滚动;loopedList 把 list 复制一份实现无缝循环
|
// marqueePaused: true 时给外层加 is-marquee-paused class → animation-play-state: paused
|
||||||
scrollTop: 0,
|
// marqueeResumeTimer: touchend 后延迟恢复
|
||||||
autoScrollTimer: null, // setTimeout(16ms) id(60fps tick,跨端平滑,替代 setInterval 30ms)
|
marqueePaused: false,
|
||||||
autoScrollResumeTimer: null, // touchend 后延迟恢复
|
marqueeResumeTimer: null
|
||||||
_scrollAccumulator: 0, // sub-pixel 累计,每帧 +0.4,到 ≥1 才提交 scrollTop
|
|
||||||
// ========== 步进模式(scroll-step + pause)状态 ==========
|
|
||||||
// 滚动一下 + 停留一会:每次把 scrollTop 推到下一个 item 起点,停 pauseDuration ms,再推下一个
|
|
||||||
// 跳回 0(列表末尾 → 列表开头)时也走 stopPause + 立即 0,视觉无感
|
|
||||||
_stepCurrentIndex: 0, // 当前已显示到的 item 索引(0..N-1)
|
|
||||||
_stepPhase: 'idle', // 'idle' | 'scrolling' | 'pausing'
|
|
||||||
_stepResetting: false // true=正在跳回 0,scrollWithAnimation computed 翻成 false,本次 scrollTop=0 赋值无动画
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getList();
|
this.getList();
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
// 自动滚动由 watch 'list.length' 在 list 加载完后启动(mounted 时 list 仍为空)
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.stopAutoScroll();
|
if (this.marqueeResumeTimer) {
|
||||||
if (this.autoScrollResumeTimer) {
|
clearTimeout(this.marqueeResumeTimer);
|
||||||
clearTimeout(this.autoScrollResumeTimer);
|
this.marqueeResumeTimer = null;
|
||||||
this.autoScrollResumeTimer = null;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
// 组件刷新监听
|
// 组件刷新监听
|
||||||
componentRefresh() {
|
componentRefresh() {
|
||||||
this.getList();
|
this.getList();
|
||||||
},
|
|
||||||
// list 加载完后启动自动滚动(mounted 时 list 还是空,需监听 list 长度变化)
|
|
||||||
'list.length'(newLen) {
|
|
||||||
if (newLen > 1 && this.useScroll) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.startAutoScroll();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -324,9 +303,8 @@ export default {
|
|||||||
return Object.assign({ color: '#999ca7', iconSize: 24, fontSize: 24 },
|
return Object.assign({ color: '#999ca7', iconSize: 24, fontSize: 24 },
|
||||||
this.value.readWrapStyle || {});
|
this.value.readWrapStyle || {});
|
||||||
},
|
},
|
||||||
// ========== 滚动配置(同时兼容老数据 swiperConfig 和新字段 scrollMode) ==========
|
// ========== 滚动配置(兼容老数据 swiperConfig;现在用 CSS 跑马灯实现)==========
|
||||||
// 老数据 swiperConfig 字段:{ vertical, autoplay, circular, interval, duration, displayMultipleItems, swiperHeight }
|
// scrollMode: 'none' 关闭滚动,显示普通列表;其他值均走 CSS 跑马灯
|
||||||
// 新数据 scrollMode 字段:'none' | 'vertical'(默认 vertical,与老行为一致)
|
|
||||||
swiperConfig() {
|
swiperConfig() {
|
||||||
return this.value.swiperConfig || {};
|
return this.value.swiperConfig || {};
|
||||||
},
|
},
|
||||||
@@ -337,59 +315,39 @@ export default {
|
|||||||
useScroll() {
|
useScroll() {
|
||||||
return this.scrollMode !== 'none' && this.list.length > 1;
|
return this.scrollMode !== 'none' && this.list.length > 1;
|
||||||
},
|
},
|
||||||
// scroll-view 的 scroll-with-animation 绑定
|
// 安全的 display-multiple-items(避免窗口高度 > 内容高度时出现空白)
|
||||||
// 连贯模式: false(每帧 scrollTop += 1 同步生效,避免被 200ms 缓动打断造成频闪)
|
|
||||||
// 步进模式:
|
|
||||||
// - 推进中(scrolling 阶段,推 scrollTop 到下一个 item 起点): true(走 300-500ms 缓动,视觉是「滚一下」)
|
|
||||||
// - 跳回 0 那一瞬(从 list 末尾 → 第二份 item 0 / 直接跳到 0): false(无动画,避免「向前滚回」)
|
|
||||||
// - 暂停中(pausing 阶段): 当前 scrollTop 不动,binding 值无影响
|
|
||||||
// 实现:_stepResetting 标志位标识「正在跳回 0,本次 scrollTop 赋值不要动画」
|
|
||||||
scrollWithAnimation() {
|
|
||||||
if (!this.stepPauseEnabled) return false;
|
|
||||||
// 步进模式:正在跳回 0 时返回 false,否则 true
|
|
||||||
return !this._stepResetting;
|
|
||||||
},
|
|
||||||
// 安全的 display-multiple-items(确保不大于实际 list 长度,避免 swiper 报错)
|
|
||||||
safeDisplayMultipleItems() {
|
safeDisplayMultipleItems() {
|
||||||
var displayCount = this.swiperConfig.displayMultipleItems || 3;
|
var displayCount = this.swiperConfig.displayMultipleItems || 3;
|
||||||
var actualCount = this.list.length;
|
var actualCount = this.list.length;
|
||||||
return actualCount > 0 ? Math.min(displayCount, actualCount) : 1;
|
return actualCount > 0 ? Math.min(displayCount, actualCount) : 1;
|
||||||
},
|
},
|
||||||
// 是否启用「滚动一下 + 停留一会」步进模式
|
// 跑马灯动画时长(秒)
|
||||||
// 默认 false(走 sub-pixel 连贯滚动);true 时按 item 粒度滚动,每滚一个 item 停 pauseDuration ms 再滚下一个
|
// ① 若 swiperConfig.marqueeDuration 显式传,直接使用(秒,允许小数)
|
||||||
stepPauseEnabled() {
|
// ② 否则根据 list 长度估算:每 1 条 item 走 4 秒(慢速,阅读友好,≈ 15-30rpx/s)
|
||||||
return !!this.swiperConfig.stepPause;
|
// 为什么不用 _itemHeight 算: _itemHeight 是 rpx 估算值,H5 端换算成 vw 后再
|
||||||
|
// 与 CSS 动画的秒数匹配很麻烦;采用"按条数线性时长"更跨端一致,且阅读节奏可控。
|
||||||
|
marqueeDuration() {
|
||||||
|
var cfg = this.swiperConfig || {};
|
||||||
|
var explicit = parseFloat(cfg.marqueeDuration);
|
||||||
|
if (!isNaN(explicit) && explicit > 0) return explicit;
|
||||||
|
var n = (this.list && this.list.length) || 0;
|
||||||
|
// 每 1 条 4 秒;最少 8 秒(避免 2 条时飞快)
|
||||||
|
return Math.max(8, n * 4);
|
||||||
},
|
},
|
||||||
// 步进模式停留时长(ms),默认 1500
|
// 跑马灯动画起点延迟(默认 1 秒,给首条内容 1 秒阅读时间)
|
||||||
pauseDuration() {
|
marqueeDelay() {
|
||||||
var d = parseInt(this.swiperConfig.pauseDuration);
|
var cfg = this.swiperConfig || {};
|
||||||
return d > 0 ? d : 1500;
|
var explicit = parseFloat(cfg.marqueeDelay);
|
||||||
|
if (!isNaN(explicit) && explicit >= 0) return explicit;
|
||||||
|
return 1;
|
||||||
},
|
},
|
||||||
// 步进模式单次滚动动画时长(ms),默认 500(uni-app scroll-view 默认动画 300ms,留 buffer)
|
// 内层 .marquee-track 的 inline style(动画时长 + 起点延迟)
|
||||||
stepAnimDuration() {
|
// 其余动画属性走 CSS: name / timing-function / iteration-count
|
||||||
var d = parseInt(this.swiperConfig.stepAnimDuration);
|
marqueeTrackStyle() {
|
||||||
return d > 0 ? d : 500;
|
return this.$util.objToStyle({
|
||||||
},
|
'animation-duration': this.marqueeDuration + 's',
|
||||||
// swiper 高度(rpx)优先级:
|
'animation-delay': '-' + this.marqueeDelay + 's'
|
||||||
// 1. 老/新数据显式设置了 swiperConfig.swiperHeight(>0) → 配置值(用户强制覆盖)
|
});
|
||||||
// 2. 否则按 (maxItemHeight + itemGap) × N 计算(精确,不加分摊 buffer),其中:
|
|
||||||
// - maxItemHeight: list 中最高 item 的自然高度(动态,见 _itemHeight)
|
|
||||||
// - itemGap: 用户配置的列表项间距(0-60rpx,默认 16)
|
|
||||||
// - N: displayMultipleItems(每页行数)
|
|
||||||
// 每个 swiper-item slot 高度 = (maxItemHeight + itemGap),精确等于 perRow。
|
|
||||||
// 所有 item 通过 CSS var --article-swiper-item-min-height 强制 min-height = maxItemHeight,
|
|
||||||
// 保证每条 item 视觉高度一致;swiper-item-content 的 padding-bottom = itemGap 制造
|
|
||||||
// 固定的视觉间距,无论 item 实际内容多高,间距恒为 itemGap。
|
|
||||||
// 早期版本在末尾 +10,被 swiper 平均分摊到每个 slot(10/N),导致间距 = itemGap + 10/N,
|
|
||||||
// 且随 N 变化不可控。现已删除。
|
|
||||||
// 3. list 尚未加载时(初始空状态)走 _staticMaxItemHeight 兜底(全字段最坏情况)。
|
|
||||||
swiperHeight() {
|
|
||||||
// 1. 显式设置了 swiperHeight (>0) → 用户强制覆盖
|
|
||||||
var configHeight = parseInt(this.swiperConfig.swiperHeight) || 0;
|
|
||||||
if (configHeight > 0) return configHeight;
|
|
||||||
// 2. (maxItemHeight + itemGap) × N,精确等于 perRow × N,不分摊 buffer
|
|
||||||
var itemGap = this._num(this.value.itemGap, 16);
|
|
||||||
return (this.maxItemHeight + itemGap) * this.safeDisplayMultipleItems;
|
|
||||||
},
|
},
|
||||||
// list 中最高 item 的自然高度(动态) — swiperHeight + warpWrapStyle CSS 变量都用它
|
// list 中最高 item 的自然高度(动态) — swiperHeight + warpWrapStyle CSS 变量都用它
|
||||||
// list 为空时走 _staticMaxItemHeight 兜底
|
// list 为空时走 _staticMaxItemHeight 兜底
|
||||||
@@ -633,154 +591,21 @@ export default {
|
|||||||
cover_img: ''
|
cover_img: ''
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// ========== scroll-view 自动滚动控制(两种模式互不干扰) ==========
|
// ========== 跑马灯触摸控制(CSS animation-play-state)==========
|
||||||
// 模式 1: 连贯滚动(sub-pixel + 16ms tick) — 默认
|
// 用户触摸 → animation-play-state: paused(给外层加 is-marquee-paused class)
|
||||||
// 设计目标:消除微信小程序「频闪」 + 跨端统一平滑滚动
|
onMarqueeTouchStart() {
|
||||||
// 旧方案(频闪根因):setInterval(30ms) + scrollTop += 1 + scroll-with-animation="true"
|
this.marqueePaused = true;
|
||||||
// - 30ms ≈ 33fps,与屏幕 60fps 不同步,出现周期性跳帧
|
if (this.marqueeResumeTimer) {
|
||||||
// - scroll-with-animation 让每次 scrollTop 变化都跑 200ms 缓动,30ms 内动画未完成又被打断
|
clearTimeout(this.marqueeResumeTimer);
|
||||||
// - MP-WXSS scroll-view 渲染管线压力大,叠加动画 → 肉眼可见 1 像素级跳变
|
this.marqueeResumeTimer = null;
|
||||||
// 新方案:
|
|
||||||
// - setTimeout(16ms) tick(60fps):跨端一致(MP 无全局 rAF,setTimeout 跨端可用)
|
|
||||||
// - 每帧累加 0.4px(sub-pixel 抗锯齿),到 ≥ 1 才提交 scrollTop(避免每帧触发 vue 响应式)
|
|
||||||
// - scroll-with-animation="false":scrollTop 同步生效,不启动 200ms 缓动
|
|
||||||
// - 跳回 0 用 scrollTop 强制赋值,无动画 → 视觉无感
|
|
||||||
// - 速度: 0.4 × 60 = 24 px/秒,慢速平稳
|
|
||||||
//
|
|
||||||
// 模式 2: 步进滚动(滚一下 + 停留一会) — 后端 stepPause=true 时启用
|
|
||||||
// 语义:每滚一个 item 停 pauseDuration ms 再滚下一个,循环接续
|
|
||||||
// 状态机: idle → scrolling(scrollTop 推到下一个 item 起点,scroll-with-animation=true) →
|
|
||||||
// pausing(setTimeout(pauseDuration)) → scrolling(下一个) → ...
|
|
||||||
// 跳回 0(末尾 → 开头):scrolling phase 立即把 scrollTop 设为 0(scroll-with-animation 关),视觉无感
|
|
||||||
// 步进模式用 scroll-with-animation=true 是关键:每次推 scrollTop 是「滚一下」动画,不是直接跳
|
|
||||||
//
|
|
||||||
// 启动/暂停/恢复(两种模式共用):
|
|
||||||
// - 启动:startAutoScroll 根据 stepPauseEnabled 分发
|
|
||||||
// - 暂停:触摸时 stopAutoScroll,清除 resume timer
|
|
||||||
// - 恢复:触摸结束 2s 后再 startAutoScroll
|
|
||||||
// - 销毁:beforeDestroy 清 setTimeout
|
|
||||||
_scrollTick() {
|
|
||||||
// 模式 1 连贯滚动 tick
|
|
||||||
var STEP = 0.4;
|
|
||||||
this._scrollAccumulator += STEP;
|
|
||||||
if (this._scrollAccumulator >= 1) {
|
|
||||||
var inc = Math.floor(this._scrollAccumulator);
|
|
||||||
this._scrollAccumulator -= inc;
|
|
||||||
this.scrollTop += inc;
|
|
||||||
// 滚到第一份底部,跳回 0
|
|
||||||
if (this.firstListHeight > 0 && this.scrollTop >= this.firstListHeight) {
|
|
||||||
this.scrollTop = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 下一帧
|
|
||||||
var self = this;
|
|
||||||
this.autoScrollTimer = setTimeout(function() {
|
|
||||||
self._scrollTick();
|
|
||||||
}, 16);
|
|
||||||
},
|
|
||||||
// 模式 2 步进滚动:推 scrollTop 到下一个 item 起点,停 pauseDuration,再推下一个,循环接续
|
|
||||||
// 循环处理:list 末尾 → 跳回 0
|
|
||||||
// 翻 _stepResetting = true → scrollWithAnimation computed 翻成 false →
|
|
||||||
// $nextTick 等 scroll-view 接受新 prop → scrollTop = 0(无动画)→ 50ms 后翻回 → 继续推下一个
|
|
||||||
// 状态机: idle → scrolling(scrollTop 推到下一个 item 起点,scroll-with-animation=true 走缓动) →
|
|
||||||
// pausing(setTimeout(pauseDuration)) → scrolling(下一个) → ...
|
|
||||||
_stepAdvance() {
|
|
||||||
var offsets = this._itemOffsetArr;
|
|
||||||
if (offsets.length === 0) {
|
|
||||||
this._stepPhase = 'idle';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var n = offsets.length;
|
|
||||||
var nextIndex = this._stepCurrentIndex + 1;
|
|
||||||
var isLoop = false;
|
|
||||||
if (nextIndex >= n) {
|
|
||||||
// 末尾 → 跳回开头(无动画,scrollTop 设为 0)
|
|
||||||
nextIndex = 0;
|
|
||||||
isLoop = true;
|
|
||||||
}
|
|
||||||
this._stepCurrentIndex = nextIndex;
|
|
||||||
this._stepPhase = 'scrolling';
|
|
||||||
var self = this;
|
|
||||||
if (isLoop) {
|
|
||||||
// 翻 _stepResetting 让 scroll-with-animation=false,scrollTop 设为 0
|
|
||||||
this._stepResetting = true;
|
|
||||||
this.$nextTick(function() {
|
|
||||||
self.scrollTop = 0;
|
|
||||||
// 等 50ms 让 scroll-view 接受新位置,再翻回
|
|
||||||
self.autoScrollTimer = setTimeout(function() {
|
|
||||||
self._stepResetting = false;
|
|
||||||
self._stepPhase = 'scrolling';
|
|
||||||
self._stepAdvance();
|
|
||||||
}, 50);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 正常推进:scrollTop 推到下一个 item 起点(scroll-with-animation=true 走 300-500ms 缓动)
|
|
||||||
this.scrollTop = offsets[nextIndex];
|
|
||||||
// 等动画结束 → 进入 pausing
|
|
||||||
this.autoScrollTimer = setTimeout(function() {
|
|
||||||
self._stepPhase = 'pausing';
|
|
||||||
self.autoScrollTimer = setTimeout(function() {
|
|
||||||
self._stepPhase = 'scrolling';
|
|
||||||
self._stepAdvance();
|
|
||||||
}, self.pauseDuration);
|
|
||||||
}, this.stepAnimDuration);
|
|
||||||
},
|
|
||||||
startAutoScroll() {
|
|
||||||
if (this.autoScrollTimer) return;
|
|
||||||
if (!this.list || this.list.length <= 1) return;
|
|
||||||
if (this.stepPauseEnabled) {
|
|
||||||
// 模式 2 步进:从 item 0 开始,首次立刻滚到 item 1(scroll-with-animation=true 走缓动)
|
|
||||||
this._stepCurrentIndex = 0;
|
|
||||||
this._stepPhase = 'scrolling';
|
|
||||||
this.scrollTop = 0;
|
|
||||||
var self = this;
|
|
||||||
// 首次立即推进(不预先 pause,符合「滚一下 + 停留一会」语义)
|
|
||||||
this.autoScrollTimer = setTimeout(function() {
|
|
||||||
self._stepAdvance();
|
|
||||||
}, 16);
|
|
||||||
} else {
|
|
||||||
// 模式 1 连贯
|
|
||||||
this._scrollAccumulator = 0;
|
|
||||||
var self = this;
|
|
||||||
this.autoScrollTimer = setTimeout(function() {
|
|
||||||
self._scrollTick();
|
|
||||||
}, 16);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stopAutoScroll() {
|
// 用户松手 → 2s 后继续滚动(给用户阅读时间; 动画会从暂停位置继续,不会跳变)
|
||||||
if (this.autoScrollTimer) {
|
onMarqueeTouchEnd() {
|
||||||
clearTimeout(this.autoScrollTimer);
|
if (this.marqueeResumeTimer) clearTimeout(this.marqueeResumeTimer);
|
||||||
this.autoScrollTimer = null;
|
|
||||||
}
|
|
||||||
this._stepPhase = 'idle';
|
|
||||||
},
|
|
||||||
// 用户触摸 → 暂停自动滚动(touchend 2s 后由 onTouchEnd 恢复)
|
|
||||||
onAutoScrollTouchStart() {
|
|
||||||
this.stopAutoScroll();
|
|
||||||
if (this.autoScrollResumeTimer) {
|
|
||||||
clearTimeout(this.autoScrollResumeTimer);
|
|
||||||
this.autoScrollResumeTimer = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 用户松手 → 2s 后恢复自动滚动(给用户阅读时间)
|
|
||||||
// 步进模式:不重置 _stepCurrentIndex,从用户松手时所在 item 继续滚下一个
|
|
||||||
// 连贯模式:正常重启 _scrollTick
|
|
||||||
onAutoScrollTouchEnd() {
|
|
||||||
if (this.autoScrollResumeTimer) clearTimeout(this.autoScrollResumeTimer);
|
|
||||||
var self = this;
|
var self = this;
|
||||||
this.autoScrollResumeTimer = setTimeout(function() {
|
this.marqueeResumeTimer = setTimeout(function() {
|
||||||
if (self.stepPauseEnabled) {
|
self.marqueePaused = false;
|
||||||
// 步进模式:保留 _stepCurrentIndex,从 pausing 阶段继续
|
|
||||||
self._stepPhase = 'pausing';
|
|
||||||
self.autoScrollTimer = setTimeout(function() {
|
|
||||||
self._stepPhase = 'scrolling';
|
|
||||||
self._stepAdvance();
|
|
||||||
}, self.pauseDuration);
|
|
||||||
} else {
|
|
||||||
// 连贯模式:正常启动
|
|
||||||
self.startAutoScroll();
|
|
||||||
}
|
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -823,20 +648,36 @@ export default {
|
|||||||
border-bottom-right-radius: var(--article-bottom-around-radius, 0);
|
border-bottom-right-radius: var(--article-bottom-around-radius, 0);
|
||||||
|
|
||||||
.list-wrap {
|
.list-wrap {
|
||||||
// ============ 自动垂直滚动(scroll-view 实现) ============
|
// ============ 自动垂直滚动(CSS 跑马灯实现) ============
|
||||||
// 设计目标:item 紧凑(自然高度) + 间距恒定(itemGap)
|
// 设计目标:item 紧凑(自然高度) + 间距恒定(itemGap) + 持续向上滚动,循环不跳变
|
||||||
// 实现方式:item 走基础 .item 规则的 margin-bottom: var(--article-item-gap),
|
// 外层 .auto-scroll-list 固定高度(= _visibleWindowHeight,即窗口可见 N 行),overflow:hidden;
|
||||||
// 不再强制 min-height 让 item 用内容撑起,短 item 不会被拉高产生空白。
|
// 内层 .marquee-track 放置 loopedList(A+A 两份),
|
||||||
// scrollTop 累加 + loopedList 复制一份实现无缝循环滚动。
|
// @keyframes marquee-up 把 translateY 从 0 → -第一份高度,linear + infinite 循环;
|
||||||
// vs 旧 swiper 方案:旧方案用 min-height 强制等高 + swiper-item-content padding-bottom 制造间距,
|
// 第一份末行滚出窗口后,第二份首行刚好顶替,视觉上无缝接续。
|
||||||
// 导致短 item 下方空一大块(违反"紧凑"诉求);新方案天然紧凑。
|
// 动画时长 = 第一份高度 / 速度(默认 ≈ 20rpx/秒 ≈ 10px/秒,慢速平稳,app 阅读友好);
|
||||||
|
// 通过 swiperConfig.marqueeDuration(秒,不传走默认)可手动覆盖。
|
||||||
|
// 触摸暂停:touchstart 加 paused class,animation-play-state=paused;
|
||||||
|
// touchend 2s 后移除 paused class,继续滚动。
|
||||||
.auto-scroll-list {
|
.auto-scroll-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.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 命中,
|
// 关键:loopedList 把 list 复制了一份,最后一条 .item(2N-1)也会被 :last-of-type 命中,
|
||||||
// 导致其 margin-bottom: 0,而前面所有 item 是 itemGap。
|
// 导致其 margin-bottom: 0,而前面所有 item 是 itemGap。
|
||||||
// 这会破坏无缝循环:scrollTop 跳回 0 时,用户看到「item 2N-1(无下间距)→ item 0(itemGap)」
|
// 这会破坏无缝循环:滚到 A 末行与 A+A 中第二份首行拼接处时,突然多出一截空白,视觉闪一下。
|
||||||
// 突然多出一截空白,视觉闪一下。
|
|
||||||
// 修复:scroll-view 内所有 item 一视同仁,最后一条也保持 itemGap。
|
// 修复:scroll-view 内所有 item 一视同仁,最后一条也保持 itemGap。
|
||||||
.item {
|
.item {
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
@@ -845,6 +686,18 @@ 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 {
|
.item {
|
||||||
margin-bottom: var(--article-item-gap, 16rpx);
|
margin-bottom: var(--article-item-gap, 16rpx);
|
||||||
/* 背景 / 圆角通过 CSS 变量从 .article-wrap 继承(由 warpWrapStyle 注入) */
|
/* 背景 / 圆角通过 CSS 变量从 .article-wrap 继承(由 warpWrapStyle 注入) */
|
||||||
@@ -927,10 +780,21 @@ export default {
|
|||||||
.iconfont {
|
.iconfont {
|
||||||
/* 图标大小由 CSS 变量驱动 */
|
/* 图标大小由 CSS 变量驱动 */
|
||||||
font-size: var(--article-meta-icon-size, 24rpx);
|
font-size: var(--article-meta-icon-size, 24rpx);
|
||||||
/* 图标与数字之间的视觉间隔:之前 6rpx 太挤,数字 1k / 1.2万 看起来"粘"在图标上;
|
/* 兼容老的 iconfont 写法(图与数字间距) */
|
||||||
10rpx 给到舒服的阅读节奏,与 view.icon(uni-icons 在 .num 内)的内边距不冲突 */
|
|
||||||
margin-right: 10rpx;
|
margin-right: 10rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 图与数字之间的视觉间隔。
|
||||||
|
之前用 .uni-icons { margin-right } 仅 H5 生效:
|
||||||
|
.uni-icons 是 <uni-icons> 组件内部 <text> 的 class,
|
||||||
|
微信小程序 / 鸿蒙元服务默认 styleIsolation=isolated,
|
||||||
|
父级选择器穿透不进组件 shadow root,样式不生效。
|
||||||
|
改成给数字 <text> 加 margin-left:
|
||||||
|
- text 是 .num 的直接子元素,与 .num 同组件,不受隔离影响
|
||||||
|
- H5 / 微信小程序 / 鸿蒙 三个环境均生效 */
|
||||||
|
text {
|
||||||
|
margin-left: 10rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
|
|||||||
Reference in New Issue
Block a user