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:
@@ -18,7 +18,8 @@ import { FbColors, FbMetric } from '../common/Theme';
|
||||
import { FbAppState, FB_WIDE_BREAKPOINT, fbAppState } from '../common/Store';
|
||||
import { FbDrawerShell } from '../components/Drawer';
|
||||
import { FbTopBar } from '../components/TopBar';
|
||||
import { FbFloatDock } from '../components/FloatDock';
|
||||
import { FB_GRIP, fbStartGripWatch } from '../common/FbGrip';
|
||||
import { FbFloatDock, FB_DOCK_HIDE_Y } from '../components/FloatDock';
|
||||
import { FbHomePage } from './home/FbHomePage';
|
||||
import { FbMoviesPage } from './movies/FbMoviesPage';
|
||||
import { FbTripsPage } from './trips/FbTripsPage';
|
||||
@@ -31,6 +32,25 @@ import { FbLedgerPage } from './ledger/FbLedgerPage';
|
||||
import { FbBooksPage } from './books/FbBooksPage';
|
||||
import { FbNotesPage, FbNotificationsPage } from './misc/FbMiscPages';
|
||||
import { FbSettingsPage } from './settings/FbSettingsPage';
|
||||
import { FbLoginPage } from './login/FbLoginPage';
|
||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { FB_SERVER_STORE } from '../data/FbServerStore';
|
||||
import { FB_DEVICE_STORE } from '../data/FbDeviceStore';
|
||||
import { FB_SESSION, SESSION_NEED_LOGIN, SESSION_OFFLINE } from '../data/FbSession';
|
||||
import { fbOnUnauthorized } from '../data/FbHttp';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
|
||||
/**
|
||||
* 小睡一会儿(毫秒)。
|
||||
*
|
||||
* ⚠️ ArkTS 里没有 `setTimeout` 的 Promise 版,启动检查要"轮询等就绪"
|
||||
* 就需要这样一个工具。用它而不是忙等(忙等会占满 CPU)。
|
||||
*/
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise<void>((resolve: () => void): void => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
@Entry
|
||||
@ComponentV2
|
||||
@@ -38,8 +58,255 @@ struct Index {
|
||||
/** 全局状态(抽屉 / 当前模块 / 宽窄屏) */
|
||||
@Local app: FbAppState = fbAppState();
|
||||
|
||||
/**
|
||||
* 是否已经"进到应用里"。
|
||||
*
|
||||
* 🔴 三种状态,必须分开(别用两个布尔硬凑):
|
||||
* · `checking` —— 启动时正在看"有没有令牌"(很短,但**必须有**,
|
||||
* 否则会先闪一下登录页再跳进主界面);
|
||||
* · 有令牌 / 用户选了"稍后再说" ⇒ `entered = true` ⇒ 显示主界面;
|
||||
* · 没令牌且用户没跳过 ⇒ 显示登录页。
|
||||
*/
|
||||
@Local entered: boolean = false;
|
||||
/** 启动检查是否已完成(false = 还在看令牌,先显示空白/logo) */
|
||||
@Local bootChecked: boolean = false;
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.measureScreen();
|
||||
this.readStatusBar();
|
||||
// 🔴 「质感握姿」(用户 2026-10-16 第 4 条):左手握→抽屉从左出、右手握→从右出。
|
||||
// ⇒ 它决定**顶栏家书图标贴哪边**(见 TopBar)与抽屉从哪侧滑出(见 Drawer)。
|
||||
// ⚠️ 能力不支持(API<20)时会自己 catch 住,不影响启动。
|
||||
fbStartGripWatch();
|
||||
/* 🔴 v57:注册「令牌失效」回调。
|
||||
任何业务请求收到 401 → 网络层直接调这里 → 清本地令牌 + 回登录页。
|
||||
⚠️ 只注册一次(重复注册会覆盖成新的 this,无害但没必要)。 */
|
||||
fbOnUnauthorized((): void => {
|
||||
this.lockOut();
|
||||
});
|
||||
this.bootCheck();
|
||||
}
|
||||
|
||||
/** 离开页面时注销握姿监听(⚠️ 不注销会在重建后叠加回调) */
|
||||
aboutToDisappear(): void {
|
||||
FB_GRIP.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读状态栏高度(由 `EntryAbility` 在全屏生效后量好、放进 AppStorage)。
|
||||
*
|
||||
* 🔴 为什么要读它(用户 2026-10-13 要"全沉浸"):
|
||||
* 打开 `setWindowLayoutFullScreen(true)` 之后窗口**不再为状态栏让位**,
|
||||
* 内容会真的画到状态栏底下。各页顶栏必须自己往下让出这一段,
|
||||
* 否则首屏"家书"标题会被时间/信号图标压住。
|
||||
* ⚠️ 存进 `app.statusBarH` 而不是各页自己读 AppStorage:
|
||||
* 它是 `@Trace` 的,量出来(异步)之后各页会**自动重排**;
|
||||
* 各页自己读一次的话,量完不会通知它们刷新。
|
||||
* ⚠️ 量不到(=0)也能用:内容仍从最顶上开始画,只是标题贴上去了 ——
|
||||
* 比猜一个数字把标题推到屏幕中间好。所以要**轮询等**一小会儿。
|
||||
*/
|
||||
private async readStatusBar(): Promise<void> {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const h: number = AppStorage.get<number>('fbStatusBarH') ?? -1;
|
||||
if (h >= 0) {
|
||||
this.app.statusBarH = h;
|
||||
return;
|
||||
}
|
||||
await sleep(50);
|
||||
}
|
||||
this.app.statusBarH = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动检查:有令牌就直接进主界面,否则显示登录页。
|
||||
*
|
||||
* ⚠️ 令牌与**服务器地址**的读取都是**同步**的(见两个 store 的说明),
|
||||
* 所以这里其实很快;但要 `await` 一下它们的**预加载**,
|
||||
* 否则会误判成"没登录 / 没配地址"(EntryAbility 里是异步预加载的)。
|
||||
* ⇒ 用一个小轮询等它们就绪(最多约 1 秒),避免"有令牌却显示登录页"
|
||||
* 以及"明明配过地址却让人重填"。
|
||||
*/
|
||||
private async bootCheck(): Promise<void> {
|
||||
for (let i = 0; i < 40; i++) {
|
||||
/* 🔴 四个都要就绪:
|
||||
· 令牌 —— 有没有登录过(决定要不要验);
|
||||
· 地址 —— 打哪个服务器(v55 起地址是持久化配置);
|
||||
· 设备身份 —— 登录/校验请求要带 device_id(v57);
|
||||
· 会话时间 —— 离线多久了(v57,离线超 24h 判锁)。
|
||||
只等其中一部分,会出现"有令牌却显示登录页"或"没 device_id 就去登录"
|
||||
这种看起来莫名其妙的状态。 */
|
||||
if (FB_TOKEN_STORE.isReady() && FB_SERVER_STORE.isReady()
|
||||
&& FB_DEVICE_STORE.isReady() && FB_SESSION.isReady()) {
|
||||
break;
|
||||
}
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
if (!FB_TOKEN_STORE.has()) {
|
||||
// 从没登录过(或已退出登录):停在登录页。
|
||||
// ⚠️ 首次使用允许"先不登录"(本地优先),但**被踢/退出后不允许** ——
|
||||
// 那会把"v57 的安全策略"变成一个可绕过的按钮(见 build 里的 allowSkip)。
|
||||
this.entered = false;
|
||||
this.bootChecked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
/* 🔴 v57(用户 2026-10-17):「每过 24 小时,我们就要请求一下服务器,
|
||||
去验证一下这个 Token 是不是还是有效的」+「应用一打开就发现 Token 失效」。
|
||||
⇒ 有令牌就**每次启动都验一次**(不是每 24h 才验一次)。
|
||||
只有"打开时联网"才可能发现被踢,这一步就是那个时机。 */
|
||||
const ctx: common.Context | undefined = this.abilityCtx();
|
||||
if (ctx === undefined) {
|
||||
// 拿不到上下文(极罕见):不要把用户卡死,按本地数据先用着
|
||||
this.entered = true;
|
||||
this.bootChecked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const r = await FB_SESSION.check(ctx);
|
||||
if (r.state === SESSION_NEED_LOGIN) {
|
||||
this.app.lockReason = r.reason;
|
||||
this.app.loginStartStep = 2;
|
||||
this.app.needLogin = true;
|
||||
this.app.offlineLocked = false;
|
||||
this.entered = false;
|
||||
} else if (r.state === SESSION_OFFLINE) {
|
||||
/* 离线超过 24 小时:**锁界面但不删令牌**。
|
||||
联网校验通过后自动恢复 —— 用户不用重新输密码(这正是他要的
|
||||
"一直联网就不用每天登录")。 */
|
||||
this.app.lockReason = r.reason;
|
||||
this.app.offlineLocked = true;
|
||||
this.app.needLogin = false;
|
||||
this.entered = false;
|
||||
} else {
|
||||
this.app.lockReason = '';
|
||||
this.app.needLogin = false;
|
||||
this.app.offlineLocked = false;
|
||||
this.entered = true;
|
||||
}
|
||||
this.bootChecked = true;
|
||||
}
|
||||
|
||||
/** 取 Ability 上下文(失败返回 undefined,不抛) */
|
||||
private abilityCtx(): common.Context | undefined {
|
||||
try {
|
||||
return this.getUIContext().getHostContext();
|
||||
} catch (err) {
|
||||
hilog.warn(0x0000, 'FamilyBook', '取 Ability Context 失败:%{public}s', JSON.stringify(err));
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v57:离线超过 24 小时的锁屏。
|
||||
*
|
||||
* 用户原话:「离线超过 24 个小时就硬退出。」
|
||||
*
|
||||
* ⚠️ 这里**不做成登录页**:用户只是没联网,不该被要求重输密码。
|
||||
* 联网后校验通过会自动回主界面(见 `retrySession`);
|
||||
* 只有当服务端明确回 401(= 真被踢了)才转去登录页。
|
||||
*/
|
||||
@Builder
|
||||
offlineLockView() {
|
||||
Column() {
|
||||
Blank()
|
||||
Column() {
|
||||
Text('需要联网确认')
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.app.lockReason)
|
||||
.fontSize(14)
|
||||
.fontColor(FbColors.muted)
|
||||
.textAlign(TextAlign.Center)
|
||||
.lineHeight(21)
|
||||
.margin({ top: 12 })
|
||||
Button('重新连接')
|
||||
.fontSize(15)
|
||||
.height(44)
|
||||
.width(170)
|
||||
.margin({ top: 26 })
|
||||
.onClick((): void => {
|
||||
this.retrySession();
|
||||
})
|
||||
Text('数据还在。联网确认后自动恢复,不用重新登录。')
|
||||
.fontSize(12)
|
||||
.fontColor(FbColors.faint)
|
||||
.textAlign(TextAlign.Center)
|
||||
.lineHeight(18)
|
||||
.margin({ top: 14 })
|
||||
}
|
||||
.padding({ left: 32, right: 32 })
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
/** 离线锁屏上的「重新连接」:再验一次,通过就自动放行 */
|
||||
private async retrySession(): Promise<void> {
|
||||
const ctx: common.Context | undefined = this.abilityCtx();
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
const r = await FB_SESSION.check(ctx);
|
||||
if (r.state === SESSION_NEED_LOGIN) {
|
||||
// 联网之后服务端说"这枚令牌没了" → 这才是真的被踢,转登录页
|
||||
this.app.offlineLocked = false;
|
||||
this.app.needLogin = true;
|
||||
this.app.loginStartStep = 2;
|
||||
this.app.lockReason = r.reason;
|
||||
this.entered = false;
|
||||
} else if (r.state === SESSION_OFFLINE) {
|
||||
this.app.lockReason = r.reason; // 还是连不上,留在锁屏
|
||||
} else {
|
||||
this.app.offlineLocked = false;
|
||||
this.app.needLogin = false;
|
||||
this.app.lockReason = '';
|
||||
this.entered = true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 🔴 v57:令牌失效(任何请求 401)→ 立刻锁定回登录页。
|
||||
*
|
||||
* 用户原话:「运行期间任何一个请求返回 401,立刻锁定跳登录页 ——
|
||||
* 这是真正的兜底,因为同步/写入本来就在不断发请求。」
|
||||
*
|
||||
* ⚠️ 由 `FbHttp` 的网络层统一回调(不是每个页面自己判),
|
||||
* 所以**漏不掉**;登录接口自己传了 `authProbe` 不会触发。
|
||||
*/
|
||||
private lockOut(): void {
|
||||
if (this.app.needLogin) {
|
||||
return; // 已经在登录页了,别反复设状态(会让界面闪)
|
||||
}
|
||||
const ctx: common.Context | undefined = this.abilityCtx();
|
||||
if (ctx !== undefined) {
|
||||
// ⚠️ 先清本地令牌(主防线:捡到手机的人拿不到它),再通知界面
|
||||
FB_TOKEN_STORE.clear(ctx).catch((err: BusinessError): void => {
|
||||
hilog.warn(0x0000, 'FamilyBook', '清令牌失败:%{public}s', JSON.stringify(err));
|
||||
});
|
||||
FB_SESSION.clear(ctx).catch((err: BusinessError): void => {
|
||||
hilog.warn(0x0000, 'FamilyBook', '清会话记录失败:%{public}s', JSON.stringify(err));
|
||||
});
|
||||
}
|
||||
this.app.lockReason = '这台设备已经被退出登录,请重新登录';
|
||||
this.app.loginStartStep = 2;
|
||||
this.app.needLogin = true;
|
||||
this.app.offlineLocked = false;
|
||||
this.entered = false;
|
||||
}
|
||||
|
||||
/** 登录成功:进主界面(会话信息已由登录页写进 `app.session`) */
|
||||
private onLoggedIn(): void {
|
||||
this.app.session.loggedIn = true;
|
||||
// 🔴 复位"要登录"的请求 —— 不复位的话,下一次渲染又会判回登录页(死循环)
|
||||
this.app.needLogin = false;
|
||||
this.app.loginStartStep = 0;
|
||||
// v57:登录成功 = 会话刚校验过,两种锁都要撤掉
|
||||
this.app.lockReason = '';
|
||||
this.app.offlineLocked = false;
|
||||
this.entered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,27 +335,97 @@ struct Index {
|
||||
// 🔴 用 Stack 而不是 Column:底部快捷栏要**浮在内容之上**(用户要求"不要固定在下边,
|
||||
// 要浮着")。写在 Column 里它就是"占一行的固定底栏"(老样式)。
|
||||
Stack({ alignContent: Alignment.Bottom }) {
|
||||
Column() {
|
||||
FbDrawerShell({ app: this.app }) {
|
||||
this.mainArea()
|
||||
// ---- 启动检查还没完:先给一块底色,别闪 ----
|
||||
if (!this.bootChecked) {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
} else if (this.app.offlineLocked) {
|
||||
// ---- v57:离线超过 24 小时 —— 锁界面(**不清令牌**,联网即恢复)----
|
||||
this.offlineLockView()
|
||||
} else if (!this.entered || this.app.needLogin) {
|
||||
// ---- 未登录 / 被要求重新登录:登录页(**不显示抽屉/悬浮栏**)----
|
||||
// 🔴 v55:`needLogin` 是设置页「退出登录 / 更换服务器」置的位 ——
|
||||
// 本应用没有路由栈,登录页只能靠这个状态位回到前台。
|
||||
FbLoginPage({
|
||||
// 传 app 只为让它拿到**状态栏高度**(全屏布局下登录页也要自己让位)
|
||||
app: this.app,
|
||||
// 从第几步开始(1=连地址一起换,2=只重登,0=自动)
|
||||
startStep: this.app.loginStartStep,
|
||||
/* 🔴 v57:只有"从没登录过"才允许「先不登录」。
|
||||
被踢 / 主动退出后必须重新登录 —— 用户原话:
|
||||
「退到登录页面之后,里面所有的资源、所有的内容都是不允许看的。」
|
||||
留着那个按钮就等于留了一扇后门(v58 上了加密才真正堵死)。 */
|
||||
allowSkip: this.app.lockReason === '',
|
||||
onLoggedIn: (): void => {
|
||||
this.onLoggedIn();
|
||||
},
|
||||
onSkip: (): void => {
|
||||
// 「先不登录」:不登录也进主界面 —— 本地优先架构下,
|
||||
// 没令牌照样能看已同步的数据。把用户堵在登录页是错的。
|
||||
// ⚠️ 必须同时复位 needLogin,否则又会被判回登录页。
|
||||
this.app.needLogin = false;
|
||||
this.app.loginStartStep = 0;
|
||||
this.entered = true;
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// ---- 主界面 ----
|
||||
Column() {
|
||||
FbDrawerShell({ app: this.app }) {
|
||||
this.mainArea()
|
||||
}
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
// 底部悬浮快捷栏(日程 / 名册 / 任务 / 笔记)
|
||||
// ⚠️ 只在窄屏显示:宽屏时抽屉常驻,且屏幕宽,用不上这个快捷入口。
|
||||
// ⚠️ 抽屉展开时不显示 —— 那时用户正在抽屉里选模块,浮层再叠一个反而乱。
|
||||
if (!this.app.wideLayout && !this.app.drawerOpen) {
|
||||
FbFloatDock({ app: this.app })
|
||||
// 底部悬浮快捷栏(日程 / 名册 / 任务 / 笔记)
|
||||
// ⚠️ 只在窄屏显示:宽屏时抽屉常驻,且屏幕宽,用不上这个快捷入口。
|
||||
// ⚠️ 抽屉展开时不显示 —— 那时用户正在抽屉里选模块,浮层再叠一个反而乱。
|
||||
// 🔴 **浮层开着时也不显示**(用户 2026-10-15 实测的真 bug):
|
||||
// 悬浮栏在**比页面浮层更外层**的 Stack 里 ⇒ 层级更高。
|
||||
// 详情卡片底部的「编辑 / 关闭」(y≈2207-2331)与悬浮栏(y≈2240-2350)
|
||||
// 在屏幕上重叠,点按钮被悬浮栏抢走 ⇒ **直接跳到日历页**。
|
||||
// ⇒ 用 `sheetOpen()` 让位(收起 + 不接收点击)。
|
||||
if (!this.app.wideLayout && !this.app.drawerOpen && !this.app.sheetOpen()) {
|
||||
// ⚠️ 外面这层 Column 是**必需的**:Stack 里的子组件会按内容宽度收缩,
|
||||
// 不给它一个 width('100%') 的宿主,`FbFloatDock` 内部的左右内缩就无处生效
|
||||
// (悬浮栏会缩成一小团靠在某一边)。
|
||||
Column() {
|
||||
FbFloatDock({ app: this.app })
|
||||
}
|
||||
.width('100%')
|
||||
// 🔴 **随滚动收起 / 唤回**(用户 2026-10-13 要的 IT 之家那套):
|
||||
// 向上滚内容 ⇒ 向下平移出屏幕(连同透明度一起淡出);
|
||||
// 向下滚回看 ⇒ 平移回原位。
|
||||
//
|
||||
// ⚠️ 用 `translate` 平移而不是 `visibility` 开关:
|
||||
// 平移是**连续的**(有动画过程),看起来是"滑走";
|
||||
// 显隐是突变的,看着像"啪一下没了"。
|
||||
// ⚠️ 平移量取 `FB_DOCK_HIDE_Y`(= 栏高 + 下边距 + 余量)——
|
||||
// 必须**完全移出可视区**,否则底下会露出半截胶囊边。
|
||||
// 用导出的常量而不是写死数字:它和栏高/边距是同一个几何量,
|
||||
// 写死两处必然漂移。
|
||||
.translate({ y: this.app.dockHidden ? FB_DOCK_HIDE_Y + 'vp' : '0vp' })
|
||||
.opacity(this.app.dockHidden ? 0 : 1)
|
||||
// 位移动画:出入对称、约 220ms 是"跟手但不拖沓"的区间
|
||||
.animation({ duration: 220, curve: Curve.Friction })
|
||||
}
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
// 沉浸式:让**根容器背景**延伸到状态栏/手势条区域,但不移动子组件。
|
||||
// 配合 EntryAbility 里"系统栏与顶栏同色",通知栏与标题栏融为一体。
|
||||
.backgroundColor(FbColors.paper)
|
||||
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
|
||||
// 🔴 这里**不再用 `expandSafeArea`**(用户 2026-10-13 改"全沉浸"后)。
|
||||
//
|
||||
// 原因:安全区扩展只在**窗口仍然避让系统栏**的前提下才有意义
|
||||
// (它做的事是"让背景多画进系统栏那条,但子组件不动")。
|
||||
// 现在窗口已经开了 `setWindowLayoutFullScreen(true)`——
|
||||
// 内容本来就铺满整屏、系统栏也全透明,再叠加 `expandSafeArea` 是多余的;
|
||||
// 而且两者叠加会出现"背景扩一次、内容又扩一次"的重复计算风险。
|
||||
// ⇒ 全屏这条路线下,**让位交给顶栏自己的 padding.top**(见 FbTopBar),
|
||||
// 边界一清二楚。
|
||||
}
|
||||
|
||||
/** 内容区:当前模块页(含顶栏 —— 顶栏由页面放进自己的滚动流,见 topBarSlot) */
|
||||
@@ -112,6 +449,12 @@ struct Index {
|
||||
* ⚠️ 状态栏由系统直接涂成白色(EntryAbility 里 `statusBarColor = surface`,
|
||||
* 与顶栏同色)。所以顶栏滚走之后,通知栏区域仍由系统保持白色,
|
||||
* 不会露出页面底色 —— 不需要在页面上再铺"白色压条"。
|
||||
*
|
||||
* ⚠️ v55 顺手清掉了一段残留:这上面原本还有一个**孤立的 `@Builder`**
|
||||
* (是早期"临时诊断条"删剩的壳)。它让编译器报
|
||||
* "Duplicate '@Builder' decorators for method are not allowed" ——
|
||||
* 行为上没坏(生效的是下面这个),但每轮编译都刷一条假错误,
|
||||
* 真出问题时很容易被它盖住。
|
||||
*/
|
||||
@Builder
|
||||
topBarSlot() {
|
||||
|
||||
Reference in New Issue
Block a user