v0.4.0 顶栏改白色 + 通知栏同色 + 顶栏随页面滚动

用户 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。
This commit is contained in:
2026-10-12 10:06:55 +08:00
parent d6bda758d8
commit 0816e25988
15 changed files with 1081 additions and 942 deletions
+21 -23
View File
@@ -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 {
@@ -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));
+32 -26
View File
@@ -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 }) {
+60 -49
View File
@@ -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<FbBookItem>) => {
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<FbBookItem>): 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<FbBookItem>) => {
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<FbBookItem>): 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)
}
/** 一本书:书脊色块(代替封面)+ 书名/作者 + 评分/笔记/进度 */
@@ -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)
}
/** 上一月 / 下一月按钮 */
+110 -99
View File
@@ -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)
}
/** 即将到来的大数字卡 */
+8 -2
View File
@@ -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 定的:这一块**要白色卡片**("这样显得好看")。
+195 -186
View File
@@ -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<FbLedgerEntry>) => {
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<FbLedgerEntry>): 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<FbLedgerEntry>) => {
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<FbLedgerEntry>): 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)
}
/** 一条账目:名字 + 归属人 + 金额 + 盘点入口 */
+120 -97
View File
@@ -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
@@ -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)+ 片名 + 元信息 + 同看的人 */
@@ -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)
}
// ============================================================
@@ -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<FbResourceItem>) => {
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<FbResourceItem>): 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<FbResourceItem>) => {
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<FbResourceItem>): 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)
}
/** 一项资源卡 */
@@ -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 }) {
+73 -62
View File
@@ -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<FbTaskItem>) => {
ListItemGroup({ header: this.groupHeader(g) }) {
ForEach(g.items, (t: FbTaskItem) => {
ListItem() {
this.taskCard(t)
}
}, (t: FbTaskItem): string => 'tk_' + t.id.toString())
}
}, (g: FbGroup<FbTaskItem>): 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<FbTaskItem>) => {
ListItemGroup({ header: this.groupHeader(g) }) {
ForEach(g.items, (t: FbTaskItem) => {
ListItem() {
this.taskCard(t)
}
}, (t: FbTaskItem): string => 'tk_' + t.id.toString())
}
}, (g: FbGroup<FbTaskItem>): 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)
}
/** 看板筛选胶囊 */
+55 -44
View File
@@ -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)
}
/** 一张旅程卡:标题 + 时间 + 进度;展开后接行程时间轴 */