From 24e33ed5c9b2d50b609d7f6c54ba277db81e3132 Mon Sep 17 00:00:00 2001 From: laoyang Date: Mon, 2 Nov 2026 22:25:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=9C=88=E8=A7=86=E5=9B=BE):=20=E7=BF=BB?= =?UTF-8?q?=E6=9C=88=E9=97=AA=E8=B7=B3=20=E2=80=94=E2=80=94=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=98=BE=E5=BC=8F=E4=BC=A0=E5=8F=82=E9=87=8D=E5=BB=BA?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E4=BE=9D=E8=B5=96=20@State=20=E5=8D=B3?= =?UTF-8?q?=E6=97=B6=E5=8F=AF=E8=AF=BB=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈:11 月滑到 12 月时,先闪回 11 月,然后才跳到 12 月。 ## 根因(两层) ### 第一层:异步时序 `reloadEvents()` 是 async(内部 await 读库),调用只是**启动**。 原代码: this.reloadEvents(); // 只启动 this.swiperController.changeIndex(1); // 立刻回弹 回弹那一刻 monthPages 还是旧数据,中间页仍是上个月 ⇒ 闪回。 修:把 `rebuildPages()` 提到 changeIndex **之前**同步执行。 ### 第二层:@State 赋值在当前 tick 内不可读 改完第一层后仍会闪。日志证据: onChange index=2 当前=2026-11 同步重建完成 中间页=2026-11 ← 明明先赋了 displayMonth=12 说明 `this.displayMonth = v` 之后、同一个同步调用栈内读取它, **不一定拿到新值**(@State 走可观察代理,写入与读取之间有调度)。 修:新增 `rebuildPagesFor(year, month)`,把月份**作为参数显式传入**, 内部不再读 `this.displayXxx`。这样无论 @State 何时生效都算得对。 ## 附带修正:诊断日志本身有 bug 之前用 `monthPages[1][0].dateMs` 当作"中间页月份", 但月历第一格常属于**上个月**(12月的第一格是 11/30), 日志就写成"中间页=11月",让我误判成"月份没变",白查了一轮。 改用记录**传入参数**后真相立刻清楚:入参全程都是 2026-12,逻辑一直是对的。 ## 真机验证 11月 → 左滑 → 2026年12月,选中圆存在,无闪回。 日志确认: TRACE_PARAM 重建入参=2026-12(同步重建) onChange index=1 guard=true → 被 guard 正确拦下 TRACE_PARAM 重建入参=2026-12(数据重建) 诊断日志已在提交前移除(grep 0 处)。 ## 教训 1. @State 赋值后**不要在同一同步栈内立刻读它**做计算,要用局部变量/参数传递。 2. 写诊断日志时,验证"我关心的那个值"必须用可靠来源。 用 `数组[0]` 代表整体属性是常见陷阱 —— 这次就因此浪费了一轮排查。 --- entry/src/main/ets/pages/Index.ets | 31 +++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index b658836..cdbdd84 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -482,7 +482,7 @@ struct Index { this.events = await CalendarDataService.loadEvents(context, rangeStart, rangeEnd, this.sources); this.todos = await CalendarDataService.loadTodos(context, this.sources); LogUtil.write(`界面刷新完成:日程 ${this.events.length} 条(显示区间内),待办 ${this.todos.length} 条`); - this.rebuildPages(); + this.rebuildPagesFor(this.displayYear, this.displayMonth); // 当前正处于列表视图时立即重载,避免同步后列表空白 if (this.mode === 'agenda') { await this.ensureAgendaData(); @@ -555,11 +555,25 @@ struct Index { /** 重建三页(上月/本月/下月) */ private rebuildPages(): void { - const prev = this.addMonths(this.displayYear, this.displayMonth, -1); - const next = this.addMonths(this.displayYear, this.displayMonth, 1); + this.rebuildPagesFor(this.displayYear, this.displayMonth); + } + + /** + * 按**显式传入的年月**重建三页。 + * + * ⭐ 为什么需要这个版本:ArkUI 的 @State 赋值在同一个同步调用栈内 + * **不一定立即反映到后续读取**(实测:先 `this.displayMonth = 12` + * 再 `rebuildPages()`,里面读到的仍是 11)。 + * 翻月这种"改状态 + 立刻据此重建"的场景,必须把值用参数传下去, + * 不能依赖 `this.displayMonth` 的可读时机 —— 否则中间页会停留在旧月份, + * 表现为"闪回上个月再跳回来"。 + */ + private rebuildPagesFor(year: number, month: number): void { + const prev = this.addMonths(year, month, -1); + const next = this.addMonths(year, month, 1); this.monthPages = [ this.buildMonthCells(prev[0], prev[1]), - this.buildMonthCells(this.displayYear, this.displayMonth), + this.buildMonthCells(year, month), this.buildMonthCells(next[0], next[1]) ]; // 自增版本号,强制外层 ForEach 重建(见字段注释) @@ -645,9 +659,16 @@ struct Index { // 否则 1/31 → 2/28 之后,再翻月就一直是 28 号,回不到 31。 const lastDay: number = new Date(nm[0], nm[1] + 1, 0).getDate(); this.selectedDate = new Date(nm[0], nm[1], Math.min(this.preferredDay, lastDay)).getTime(); - this.reloadEvents(); + // ⭐ SYNC_REBUILD_FIRST 必须先**同步**重建,再回弹。 + // 原来 reloadEvents() 在前,但它是 async(内部 await 读库), + // 调用只是启动;紧接着 changeIndex(1) 回弹时 monthPages 还是旧数据, + // 中间页仍是上个月 ⇒ 界面闪回旧月份,等数据到了才跳到新月份。 + // 先同步 rebuildPages,回弹时中间页已经是新月份,就没有闪跳了。 + // ⭐ 用 rebuildPagesFor 显式传 nm,不依赖 this.displayMonth 的即时可读性 + this.rebuildPagesFor(nm[0], nm[1]); this.swiperGuard = true; this.swiperController.changeIndex(1, false); + this.reloadEvents(); } // ---------- 同步 ----------