From 8ca1eb76e0a7b89e3445ece75ed684b857ea7e40 Mon Sep 17 00:00:00 2001 From: laoyang Date: Mon, 2 Nov 2026 17:12:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=9C=88=E8=A7=86=E5=9B=BE):=20=E9=A6=96?= =?UTF-8?q?=E5=B1=8F=E4=B8=8D=E5=8F=98=E8=89=B2=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E7=9C=9F=E6=AD=A3=E6=A0=B9=E5=9B=A0=E6=98=AF=E5=A4=96=E5=B1=82?= =?UTF-8?q?=20ForEach=20key=20=E4=B8=8D=E5=8F=98=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版只改内层 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 都必须随数据变化。 --- entry/src/main/ets/pages/Index.ets | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index ec5e95e..dc5c9f5 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -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 = 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)