v83: 后台定期同步(四层调度,策略照 SyncCalendar)
用户原话:「另外,要有定期同步的功能,就是家书app在后台,也能自动同步
数据,这样打开app,基本上数据就是最新的。同步的策略,去synccalendar找,
里面有办法。」
四层调度(缺一层就会在某种场景下"永远不同步"):
1. 前台:onForeground 时若距上次同步超过 5 分钟,补一次
2. 退后台:requestSuspendDelay 短时任务(单次≤3分钟、单日10分钟配额)
3. 延迟任务:workScheduler(周期 2 小时 —— 系统按活跃分组压制,
请求更短不报错但不生效)
4. 桌面卡片:updateDuration:1(=30 分钟,唯一不受压制的通道,
每天上限 50 次);先推卡片再触发同步
关键约束(写错了都不报错,只是静默失效):
- WorkSchedulerExtensionAbility 单次回调上限 2 分钟,必须在 finally 里 stopWork
- 后台扩展跑在独立进程,EntryAbility.onCreate 的初始化一步都没跑过
⇒ prepare() 必须补齐 token store / module token / FbDb.open
- 回前台必须 cancelSuspendDelay 归还配额,否则两三天后用光
- workInfo 要带 isPersisted,否则重启后任务永久消失
- abilityName 必须与 module.json5 声明一致,对不上时 startWork 不报错但不回调
- ApplicationInfo 没有 bundleName 字段,包名在 .name
其他:
- 卡片驱动的同步有节流(CARD_MIN_GAP_MS 25 分钟 < 卡片刷新 30 分钟,
反了就是每次刷新都被跳过、一次都不跑)
- 设置页新增「同步」分组:开关 + 上次同步时间 + 说明
同时修:首页日程红线左侧缺一块(nowLayer 从 Row{圆点;线} 改成单个
Column 满宽 —— Row 里圆点占行首,线只能从第 5vp 起画)
守卫 1106/0、v83 反向验证 10/10 全中、已装机。
This commit is contained in:
@@ -320,26 +320,30 @@ struct FbCard4x4 {
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
/** 第 3 层:当前时间红线(只有今天画) */
|
||||
/** 第 3 层:当前时间红线(只有今天画)
|
||||
*
|
||||
* 🔴🔴 v82(用户 2026-10-23):「鸿蒙端的首页日程,红线也短了,左侧缺一块。」
|
||||
* 真根因(与同时修的网页端同一个形状):原来的红线是
|
||||
* `Row { 4vp 圆点; 线(layoutWeight 1) }` —— **圆点占了行首那 4vp**,
|
||||
* 线只能从第 5vp 起画。用户看到的就是"左端缺一块"。
|
||||
* 而且用户在同一条消息里明确说了:「那个红线左侧是不是还有一个红点?
|
||||
* 如果有,去掉。」⇒ 圆点本来就不该存在。
|
||||
* ⇒ 现在整条线自己铺满 `width('100%')`,行首不再有任何占位元素。
|
||||
* ⚠️ 别改回 `Row { 点; 线 }` 那种写法 —— 只要行首有子元素,线就一定短一截。
|
||||
* ⚠️ `borderRadius(1)` 保留:2vp 的线加 1vp 圆角正好是胶囊形,
|
||||
* 与网页端 `.ag-now` 的观感一致(网页端是 2px 无圆角,视觉上等价)。
|
||||
*/
|
||||
@Builder
|
||||
private nowLayer() {
|
||||
Column() {
|
||||
if (this.nowOffsetVp() >= 0) {
|
||||
Blank().height(this.nowOffsetVp()).hitTestBehavior(HitTestMode.None)
|
||||
Row() {
|
||||
Column()
|
||||
.width(4)
|
||||
.height(4)
|
||||
.borderRadius(2)
|
||||
.backgroundColor('#C0533F')
|
||||
Column()
|
||||
.height(2)
|
||||
.layoutWeight(1)
|
||||
.borderRadius(1)
|
||||
.backgroundColor('#C0533F')
|
||||
}
|
||||
.width('100%')
|
||||
.hitTestBehavior(HitTestMode.None)
|
||||
Column()
|
||||
.width('100%')
|
||||
.height(2)
|
||||
.borderRadius(1)
|
||||
.backgroundColor('#C0533F')
|
||||
.hitTestBehavior(HitTestMode.None)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
@@ -317,26 +317,30 @@ struct FbCard6x4 {
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
/** 第 3 层:当前时间红线(只有今天画) */
|
||||
/** 第 3 层:当前时间红线(只有今天画)
|
||||
*
|
||||
* 🔴🔴 v82(用户 2026-10-23):「鸿蒙端的首页日程,红线也短了,左侧缺一块。」
|
||||
* 真根因(与同时修的网页端同一个形状):原来的红线是
|
||||
* `Row { 4vp 圆点; 线(layoutWeight 1) }` —— **圆点占了行首那 4vp**,
|
||||
* 线只能从第 5vp 起画。用户看到的就是"左端缺一块"。
|
||||
* 而且用户在同一条消息里明确说了:「那个红线左侧是不是还有一个红点?
|
||||
* 如果有,去掉。」⇒ 圆点本来就不该存在。
|
||||
* ⇒ 现在整条线自己铺满 `width('100%')`,行首不再有任何占位元素。
|
||||
* ⚠️ 别改回 `Row { 点; 线 }` 那种写法 —— 只要行首有子元素,线就一定短一截。
|
||||
* ⚠️ `borderRadius(1)` 保留:2vp 的线加 1vp 圆角正好是胶囊形,
|
||||
* 与网页端 `.ag-now` 的观感一致(网页端是 2px 无圆角,视觉上等价)。
|
||||
*/
|
||||
@Builder
|
||||
private nowLayer() {
|
||||
Column() {
|
||||
if (this.nowOffsetVp() >= 0) {
|
||||
Blank().height(this.nowOffsetVp()).hitTestBehavior(HitTestMode.None)
|
||||
Row() {
|
||||
Column()
|
||||
.width(4)
|
||||
.height(4)
|
||||
.borderRadius(2)
|
||||
.backgroundColor('#C0533F')
|
||||
Column()
|
||||
.height(2)
|
||||
.layoutWeight(1)
|
||||
.borderRadius(1)
|
||||
.backgroundColor('#C0533F')
|
||||
}
|
||||
.width('100%')
|
||||
.hitTestBehavior(HitTestMode.None)
|
||||
Column()
|
||||
.width('100%')
|
||||
.height(2)
|
||||
.borderRadius(1)
|
||||
.backgroundColor('#C0533F')
|
||||
.hitTestBehavior(HitTestMode.None)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
@@ -29,6 +29,7 @@ import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { fbApi } from '../../data/FbHttp';
|
||||
import { FB_SESSION } from '../../data/FbSession';
|
||||
import { FB_DEVICE_STORE, FB_APP_VERSION } from '../../data/FbDeviceStore';
|
||||
import { FB_BG_SYNC } from '../../common/FbBackgroundSync';
|
||||
|
||||
const DOMAIN: number = 0x0000;
|
||||
const TAG: string = 'FbSettings';
|
||||
@@ -54,6 +55,17 @@ export struct FbSettingsPage {
|
||||
@Local tokenNote: string = '';
|
||||
/** 「手动粘贴令牌」高级入口是否展开(v55:已不是主路径) */
|
||||
@Local advancedOpen: boolean = false;
|
||||
/**
|
||||
* 🔴 v83「后台自动同步」开关的界面值。
|
||||
*
|
||||
* ⚠️ 不能直接读 `FB_BG_SYNC.isEnabled()`(它是 async / 读盘)——
|
||||
* ArkUI 的构建函数里放不了异步取值,而且读盘结果回来前会先渲染一帧默认值。
|
||||
* ⇒ 照本项目一贯的做法:`aboutToAppear` 里异步读一次落进 `@Local`,
|
||||
* 用户拨动时先改内存再异步落盘(拨了立刻有反馈)。
|
||||
*/
|
||||
@Local bgSyncOn: boolean = true;
|
||||
/** 上次同步时间的人话('' = 还没读过 / 从没同步过) */
|
||||
@Local bgLastNote: string = '';
|
||||
/** 设置页的主题色(与模块「个人设置」一致,取砖红) */
|
||||
private accent: string = '#A84A28';
|
||||
|
||||
@@ -65,6 +77,80 @@ export struct FbSettingsPage {
|
||||
*/
|
||||
aboutToAppear(): void {
|
||||
this.tokenNote = '';
|
||||
this.loadBgSync();
|
||||
}
|
||||
|
||||
/** 读后台同步开关 + 上次同步时间(异步一次,落进 @Local 供界面用) */
|
||||
private loadBgSync(): void {
|
||||
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
FB_BG_SYNC.isEnabled(ctx).then((on: boolean): void => {
|
||||
this.bgSyncOn = on;
|
||||
}).catch((): void => {
|
||||
});
|
||||
FB_BG_SYNC.lastSyncAt(ctx).then((at: number): void => {
|
||||
this.bgLastNote = this.agoText(at);
|
||||
}).catch((): void => {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 把"上次同步时间戳"说成人话。
|
||||
*
|
||||
* ⚠️ 分档不要写太细("37 分钟前"这种精度对用户没意义,还容易显得一直不更新):
|
||||
* 1 小时内按分钟、一天内按小时、再往上按天。
|
||||
* ⚠️ `at <= 0` 是"从没同步过",不能说成"1970 年"。
|
||||
*/
|
||||
private agoText(at: number): string {
|
||||
if (at <= 0) {
|
||||
return '还没同步过';
|
||||
}
|
||||
const mins: number = Math.floor((Date.now() - at) / 60000);
|
||||
if (mins < 1) {
|
||||
return '刚刚';
|
||||
}
|
||||
if (mins < 60) {
|
||||
return mins.toString() + ' 分钟前';
|
||||
}
|
||||
const hours: number = Math.floor(mins / 60);
|
||||
if (hours < 24) {
|
||||
return hours.toString() + ' 小时前';
|
||||
}
|
||||
return Math.floor(hours / 24).toString() + ' 天前';
|
||||
}
|
||||
|
||||
/**
|
||||
* 拨动「后台自动同步」开关。
|
||||
*
|
||||
* 🔴 v83(用户 2026-10-23「要有定期同步的功能,家书app在后台,也能自动同步数据,
|
||||
* 这样打开app,基本上数据就是最新的」)。
|
||||
* ⚠️ 先改内存再落盘:落盘要读 preferences(几十毫秒),
|
||||
* 等它回来才动开关的话,用户会觉得"拨了没反应"。
|
||||
* ⚠️ 关掉时要顺手取消延迟任务与短时任务(见 `FB_BG_SYNC.setEnabled`)——
|
||||
* 只写一个标记而任务还在系统里排着,等于"关了还在后台偷偷跑"。
|
||||
*/
|
||||
private toggleBgSync(on: boolean): void {
|
||||
this.bgSyncOn = on;
|
||||
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
FB_BG_SYNC.setEnabled(ctx, on).catch((err: BusinessError): void => {
|
||||
hilog.warn(DOMAIN, TAG, '写后台同步开关失败:%{public}s', JSON.stringify(err));
|
||||
});
|
||||
// 开启时立刻补一次(用户刚打开这个开关,多半就是想让数据现在对齐)
|
||||
if (on) {
|
||||
FB_BG_SYNC.runOnceResumed(ctx, 'foreground').then((): void => {
|
||||
FB_BG_SYNC.lastSyncAt(ctx).then((at: number): void => {
|
||||
this.bgLastNote = this.agoText(at);
|
||||
}).catch((): void => {
|
||||
});
|
||||
}).catch((err: BusinessError): void => {
|
||||
hilog.warn(DOMAIN, TAG, '开启后台同步后补同步失败:%{public}s', JSON.stringify(err));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -415,6 +501,55 @@ export struct FbSettingsPage {
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== 同步 ====================
|
||||
/* 🔴 v83(用户 2026-10-23):「要有定期同步的功能,家书app在后台,
|
||||
也能自动同步数据,这样打开app,基本上数据就是最新的。」
|
||||
⇒ 这一组回答用户两个问题:**它开没开**(开关)、**上次什么时候**(时间)。
|
||||
⚠️ 提示文案必须写清"多久一次",否则用户会以为"实时",
|
||||
看到 30 分钟前的数据就认为坏了(分层调度的真实节奏见
|
||||
`common/FbBackgroundSync.ets` 的文件头)。 */
|
||||
Column({ space: 0 }) {
|
||||
this.rowTitle('同步')
|
||||
Row({ space: 12 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('后台自动同步')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('退到后台也自动对齐,约 30 分钟一次')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.bgSyncOn })
|
||||
.selectedColor(FbColors.brand)
|
||||
.onChange((on: boolean): void => {
|
||||
this.toggleBgSync(on);
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 9, bottom: 9 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
this.plainRow('上次同步', this.bgLastNote === '' ? '读取中…' : this.bgLastNote)
|
||||
/* ⚠️ 说明为什么"不是即时":不给这句,用户切到别的应用改完再回来
|
||||
没看到变化,就会认为同步坏了。 */
|
||||
Text('回到家书时会立刻补一次;在别的应用里改的数据,最迟半小时能同步过来。')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
.padding({ top: 2, bottom: 2 })
|
||||
}
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ==================== 抽屉里显示哪些模块 ====================
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 8 }) {
|
||||
|
||||
Reference in New Issue
Block a user