fix(wish-tree): 弹幕一行一条排队播放,不再重叠
Publish Mini Program Dev Version / publish (push) Successful in 1m4s

- 顶部滚动行上移紧贴置顶轮播(16%→12%)
- 树桩3行改为68/73/78%,更紧凑
- 核心修复重叠:弃用CSS无限循环+负delay(同屏多条),
  改为JS定时器按行调度,同一行一条飘完再出下一条,循环播放
- 弹幕动画改为只播一次,key含时间戳保证切换时重新触发动画
- 页面onHide/onUnload停止定时器,onShow恢复
This commit is contained in:
gouki
2026-08-10 13:27:19 +00:00
parent 87f5fa2478
commit a29328f35b
3 changed files with 106 additions and 32 deletions
+96 -23
View File
@@ -75,6 +75,21 @@ Page({
this.loadProducts();
},
onUnload() {
this.stopDanmakuScheduler();
},
onHide() {
this.stopDanmakuScheduler();
},
onShow() {
// 从后台/下一页返回时,若已有弹幕数据则恢复调度
if (this.danmakuRows && this.danmakuRows.length && !(this.danmakuTimers && this.danmakuTimers.length)) {
this.startDanmakuScheduler(this.danmakuRows);
}
},
onPullDownRefresh() {
const { currentTreeIndex, trees } = this.data;
const tree = trees[currentTreeIndex];
@@ -224,16 +239,75 @@ Page({
this.setData({
[`trees[${index}].hangWishes`]: cache.hangWishes,
topWishes: cache.top,
currentDanmaku: cache.danmaku,
});
// 启动弹幕调度:每行一条完毕后再出下一条
this.startDanmakuScheduler(cache.danmaku);
},
// 启动弹幕调度:每行同一时刻只播一条,前一条飘完再出下一条(循环)
startDanmakuScheduler(rows) {
this.stopDanmakuScheduler();
this.danmakuRows = rows || [];
this.danmakuTimers = [];
if (!this.danmakuRows.length) {
this.setData({ currentDanmaku: [] });
return;
}
const GAP = 1.5; // 同一行前一条飘完后,间隔再出下一条(秒)
// 每行独立调度:row 行的第 idx 条播完后,隔 GAP 秒播第 idx+1 条
const playRow = (row, idx) => {
const rowData = this.danmakuRows[row];
if (!rowData) return;
const item = rowData.list[idx % rowData.list.length];
// 更新该行当前弹幕(用行号作为 key 保证 setData 局部刷新)
this.setData({
[`currentDanmaku[${row}]`]: {
key: `${row}-${idx % rowData.list.length}-${Date.now()}`,
id: item.id,
content: item.content,
author: item.author,
type: item.type,
top: rowData.top,
duration: rowData.duration,
},
});
// 前一条飘完 + 间隔后,播下一条
const timer = setTimeout(() => {
playRow(row, idx + 1);
}, (rowData.duration + GAP) * 1000);
this.danmakuTimers[row] = timer;
};
// 初始化 currentDanmaku 数组长度,并启动每行
this.setData({ currentDanmaku: new Array(this.danmakuRows.length).fill(null) });
this.danmakuRows.forEach((_, row) => {
// 各行错开启动,避免 4 行同时从右侧涌出
const startTimer = setTimeout(() => playRow(row, 0), row * 1200);
this.danmakuTimers.push(startTimer);
});
},
// 停止弹幕调度
stopDanmakuScheduler() {
if (this.danmakuTimers) {
this.danmakuTimers.forEach((t) => clearTimeout(t));
this.danmakuTimers = [];
}
},
// 构建弹幕:共 4 行(行 0-2 树桩处三行,行 3 顶部置顶轮播下方)
// 同行统一慢速,按条数均匀错开起始位置,避免互相盖住
// 按行分组返回,由 JS 定时器控制:同一行一条完毕后再出下一条,不重叠
buildDanmaku(list) {
const DURATION_BOTTOM = 32; // 树桩处一飘完全屏的秒数(慢速)
const DURATION_TOP = 26; // 顶部行稍快一点
const ROW_TOPS = [64, 71, 78, 16]; // 前三行靠近树桩,第四行在顶部
const DURATION_BOTTOM = 30; // 树桩处一飘完全屏的秒数(慢速)
const DURATION_TOP = 24; // 顶部行稍快一点
// 行位置(屏幕百分比):
// - 行3 顶部行紧贴置顶轮播下方(稍有空隙)
// - 行0-2 树桩处三行,第一条68%,三行紧凑(间隔5%)
const ROW_TOPS = [68, 73, 78, 12];
const ROW_DURATIONS = [DURATION_BOTTOM, DURATION_BOTTOM, DURATION_BOTTOM, DURATION_TOP];
// 轮流分配到 4 行
const rows = [[], [], [], []];
@@ -241,24 +315,17 @@ Page({
rows[i % 4].push(w);
});
const items = [];
rows.forEach((rowList, row) => {
if (rowList.length === 0) return;
const duration = row === 3 ? DURATION_TOP : DURATION_BOTTOM;
rowList.forEach((w, idx) => {
items.push({
id: w.id,
content: w.content,
author: w.author || '匿名',
type: w.type,
top: ROW_TOPS[row],
duration,
// 均匀间隔:同屏内前后错开,不会盖住其他弹幕
delay: -(idx * duration / rowList.length),
});
});
});
return items;
// 每行存该行的所有弹幕(带位置/时长),供定时器轮流播放
return rows.map((rowList, row) => ({
top: ROW_TOPS[row],
duration: ROW_DURATIONS[row],
list: rowList.map((w) => ({
id: w.id,
content: w.content,
author: w.author || '匿名',
type: w.type,
})),
})).filter((r) => r.list.length > 0);
},
// 格式化许愿
@@ -482,4 +549,10 @@ Page({
path: '/pages/wish-tree/wish-tree',
};
},
onShareTimeline() {
return {
title: '快来许愿树许下你的愿望吧!'
};
}
});
+7 -7
View File
@@ -45,14 +45,14 @@
</swiper-item>
</swiper>
<!-- 弹幕层(超出50条的心愿缓缓飘过) -->
<!-- 弹幕层(超出30条的心愿缓缓飘过,每行一条完毕再出下一条 -->
<view class="danmaku-layer">
<view
wx:for="{{currentDanmaku}}"
wx:key="id"
class="danmaku-item {{item.type === 'paid' ? 'paid' : ''}}"
style="top: {{item.top}}%; animation-duration: {{item.duration}}s; animation-delay: {{item.delay}}s;"
>{{item.content}} · {{item.author}}</view>
<block wx:for="{{currentDanmaku}}" wx:key="key" wx:if="{{item}}">
<view
class="danmaku-item {{item.type === 'paid' ? 'paid' : ''}}"
style="top: {{item.top}}%; animation-duration: {{item.duration}}s;"
>{{item.content}} · {{item.author}}</view>
</block>
</view>
<!-- 边缘暗角 -->
+3 -2
View File
@@ -142,7 +142,8 @@
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.6);
animation-name: danmaku;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-iteration-count: 1;
animation-fill-mode: both;
will-change: transform;
}
@@ -608,7 +609,7 @@
}
}
/* 弹幕:从屏幕右侧飘到左侧 */
/* 弹幕:从屏幕右侧飘到左侧。每条只播一次,由 JS 控制一行一条排队出现。 */
@keyframes danmaku {
0% {
transform: translateX(100vw);