v0.7.0 接上真实服务器:登录 / 设备会话 / 全量同步与各模块真实数据
从「演示数据壳子」走到「真的能连家里的服务器用」。
登录(用户 2026-10-17「完整的登录功能」)
· 首次启动先填服务器地址 → /api/v1/ping 自报门户(认得出"这地址上真是家书")
· 手机号或用户名 + 密码 → 拿令牌;失败提示统一,不泄露账号是否存在
· FbDeviceStore:首次启动生成并持久化设备 UUID(三方应用拿不到 UDID),
设备名取机型(marketName),并上报 platform / app_version
设备会话(v57,用户 2026-10-17)
· 一把令牌 = 一台设备的一条会话;每次启动打一次 /me(有网就验)
· 两种锁分开:need_login(401/被踢 → 清令牌)与 offline_expired
(离线超 24h → 只锁界面、不清数据、联网自动恢复)——
混成一种就会违背用户要的"一直联网就不用每天输密码"
· 运行期任何请求 401 → 网络层全局回调立刻锁定跳登录页(FbHttp 的 authProbe
让登录/校验类请求跳过,避免"两个地方同时跳登录页")
· 退出登录:先清本地令牌(主防线),再尽力通知服务端
· 设置页显示版本号与「这台设备」
数据层
· FbDb / FbSchema / FbParse / 各 Repo(日程 / 名册 / 观影 / 模块)+ FbSyncAll
· 首页改为服务端 /home(农历/节气/纪念日/生日/天气/统计/往年的今天)
· 日程页用 FbDayEventPanel,与网页端显示方式/红线/并排三处一致
验证:ArkTS 静态守卫 231/0;hvigor 编译 BUILD SUCCESSFUL(entry-default-signed.hap)。
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 家书 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/<id>/entries` —— 路径带 id,
|
||||
* 没法在静态表里穷举)。缓存键也用完整路径,天然互不冲突。
|
||||
*/
|
||||
export async function loadPath<T>(cacheKey: string, path: string, parse: (json: string) => T,
|
||||
sink: (data: T, fromCache: boolean) => void,
|
||||
onNote?: (note: string) => void): Promise<void> {
|
||||
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<string, string> = {
|
||||
'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',
|
||||
'kin': '/api/v1/kin',
|
||||
};
|
||||
|
||||
/**
|
||||
* 装配一个模块:读缓存 + 拉网络,每次拿到**解析后**的数据回调。
|
||||
*
|
||||
* @param name 模块名
|
||||
* @param parse JSON → 端上类型的解析函数
|
||||
* @param sink 数据到位回调(会被调 1~2 次:缓存一次、网络一次)
|
||||
* @param onNote 可选:提示文案(同步失败/未登录时给页面显示一行小字)
|
||||
*/
|
||||
export async function loadModule<T>(name: FbModuleName, parse: (json: string) => T,
|
||||
sink: (data: T, fromCache: boolean) => void,
|
||||
onNote?: (note: string) => void): Promise<void> {
|
||||
|
||||
const path: string = 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user