/** * 家书 FamilyBook · 页面数据接入辅助(鸿蒙端) * * 各模块页面的数据装配**都是同一套动作**: * ① 页面出现 → ② 调仓库 `load()` → ③ 先拿缓存渲染、再拿网络刷新 → ④ 解析成端上类型。 * 这份文件把 ②④ 收成一处,页面只写"我关心哪个模块、怎么解析"。 * * 🔴 为什么要抽(而不是每页各写一遍): * 10 个模块 × 同一段 20 行样板 = 200 行重复代码, * 而重复代码的代价不是"多打几个字",是**改一处漏九处** * (本项目在别的地方已经吃过这个亏)。 */ import { hilog } from '@kit.PerformanceAnalysisKit'; import { FbModuleLoad, FbModuleName, FbModuleRepo } from './FbModuleRepo'; const DOMAIN: number = 0x0000; const TAG: string = 'FbModuleLoad'; /** * 一次性加载(**直接给路径**,不必登记在上面的静态表里)。 * * ⚠️ 给"两级接口"用(如账本明细 `/ledger//entries` —— 路径带 id, * 没法在静态表里穷举)。缓存键也用完整路径,天然互不冲突。 */ export async function loadPath(cacheKey: string, path: string, parse: (json: string) => T, sink: (data: T, fromCache: boolean) => void, onNote?: (note: string) => void): Promise { await FbModuleRepo.inst().load(cacheKey, path, (r: FbModuleLoad): void => { let data: T; try { data = parse(r.json); } catch (err) { hilog.error(DOMAIN, TAG, '解析 %{public}s 失败:%{public}s', cacheKey, JSON.stringify(err)); return; } if (onNote !== undefined && r.error !== '') { onNote(r.error); } sink(data, r.fromCache); }); } /** 模块 → 接口路径 */ export const FB_MODULE_PATH: Record = { 'home': '/api/v1/home', 'calendar': '/api/v1/calendar', 'days': '/api/v1/days', 'tasks': '/api/v1/tasks', 'trips': '/api/v1/trips', 'ledger': '/api/v1/ledger', 'resources': '/api/v1/resources', 'books': '/api/v1/books', 'notes': '/api/v1/notes', 'notifications': '/api/v1/notifications', }; /** * 装配一个模块:读缓存 + 拉网络,每次拿到**解析后**的数据回调。 * * @param name 模块名 * @param parse JSON → 端上类型的解析函数 * @param sink 数据到位回调(会被调 1~2 次:缓存一次、网络一次) * @param onNote 可选:提示文案(同步失败/未登录时给页面显示一行小字) * @param pathOverride 可选:**改掉这个模块的请求路径**(带查询串)。 * * 🔴🔴 v59(2026-10-18,用户报「列表这个视图,今天的数据缺一条」的真根因): * 服务端 `/api/v1/calendar` 的 `from`/`to` **默认只给"本月"** * (`Api::str('from', date('Y-m-01'))` / `date('Y-m-t')`)。 * 而日程页的**列表视图**要渲染 `-7 ~ +60` 天 —— 跨到下个月去, * 下月那一段**服务端根本没返回** ⇒ 落在下月的日程(包括"今天"往后那几天) * 在列表里凭空少掉。 * ⚠️ 月视图看起来正常,是因为它本来就只看本月 ⇒ 这个 bug 只在列表暴露。 * ⇒ 日程页显式传 `from`/`to`(覆盖它要显示的那一整段),不要依赖默认值。 * * ⚠️ 只改**请求路径**、不动缓存键(缓存仍按模块名存): * 否则同一个模块会出现两个缓存键、切视图时互相覆盖。 * ⚠️ 这也意味着"列表拉的比月视图多"时,缓存里会留下更宽的一段 —— * 这是**好事**(月视图也能直接命中缓存),但**不要**指望缓存里的范围 * 与当前视图严格一致;页面永远按自己需要的日期去索引。 */ /** * @param name 缓存键。**接收任意字符串**而不是只收 `FbModuleName`: * 🔴 2026-10-30:日程改成全量同步后,缓存键要带上请求窗口 * (`calendar:1970-01-01:2100-12-31`),否则"旧窗口的数据"会被当成 * 新窗口的数据显示 —— 那正是"翻月没数据/数据不对"的根因之一。 * `FbModuleRepo.load` 本来就是 `string`,这里放宽是为了让调用方传得进来。 * @param timeoutMs 可选:本次请求超时(毫秒)。 * 🔴 2026-10-30:日程改成一次拉**全量**(约 9.3MB / 28628 条), * 默认 15 秒在移动网络下会超时;超时后 `FbModuleRepo` 退回旧缓存, * 表现成"翻月没数据"且极难查。⇒ 大响应必须能放宽超时。 */ export async function loadModule(name: string, parse: (json: string) => T, sink: (data: T, fromCache: boolean) => void, onNote?: (note: string) => void, pathOverride?: string, timeoutMs?: number): Promise { const path: string = pathOverride !== undefined && pathOverride !== '' ? pathOverride : (FB_MODULE_PATH[name] !== undefined ? FB_MODULE_PATH[name] : ''); if (path === '') { hilog.warn(DOMAIN, TAG, '模块 %{public}s 没有配路径', name); return; } await FbModuleRepo.inst().load(name, path, (r: FbModuleLoad): void => { // ⚠️ 解析失败**不能抛**:缓存/接口的形状变了(或确实是空), // 抛出去会让页面白屏。这里给个空数组兜住,并留一条日志。 let data: T; try { data = parse(r.json); } catch (err) { hilog.error(DOMAIN, TAG, '解析 %{public}s 失败:%{public}s', name, JSON.stringify(err)); return; } if (onNote !== undefined && r.error !== '') { onNote(r.error); } sink(data, r.fromCache); }); }