fix(月视图): 翻月闪跳 —— 改为显式传参重建,不依赖 @State 即时可读 #6

用户反馈: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]` 代表整体属性是常见陷阱 —— 这次就因此浪费了一轮排查。
This commit is contained in:
2026-11-02 22:25:00 +08:00
parent 6305b4a7f0
commit 24e33ed5c9
+26 -5
View File
@@ -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();
}
// ---------- 同步 ----------