refactor(diy-article): 替换scroll-view滚动为CSS跑马灯实现
移除原scroll-view自动滚动逻辑,改用CSS animation实现无缝跑马灯效果,解决跨端滚动跳变问题,移除JS定时器驱动逻辑,提升动画流畅度和跨端一致性,优化触摸暂停恢复逻辑。
This commit is contained in:
@@ -3,58 +3,56 @@
|
||||
<view class="article-wrap" :style="warpWrapStyle">
|
||||
<!-- 注: :class 用内联数组,与 ns-newform 对齐;computed listClass/itemClass 已移除(避免数组 toString 用逗号 join 的问题) -->
|
||||
<view :class="['list-wrap', value.style || 'style-1']">
|
||||
<!-- 自动垂直滚动(scroll-view 实现,默认开启,兼容老数据:无 scrollMode/swiperConfig 字段也走这里)
|
||||
设计:item 自然高度 + 固定 itemGap,scrollTop 累加 + loopedList 复制实现无缝循环
|
||||
vs 旧 swiper 方案:旧方案用 min-height 强制等高 → 短 item 下方空一大块,不符合"紧凑"诉求
|
||||
新方案:item 完全用内容撑起,每条 item 后固定 itemGap 空白,视觉间距天然恒等 -->
|
||||
<scroll-view v-if="list.length > 0 && useScroll" class="auto-scroll-list"
|
||||
<!-- 自动垂直滚动(CSS animation 跑马灯实现,默认开启)
|
||||
设计:loopedList 把 list 复制成 A+A 两份;外层容器固定高度(窗口)overflow:hidden,
|
||||
内层容器用 @keyframes translateY 从 0 → -第一份高度,动画循环无缝接续。
|
||||
优势:① 无 JS setInterval 驱动 scrollTop,避免 uni-app scroll-view 在 H5/MP 下单位不一致
|
||||
导致"滚到底后跳变"问题;② 浏览器原生动画,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"
|
||||
scroll-y="true"
|
||||
:scroll-top="scrollTop"
|
||||
:scroll-with-animation="scrollWithAnimation"
|
||||
:show-scrollbar="false"
|
||||
:enhanced="false"
|
||||
:bounces="false"
|
||||
:throttle="false"
|
||||
@touchstart="onAutoScrollTouchStart"
|
||||
@touchend="onAutoScrollTouchEnd">
|
||||
<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' : '']"
|
||||
:style="itemStyle"
|
||||
@tap.stop="toDetail(item)">
|
||||
<!-- 封面图:无图时隐藏图位;style-4 纯文字始终不显示 -->
|
||||
<view class="article-img" v-if="item.cover_img && value.style !== 'style-4'">
|
||||
<image class="cover-img" :src="$util.img(item.cover_img)" mode="aspectFill"
|
||||
@error="imgError(index % list.length)" show-menu-by-longpress></image>
|
||||
</view>
|
||||
<view class="info-wrap">
|
||||
<text class="title">{{ item.article_title }}</text>
|
||||
<text class="desc"
|
||||
v-if="abstractStyle.support && abstractStyle.control && item.article_abstract">
|
||||
{{ item.article_abstract }}
|
||||
</text>
|
||||
<view class="tag-list"
|
||||
v-if="tagStyle.support && tagStyle.control && item.tag_names">
|
||||
<text class="tag" v-for="(tn, ti) in tagArr(item.tag_names)" :key="ti">{{ tn }}</text>
|
||||
@touchstart="onMarqueeTouchStart"
|
||||
@touchend="onMarqueeTouchEnd">
|
||||
<view class="marquee-track" :style="marqueeTrackStyle">
|
||||
<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' : '']"
|
||||
:style="itemStyle"
|
||||
@tap.stop="toDetail(item)">
|
||||
<!-- 封面图:无图时隐藏图位;style-4 纯文字始终不显示 -->
|
||||
<view class="article-img" v-if="item.cover_img && value.style !== 'style-4'">
|
||||
<image class="cover-img" :src="$util.img(item.cover_img)" mode="aspectFill"
|
||||
@error="imgError(index % list.length)" show-menu-by-longpress></image>
|
||||
</view>
|
||||
<view class="read-wrap">
|
||||
<block v-if="item.category_name">
|
||||
<text class="category-icon"></text>
|
||||
<text class="category-name">{{ item.category_name }}</text>
|
||||
</block>
|
||||
<view class="num" v-if="readNumStyle.support && readNumStyle.control">
|
||||
<uni-icons type="eye" size="12" :color="metaColor" />
|
||||
<text>{{ _humanizeNum(item.read_num || 0) }}</text>
|
||||
<view class="info-wrap">
|
||||
<text class="title">{{ item.article_title }}</text>
|
||||
<text class="desc"
|
||||
v-if="abstractStyle.support && abstractStyle.control && item.article_abstract">
|
||||
{{ item.article_abstract }}
|
||||
</text>
|
||||
<view class="tag-list"
|
||||
v-if="tagStyle.support && tagStyle.control && item.tag_names">
|
||||
<text class="tag" v-for="(tn, ti) in tagArr(item.tag_names)" :key="ti">{{ tn }}</text>
|
||||
</view>
|
||||
<view class="num" v-if="dianzanStyle.support && dianzanStyle.control">
|
||||
<uni-icons type="star" size="12" :color="metaColor" />
|
||||
<text>{{ _humanizeNum(item.dianzan_num || 0) }}</text>
|
||||
<view class="read-wrap">
|
||||
<block v-if="item.category_name">
|
||||
<text class="category-icon"></text>
|
||||
<text class="category-name">{{ item.category_name }}</text>
|
||||
</block>
|
||||
<view class="num" v-if="readNumStyle.support && readNumStyle.control">
|
||||
<uni-icons type="eye" size="12" :color="metaColor" />
|
||||
<text>{{ _humanizeNum(item.read_num || 0) }}</text>
|
||||
</view>
|
||||
<view class="num" v-if="dianzanStyle.support && dianzanStyle.control">
|
||||
<uni-icons type="star" size="12" :color="metaColor" />
|
||||
<text>{{ _humanizeNum(item.dianzan_num || 0) }}</text>
|
||||
</view>
|
||||
<text class="date" v-if="dateStyle.support && dateStyle.control">{{ $util.timeStampTurnTime(item.create_time, 'date') }}</text>
|
||||
</view>
|
||||
<text class="date" v-if="dateStyle.support && dateStyle.control">{{ $util.timeStampTurnTime(item.create_time, 'date') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 非滚动模式(scrollMode==='none')时的普通列表 -->
|
||||
<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"
|
||||
@@ -129,45 +127,26 @@ export default {
|
||||
headHeight: '160rpx',
|
||||
textRows: 2
|
||||
},
|
||||
// ========== 滚动模式(scroll-view 自动滚动)状态 ==========
|
||||
// scrollTop 累加驱动滚动;loopedList 把 list 复制一份实现无缝循环
|
||||
scrollTop: 0,
|
||||
autoScrollTimer: null, // setTimeout(16ms) id(60fps tick,跨端平滑,替代 setInterval 30ms)
|
||||
autoScrollResumeTimer: null, // touchend 后延迟恢复
|
||||
_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 赋值无动画
|
||||
// ========== 跑马灯状态(CSS animation 驱动,替代 scroll-view + scrollTop)==========
|
||||
// marqueePaused: true 时给外层加 is-marquee-paused class → animation-play-state: paused
|
||||
// marqueeResumeTimer: touchend 后延迟恢复
|
||||
marqueePaused: false,
|
||||
marqueeResumeTimer: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
mounted() {
|
||||
// 自动滚动由 watch 'list.length' 在 list 加载完后启动(mounted 时 list 仍为空)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopAutoScroll();
|
||||
if (this.autoScrollResumeTimer) {
|
||||
clearTimeout(this.autoScrollResumeTimer);
|
||||
this.autoScrollResumeTimer = null;
|
||||
if (this.marqueeResumeTimer) {
|
||||
clearTimeout(this.marqueeResumeTimer);
|
||||
this.marqueeResumeTimer = null;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 组件刷新监听
|
||||
componentRefresh() {
|
||||
this.getList();
|
||||
},
|
||||
// list 加载完后启动自动滚动(mounted 时 list 还是空,需监听 list 长度变化)
|
||||
'list.length'(newLen) {
|
||||
if (newLen > 1 && this.useScroll) {
|
||||
this.$nextTick(() => {
|
||||
this.startAutoScroll();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -324,9 +303,8 @@ export default {
|
||||
return Object.assign({ color: '#999ca7', iconSize: 24, fontSize: 24 },
|
||||
this.value.readWrapStyle || {});
|
||||
},
|
||||
// ========== 滚动配置(同时兼容老数据 swiperConfig 和新字段 scrollMode) ==========
|
||||
// 老数据 swiperConfig 字段:{ vertical, autoplay, circular, interval, duration, displayMultipleItems, swiperHeight }
|
||||
// 新数据 scrollMode 字段:'none' | 'vertical'(默认 vertical,与老行为一致)
|
||||
// ========== 滚动配置(兼容老数据 swiperConfig;现在用 CSS 跑马灯实现)==========
|
||||
// scrollMode: 'none' 关闭滚动,显示普通列表;其他值均走 CSS 跑马灯
|
||||
swiperConfig() {
|
||||
return this.value.swiperConfig || {};
|
||||
},
|
||||
@@ -337,59 +315,39 @@ export default {
|
||||
useScroll() {
|
||||
return this.scrollMode !== 'none' && this.list.length > 1;
|
||||
},
|
||||
// scroll-view 的 scroll-with-animation 绑定
|
||||
// 连贯模式: 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 报错)
|
||||
// 安全的 display-multiple-items(避免窗口高度 > 内容高度时出现空白)
|
||||
safeDisplayMultipleItems() {
|
||||
var displayCount = this.swiperConfig.displayMultipleItems || 3;
|
||||
var actualCount = this.list.length;
|
||||
return actualCount > 0 ? Math.min(displayCount, actualCount) : 1;
|
||||
},
|
||||
// 是否启用「滚动一下 + 停留一会」步进模式
|
||||
// 默认 false(走 sub-pixel 连贯滚动);true 时按 item 粒度滚动,每滚一个 item 停 pauseDuration ms 再滚下一个
|
||||
stepPauseEnabled() {
|
||||
return !!this.swiperConfig.stepPause;
|
||||
// 跑马灯动画时长(秒)
|
||||
// ① 若 swiperConfig.marqueeDuration 显式传,直接使用(秒,允许小数)
|
||||
// ② 否则根据 list 长度估算:每 1 条 item 走 4 秒(慢速,阅读友好,≈ 15-30rpx/s)
|
||||
// 为什么不用 _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
|
||||
pauseDuration() {
|
||||
var d = parseInt(this.swiperConfig.pauseDuration);
|
||||
return d > 0 ? d : 1500;
|
||||
// 跑马灯动画起点延迟(默认 1 秒,给首条内容 1 秒阅读时间)
|
||||
marqueeDelay() {
|
||||
var cfg = this.swiperConfig || {};
|
||||
var explicit = parseFloat(cfg.marqueeDelay);
|
||||
if (!isNaN(explicit) && explicit >= 0) return explicit;
|
||||
return 1;
|
||||
},
|
||||
// 步进模式单次滚动动画时长(ms),默认 500(uni-app scroll-view 默认动画 300ms,留 buffer)
|
||||
stepAnimDuration() {
|
||||
var d = parseInt(this.swiperConfig.stepAnimDuration);
|
||||
return d > 0 ? d : 500;
|
||||
},
|
||||
// swiper 高度(rpx)优先级:
|
||||
// 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;
|
||||
// 内层 .marquee-track 的 inline style(动画时长 + 起点延迟)
|
||||
// 其余动画属性走 CSS: name / timing-function / iteration-count
|
||||
marqueeTrackStyle() {
|
||||
return this.$util.objToStyle({
|
||||
'animation-duration': this.marqueeDuration + 's',
|
||||
'animation-delay': '-' + this.marqueeDelay + 's'
|
||||
});
|
||||
},
|
||||
// list 中最高 item 的自然高度(动态) — swiperHeight + warpWrapStyle CSS 变量都用它
|
||||
// list 为空时走 _staticMaxItemHeight 兜底
|
||||
@@ -633,154 +591,21 @@ export default {
|
||||
cover_img: ''
|
||||
});
|
||||
},
|
||||
// ========== scroll-view 自动滚动控制(两种模式互不干扰) ==========
|
||||
// 模式 1: 连贯滚动(sub-pixel + 16ms tick) — 默认
|
||||
// 设计目标:消除微信小程序「频闪」 + 跨端统一平滑滚动
|
||||
// 旧方案(频闪根因):setInterval(30ms) + scrollTop += 1 + scroll-with-animation="true"
|
||||
// - 30ms ≈ 33fps,与屏幕 60fps 不同步,出现周期性跳帧
|
||||
// - scroll-with-animation 让每次 scrollTop 变化都跑 200ms 缓动,30ms 内动画未完成又被打断
|
||||
// - MP-WXSS scroll-view 渲染管线压力大,叠加动画 → 肉眼可见 1 像素级跳变
|
||||
// 新方案:
|
||||
// - 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;
|
||||
}
|
||||
// ========== 跑马灯触摸控制(CSS animation-play-state)==========
|
||||
// 用户触摸 → animation-play-state: paused(给外层加 is-marquee-paused class)
|
||||
onMarqueeTouchStart() {
|
||||
this.marqueePaused = true;
|
||||
if (this.marqueeResumeTimer) {
|
||||
clearTimeout(this.marqueeResumeTimer);
|
||||
this.marqueeResumeTimer = null;
|
||||
}
|
||||
// 下一帧
|
||||
},
|
||||
// 用户松手 → 2s 后继续滚动(给用户阅读时间; 动画会从暂停位置继续,不会跳变)
|
||||
onMarqueeTouchEnd() {
|
||||
if (this.marqueeResumeTimer) clearTimeout(this.marqueeResumeTimer);
|
||||
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() {
|
||||
if (this.autoScrollTimer) {
|
||||
clearTimeout(this.autoScrollTimer);
|
||||
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;
|
||||
this.autoScrollResumeTimer = setTimeout(function() {
|
||||
if (self.stepPauseEnabled) {
|
||||
// 步进模式:保留 _stepCurrentIndex,从 pausing 阶段继续
|
||||
self._stepPhase = 'pausing';
|
||||
self.autoScrollTimer = setTimeout(function() {
|
||||
self._stepPhase = 'scrolling';
|
||||
self._stepAdvance();
|
||||
}, self.pauseDuration);
|
||||
} else {
|
||||
// 连贯模式:正常启动
|
||||
self.startAutoScroll();
|
||||
}
|
||||
this.marqueeResumeTimer = setTimeout(function() {
|
||||
self.marqueePaused = false;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
@@ -823,27 +648,55 @@ export default {
|
||||
border-bottom-right-radius: var(--article-bottom-around-radius, 0);
|
||||
|
||||
.list-wrap {
|
||||
// ============ 自动垂直滚动(scroll-view 实现) ============
|
||||
// 设计目标:item 紧凑(自然高度) + 间距恒定(itemGap)
|
||||
// 实现方式:item 走基础 .item 规则的 margin-bottom: var(--article-item-gap),
|
||||
// 不再强制 min-height 让 item 用内容撑起,短 item 不会被拉高产生空白。
|
||||
// scrollTop 累加 + loopedList 复制一份实现无缝循环滚动。
|
||||
// vs 旧 swiper 方案:旧方案用 min-height 强制等高 + swiper-item-content padding-bottom 制造间距,
|
||||
// 导致短 item 下方空一大块(违反"紧凑"诉求);新方案天然紧凑。
|
||||
.auto-scroll-list {
|
||||
width: 100%;
|
||||
// ============ 自动垂直滚动(CSS 跑马灯实现) ============
|
||||
// 设计目标:item 紧凑(自然高度) + 间距恒定(itemGap) + 持续向上滚动,循环不跳变
|
||||
// 外层 .auto-scroll-list 固定高度(= _visibleWindowHeight,即窗口可见 N 行),overflow:hidden;
|
||||
// 内层 .marquee-track 放置 loopedList(A+A 两份),
|
||||
// @keyframes marquee-up 把 translateY 从 0 → -第一份高度,linear + infinite 循环;
|
||||
// 第一份末行滚出窗口后,第二份首行刚好顶替,视觉上无缝接续。
|
||||
// 动画时长 = 第一份高度 / 速度(默认 ≈ 20rpx/秒 ≈ 10px/秒,慢速平稳,app 阅读友好);
|
||||
// 通过 swiperConfig.marqueeDuration(秒,不传走默认)可手动覆盖。
|
||||
// 触摸暂停:touchstart 加 paused class,animation-play-state=paused;
|
||||
// touchend 2s 后移除 paused class,继续滚动。
|
||||
.auto-scroll-list {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
// 关键:loopedList 把 list 复制了一份,最后一条 .item(2N-1)也会被 :last-of-type 命中,
|
||||
// 导致其 margin-bottom: 0,而前面所有 item 是 itemGap。
|
||||
// 这会破坏无缝循环:scrollTop 跳回 0 时,用户看到「item 2N-1(无下间距)→ item 0(itemGap)」
|
||||
// 突然多出一截空白,视觉闪一下。
|
||||
// 修复:scroll-view 内所有 item 一视同仁,最后一条也保持 itemGap。
|
||||
.item {
|
||||
&:last-of-type {
|
||||
margin-bottom: var(--article-item-gap, 16rpx);
|
||||
.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 命中,
|
||||
// 导致其 margin-bottom: 0,而前面所有 item 是 itemGap。
|
||||
// 这会破坏无缝循环:滚到 A 末行与 A+A 中第二份首行拼接处时,突然多出一截空白,视觉闪一下。
|
||||
// 修复:scroll-view 内所有 item 一视同仁,最后一条也保持 itemGap。
|
||||
.item {
|
||||
&:last-of-type {
|
||||
margin-bottom: var(--article-item-gap, 16rpx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关键: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);
|
||||
@@ -927,10 +780,21 @@ export default {
|
||||
.iconfont {
|
||||
/* 图标大小由 CSS 变量驱动 */
|
||||
font-size: var(--article-meta-icon-size, 24rpx);
|
||||
/* 图标与数字之间的视觉间隔:之前 6rpx 太挤,数字 1k / 1.2万 看起来"粘"在图标上;
|
||||
10rpx 给到舒服的阅读节奏,与 view.icon(uni-icons 在 .num 内)的内边距不冲突 */
|
||||
/* 兼容老的 iconfont 写法(图与数字间距) */
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user