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,161 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 日程设置(鸿蒙端)
|
||||
*
|
||||
* 存"日程模块自己的偏好"——它们是**这台设备上的人怎么看日历**,
|
||||
* 与服务器无关、也不该同步给别人。
|
||||
*
|
||||
* 🔴 用户 2026-10-15:「我们家书的日程功能模块,包括它的界面设计和功能,
|
||||
* 跟 SyncCalendar 做得一样,就差不多。……有几种不同的视图方式,
|
||||
* 然后包括下面日程的显示啊什么之类的。」
|
||||
* ⇒ 对齐 SyncCalendar 的设置项:
|
||||
* · **默认视图**(月 / 周 / 列表)—— 打开日程页时先用哪个
|
||||
* · **日程显示方式**(时间轴 / 列表)—— 同一天里日程怎么画
|
||||
* · **新建日程落到哪本日历**(默认日历本)
|
||||
* · **哪些日历本可见**(隐藏不想看的那几本)
|
||||
*
|
||||
* 🔴 为什么用 `preferences` 而不是本地库:都是**单个键值**,
|
||||
* 不是多行记录(同一范式见 `FbTokenStore` 的说明)。
|
||||
* ⚠️ 必须**同步读**(`get*()`):页面一渲染就要知道用哪个视图,
|
||||
* 若只能 await,每个调用点都要 async、把简单逻辑搞碎。
|
||||
* ⇒ 用"启动时预加载进内存 + 写时同时更新内存"(同 FbTokenStore)。
|
||||
*/
|
||||
|
||||
import { preferences } from '@kit.ArkData';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
|
||||
const DOMAIN: number = 0x0000;
|
||||
const TAG: string = 'FbCalSettings';
|
||||
|
||||
/** preferences 实例名 */
|
||||
const STORE_NAME: string = 'fb_calendar';
|
||||
|
||||
const K_VIEW: string = 'default_view'; // month | week | agenda
|
||||
const K_STYLE: string = 'display_style'; // timeline | list
|
||||
const K_BOOK: string = 'default_book_id'; // 新建日程默认落到哪本
|
||||
const K_HIDDEN: string = 'hidden_books'; // 逗号分隔的日历本 id
|
||||
|
||||
/** 视图:月 / 周 / 列表(与 SyncCalendar 的 VIEW_ORDER 同一套) */
|
||||
export type FbCalView = 'month' | 'week' | 'agenda';
|
||||
/** 日程画法:时间轴 / 列表(与 SyncCalendar 的 displayStyle 同一套) */
|
||||
export type FbCalStyle = 'timeline' | 'list';
|
||||
|
||||
class FbCalSettingsImpl {
|
||||
private view: FbCalView = 'month';
|
||||
private style: FbCalStyle = 'timeline';
|
||||
private bookId: number = 0;
|
||||
private hidden: string = '';
|
||||
private store: preferences.Preferences | null = null;
|
||||
private ready: boolean = false;
|
||||
|
||||
/** 预加载进内存(应用启动时调一次;幂等) */
|
||||
async init(ctx: common.Context): Promise<void> {
|
||||
if (this.ready) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.store = await preferences.getPreferences(ctx, STORE_NAME);
|
||||
const v: preferences.ValueType = await this.store.get(K_VIEW, 'month');
|
||||
const s: preferences.ValueType = await this.store.get(K_STYLE, 'timeline');
|
||||
const b: preferences.ValueType = await this.store.get(K_BOOK, 0);
|
||||
const h: preferences.ValueType = await this.store.get(K_HIDDEN, '');
|
||||
this.view = (v === 'week' || v === 'agenda') ? v : 'month';
|
||||
this.style = (s === 'list') ? 'list' : 'timeline';
|
||||
this.bookId = typeof b === 'number' ? b : 0;
|
||||
this.hidden = typeof h === 'string' ? h : '';
|
||||
this.ready = true;
|
||||
hilog.info(DOMAIN, TAG, '日程设置预加载完成:%{public}s / %{public}s', this.view, this.style);
|
||||
} catch (err) {
|
||||
// ⚠️ 读不到就用默认值,**不要**让启动挂掉
|
||||
hilog.warn(DOMAIN, TAG, '日程设置预加载失败:%{public}s', JSON.stringify(err));
|
||||
this.ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
return this.ready;
|
||||
}
|
||||
|
||||
// ---- 同步读 ----
|
||||
getView(): FbCalView {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
getStyle(): FbCalStyle {
|
||||
return this.style;
|
||||
}
|
||||
|
||||
getBookId(): number {
|
||||
return this.bookId;
|
||||
}
|
||||
|
||||
/** 被隐藏的日历本 id 列表 */
|
||||
hiddenIds(): number[] {
|
||||
const out: number[] = [];
|
||||
if (this.hidden === '') {
|
||||
return out;
|
||||
}
|
||||
for (const s of this.hidden.split(',')) {
|
||||
const n: number = parseInt(s, 10);
|
||||
if (!isNaN(n) && n > 0) {
|
||||
out.push(n);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 某本日历是否可见(默认都可见) */
|
||||
isVisible(id: number): boolean {
|
||||
return this.hiddenIds().indexOf(id) < 0;
|
||||
}
|
||||
|
||||
// ---- 异步写(同时更新内存) ----
|
||||
async setView(v: FbCalView): Promise<void> {
|
||||
this.view = v;
|
||||
await this.put(K_VIEW, v);
|
||||
}
|
||||
|
||||
async setStyle(s: FbCalStyle): Promise<void> {
|
||||
this.style = s;
|
||||
await this.put(K_STYLE, s);
|
||||
}
|
||||
|
||||
async setBookId(id: number): Promise<void> {
|
||||
this.bookId = id;
|
||||
await this.put(K_BOOK, id);
|
||||
}
|
||||
|
||||
/** 设置某本日历的可见性 */
|
||||
async setVisible(id: number, visible: boolean): Promise<void> {
|
||||
const ids: number[] = this.hiddenIds();
|
||||
const at: number = ids.indexOf(id);
|
||||
if (visible && at >= 0) {
|
||||
ids.splice(at, 1);
|
||||
} else if (!visible && at < 0) {
|
||||
ids.push(id);
|
||||
}
|
||||
const parts: string[] = [];
|
||||
for (const n of ids) {
|
||||
parts.push(n.toString());
|
||||
}
|
||||
this.hidden = parts.join(',');
|
||||
await this.put(K_HIDDEN, this.hidden);
|
||||
}
|
||||
|
||||
private async put(key: string, val: preferences.ValueType): Promise<void> {
|
||||
try {
|
||||
if (this.store === null) {
|
||||
hilog.warn(DOMAIN, TAG, '还没有 store,设置没能落盘(重启会丢)');
|
||||
return;
|
||||
}
|
||||
await this.store.put(key, val);
|
||||
// ⚠️ `put` 只写内存,**必须 `flush`** 才落盘(同 FbTokenStore 的坑)
|
||||
await this.store.flush();
|
||||
} catch (err) {
|
||||
hilog.error(DOMAIN, TAG, '日程设置保存失败:%{public}s', JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 全局单例 */
|
||||
export const FB_CAL_SETTINGS: FbCalSettingsImpl = new FbCalSettingsImpl();
|
||||
Reference in New Issue
Block a user