fix(月视图): 首屏不变色 —— 真正根因是外层 ForEach key 不变 #6

上一版只改内层 key 没修好,因为问题不在内层。

## 真正根因
外层 ForEach 的 key 是:
    (cells) => String(cells[0].dateMs)     // "这页第一天"
重建前后这个值**完全相同**(还是那个月的 1 号)。
⇒ ArkUI 判定"这一项没变" ⇒ **不重新执行 itemGenerator**
⇒ 里面的 chunkCells(cells) 不重算
⇒ 内层 ForEach 收到的还是**旧的 cells 数组**,旧数组里 hasEvent 仍是 false

内层 key 再怎么改都没用 —— 内层压根没被重新调用。

## 修法
新增 `@State monthDataVersion`,每次 rebuildPages 后自增,拼进外层 key:

    (cells) => `${cells[0].dateMs}_v${this.monthDataVersion}`

key 一变 → 外层重建 → 内层重新执行 → 颜色正确。

为什么不用"把整页 hasEvent 拼成 key":一页 42 格,key 会长很多,
且会参与 diff 比较,有性能代价。版本号是 O(1),语义也更清楚。

## 同时补了诊断日志
buildMonthCells 里输出:事件条数 / 有事件日期数 / 本页命中格数 / 当前版本号。
用来判断问题卡在哪一层:
  · "有事件的日期 0 天" → 数据层没算对
  · "命中 X 格" 但界面没变色 → 渲染/key 问题

## ⚠️ 教训(两层 ForEach 嵌套时)
这类 bug 我第一轮判断错了:只看到最内层 key 缺 hasEvent 就改了,
没意识到**外层 key 不变会让整棵子树被复用**,内层改什么都没用。
排查嵌套 ForEach 的刷新问题时,要从**最外层**往里查,
每一层的 key 都必须随数据变化。
This commit is contained in:
2026-11-02 17:12:55 +08:00
parent d49b041a2c
commit 8ca1eb76e0
+21 -5
View File
@@ -83,6 +83,12 @@ struct Index {
@State agendaEvents: DisplayEvent[] = []; // 列表视图全量数据(今天 ~ 未来2年)
private agendaStale: boolean = true; // 全量数据是否需要重新加载
@State monthPages: MonthCell[][] = [[], [], []]; // 上月/本月/下月
/** ⭐ 月视图数据版本号:每次 rebuildPages 后自增,拼进外层 ForEach 的 key。
* 为什么需要它:外层 key 原本用"这页第一天"(cells[0].dateMs),
* 数据重载前后这个值完全相同,ArkUI 会判定"同一项"而**复用整棵子树**,
* 连 itemGenerator 都不重新执行 ⇒ 内层永远拿到旧的 cells(hasEvent 全 false)。
* 只改内层 key 是没用的,必须让外层 key 变。 */
@State monthDataVersion: number = 0;
@State syncing: boolean = false;
@State loading: boolean = true;
@State menuOpen: boolean = false; // 顶部 ≡ 下拉菜单
@@ -517,10 +523,8 @@ struct Index {
const gridStart: number = first - offset * 86400000;
const todayStart: number = this.startOfDay(Date.now());
const eventDays: Set<number> = this.buildEventDaySet();
// DOT_DEBUG 临时:确认集合算出来了没有
if (month >= 0) {
LogUtil.write(`月格点标记 ${year}-${month + 1}:事件 ${this.events.length} 条,有事件的日期 ${eventDays.size} 天`);
}
// 诊断:本次构建里 hasEvent=true 的格子数(用于排查"首屏不变色")
let flagTrue: number = 0;
const cells: MonthCell[] = [];
for (let i = 0; i < 42; i++) {
const dateMs: number = gridStart + i * 86400000;
@@ -532,8 +536,16 @@ struct Index {
cell.isToday = dateMs === todayStart;
cell.lunar = LunarUtil.lunarDayText(dateMs);
cell.hasEvent = eventDays.has(this.startOfDay(dateMs));
if (cell.hasEvent) {
flagTrue++;
}
cells.push(cell);
}
if (month >= 0) {
LogUtil.write(`月视图构建 ${year}-${month + 1}:事件 ${this.events.length} 条,`
+ `有事件的日期 ${eventDays.size} 天,本页命中 ${flagTrue} 格,`
+ `版本 v${this.monthDataVersion}`);
}
return cells;
}
@@ -546,6 +558,8 @@ struct Index {
this.buildMonthCells(this.displayYear, this.displayMonth),
this.buildMonthCells(next[0], next[1])
];
// 自增版本号,强制外层 ForEach 重建(见字段注释)
this.monthDataVersion = this.monthDataVersion + 1;
this.rebuildDayTimeline();
}
@@ -1827,7 +1841,9 @@ struct Index {
.width('100%')
// 注意:不要给页面设百分比高度——竖屏时 Swiper 是自然高度,
// 子组件 height('100%') 对 auto 父容器解析为 0,整月网格会塌陷
}, (cells: MonthCell[]) => String(cells[0].dateMs))
// ⚠️ 必须带版本号:只按"这页第一天"做 key 时,数据重载前后 key 不变,
// ArkUI 会复用整棵子树、不重跑 itemGenerator,内层就拿不到新数据。
}, (cells: MonthCell[]) => `${cells.length > 0 ? cells[0].dateMs : 0}_v${this.monthDataVersion}`)
}
.index(1)
.loop(false)