v0.1.0 界面框架:抽屉骨架 + 13 个模块页面

对应服务端 FamilyBook(家书)的鸿蒙客户端首版。本轮只做**界面框架**,
数据全部是演示数据 —— 服务端尚无客户端接口,所有增删改只做入口与样式,
不落任何数据。

## 骨架
左侧抽屉 + 右侧内容。抽屉两套形态由窗口短边决定(≥600vp 为宽屏):
宽屏常驻侧栏、窄屏滑出层。判据取「短边」而非「宽度」,因为短边在旋转时
不变,比例接近 1:1 的设备横竖屏布局天然一致。

## 模块(与 Web 端 Module::LIST 一一对应,key 即路由名)
首页、家谱、名册、观影、旅程、日程、资源、任务、纪念日、财务、读书、
导航、笔记;另有通知消息、个人设置两个附加页。

## 与 Web 端保持一致的几处
· 模块清单唯一来源在 model/Modules.ets,加模块只改那一处;
· 模块主题色三档由主色按 HSL 压明度算出,预设色板与 Web 端逐值一致;
· 术语沿用 Web 端固定下来的口径(旅程 ⊃ 行程;观影的「影院」不单独占
  抽屉位,从模块内设置进);
· 页面结构照 Web 端的信息层次(首页信息面板 / 财务概览在账目之前 /
  纪念日「即将到来」在顶 / 任务按状态而非按看板)。

## 编译验证
hvigorw assembleHap --mode module -p product=default
→ BUILD SUCCESSFUL,0 error / 0 warning(含一次删 build 后的干净重建)

过程中修掉的编译期问题:
· Circle().fill() 是 API 26 才有的重载(compatibleSdk 是 24)
  → 换「方块 + 圆角」画圆,零版本风险;
· setWindowBackgroundColor 对 phone/tablet 不可用 → 去掉(页面已铺满同色);
· window.getLastWindow() 只有异步版 → 由 Ability 侧同步取窗存 AppStorage;
· 颜色类 @Param 声明成 string 导致传 Resource 报错 → 统一改 ResourceColor;
· 页面在 pages/<模块>/ 下,公共件 import 相对路径要多退一级。

(首轮编译 95 error / 18 warn,逐条定位到 5 类根因后归零。)
This commit is contained in:
2026-10-11 22:07:48 +08:00
commit 41e3aa3d95
80 changed files with 6985 additions and 0 deletions
@@ -0,0 +1,343 @@
/**
* 家书 FamilyBook · 设置页(鸿蒙端)
*
* 对应 Web 端两处设置:
* · `templates/settings/me.php`(个人设置:我的资料、昵称、模块显隐与排序、主题色)
* · `templates/settings/_modules.php`(模块勾选 + 拖动排序 + 每模块配色)
*
* 端上把它做成**个人设置**一页(通知、模块显隐、主题、关于),
* 并把「哪些模块出现在抽屉里」这一项真正接上 —— 因为它是抽屉形态的核心配置:
* 抽屉里列什么,正是由这里决定的。
*/
import { FbColors, FbMetric, fbPalette } from '../../common/Theme';
import { FbAppState, fbAppState } from '../../common/Store';
import { FbModuleDef, FB_MODULES, FB_PRESET_COLORS } from '../../model/Modules';
import { FbAvatar, FbDot, FbTag } from '../../components/Widgets';
@ComponentV2
export struct FbSettingsPage {
@Param app: FbAppState = fbAppState();
/** 抽屉里勾选着的模块 key */
@Local checked: string[] = ['home', 'people', 'movies', 'trips', 'calendar',
'resources', 'tasks', 'days', 'ledger', 'books', 'nav', 'notes'];
/** 当前展开的配色分组('' = 都没展开) */
@Local colorOpenKey: string = '';
build() {
Scroll() {
Column({ space: 0 }) {
// ==================== 我的资料 ====================
Row({ space: 14 }) {
FbAvatar({
name: this.app.session.me.name,
color: this.app.session.me.avatarColor,
dia: 56,
fontSizeVp: 22
})
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(this.app.session.me.name)
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
if (this.app.session.me.role === 'admin') {
FbTag({ text: '管理员', tone: 'brand' })
}
}
.alignItems(VerticalAlign.Center)
Text(this.app.session.familyTitle + ' · ' + this.app.session.me.kinship)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Image($r('app.media.fb_ic_forward'))
.width(14)
.height(14)
.renderMode(ImageRenderMode.Template)
.fillColor(FbColors.faint)
}
.width('100%')
.padding(FbMetric.cardPad)
.backgroundColor(FbColors.surface)
.borderRadius(FbMetric.radius)
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
// ==================== 通知 ====================
Column({ space: 0 }) {
this.rowTitle('通知')
this.switchRow('新消息提醒', '共享 / 任务 / 日程变动时通知我', true)
this.switchRow('到期提醒', '日程开始前 10 分钟提醒', true)
this.switchRow('逾期提醒', '任务逾期时提醒我', true)
}
.width('100%')
.padding(FbMetric.cardPad)
.backgroundColor(FbColors.surface)
.borderRadius(FbMetric.radius)
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
// ==================== 抽屉里显示哪些模块 ====================
Column({ space: 0 }) {
Row({ space: 8 }) {
Text('抽屉里的模块')
.fontSize(FbMetric.fontBody)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
Blank()
Text(this.checked.length.toString() + '/' + FB_MODULES.length.toString())
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
}
.width('100%')
.padding({ bottom: 4 })
Text('首页与会话相关的入口始终保留;家谱默认收起(不需要每天看)')
.fontSize(10)
.fontColor(FbColors.faint)
.width('100%')
.padding({ bottom: 8 })
ForEach(FB_MODULES, (m: FbModuleDef) => {
this.moduleToggleRow(m)
}, (m: FbModuleDef): string => 'set_' + m.key)
}
.width('100%')
.padding(FbMetric.cardPad)
.backgroundColor(FbColors.surface)
.borderRadius(FbMetric.radius)
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
.alignItems(HorizontalAlign.Start)
// ==================== 模块主题色 ====================
Column({ space: 0 }) {
Text('模块主题色')
.fontSize(FbMetric.fontBody)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.width('100%')
Text('一个家认同一套配色 —— 换个色,整个模块的按钮与强调色一起变')
.fontSize(10)
.fontColor(FbColors.faint)
.width('100%')
.padding({ top: 4, bottom: 8 })
ForEach(FB_MODULES, (m: FbModuleDef) => {
this.colorRow(m)
}, (m: FbModuleDef): string => 'col_' + m.key)
}
.width('100%')
.padding(FbMetric.cardPad)
.backgroundColor(FbColors.surface)
.borderRadius(FbMetric.radius)
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
.alignItems(HorizontalAlign.Start)
// ==================== 关于 ====================
Column({ space: 0 }) {
this.rowTitle('关于')
this.plainRow('版本', '0.1.0(界面框架)')
this.plainRow('数据来源', '待接入 · 当前为演示数据')
this.plainRow('服务器', '未配置')
}
.width('100%')
.padding(FbMetric.cardPad)
.backgroundColor(FbColors.surface)
.borderRadius(FbMetric.radius)
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 })
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor(FbColors.paper)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
.align(Alignment.TopStart)
}
// ============================================================
// 行件
// ============================================================
/** 一个模块的「显隐」开关行 */
@Builder
moduleToggleRow(m: FbModuleDef) {
Row({ space: 12 }) {
Stack({ alignContent: Alignment.Center }) {
Image(this.iconOf(m.key))
.width(17)
.height(17)
.renderMode(ImageRenderMode.Template)
.fillColor(fbPalette(m.color).brand)
}
.width(32)
.height(32)
.borderRadius(9)
.backgroundColor(fbPalette(m.color).brand + '14')
Column({ space: 2 }) {
Text(m.name)
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
Text(m.desc)
.fontSize(10)
.fontColor(FbColors.faint)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 首页不可关(它是落地页)
if (m.key === 'home') {
Text('常驻')
.fontSize(10)
.fontColor(FbColors.faint)
} else {
Toggle({ type: ToggleType.Switch, isOn: this.isChecked(m.key) })
.selectedColor(fbPalette(m.color).brand)
.onChange((on: boolean) => {
this.toggle(m.key, on);
})
}
}
.width('100%')
.padding({ top: 9, bottom: 9 })
.alignItems(VerticalAlign.Center)
}
/** 一个模块的配色行(点标题展开预设色板) */
@Builder
colorRow(m: FbModuleDef) {
Column({ space: 0 }) {
Row({ space: 10 }) {
FbDot({ dia: 16, fillC: m.color })
Text(m.name)
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.layoutWeight(1)
Text(this.colorOpenKey === m.key ? '收起' : '换色')
.fontSize(10)
.fontColor(FbColors.brand)
}
.width('100%')
.padding({ top: 9, bottom: 9 })
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.colorOpenKey = this.colorOpenKey === m.key ? '' : m.key;
})
if (this.colorOpenKey === m.key) {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(FB_PRESET_COLORS, (c: string) => {
Stack({ alignContent: Alignment.Center }) {
FbDot({ dia: 26, fillC: c })
if (c === m.color) {
Image($r('app.media.fb_ic_check'))
.width(13)
.height(13)
.renderMode(ImageRenderMode.Template)
.fillColor(Color.White)
}
}
.margin({ right: 8, bottom: 8 })
}, (c: string): string => 'sw_' + m.key + '_' + c)
}
.width('100%')
.padding({ bottom: 6 })
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
/** 小标题 */
@Builder
rowTitle(title: string) {
Text(title)
.fontSize(FbMetric.fontBody)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.width('100%')
.padding({ bottom: 6 })
}
/** 带开关的设置行 */
@Builder
switchRow(title: string, hint: string, on: boolean) {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text(title)
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
Text(hint)
.fontSize(10)
.fontColor(FbColors.faint)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: on })
.selectedColor(FbColors.brand)
}
.width('100%')
.padding({ top: 9, bottom: 9 })
.alignItems(VerticalAlign.Center)
}
/** 只读信息行 */
@Builder
plainRow(label: string, value: string) {
Row() {
Text(label)
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
Blank()
Text(value)
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.faint)
}
.width('100%')
.padding({ top: 9, bottom: 9 })
.alignItems(VerticalAlign.Center)
}
private isChecked(key: string): boolean {
return this.checked.indexOf(key) >= 0;
}
private toggle(key: string, on: boolean): void {
if (on) {
if (this.checked.indexOf(key) < 0) {
this.checked.push(key);
}
} else {
this.checked = this.checked.filter((k: string): boolean => k !== key);
}
}
private iconOf(key: string): Resource {
switch (key) {
case 'home': return $r('app.media.fb_ic_home');
case 'kin': return $r('app.media.fb_ic_kin');
case 'people': return $r('app.media.fb_ic_people');
case 'movies': return $r('app.media.fb_ic_movies');
case 'trips': return $r('app.media.fb_ic_trips');
case 'calendar': return $r('app.media.fb_ic_calendar');
case 'resources': return $r('app.media.fb_ic_resources');
case 'tasks': return $r('app.media.fb_ic_tasks');
case 'days': return $r('app.media.fb_ic_days');
case 'ledger': return $r('app.media.fb_ic_ledger');
case 'books': return $r('app.media.fb_ic_books');
case 'nav': return $r('app.media.fb_ic_nav');
case 'notes': return $r('app.media.fb_ic_notes');
default: return $r('app.media.fb_ic_dot');
}
}
}