名册加内存缓存 + 编辑框自己拉一份 (#92)

用户 2026-11-05:「速度比较慢。刚点开的时候显示的是『成员 #1、成员 #3』
这样的样式,是不是因为没读到人的信息?」

是。占位项(`成员 #id`)就是 roster 还没到位时的兜底。

**慢的真因**:`FbPersonRepo.list()` 是**全表查询**
(800 人 + JOIN person_roster + 标签 GROUP_CONCAT 子查询),
而每个用到选人的组件都在首次打开时各自调一次 ——
首页、任务面板、观影、旅程、资源、账本…逐个来一遍,
同一份数据反复查、反复解析。

① FbPersonRepo 加内存缓存(5 分钟)
   · 缓存**只对整表查询**有意义;带关键词时在内存里过滤(同一份 `matches` 口径,
     行为与"查库再过滤"完全一致,且更快)。
   · 缓存的是**同一份数组引用**:调用方都只读不改(现有代码符合),
     不做深拷贝(拷 800 个对象本身就不便宜)。
   · 新增 `FbPersonRepo.invalidate()`。

② 同步完成后失效
   `FbSyncAll` 收尾时,只要这次不是"全部跳过",就 `FbPersonRepo.invalidate()`。
   ⚠️ 不清的话:用户改了头像/名字、同步完再点开还是旧的那份,要等 5 分钟过期。
   ⚠️ 只在 "skipped < total" 时清:全部跳过说明服务端没变,缓存本身有效。

③ 编辑框自己也在 aboutToAppear 里要一份
   父页面(首页)的 `dayRoster` 是异步加载的,点开那一瞬可能还是空。
   ⇒ 组件自己调 `list()`(有缓存时是**瞬时返回**),
     查找时"先查自己那份、再查父页面传的"。
   这样占位项一闪而过甚至看不到。
   ⚠️ 两份合并而不是只认一份:父页面可能后到(`@Param` 变化会重画),
      组件自己拉的也可能失败(那就靠父页面的)。

编译通过,已装真机。
This commit is contained in:
2026-11-05 21:01:28 +08:00
parent 9047fb411d
commit 06e6e3798a
3 changed files with 120 additions and 17 deletions
@@ -27,7 +27,8 @@ import { FbPersonPickSheet } from './FbPersonPickSheet';
import { FbAvatar } from './Widgets';
import { fbImageSrc } from '../data/FbAttachQueue';
import { fbCnDate } from './FbDateTimeField';
import { FbRosterPerson } from '../data/FbPersonRepo';
import { FbRosterPerson, FbPersonRepo } from '../data/FbPersonRepo';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { createDay, updateDay, deleteDay, FbDayDraft } from '../data/FbDayRepo';
import { FbApiResult } from '../data/FbHttp';
@@ -82,10 +83,29 @@ export struct FbDayEditSheet {
@Local personIds: number[] = [];
@Local personPickOpen: boolean = false;
@Local saving: boolean = false;
/**
* 组件自己拉到的名册(#92b)。
*
* 🔴 用户 2026-11-05:「速度比较慢。刚点开的时候显示的是『成员 #1、成员 #3』
* 这样的样式,是不是因为没读到人的信息?」
*
* 是。父页面(首页)的 `dayRoster` 是**异步**加载的,点开编辑框那一瞬
* 可能还是空;而 `pickedSlots()` 查不到就出占位。
*
* ⇒ 组件自己也在 `aboutToAppear` 里要一份:
* `FbPersonRepo.list()` 现在有内存缓存(#92),
* 首页已经拉过的话这里是**瞬时返回**,占位一闪而过甚至看不到。
* ⚠️ 用**两份合并**(先 self 再 param)而不是只认其中一份:
* 父页面可能后到(那时 `@Param` 变化会重画),
* 组件自己拉的也可能失败(那就靠父页面的)。
*/
@Local selfRoster: FbRosterPerson[] = [];
@Local confirmingDelete: boolean = false;
@Local note2: string = '';
aboutToAppear(): void {
/* #92b:自己确保拿到名册(有缓存时瞬时) */
this.ensureRoster();
this.title = this.editingTitle;
this.kind = this.editingKind;
this.note = this.editingNote;
@@ -123,11 +143,36 @@ export struct FbDayEditSheet {
* 原来直接"查不到就跳过" ⇒ chips 区一片空白 ⇒ 看着像数据丢了。
* 可数据其实一直在 `personIds` 里。
*/
/**
* 自己拉一的名册(#92b)。
* ⚠️ 失败不弹错:名册拿不到时 `pickedSlots()` 会出占位项,
* 编辑功能本身不受影响(保存时用的是 `personIds`,不依赖名册)。
*/
private async ensureRoster(): Promise<void> {
if (this.selfRoster.length > 0) {
return;
}
try {
this.selfRoster = await FbPersonRepo.inst().list();
} catch (err) {
hilog.warn(0x0000, 'FbDayEditSheet', '拉名册失败:%{public}s', JSON.stringify(err));
}
}
/** 按 id 找人:先查自己那份,再查父页面传的(#92b) */
private findPerson(id: number): FbRosterPerson | undefined {
let p: FbRosterPerson | undefined =
this.selfRoster.find((x: FbRosterPerson): boolean => x.id === id);
if (p === undefined) {
p = this.roster.find((x: FbRosterPerson): boolean => x.id === id);
}
return p;
}
private pickedSlots(): FbPickSlot[] {
const out: FbPickSlot[] = [];
for (const id of this.personIds) {
const p: FbRosterPerson | undefined =
this.roster.find((x: FbRosterPerson): boolean => x.id === id);
const p: FbRosterPerson | undefined = this.findPerson(id);
if (p !== undefined) {
out.push({
id: id,