From 0816e2598869fabd2d3aa89dda63ab5bd8367ccb Mon Sep 17 00:00:00 2001 From: laoyang Date: Mon, 12 Oct 2026 10:06:55 +0800 Subject: [PATCH] =?UTF-8?q?v0.4.0=20=E9=A1=B6=E6=A0=8F=E6=94=B9=E7=99=BD?= =?UTF-8?q?=E8=89=B2=20+=20=E9=80=9A=E7=9F=A5=E6=A0=8F=E5=90=8C=E8=89=B2?= =?UTF-8?q?=20+=20=E9=A1=B6=E6=A0=8F=E9=9A=8F=E9=A1=B5=E9=9D=A2=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 08:56 的三条要求: 1. 家书标题栏用浅色(下面色块的背景色); 2. 上面通知栏也是浅色,两者融为一体; 3. 向上滑动时标题栏跟着向上滑,不固定在原位。 (追问确认:顶栏底色 = 白;「品牌条 + 页面标题条」一起随滚动消失。) ## ① 系统栏改「不透明 + 与顶栏同色」,弃用透明方案 原来走「系统栏全透明 + 让紧邻元素透出同底色」。这版顶栏要用白色, 若仍走透明方案就得再造一层"白色压条"去刷白状态栏,太绕。 ⇒ 直接 `statusBarColor = surface`(白)、`navigationBarColor = page_bg`(纸色)。 这是**配置出来的同色**,比"指望紧邻元素恰好同色"稳得多(后者顶栏一改底色就出色带)。 副产品:不再需要 `setWindowBackgroundColor` ⇒ 连那条 unavailable 告警也消失, 编译回到 **0 error / 0 warning**。 ## ② 顶栏随滚动:整页包进 Scroll,顶栏作为内容第一个元素 原来 `Column { 页头; List/Grid(layoutWeight 1) }` —— 顶栏在滚动区**外面**, 所以固定不动。改为 `Scroll { Column { this.topBar(); 页头; 列表 } }`。 顶栏由 Index 以 `@BuilderParam topBar` 插槽下发(`topBarSlot`),13 个页面全部接入。 ⚠️ 同步必须处理的两点: · 列表的 `.layoutWeight(1)` 要删 —— Scroll>Column 里高度由内容决定, layoutWeight 会去分"剩余空间"(=0) ⇒ 列表塌成 0 高、整页只剩顶栏和页头; · List/Grid 不能自己再滚动(去 scrollBar/edgeEffect)—— 两层滚动容器会抢手势。 ⚠️ ArkUI 限制:带尾随 lambda 的组件只能有 1 个 `@BuilderParam` (首页原有闲置的 topActions 已删)。 ## ③ 顶栏白色 + logo 可辨认 logo 是「米色底(74%) + 砖红图案(24%)」的实心方图,放在米色上底板隐形、 只剩图案发飘(用户说"看不清")。今加一层极浅描边把底板"托"出来。 顶栏高度 56→48vp。 ## 验证(真机,全部通过) · 沉浸式:状态栏色 `#FFFFFF` == 顶栏色 `#FFFFFF` ✓ · 顶栏随滚动:「家书」标题 y=140 → 滑动后**已滚出可视区** ✓ · 顶栏滚走后状态栏仍为 `#FFFFFF`(未露出页面内容)✓ · 14 个页面遍历 **14/14**;深验(每页特征文本)**12/12** · 编译 **0 error / 0 warning** ## 过程记录:批量改 12 个文件失败两次(已回滚重做) 第 1 版按缩进正则切分 ⇒ 闭合括号错乱;第 2 版跳字符时**没消费 Column 的 修饰符链** ⇒ 吃掉 `@Builder` 声明、**636 个报错**。 第 3 版改为**按行 + 括号深度解析**,且**改完立刻做结构断言** (括号配平、`this.topBar()` 恰好 +1),不满足就不写入;先单页试跑再推广。 教训:批量改多文件时正则不可靠,必须配结构自检 + 单页试点。 配套修掉 `_tooling/familybook_harmonyos_run.sh` 一个"假成功": `hdc list targets` 无设备时输出字面量 `[Empty]`,原来"非空即通过" ⇒ 拿 `[Empty]` 当序列号去装、还打印「✓ 设备:[Empty]」。已显式排除并 die。 --- entry/src/main/ets/components/TopBar.ets | 44 +- .../main/ets/entryability/EntryAbility.ets | 35 +- entry/src/main/ets/pages/Index.ets | 58 +-- .../src/main/ets/pages/books/FbBooksPage.ets | 109 ++--- .../ets/pages/calendar/FbCalendarPage.ets | 359 +++++++++-------- entry/src/main/ets/pages/days/FbDaysPage.ets | 209 +++++----- entry/src/main/ets/pages/home/FbHomePage.ets | 10 +- .../main/ets/pages/ledger/FbLedgerPage.ets | 381 +++++++++--------- entry/src/main/ets/pages/misc/FbMiscPages.ets | 217 +++++----- .../main/ets/pages/movies/FbMoviesPage.ets | 180 +++++---- .../main/ets/pages/people/FbPeoplePage.ets | 44 +- .../ets/pages/resources/FbResourcesPage.ets | 135 ++++--- .../ets/pages/settings/FbSettingsPage.ets | 8 + .../src/main/ets/pages/tasks/FbTasksPage.ets | 135 ++++--- .../src/main/ets/pages/trips/FbTripsPage.ets | 99 +++-- 15 files changed, 1081 insertions(+), 942 deletions(-) diff --git a/entry/src/main/ets/components/TopBar.ets b/entry/src/main/ets/components/TopBar.ets index e71a85d..b009729 100644 --- a/entry/src/main/ets/components/TopBar.ets +++ b/entry/src/main/ets/components/TopBar.ets @@ -1,18 +1,18 @@ /** * 家书 FamilyBook · 顶栏(鸿蒙端) * - * 形态照用户指的本机另一应用(SyncCalendar): - * **应用图标 + 标题贴左上,右侧放通知/头像** —— 就是一个普通 Row, - * **自己不做任何状态栏留白**。 + * 用户 2026-10-12 定的形态: + * · **应用图标 + 「家书」标题**贴左上(下行小字显示当前模块); + * · 顶栏底色**白色**(与下面的个人信息卡同色,连成一片); + * · 顶栏**并入页面滚动流**,向上滑动时跟着内容一起滑走 + * (所以它本身不固定、也不自己处理状态栏留白)。 * - * 🔴 沉浸式靠两层配合,顶栏本身不参与: - * ① EntryAbility:系统栏涂成全透明(`#00000000`); - * ② 根容器:`expandSafeArea` 让背景铺到状态栏区域。 - * 于是「顶栏上方那一块」(其实是根容器背景)与通知栏完全融为一体, - * 而顶栏内容本身就在安全区内、位置不动 —— 不用算状态栏高度,也就不会算错。 + * 🔴 与状态栏的关系:顶栏是白色,而系统栏被涂成全透明、透出的是**根容器底色**。 + * 所以要让"通知栏与顶栏融为一体",根容器在顶栏这一段必须是白色 —— + * 由 `Index.ets` 在顶部铺一层与顶栏同高的白色块来实现(页面滚动时它不动, + * 保证通知栏区域永远是白的,不会滚出内容)。 * - * ⚠️ 顶栏**不放**「设置」—— 与 Web 端一致:设置收在抽屉底部的账号区里, - * 顶栏只留日常入口,再把管理入口顶出来会把顶栏挤满。 + * ⚠️ 顶栏**不放**「设置」—— 与 Web 端一致:设置收在抽屉底部的账号区里。 */ import { FbColors, FbIcon, FbMetric } from '../common/Theme'; @@ -46,11 +46,15 @@ export struct FbTopBar { }) } - // ---- 应用图标 + 标题(贴左,与抽屉里的品牌区同款,一眼认出是"家书")---- + // ---- 应用图标 ---- + // ⚠️ 必须给描边:logo 是「米色底(74%) + 砖红图案(24%)」的实心方图, + // 放在白色/米色上时底板与背景几乎同色,只剩砖红图案发飘、看不清。 + // 一层极浅的描边就能把底板"托"出来(本机范本 SyncCalendar 也给了描边)。 Image($r('app.media.fb_logo')) .width(30) .height(30) .borderRadius(8) + .border({ width: 1, color: FbColors.line }) .flexShrink(0) Column({ space: 0 }) { @@ -59,11 +63,10 @@ export struct FbTopBar { .fontWeight(FontWeight.Bold) .fontColor(FbColors.ink) .maxLines(1) - // 副标题显示当前模块名 —— 这样"品牌 + 当前位置"一行说清, - // 也省掉原来那个只写模块名的孤零零标题 + // 副标题显示当前模块名 —— "品牌 + 当前位置"一行说清 Text(this.moduleName()) .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) + .fontColor(FbColors.muted) .maxLines(1) } .alignItems(HorizontalAlign.Start) @@ -111,17 +114,12 @@ export struct FbTopBar { } .width('100%') .height(FbMetric.topBarH) - // 左右留 14;上下尽量薄(顶栏默认贴状态栏下方,不留额外空隙) .padding({ left: 14, right: 10, top: 4, bottom: 4 }) .alignItems(VerticalAlign.Center) - // 🔴 **不设 backgroundColor** —— 这是"融为一体"的最后一块拼图。 - // 顶栏没设背景时,它透出的是根容器的底色(page_bg,宣纸米); - // 而状态栏被涂成全透明、透出的也是根容器底色 ⇒ 两者**天然同色**。 - // - // ⚠️ 之前给顶栏设了 `FbColors.surface`(白色),于是变成 - // 「状态栏(米色)+ 顶栏(白色)」上下拼接 —— 正是用户说的"没融为一体"。 - // 本机范本 SyncCalendar 的 header() 也**没有 backgroundColor**,就是这个道理。 - // 教训:想与系统栏同色,就别给紧邻元素单独上色,让它透出共同底色。 + // 顶栏底色 = 白色(与下面的个人信息卡同色) + .backgroundColor(FbColors.surface) + // 底线:与下方内容(同是白色卡)之间给一条极浅的分隔,避免"两块白糊在一起" + .border({ width: { bottom: 1 }, color: FbColors.line2 }) } private moduleName(): string { diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index e8380ea..02e745d 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -128,32 +128,29 @@ export default class EntryAbility extends UIAbility { /** * 系统栏配色。 * - * 🔴 核心手法(照 SyncCalendar 那套,实测最干净): - * **系统栏涂成全透明 `#00000000`**,再让窗口底色 = 页面背景色透出来。 - * 这样顶部通知栏的颜色天然等于"它下面那一块"的颜色 —— 就是用户要的 - * 「上面那一块和通知栏完全融为一体」。 + * 用户 2026-10-12 定的要求: + * ① 家书标题栏用**浅色(白)**; + * ② 上面通知栏也是**浅色**,两者融为一体。 * - * ⚠️ 曾经的做法是把状态栏设成某个具体色(先设页面底色、又改成顶栏底色), - * 结果都不对:状态栏是**独立于页面**的一层,给它写死颜色就必然和它下面的 - * 内容割裂(顶栏是白的、状态栏是米色的,看着就是"没对齐")。 - * 透明才是正解 —— 它不需要知道下面是白还是纸色。 + * ⇒ 做法:把状态栏设成与顶栏**同一个颜色**(`surface` = 白)。 + * 这是一个"配置出来的"同色,比"让系统栏透明、再指望紧邻元素恰好同色"稳得多 —— + * 后者一旦顶栏改了底色就会立刻出现色带(已经踩过一次)。 * - * ⚠️ `setWindowBackgroundColor` 对 phone/tablet 会报 unavailable(编译器告警), - * 但**这里必须调**:透明系统栏要透出的就是它。告警可忽略(SyncCalendar 同样这么用)。 + * ⚠️ 底部导航条取**页面底色**(宣纸米),与页面下部同色,不突兀。 + * ⚠️ 取色走 resourceManager(经 dark/ 限定符解析)—— 深色模式一并生效。 */ private applySystemBars(win: window.Window): void { - const pageBg: string = this.themeColor($r('app.color.page_bg').id, '#F7F3EA'); - // 前景(状态栏里的时间/电量)按页面底色亮度取反,深色模式自动变浅色字 - const fg: string = this.isDarkBg(pageBg) ? '#FFFFFF' : '#2C231D'; + const surfaceBg: string = this.themeColor($r('app.color.surface').id, '#FFFFFF'); + const paperBg: string = this.themeColor($r('app.color.page_bg').id, '#F7F3EA'); try { win.setWindowSystemBarProperties({ - statusBarColor: '#00000000', - statusBarContentColor: fg, - navigationBarColor: '#00000000', - navigationBarContentColor: fg + // 顶部:与顶栏同色(白)⇒ 通知栏与标题栏融为一体 + statusBarColor: surfaceBg, + statusBarContentColor: this.isDarkBg(surfaceBg) ? '#FFFFFF' : '#2C231D', + // 底部:与页面底色同色 + navigationBarColor: paperBg, + navigationBarContentColor: this.isDarkBg(paperBg) ? '#FFFFFF' : '#2C231D' }); - // 窗口底色 = 页面背景色,供透明系统栏透出 - win.setWindowBackgroundColor(pageBg); } catch (err) { hilog.error(DOMAIN, 'FamilyBook', 'setSystemBarProperties failed: %{public}s', JSON.stringify(err)); diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 69745a0..f1f555c 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -82,58 +82,64 @@ struct Index { .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } - /** 内容区:顶栏 + 当前模块页 */ + /** 内容区:当前模块页(含顶栏 —— 顶栏由页面放进自己的滚动流,见 topBarSlot) */ @Builder mainArea() { Column() { - // 顶栏:不再需要自己做状态栏留白(背景已由 expandSafeArea 铺到顶, - // 内容本身就在安全区内) - FbTopBar({ app: this.app }) { - } - - // ---- 模块页 ---- - Column() { - this.moduleContent() - } - .layoutWeight(1) - .width('100%') + this.moduleContent() } .width('100%') .height('100%') .backgroundColor(FbColors.paper) } + /** + * 顶栏内容(交给各模块页放进**自己的滚动流最前面** ⇒ 跟着内容一起滚走)。 + * + * 🔴 用户要求:「向上滑动的时候,标题栏也跟着向上滑动,不需要固定在当前位置」 + * ⇒ 顶栏不能再像以前那样摆在滚动容器**外面**(那样它是固定的)。 + * 交给页面承载、页面把它放在 Scroll/List 的内容最前面,才会跟着滚。 + * + * ⚠️ 状态栏由系统直接涂成白色(EntryAbility 里 `statusBarColor = surface`, + * 与顶栏同色)。所以顶栏滚走之后,通知栏区域仍由系统保持白色, + * 不会露出页面底色 —— 不需要在页面上再铺"白色压条"。 + */ + @Builder + topBarSlot() { + FbTopBar({ app: this.app }) { + } + } + /** 按当前模块渲染对应页面(用 if/else 分发:ArkUI 的 build 里不允许 switch) */ @Builder moduleContent() { if (this.app.activeModule === 'home') { - FbHomePage({ app: this.app }) { - } + FbHomePage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'people' || this.app.activeModule === 'kin') { - FbPeoplePage({ app: this.app }) + FbPeoplePage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'movies') { - FbMoviesPage({ app: this.app }) + FbMoviesPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'trips') { - FbTripsPage({ app: this.app }) + FbTripsPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'calendar') { - FbCalendarPage({ app: this.app }) + FbCalendarPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'tasks') { - FbTasksPage({ app: this.app }) + FbTasksPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'resources') { - FbResourcesPage({ app: this.app }) + FbResourcesPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'days') { - FbDaysPage({ app: this.app }) + FbDaysPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'ledger') { - FbLedgerPage({ app: this.app }) + FbLedgerPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'books') { - FbBooksPage({ app: this.app }) + FbBooksPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === 'notes') { - FbNotesPage({ app: this.app }) + FbNotesPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === '__notifications') { - FbNotificationsPage({ app: this.app }) + FbNotificationsPage({ app: this.app, topBar: this.topBarSlot }) } else if (this.app.activeModule === '__settings') { - FbSettingsPage({ app: this.app }) + FbSettingsPage({ app: this.app, topBar: this.topBarSlot }) } else { // 兜底:模块还没做页面时给一句提示,而不是白屏 Column({ space: 8 }) { diff --git a/entry/src/main/ets/pages/books/FbBooksPage.ets b/entry/src/main/ets/pages/books/FbBooksPage.ets index 18a9bff..7f5a252 100644 --- a/entry/src/main/ets/pages/books/FbBooksPage.ets +++ b/entry/src/main/ets/pages/books/FbBooksPage.ets @@ -16,6 +16,12 @@ import { FbEmptyBox, FbIconButton, FbProgressLine, FbRating, FbTag } from '../.. @ComponentV2 export struct FbBooksPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 模块主题色(读书 = 藕紫) */ @@ -23,61 +29,66 @@ export struct FbBooksPage { private all: FbBookItem[] = FB_DEMO_BOOKS; build() { - Column() { - // ---- 页头 ---- - Row({ space: 8 }) { - Column({ space: 2 }) { - Text('书架') - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text(this.all.length.toString() + ' 本 · ' - + this.readingCount().toString() + ' 本在读 · ' - + this.noteCount().toString() + ' 条笔记') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) + Scroll() { + Column() { + this.topBar() - // 上传 / 统计(Web 端在设置菜单里) - FbIconButton({ icon: $r('app.media.fb_ic_chart'), tint: FbColors.muted }) - FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) + // ---- 页头 ---- + Row({ space: 8 }) { + Column({ space: 2 }) { + Text('书架') + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text(this.all.length.toString() + ' 本 · ' + + this.readingCount().toString() + ' 本在读 · ' + + this.noteCount().toString() + ' 条笔记') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + // 上传 / 统计(Web 端在设置菜单里) + FbIconButton({ icon: $r('app.media.fb_ic_chart'), tint: FbColors.muted }) + FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) + .alignItems(VerticalAlign.Center) + + // ---- 分组列表 ---- + if (this.all.length === 0) { + FbEmptyBox({ + title: '书架还是空的', + hint: '传一本进来,或先记下想读的书', + icon: $r('app.media.fb_ic_books') + }) + } else { + List({ space: 0 }) { + ForEach(this.groups(), (g: FbGroup) => { + ListItemGroup({ header: this.groupHeader(g.title, g.count) }) { + ForEach(g.items, (b: FbBookItem) => { + ListItem() { + this.bookRow(b) + } + }, (b: FbBookItem): string => 'bk_' + b.id.toString()) + } + }, (g: FbGroup): string => 'bg_' + g.key) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + } } .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) - .alignItems(VerticalAlign.Center) - - // ---- 分组列表 ---- - if (this.all.length === 0) { - FbEmptyBox({ - title: '书架还是空的', - hint: '传一本进来,或先记下想读的书', - icon: $r('app.media.fb_ic_books') - }) - } else { - List({ space: 0 }) { - ForEach(this.groups(), (g: FbGroup) => { - ListItemGroup({ header: this.groupHeader(g.title, g.count) }) { - ForEach(g.items, (b: FbBookItem) => { - ListItem() { - this.bookRow(b) - } - }, (b: FbBookItem): string => 'bk_' + b.id.toString()) - } - }, (g: FbGroup): string => 'bg_' + g.key) - } - .layoutWeight(1) - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) - .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) - } } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } /** 一本书:书脊色块(代替封面)+ 书名/作者 + 评分/笔记/进度 */ diff --git a/entry/src/main/ets/pages/calendar/FbCalendarPage.ets b/entry/src/main/ets/pages/calendar/FbCalendarPage.ets index 86008cd..c6d7bf6 100644 --- a/entry/src/main/ets/pages/calendar/FbCalendarPage.ets +++ b/entry/src/main/ets/pages/calendar/FbCalendarPage.ets @@ -39,6 +39,12 @@ interface FbCalCell { @ComponentV2 export struct FbCalendarPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 模块主题色(日程 = 琥珀) */ @@ -56,191 +62,196 @@ export struct FbCalendarPage { private weekLabels: string[] = ['一', '二', '三', '四', '五', '六', '日']; build() { - Column() { - // ---- 页头:月份 + 视图切换 + 新建 + 设置 ---- - Row({ space: 8 }) { - Column({ space: 2 }) { - Text(this.showYear.toString() + ' 年 ' + this.showMonth.toString() + ' 月') - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text(this.visibleBooks().length.toString() + ' 本日历可见 · 本月 ' - + this.monthEventCount().toString() + ' 条') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) + Scroll() { + Column() { + this.topBar() - FbPrimaryButton({ - label: '新建', - bg: this.accent, - icon: $r('app.media.fb_ic_plus') - }) - FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) - } - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) - .alignItems(VerticalAlign.Center) - - Scroll() { - Column({ space: 0 }) { - - // ---- 月历卡 ---- - Column({ space: 0 }) { - // 月份切换条 - Row() { - this.navBtn('上一月', -1) - Blank() - Text('今天') - .fontSize(FbMetric.fontSmall) - .fontColor(this.accent) - .padding({ left: 12, right: 12, top: 5, bottom: 5 }) - .backgroundColor(fbPalette(this.accent).brand + '14') - .borderRadius(FbMetric.radiusPill) - .onClick(() => { - const d: Date = new Date(); - this.showYear = d.getFullYear(); - this.showMonth = d.getMonth() + 1; - this.pickedDay = d.getDate(); - }) - Blank() - this.navBtn('下一月', 1) - } - .width('100%') - .padding({ bottom: 10 }) - - // 星期表头 - Row() { - ForEach(this.weekLabels, (w: string) => { - Text(w) - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - .layoutWeight(1) - .textAlign(TextAlign.Center) - }, (w: string): string => 'wk_' + w) - } - .width('100%') - .padding({ bottom: 6 }) - - // 日期网格(7 列) - Grid() { - ForEach(this.cells(), (c: FbCalCell) => { - GridItem() { - this.dayCell(c) - } - }, (c: FbCalCell): string => 'cell_' + c.day.toString() + '_' - + (c.inMonth ? '1' : '0')) - } - .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr') - .rowsGap(2) - .width('100%') - .height(this.gridHeight()) - .scrollBar(BarState.Off) + // ---- 页头:月份 + 视图切换 + 新建 + 设置 ---- + Row({ space: 8 }) { + Column({ space: 2 }) { + Text(this.showYear.toString() + ' 年 ' + this.showMonth.toString() + ' 月') + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text(this.visibleBooks().length.toString() + ' 本日历可见 · 本月 ' + + this.monthEventCount().toString() + ' 条') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) } - .width('100%') - .padding(FbMetric.cardPad) - .backgroundColor(FbColors.surface) - .borderRadius(FbMetric.radius) - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad }) - - // ---- 日历本(可见的几本,横向排列当作图例)---- - Column({ space: 10 }) { - Row({ space: 8 }) { - ForEach(this.visibleBooks(), (b: FbCalBook) => { - Row({ space: 5 }) { - FbDot({ dia: 7, fillC: b.color }) - Text(b.name) - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.muted) - .maxLines(1) - } - .padding({ right: 10 }) - }, (b: FbCalBook): string => 'book_' + b.id.toString()) - } - .width('100%') - .alignItems(VerticalAlign.Center) - } - .width('100%') - .padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 12 }) .alignItems(HorizontalAlign.Start) + .layoutWeight(1) - // ---- 选中日的日程 ---- - Column({ space: 0 }) { - Row() { - Text(this.pickedDay.toString() + ' 日') - .fontSize(FbMetric.fontSmall) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Blank() - Text(this.todayEvents.length.toString() + ' 条日程') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .width('100%') - .padding({ left: 4, right: 4, top: 12, bottom: 8 }) - - Column({ space: 0 }) { - if (this.todayEvents.length === 0) { - FbEmptyBox({ - title: '这天没有安排', - hint: '在月历里点一下某天,就能在那天加日程', - icon: $r('app.media.fb_ic_calendar') - }) - } else { - ForEach(this.todayEvents, (ev: FbCalEvent, idx: number) => { - this.eventRow(ev, idx === this.todayEvents.length - 1) - }, (ev: FbCalEvent): string => 'cev_' + ev.id.toString()) - } - } - .width('100%') - .padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: 6 }) - } - .width('100%') - .backgroundColor(FbColors.surface) - .borderRadius(FbMetric.radius) - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 }) - - // ---- 未来七天 ---- - Column({ space: 0 }) { - Row() { - Text('未来七天') - .fontSize(FbMetric.fontSmall) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Blank() - Text('共 ' + this.soonEvents.length.toString() + ' 条') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .width('100%') - .padding({ left: 4, right: 4, top: 12, bottom: 8 }) - - Column({ space: 0 }) { - ForEach(this.soonEvents, (ev: FbCalEvent, idx: number) => { - this.eventRow(ev, idx === this.soonEvents.length - 1) - }, (ev: FbCalEvent): string => 'sev_' + ev.id.toString()) - } - .width('100%') - .padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: 6 }) - } - .width('100%') - .backgroundColor(FbColors.surface) - .borderRadius(FbMetric.radius) - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 }) + FbPrimaryButton({ + label: '新建', + bg: this.accent, + icon: $r('app.media.fb_ic_plus') + }) + FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) } .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) + .alignItems(VerticalAlign.Center) + + Scroll() { + Column({ space: 0 }) { + + // ---- 月历卡 ---- + Column({ space: 0 }) { + // 月份切换条 + Row() { + this.navBtn('上一月', -1) + Blank() + Text('今天') + .fontSize(FbMetric.fontSmall) + .fontColor(this.accent) + .padding({ left: 12, right: 12, top: 5, bottom: 5 }) + .backgroundColor(fbPalette(this.accent).brand + '14') + .borderRadius(FbMetric.radiusPill) + .onClick(() => { + const d: Date = new Date(); + this.showYear = d.getFullYear(); + this.showMonth = d.getMonth() + 1; + this.pickedDay = d.getDate(); + }) + Blank() + this.navBtn('下一月', 1) + } + .width('100%') + .padding({ bottom: 10 }) + + // 星期表头 + Row() { + ForEach(this.weekLabels, (w: string) => { + Text(w) + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .layoutWeight(1) + .textAlign(TextAlign.Center) + }, (w: string): string => 'wk_' + w) + } + .width('100%') + .padding({ bottom: 6 }) + + // 日期网格(7 列) + Grid() { + ForEach(this.cells(), (c: FbCalCell) => { + GridItem() { + this.dayCell(c) + } + }, (c: FbCalCell): string => 'cell_' + c.day.toString() + '_' + + (c.inMonth ? '1' : '0')) + } + .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr') + .rowsGap(2) + .width('100%') + .height(this.gridHeight()) + .scrollBar(BarState.Off) + } + .width('100%') + .padding(FbMetric.cardPad) + .backgroundColor(FbColors.surface) + .borderRadius(FbMetric.radius) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad }) + + // ---- 日历本(可见的几本,横向排列当作图例)---- + Column({ space: 10 }) { + Row({ space: 8 }) { + ForEach(this.visibleBooks(), (b: FbCalBook) => { + Row({ space: 5 }) { + FbDot({ dia: 7, fillC: b.color }) + Text(b.name) + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.muted) + .maxLines(1) + } + .padding({ right: 10 }) + }, (b: FbCalBook): string => 'book_' + b.id.toString()) + } + .width('100%') + .alignItems(VerticalAlign.Center) + } + .width('100%') + .padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 12 }) + .alignItems(HorizontalAlign.Start) + + // ---- 选中日的日程 ---- + Column({ space: 0 }) { + Row() { + Text(this.pickedDay.toString() + ' 日') + .fontSize(FbMetric.fontSmall) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Blank() + Text(this.todayEvents.length.toString() + ' 条日程') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .width('100%') + .padding({ left: 4, right: 4, top: 12, bottom: 8 }) + + Column({ space: 0 }) { + if (this.todayEvents.length === 0) { + FbEmptyBox({ + title: '这天没有安排', + hint: '在月历里点一下某天,就能在那天加日程', + icon: $r('app.media.fb_ic_calendar') + }) + } else { + ForEach(this.todayEvents, (ev: FbCalEvent, idx: number) => { + this.eventRow(ev, idx === this.todayEvents.length - 1) + }, (ev: FbCalEvent): string => 'cev_' + ev.id.toString()) + } + } + .width('100%') + .padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: 6 }) + } + .width('100%') + .backgroundColor(FbColors.surface) + .borderRadius(FbMetric.radius) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 }) + + // ---- 未来七天 ---- + Column({ space: 0 }) { + Row() { + Text('未来七天') + .fontSize(FbMetric.fontSmall) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Blank() + Text('共 ' + this.soonEvents.length.toString() + ' 条') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .width('100%') + .padding({ left: 4, right: 4, top: 12, bottom: 8 }) + + Column({ space: 0 }) { + ForEach(this.soonEvents, (ev: FbCalEvent, idx: number) => { + this.eventRow(ev, idx === this.soonEvents.length - 1) + }, (ev: FbCalEvent): string => 'sev_' + ev.id.toString()) + } + .width('100%') + .padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: 6 }) + } + .width('100%') + .backgroundColor(FbColors.surface) + .borderRadius(FbMetric.radius) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 }) + } + .width('100%') + } + .width('100%') + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } - .layoutWeight(1) .width('100%') - .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) - .align(Alignment.TopStart) } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } /** 上一月 / 下一月按钮 */ diff --git a/entry/src/main/ets/pages/days/FbDaysPage.ets b/entry/src/main/ets/pages/days/FbDaysPage.ets index 47e8082..08a97ba 100644 --- a/entry/src/main/ets/pages/days/FbDaysPage.ets +++ b/entry/src/main/ets/pages/days/FbDaysPage.ets @@ -18,6 +18,12 @@ import { FbAvatar, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../. @ComponentV2 export struct FbDaysPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 模块主题色(纪念日 = 陶土橙) */ @@ -27,116 +33,121 @@ export struct FbDaysPage { @Local kindFilter: string = ''; build() { - Column() { - // ---- 页头 ---- - Row({ space: 8 }) { - Column({ space: 2 }) { - Text('纪念日') - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text('节日 / 纪念日 / 生日,都在这一条时间线上') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) + Scroll() { + Column() { + this.topBar() - FbPrimaryButton({ - label: '添加', - bg: this.accent, - icon: $r('app.media.fb_ic_plus') - }) - FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) - } - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) - .alignItems(VerticalAlign.Center) - - Scroll() { - Column({ space: 0 }) { - - // ---- 即将到来(30 天内)---- - Column({ space: 0 }) { - Row({ space: 8 }) { - Text('即将到来') - .fontSize(FbMetric.fontSmall) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Blank() - Text('30 天内') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .width('100%') - .padding({ left: 4, right: 4, bottom: 10 }) - - if (this.upcoming().length === 0) { - Text('三十天内没有纪念日') - .fontSize(FbMetric.fontSmall) - .fontColor(FbColors.faint) - } else { - // 横向排列的「大数字」卡 - Scroll() { - Row({ space: 10 }) { - ForEach(this.upcoming(), (a: FbAnniversaryItem) => { - this.upcomingCard(a) - }, (a: FbAnniversaryItem): string => 'up_' + a.id.toString()) - } - } - .scrollable(ScrollDirection.Horizontal) - .scrollBar(BarState.Off) - .width('100%') - } + // ---- 页头 ---- + Row({ space: 8 }) { + Column({ space: 2 }) { + Text('纪念日') + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text('节日 / 纪念日 / 生日,都在这一条时间线上') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) } - .width('100%') - .padding(FbMetric.cardPad) - .backgroundColor(FbColors.surface) - .borderRadius(FbMetric.radius) - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad }) .alignItems(HorizontalAlign.Start) + .layoutWeight(1) - // ---- 全部纪念日 ---- - Row({ space: 8 }) { - this.kindChip('全部', '') - this.kindChip('生日', 'birthday') - this.kindChip('纪念日', 'anniversary') - this.kindChip('节日', 'holiday') - } - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 14, bottom: 8 }) - - if (this.filtered().length === 0) { - FbEmptyBox({ - title: '这里还没有纪念日', - hint: '名册里填上生日,它就会自动出现在这儿', - icon: $r('app.media.fb_ic_days') - }) - } else { - Column({ space: 0 }) { - ForEach(this.filtered(), (a: FbAnniversaryItem, idx: number) => { - this.annRow(a, idx === this.filtered().length - 1) - }, (a: FbAnniversaryItem): string => 'ann_' + a.id.toString()) - } - .width('100%') - .padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, top: 4, bottom: 8 }) - .backgroundColor(FbColors.surface) - .borderRadius(FbMetric.radius) - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 28 }) - } + FbPrimaryButton({ + label: '添加', + bg: this.accent, + icon: $r('app.media.fb_ic_plus') + }) + FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) } .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) + .alignItems(VerticalAlign.Center) + + Scroll() { + Column({ space: 0 }) { + + // ---- 即将到来(30 天内)---- + Column({ space: 0 }) { + Row({ space: 8 }) { + Text('即将到来') + .fontSize(FbMetric.fontSmall) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Blank() + Text('30 天内') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .width('100%') + .padding({ left: 4, right: 4, bottom: 10 }) + + if (this.upcoming().length === 0) { + Text('三十天内没有纪念日') + .fontSize(FbMetric.fontSmall) + .fontColor(FbColors.faint) + } else { + // 横向排列的「大数字」卡 + Scroll() { + Row({ space: 10 }) { + ForEach(this.upcoming(), (a: FbAnniversaryItem) => { + this.upcomingCard(a) + }, (a: FbAnniversaryItem): string => 'up_' + a.id.toString()) + } + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .width('100%') + } + } + .width('100%') + .padding(FbMetric.cardPad) + .backgroundColor(FbColors.surface) + .borderRadius(FbMetric.radius) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad }) + .alignItems(HorizontalAlign.Start) + + // ---- 全部纪念日 ---- + Row({ space: 8 }) { + this.kindChip('全部', '') + this.kindChip('生日', 'birthday') + this.kindChip('纪念日', 'anniversary') + this.kindChip('节日', 'holiday') + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 14, bottom: 8 }) + + if (this.filtered().length === 0) { + FbEmptyBox({ + title: '这里还没有纪念日', + hint: '名册里填上生日,它就会自动出现在这儿', + icon: $r('app.media.fb_ic_days') + }) + } else { + Column({ space: 0 }) { + ForEach(this.filtered(), (a: FbAnniversaryItem, idx: number) => { + this.annRow(a, idx === this.filtered().length - 1) + }, (a: FbAnniversaryItem): string => 'ann_' + a.id.toString()) + } + .width('100%') + .padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, top: 4, bottom: 8 }) + .backgroundColor(FbColors.surface) + .borderRadius(FbMetric.radius) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 28 }) + } + } + .width('100%') + } + .width('100%') + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } - .layoutWeight(1) .width('100%') - .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) - .align(Alignment.TopStart) } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } /** 即将到来的大数字卡 */ diff --git a/entry/src/main/ets/pages/home/FbHomePage.ets b/entry/src/main/ets/pages/home/FbHomePage.ets index b031197..a510839 100644 --- a/entry/src/main/ets/pages/home/FbHomePage.ets +++ b/entry/src/main/ets/pages/home/FbHomePage.ets @@ -22,9 +22,13 @@ import { FbAvatar, FbDot, FbDotLine, FbSectionTitle, FbStatTriple, FbTag } from @ComponentV2 export struct FbHomePage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); - /** 顶栏右侧插槽(由外层传入,本页不用,留空占位以保持签名统一) */ - @BuilderParam topActions: () => void; private cards: FbModuleCard[] = FB_DEMO_MODULE_CARDS; private todayEvents: FbCalEvent[] = FB_DEMO_EVENTS_TODAY; @@ -72,6 +76,8 @@ export struct FbHomePage { build() { Scroll() { Column({ space: 0 }) { + this.topBar() + // ==================== ① 个人信息卡(问候 + 今天 + 天气)==================== // 🔴 用户 2026-10-12 定的:这一块**要白色卡片**("这样显得好看")。 diff --git a/entry/src/main/ets/pages/ledger/FbLedgerPage.ets b/entry/src/main/ets/pages/ledger/FbLedgerPage.ets index dac2566..8c73e97 100644 --- a/entry/src/main/ets/pages/ledger/FbLedgerPage.ets +++ b/entry/src/main/ets/pages/ledger/FbLedgerPage.ets @@ -24,6 +24,12 @@ import { FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../componen @ComponentV2 export struct FbLedgerPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 模块主题色(财务 = 橄榄绿) */ @@ -34,207 +40,210 @@ export struct FbLedgerPage { @Local bookId: number = 0; build() { - Column() { - // ---- 页头 ---- - Row({ space: 8 }) { - Column({ space: 2 }) { - Text(this.currentBook().name) - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text(this.books.length.toString() + ' 本账 · ' - + this.currentBook().entryCount.toString() + ' 条账目') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) + Scroll() { + Column() { + this.topBar() - FbPrimaryButton({ - label: '记一笔', - bg: this.accent, - icon: $r('app.media.fb_ic_plus') - }) - FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) - } - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) - .alignItems(VerticalAlign.Center) - - // ---- 账本切换(横向)---- - Scroll() { + // ---- 页头 ---- Row({ space: 8 }) { - ForEach(this.books, (b: FbLedgerBook) => { - Row({ space: 6 }) { - Text(b.name) - .fontSize(FbMetric.fontSmall) - .fontColor(this.isCurrent(b.id) ? Color.White : FbColors.muted) - .maxLines(1) - if (b.myPerm !== 'owner') { - FbTag({ - text: b.myPerm === 'write' ? '可写' : '只读', - tone: this.isCurrent(b.id) ? 'brand' : 'muted' - }) - } - } - .padding({ left: 12, right: 12, top: 7, bottom: 7 }) - .backgroundColor(this.isCurrent(b.id) ? this.accent : FbColors.surface) - .borderRadius(FbMetric.radiusPill) - .alignItems(VerticalAlign.Center) - .onClick(() => { - this.bookId = b.id; - }) - }, (b: FbLedgerBook): string => 'bk_' + b.id.toString()) - } - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad }) - } - .scrollable(ScrollDirection.Horizontal) - .scrollBar(BarState.Off) - .width('100%') - .padding({ bottom: 10 }) - - Scroll() { - Column({ space: 0 }) { - - // ---- 概览卡 ---- - Column({ space: 12 }) { - Column({ space: 4 }) { - Text('净资产') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.muted) - Text(fbMoney(this.netCent())) - .fontSize(30) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - } - .alignItems(HorizontalAlign.Start) - .width('100%') - - // 资产 / 负债分列 - Row({ space: 0 }) { - Column({ space: 3 }) { - Text('资产') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - Text(fbMoney(this.assetCent())) - .fontSize(FbMetric.fontBody) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.green) - } - .layoutWeight(1) - .alignItems(HorizontalAlign.Start) - - Column({ space: 3 }) { - Text('负债') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - Text(fbMoney(this.debtCent())) - .fontSize(FbMetric.fontBody) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.red) - } - .layoutWeight(1) - .alignItems(HorizontalAlign.Start) - } - .width('100%') - - // 折线图占位(第二轮接图表能力后填) - Column({ space: 4 }) { - Row({ space: 6 }) { - Image($r('app.media.fb_ic_chart')) - .width(13) - .height(13) - .renderMode(ImageRenderMode.Template) - .fillColor(FbColors.faint) - Text('净资产变化(盘点成线)') - .fontSize(10) - .fontColor(FbColors.faint) - } - .alignItems(VerticalAlign.Center) - - Column() - .width('100%') - .height(56) - .borderRadius(FbMetric.radiusS) - .backgroundColor(FbColors.inputBg) - } - .width('100%') - .alignItems(HorizontalAlign.Start) - } - .width('100%') - .padding(FbMetric.cardPad) - .backgroundColor(FbColors.surface) - .borderRadius(FbMetric.radius) - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad }) - .alignItems(HorizontalAlign.Start) - - // ---- 账目(按类别分组)---- - Row({ space: 8 }) { - Text('账目') - .fontSize(FbMetric.fontSmall) + Column({ space: 2 }) { + Text(this.currentBook().name) + .fontSize(FbMetric.fontTitle) .fontWeight(FontWeight.Medium) .fontColor(FbColors.ink) - Blank() - Text(this.entries.length.toString() + ' 条 · 按类别') + Text(this.books.length.toString() + ' 本账 · ' + + this.currentBook().entryCount.toString() + ' 条账目') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } - .width('100%') - .padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 14, bottom: 8 }) + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) - if (this.entries.length === 0) { - FbEmptyBox({ - title: '这本账还没有账目', - hint: '存款、基金、保险、房子、车子都可以记进来', - icon: $r('app.media.fb_ic_ledger') - }) - } else { - ForEach(this.groups(), (g: FbGroup) => { - Column({ space: 0 }) { - // 类别小标题 + 该类别小计 - Row({ space: 8 }) { - Text(g.title) - .fontSize(FbMetric.fontTiny) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text(fbMoney(this.sumOf(g.items))) - .fontSize(10) - .fontColor(FbColors.faint) - Blank() - } - .width('100%') - .padding({ top: 10, bottom: 6 }) - - Column({ space: 0 }) { - ForEach(g.items, (e: FbLedgerEntry, idx: number) => { - this.entryRow(e, idx === g.items.length - 1) - }, (e: FbLedgerEntry): string => 'le_' + e.id.toString()) - } - .width('100%') - } - .width('100%') - .padding(FbMetric.cardPad) - .backgroundColor(FbColors.surface) - .borderRadius(FbMetric.radius) - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 12 }) - .alignItems(HorizontalAlign.Start) - }, (g: FbGroup): string => 'lg_' + g.key) - - Blank().height(16) - } + FbPrimaryButton({ + label: '记一笔', + bg: this.accent, + icon: $r('app.media.fb_ic_plus') + }) + FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) } .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) + .alignItems(VerticalAlign.Center) + + // ---- 账本切换(横向)---- + Scroll() { + Row({ space: 8 }) { + ForEach(this.books, (b: FbLedgerBook) => { + Row({ space: 6 }) { + Text(b.name) + .fontSize(FbMetric.fontSmall) + .fontColor(this.isCurrent(b.id) ? Color.White : FbColors.muted) + .maxLines(1) + if (b.myPerm !== 'owner') { + FbTag({ + text: b.myPerm === 'write' ? '可写' : '只读', + tone: this.isCurrent(b.id) ? 'brand' : 'muted' + }) + } + } + .padding({ left: 12, right: 12, top: 7, bottom: 7 }) + .backgroundColor(this.isCurrent(b.id) ? this.accent : FbColors.surface) + .borderRadius(FbMetric.radiusPill) + .alignItems(VerticalAlign.Center) + .onClick(() => { + this.bookId = b.id; + }) + }, (b: FbLedgerBook): string => 'bk_' + b.id.toString()) + } + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad }) + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .width('100%') + .padding({ bottom: 10 }) + + Scroll() { + Column({ space: 0 }) { + + // ---- 概览卡 ---- + Column({ space: 12 }) { + Column({ space: 4 }) { + Text('净资产') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.muted) + Text(fbMoney(this.netCent())) + .fontSize(30) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + } + .alignItems(HorizontalAlign.Start) + .width('100%') + + // 资产 / 负债分列 + Row({ space: 0 }) { + Column({ space: 3 }) { + Text('资产') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + Text(fbMoney(this.assetCent())) + .fontSize(FbMetric.fontBody) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.green) + } + .alignItems(HorizontalAlign.Start) + + Column({ space: 3 }) { + Text('负债') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + Text(fbMoney(this.debtCent())) + .fontSize(FbMetric.fontBody) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.red) + } + .alignItems(HorizontalAlign.Start) + } + .width('100%') + + // 折线图占位(第二轮接图表能力后填) + Column({ space: 4 }) { + Row({ space: 6 }) { + Image($r('app.media.fb_ic_chart')) + .width(13) + .height(13) + .renderMode(ImageRenderMode.Template) + .fillColor(FbColors.faint) + Text('净资产变化(盘点成线)') + .fontSize(10) + .fontColor(FbColors.faint) + } + .alignItems(VerticalAlign.Center) + + Column() + .width('100%') + .height(56) + .borderRadius(FbMetric.radiusS) + .backgroundColor(FbColors.inputBg) + } + .width('100%') + .alignItems(HorizontalAlign.Start) + } + .width('100%') + .padding(FbMetric.cardPad) + .backgroundColor(FbColors.surface) + .borderRadius(FbMetric.radius) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad }) + .alignItems(HorizontalAlign.Start) + + // ---- 账目(按类别分组)---- + Row({ space: 8 }) { + Text('账目') + .fontSize(FbMetric.fontSmall) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Blank() + Text(this.entries.length.toString() + ' 条 · 按类别') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .width('100%') + .padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 14, bottom: 8 }) + + if (this.entries.length === 0) { + FbEmptyBox({ + title: '这本账还没有账目', + hint: '存款、基金、保险、房子、车子都可以记进来', + icon: $r('app.media.fb_ic_ledger') + }) + } else { + ForEach(this.groups(), (g: FbGroup) => { + Column({ space: 0 }) { + // 类别小标题 + 该类别小计 + Row({ space: 8 }) { + Text(g.title) + .fontSize(FbMetric.fontTiny) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text(fbMoney(this.sumOf(g.items))) + .fontSize(10) + .fontColor(FbColors.faint) + Blank() + } + .width('100%') + .padding({ top: 10, bottom: 6 }) + + Column({ space: 0 }) { + ForEach(g.items, (e: FbLedgerEntry, idx: number) => { + this.entryRow(e, idx === g.items.length - 1) + }, (e: FbLedgerEntry): string => 'le_' + e.id.toString()) + } + .width('100%') + } + .width('100%') + .padding(FbMetric.cardPad) + .backgroundColor(FbColors.surface) + .borderRadius(FbMetric.radius) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 12 }) + .alignItems(HorizontalAlign.Start) + }, (g: FbGroup): string => 'lg_' + g.key) + + Blank().height(16) + } + } + .width('100%') + } + .width('100%') + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } - .layoutWeight(1) .width('100%') - .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) - .align(Alignment.TopStart) } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } /** 一条账目:名字 + 归属人 + 金额 + 盘点入口 */ diff --git a/entry/src/main/ets/pages/misc/FbMiscPages.ets b/entry/src/main/ets/pages/misc/FbMiscPages.ets index eaa2793..85235f8 100644 --- a/entry/src/main/ets/pages/misc/FbMiscPages.ets +++ b/entry/src/main/ets/pages/misc/FbMiscPages.ets @@ -23,6 +23,12 @@ import { FbAvatar, FbDot, FbEmptyBox, FbPrimaryButton, FbTag } from '../../compo @ComponentV2 export struct FbNotesPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 模块主题色(笔记 = 褐灰) */ @@ -30,61 +36,67 @@ export struct FbNotesPage { private all: FbNoteItem[] = FB_DEMO_NOTES; build() { - Column() { - Row({ space: 8 }) { - Column({ space: 2 }) { - Text('笔记') - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text(this.all.length.toString() + ' 条 · 分块 + 标签') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) + Scroll() { + Column() { + this.topBar() - FbPrimaryButton({ - label: '记一条', - bg: this.accent, - icon: $r('app.media.fb_ic_plus') - }) + Row({ space: 8 }) { + Column({ space: 2 }) { + Text('笔记') + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text(this.all.length.toString() + ' 条 · 分块 + 标签') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + FbPrimaryButton({ + label: '记一条', + bg: this.accent, + icon: $r('app.media.fb_ic_plus') + }) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 }) + .alignItems(VerticalAlign.Center) + + if (this.all.length === 0) { + FbEmptyBox({ + title: '还没有笔记', + hint: '随手记点什么,按标签归类和检索', + icon: $r('app.media.fb_ic_notes') + }) + } else { + // ⚠️ 用**两列定尺寸网格**而不是一列长卡:Web 端用户明确要求过 + // 「笔记列表要么按列表展示,要么按块展示,一行几个块,但块大小要确定, + // 显示不下就显示简要信息」—— 一列长卡一条占大半屏,很难扫。 + Grid() { + ForEach(this.all, (n: FbNoteItem) => { + GridItem() { + this.noteCard(n) + } + }, (n: FbNoteItem): string => 'nt_' + n.id.toString()) + } + .columnsTemplate('1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .layoutWeight(1) + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + } } .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 }) - .alignItems(VerticalAlign.Center) - - if (this.all.length === 0) { - FbEmptyBox({ - title: '还没有笔记', - hint: '随手记点什么,按标签归类和检索', - icon: $r('app.media.fb_ic_notes') - }) - } else { - // ⚠️ 用**两列定尺寸网格**而不是一列长卡:Web 端用户明确要求过 - // 「笔记列表要么按列表展示,要么按块展示,一行几个块,但块大小要确定, - // 显示不下就显示简要信息」—— 一列长卡一条占大半屏,很难扫。 - Grid() { - ForEach(this.all, (n: FbNoteItem) => { - GridItem() { - this.noteCard(n) - } - }, (n: FbNoteItem): string => 'nt_' + n.id.toString()) - } - .columnsTemplate('1fr 1fr') - .columnsGap(10) - .rowsGap(10) - .layoutWeight(1) - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) - .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) - } } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } @Builder @@ -148,66 +160,77 @@ export struct FbNotesPage { @ComponentV2 export struct FbNotificationsPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); private all: FbNotificationItem[] = FB_DEMO_NOTIFICATIONS; build() { - Column() { - Row({ space: 8 }) { - Column({ space: 2 }) { - Text('通知消息') - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text(this.unreadCount().toString() + ' 条未读 · 共 ' - + this.all.length.toString() + ' 条') - .fontSize(FbMetric.fontTiny) - .fontColor(this.unreadCount() > 0 ? FbColors.red : FbColors.faint) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) + Scroll() { + Column() { + this.topBar() - Text('全部已读') - .fontSize(FbMetric.fontSmall) - .fontColor(FbColors.brand) - .padding({ left: 12, right: 12, top: 7, bottom: 7 }) - .backgroundColor(FbColors.brand + '14') - .borderRadius(FbMetric.radiusPill) - .onClick(() => { - // 本轮只做界面:把本地未读标记清掉即可 - this.app.unread = 0; + Row({ space: 8 }) { + Column({ space: 2 }) { + Text('通知消息') + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text(this.unreadCount().toString() + ' 条未读 · 共 ' + + this.all.length.toString() + ' 条') + .fontSize(FbMetric.fontTiny) + .fontColor(this.unreadCount() > 0 ? FbColors.red : FbColors.faint) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Text('全部已读') + .fontSize(FbMetric.fontSmall) + .fontColor(FbColors.brand) + .padding({ left: 12, right: 12, top: 7, bottom: 7 }) + .backgroundColor(FbColors.brand + '14') + .borderRadius(FbMetric.radiusPill) + .onClick(() => { + // 本轮只做界面:把本地未读标记清掉即可 + this.app.unread = 0; + }) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 }) + .alignItems(VerticalAlign.Center) + + if (this.all.length === 0) { + FbEmptyBox({ + title: '没有新消息', + hint: '别人共享给你的资源、日程与任务都会出现在这儿', + icon: $r('app.media.fb_ic_bell') }) + } else { + List({ space: 10 }) { + ForEach(this.all, (n: FbNotificationItem) => { + ListItem() { + this.notifCard(n) + } + }, (n: FbNotificationItem): string => 'nf_' + n.id.toString()) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + } } .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 }) - .alignItems(VerticalAlign.Center) - - if (this.all.length === 0) { - FbEmptyBox({ - title: '没有新消息', - hint: '别人共享给你的资源、日程与任务都会出现在这儿', - icon: $r('app.media.fb_ic_bell') - }) - } else { - List({ space: 10 }) { - ForEach(this.all, (n: FbNotificationItem) => { - ListItem() { - this.notifCard(n) - } - }, (n: FbNotificationItem): string => 'nf_' + n.id.toString()) - } - .layoutWeight(1) - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) - .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) - } } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } @Builder diff --git a/entry/src/main/ets/pages/movies/FbMoviesPage.ets b/entry/src/main/ets/pages/movies/FbMoviesPage.ets index 374be1e..fa6111d 100644 --- a/entry/src/main/ets/pages/movies/FbMoviesPage.ets +++ b/entry/src/main/ets/pages/movies/FbMoviesPage.ets @@ -18,6 +18,11 @@ import { FbDot, FbEmptyBox, FbIconButton, FbPersonChip, FbPrimaryButton, FbRatin @ComponentV2 export struct FbMoviesPage { @Param app: FbAppState = fbAppState(); + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; private movies: FbMovieItem[] = FB_DEMO_MOVIES; private cinemas: string[] = FB_DEMO_CINEMAS; @@ -25,97 +30,106 @@ export struct FbMoviesPage { private accent: string = '#7A4E8C'; build() { - Column() { - // ---- 页头:标题 + 记一场电影 + 设置 ---- - Row({ space: 8 }) { - Column({ space: 2 }) { - Text('观影记录') - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text('共 ' + this.movies.length.toString() + ' 部 · 按观影时间排序') + Scroll() { + Column() { + this.topBar() + + // ---- 页头:标题 + 记一场电影 + 设置 ---- + Row({ space: 8 }) { + Column({ space: 2 }) { + Text('观影记录') + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text('共 ' + this.movies.length.toString() + ' 部 · 按观影时间排序') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + FbPrimaryButton({ + label: '记一场', + bg: this.accent, + icon: $r('app.media.fb_ic_plus') + }) + // 「影院」不单独占抽屉位置 —— 它是观影模块的一部分,从这里进 + FbIconButton({ + icon: $r('app.media.fb_ic_gear'), + tint: FbColors.muted, + dia: 36 + }) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) + .alignItems(VerticalAlign.Center) + + // ---- 搜索条 ---- + Row({ space: 6 }) { + Image($r('app.media.fb_ic_search')) + .width(15) + .height(15) + .renderMode(ImageRenderMode.Template) + .fillColor(FbColors.faint) + Text('搜索片名、影院、观影人…') + .fontSize(FbMetric.fontSmall) + .fontColor(FbColors.faint) + .layoutWeight(1) + .maxLines(1) + } + .width('100%') + .height(36) + .padding({ left: 12, right: 12 }) + .backgroundColor(FbColors.inputBg) + .borderRadius(FbMetric.radiusS) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 4 }) + .alignItems(VerticalAlign.Center) + + // ---- 影院数提示 ---- + Row({ space: 6 }) { + Blank() + Text('影院 ' + this.cinemas.length.toString() + ' 家') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - - FbPrimaryButton({ - label: '记一场', - bg: this.accent, - icon: $r('app.media.fb_ic_plus') - }) - // 「影院」不单独占抽屉位置 —— 它是观影模块的一部分,从这里进 - FbIconButton({ - icon: $r('app.media.fb_ic_gear'), - tint: FbColors.muted, - dia: 36 - }) - } - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) - .alignItems(VerticalAlign.Center) - - // ---- 搜索条 ---- - Row({ space: 6 }) { - Image($r('app.media.fb_ic_search')) - .width(15) - .height(15) - .renderMode(ImageRenderMode.Template) - .fillColor(FbColors.faint) - Text('搜索片名、影院、观影人…') - .fontSize(FbMetric.fontSmall) - .fontColor(FbColors.faint) - .layoutWeight(1) - .maxLines(1) - } - .width('100%') - .height(36) - .padding({ left: 12, right: 12 }) - .backgroundColor(FbColors.inputBg) - .borderRadius(FbMetric.radiusS) - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 4 }) - .alignItems(VerticalAlign.Center) - - // ---- 影院数提示 ---- - Row({ space: 6 }) { - Blank() - Text('影院 ' + this.cinemas.length.toString() + ' 家') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .width('100%') - .padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 6, bottom: 6 }) - - // ---- 海报网格 ---- - if (this.movies.length === 0) { - FbEmptyBox({ - title: '还没有观影记录', - hint: '一家人一起看过的电影,都会以海报墙的方式排在这里', - icon: $r('app.media.fb_ic_movies') - }) - } else { - Grid() { - ForEach(this.movies, (mv: FbMovieItem) => { - GridItem() { - this.movieCard(mv) - } - }, (mv: FbMovieItem): string => 'mv_' + mv.id.toString()) - } - .columnsTemplate('1fr 1fr') - .columnsGap(12) - .rowsGap(12) - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) - .layoutWeight(1) .width('100%') - .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) + .padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 6, bottom: 6 }) + + // ---- 海报网格 ---- + if (this.movies.length === 0) { + FbEmptyBox({ + title: '还没有观影记录', + hint: '一家人一起看过的电影,都会以海报墙的方式排在这里', + icon: $r('app.media.fb_ic_movies') + }) + } else { + Grid() { + ForEach(this.movies, (mv: FbMovieItem) => { + GridItem() { + this.movieCard(mv) + } + }, (mv: FbMovieItem): string => 'mv_' + mv.id.toString()) + } + .columnsTemplate('1fr 1fr') + .columnsGap(12) + .rowsGap(12) + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) + // ⚠️ 不能给 layoutWeight:本页已整体放进 Scroll, + // Column 的高度由内容决定,layoutWeight(1) 会去分"剩余空间"(此时为 0) + // ⇒ Grid 高度塌成 0、整页只有顶栏和页头。 + .width('100%') + .width('100%') + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + } } + .width('100%') } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } /** 一张电影卡:海报位(2:3)+ 片名 + 元信息 + 同看的人 */ diff --git a/entry/src/main/ets/pages/people/FbPeoplePage.ets b/entry/src/main/ets/pages/people/FbPeoplePage.ets index 8307bf5..4be7f11 100644 --- a/entry/src/main/ets/pages/people/FbPeoplePage.ets +++ b/entry/src/main/ets/pages/people/FbPeoplePage.ets @@ -30,6 +30,12 @@ interface FbKinRing { @ComponentV2 export struct FbPeoplePage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 子视图:名册(roster)/ 家谱(kin) */ @@ -43,27 +49,33 @@ export struct FbPeoplePage { private rels: FbKinRelation[] = FB_DEMO_KIN_RELATIONS; build() { - Column() { - // ---- 分段切换(名册 / 家谱)---- - Row({ space: 4 }) { - this.segBtn('名册', 'roster', '#A84A28') - this.segBtn('家谱', 'kin', '#A84A28') - } - .padding(3) - .backgroundColor(FbColors.inputBg) - .borderRadius(FbMetric.radiusPill) - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 }) + Scroll() { + Column() { + this.topBar() - if (this.subView === 'roster') { - this.rosterView() - } else { - this.kinView() + // ---- 分段切换(名册 / 家谱)---- + Row({ space: 4 }) { + this.segBtn('名册', 'roster', '#A84A28') + this.segBtn('家谱', 'kin', '#A84A28') + } + .padding(3) + .backgroundColor(FbColors.inputBg) + .borderRadius(FbMetric.radiusPill) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 }) + + if (this.subView === 'roster') { + this.rosterView() + } else { + this.kinView() + } } + .width('100%') } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } // ============================================================ diff --git a/entry/src/main/ets/pages/resources/FbResourcesPage.ets b/entry/src/main/ets/pages/resources/FbResourcesPage.ets index d088c4b..b03d19b 100644 --- a/entry/src/main/ets/pages/resources/FbResourcesPage.ets +++ b/entry/src/main/ets/pages/resources/FbResourcesPage.ets @@ -16,6 +16,12 @@ import { FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../componen @ComponentV2 export struct FbResourcesPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 模块主题色(资源 = 靛蓝) */ @@ -25,76 +31,81 @@ export struct FbResourcesPage { @Local kindFilter: string = ''; build() { - Column() { - // ---- 页头 ---- - Row({ space: 8 }) { - Column({ space: 2 }) { - Text(this.all.length.toString() + ' 项资源') - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text('证件 / 资料 / 附件,可共享给家庭成员') + Scroll() { + Column() { + this.topBar() + + // ---- 页头 ---- + Row({ space: 8 }) { + Column({ space: 2 }) { + Text(this.all.length.toString() + ' 项资源') + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text('证件 / 资料 / 附件,可共享给家庭成员') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + FbPrimaryButton({ + label: '添加', + bg: this.accent, + icon: $r('app.media.fb_ic_plus') + }) + FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) + .alignItems(VerticalAlign.Center) + + // ---- 类别筛选 ---- + Row({ space: 8 }) { + this.kindChip('全部', '') + this.kindChip('证件', 'document') + this.kindChip('资料', 'material') + Blank() + Text('共享给我 ' + this.sharedCount().toString() + ' 项') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - - FbPrimaryButton({ - label: '添加', - bg: this.accent, - icon: $r('app.media.fb_ic_plus') - }) - FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) - } - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) - .alignItems(VerticalAlign.Center) - - // ---- 类别筛选 ---- - Row({ space: 8 }) { - this.kindChip('全部', '') - this.kindChip('证件', 'document') - this.kindChip('资料', 'material') - Blank() - Text('共享给我 ' + this.sharedCount().toString() + ' 项') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 8 }) - .alignItems(VerticalAlign.Center) - - // ---- 列表 ---- - if (this.filtered().length === 0) { - FbEmptyBox({ - title: '这里还没有资源', - hint: '家里的证件、资料和附件都可以收进来,需要时一键共享', - icon: $r('app.media.fb_ic_resources') - }) - } else { - List({ space: 0 }) { - ForEach(this.groups(), (g: FbGroup) => { - ListItemGroup({ header: this.groupHeader(g.title, g.items.length.toString() + ' 项') }) { - ForEach(g.items, (r: FbResourceItem) => { - ListItem() { - this.resourceCard(r) - } - }, (r: FbResourceItem): string => 'res_' + r.id.toString()) - } - }, (g: FbGroup): string => 'rsg_' + g.key) - } - .layoutWeight(1) .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) - .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 8 }) + .alignItems(VerticalAlign.Center) + + // ---- 列表 ---- + if (this.filtered().length === 0) { + FbEmptyBox({ + title: '这里还没有资源', + hint: '家里的证件、资料和附件都可以收进来,需要时一键共享', + icon: $r('app.media.fb_ic_resources') + }) + } else { + List({ space: 0 }) { + ForEach(this.groups(), (g: FbGroup) => { + ListItemGroup({ header: this.groupHeader(g.title, g.items.length.toString() + ' 项') }) { + ForEach(g.items, (r: FbResourceItem) => { + ListItem() { + this.resourceCard(r) + } + }, (r: FbResourceItem): string => 'res_' + r.id.toString()) + } + }, (g: FbGroup): string => 'rsg_' + g.key) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + } } + .width('100%') } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } /** 一项资源卡 */ diff --git a/entry/src/main/ets/pages/settings/FbSettingsPage.ets b/entry/src/main/ets/pages/settings/FbSettingsPage.ets index 31b5e23..499f86b 100644 --- a/entry/src/main/ets/pages/settings/FbSettingsPage.ets +++ b/entry/src/main/ets/pages/settings/FbSettingsPage.ets @@ -17,6 +17,12 @@ import { FbAvatar, FbDot, FbTag } from '../../components/Widgets'; @ComponentV2 export struct FbSettingsPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 抽屉里勾选着的模块 key(与 FB_DEFAULT_NAV 一致,不含 nav) */ @@ -28,6 +34,8 @@ export struct FbSettingsPage { build() { Scroll() { Column({ space: 0 }) { + this.topBar() + // ==================== 我的资料 ==================== Row({ space: 14 }) { diff --git a/entry/src/main/ets/pages/tasks/FbTasksPage.ets b/entry/src/main/ets/pages/tasks/FbTasksPage.ets index 935d964..8592b74 100644 --- a/entry/src/main/ets/pages/tasks/FbTasksPage.ets +++ b/entry/src/main/ets/pages/tasks/FbTasksPage.ets @@ -17,6 +17,12 @@ import { FbCheckCircle, FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag @ComponentV2 export struct FbTasksPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); private boards: string[] = FB_DEMO_TASK_BOARDS; @@ -27,78 +33,83 @@ export struct FbTasksPage { @Local boardFilter: string = ''; build() { - Column() { - // ---- 页头 ---- - Row({ space: 8 }) { - Column({ space: 2 }) { - Text(this.doingCount().toString() + ' 项在做') - .fontSize(FbMetric.fontTitle) - .fontWeight(FontWeight.Medium) - .fontColor(FbColors.ink) - Text(this.todoCount().toString() + ' 项待办' - + (this.overdueCount() > 0 ? ' · ' + this.overdueCount().toString() + ' 项逾期' : '')) - .fontSize(FbMetric.fontTiny) - .fontColor(this.overdueCount() > 0 ? FbColors.red : FbColors.faint) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) + Scroll() { + Column() { + this.topBar() - FbPrimaryButton({ - label: '新建任务', - bg: this.accent, - icon: $r('app.media.fb_ic_plus') - }) - FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) - } - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) - .alignItems(VerticalAlign.Center) - - // ---- 看板筛选条(横向滚动)---- - Scroll() { + // ---- 页头 ---- Row({ space: 8 }) { - this.boardChip('全部', '') - ForEach(this.boards, (b: string) => { - this.boardChip(b, b) - }, (b: string): string => 'bd_' + b) - } - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad }) - } - .scrollable(ScrollDirection.Horizontal) - .scrollBar(BarState.Off) - .width('100%') - .padding({ bottom: 10 }) + Column({ space: 2 }) { + Text(this.doingCount().toString() + ' 项在做') + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text(this.todoCount().toString() + ' 项待办' + + (this.overdueCount() > 0 ? ' · ' + this.overdueCount().toString() + ' 项逾期' : '')) + .fontSize(FbMetric.fontTiny) + .fontColor(this.overdueCount() > 0 ? FbColors.red : FbColors.faint) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) - // ---- 按状态分组的列表 ---- - if (this.filtered().length === 0) { - FbEmptyBox({ - title: '这里没有任务', - hint: '新建时会顺带指定它属于哪个看板、做到哪一步', - icon: $r('app.media.fb_ic_tasks') - }) - } else { - List({ space: 0 }) { - ForEach(this.groups(), (g: FbGroup) => { - ListItemGroup({ header: this.groupHeader(g) }) { - ForEach(g.items, (t: FbTaskItem) => { - ListItem() { - this.taskCard(t) - } - }, (t: FbTaskItem): string => 'tk_' + t.id.toString()) - } - }, (g: FbGroup): string => 'grp_' + g.key) + FbPrimaryButton({ + label: '新建任务', + bg: this.accent, + icon: $r('app.media.fb_ic_plus') + }) + FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) } - .layoutWeight(1) .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) + .alignItems(VerticalAlign.Center) + + // ---- 看板筛选条(横向滚动)---- + Scroll() { + Row({ space: 8 }) { + this.boardChip('全部', '') + ForEach(this.boards, (b: string) => { + this.boardChip(b, b) + }, (b: string): string => 'bd_' + b) + } + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad }) + } + .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) + .width('100%') + .padding({ bottom: 10 }) + + // ---- 按状态分组的列表 ---- + if (this.filtered().length === 0) { + FbEmptyBox({ + title: '这里没有任务', + hint: '新建时会顺带指定它属于哪个看板、做到哪一步', + icon: $r('app.media.fb_ic_tasks') + }) + } else { + List({ space: 0 }) { + ForEach(this.groups(), (g: FbGroup) => { + ListItemGroup({ header: this.groupHeader(g) }) { + ForEach(g.items, (t: FbTaskItem) => { + ListItem() { + this.taskCard(t) + } + }, (t: FbTaskItem): string => 'tk_' + t.id.toString()) + } + }, (g: FbGroup): string => 'grp_' + g.key) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + } } + .width('100%') } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } /** 看板筛选胶囊 */ diff --git a/entry/src/main/ets/pages/trips/FbTripsPage.ets b/entry/src/main/ets/pages/trips/FbTripsPage.ets index 2798a8d..3982cfb 100644 --- a/entry/src/main/ets/pages/trips/FbTripsPage.ets +++ b/entry/src/main/ets/pages/trips/FbTripsPage.ets @@ -20,6 +20,12 @@ import { FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../c @ComponentV2 export struct FbTripsPage { + + /** + * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 + * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 + */ + @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); private trips: FbTripItem[] = FB_DEMO_TRIPS; @@ -30,56 +36,61 @@ export struct FbTripsPage { @Local expandedId: number = 0; build() { - Column() { - // ---- 页头操作条 ---- - Row({ space: 8 }) { - Column({ space: 1 }) { - Text('共 ' + this.trips.length.toString() + ' 个旅程') - .fontSize(FbMetric.fontSmall) - .fontColor(FbColors.ink) - Text('每个旅程里可以有若干段行程') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) + Scroll() { + Column() { + this.topBar() - FbPrimaryButton({ - label: '新建旅程', - bg: this.accent, - icon: $r('app.media.fb_ic_plus') - }) - FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) + // ---- 页头操作条 ---- + Row({ space: 8 }) { + Column({ space: 1 }) { + Text('共 ' + this.trips.length.toString() + ' 个旅程') + .fontSize(FbMetric.fontSmall) + .fontColor(FbColors.ink) + Text('每个旅程里可以有若干段行程') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + FbPrimaryButton({ + label: '新建旅程', + bg: this.accent, + icon: $r('app.media.fb_ic_plus') + }) + FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 }) + .alignItems(VerticalAlign.Center) + + if (this.trips.length === 0) { + FbEmptyBox({ + title: '还没有旅程', + hint: '去过的地方会按时间排在这里,点开能看到每一段行程', + icon: $r('app.media.fb_ic_trips') + }) + } else { + List({ space: 12 }) { + ForEach(this.trips, (t: FbTripItem) => { + ListItem() { + this.tripCard(t) + } + }, (t: FbTripItem): string => 'trip_' + t.id.toString()) + } + .width('100%') + .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + } } .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 }) - .alignItems(VerticalAlign.Center) - - if (this.trips.length === 0) { - FbEmptyBox({ - title: '还没有旅程', - hint: '去过的地方会按时间排在这里,点开能看到每一段行程', - icon: $r('app.media.fb_ic_trips') - }) - } else { - List({ space: 12 }) { - ForEach(this.trips, (t: FbTripItem) => { - ListItem() { - this.tripCard(t) - } - }, (t: FbTripItem): string => 'trip_' + t.id.toString()) - } - .layoutWeight(1) - .width('100%') - .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) - .scrollBar(BarState.Off) - .edgeEffect(EdgeEffect.Spring) - } } .width('100%') .height('100%') - .backgroundColor(FbColors.paper) - .alignItems(HorizontalAlign.Start) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .align(Alignment.TopStart) } /** 一张旅程卡:标题 + 时间 + 进度;展开后接行程时间轴 */