Files
FamilyBook_harmonyos/entry/src/main/ets/pages/resources/FbResourcesPage.ets
T
laoyang b3e1330244 v65/v66:六个空壳模块接线 + 桌面服务卡片
用户 2026-10-20:
「做几个和 syncCalendar 差不多的卡片。另外其他的功能也逐步完成……旅程的
新建旅程啊,车辆管理、出行方式、节点设置……资源也点不进去……任务也点不
进去,也没法完成……纪念日、新增纪念日……财务的显示也有问题……笔记现在
也点不进去,记笔记也记不了。」

v65 —— 六个模块(任务 / 资源 / 纪念日 / 笔记 / 账本 / 旅程)原本是**同一套
空壳模板**复制出来的:`FbPrimaryButton` 没有 `onTap`、`ListItem` 没有
`onClick`、设置菜单的 `action` 全是空串。这一版把它们全部接上真实接口:

· 任务:新建 / 编辑(`FbTaskEditSheet` + `FbTaskRepo`)、列表可点、勾完成
  (走 `/tasks/<id>/status` —— 服务端 `PATCH` **显式忽略** status,走 Patch
  会得到"看起来成功的假成功")、按看板筛选
· 资源:新增 / 查看 / 改 / 删(`FbResourceSheet` + `FbResourceRepo`);
  过滤维度从**并不存在的**「证件 / 资料」改成按权限与归属
  (`resources` 表没有 category 列,那个筛选一直是筛空的)
· 纪念日:新增 / 编辑(`FbDayEditSheet` + `FbDayRepo`);派生生日
  (id=0、来自名册)只给提示不给编辑 —— 点开 `/days/0` 必然 404
· 笔记:记一条 / 打开 / 改(`FbNoteSheet` + `FbNoteRepo`),正文按 blocks 存
· 账本:记一笔(`FbLedgerEntrySheet` + `FbLedgerRepo`);修掉财务显示自相
  矛盾 —— 资产/负债改回**服务端 summary**(原来端上按金额正负号重算,与
  服务端"负债取绝对值"的口径打架,会算出 净资产 ≠ 资产 − 负债)
· 旅程:题图卡片(封面 + 标题 + 城市)+ 行程 + 新建旅程 + 新建节点

服务端同时修掉两处**静默数据丢失**(网页端因提交格式凑巧没暴露,只有走
API 的客户端会中招):
· 新建笔记传的是 `body`,而 `Note::create()` 只读 `blocks` ⇒ 新笔记只有标题
· 改笔记时 `blocks` 恒为空,而 `Note::update()` 是整组替换 ⇒ 只改标题就把
  正文全删光;另修 `annivPeopleOf()`(`find()` 的 SQL 不含 people,原来那个
  兜底恒为空 ⇒ 只改纪念日标题会清空关联人)

v66 —— 桌面服务卡片(用户选了「桌面服务卡片」这一项):
· `FbFormAbility`(FormExtensionAbility)+ `form_config.json` 三块尺寸:
  2×2 今日概览 / 2×4 左右分栏 / 4×4 今日全景
· `FbCardData` 只打 `/home` 一个接口(与网页首页同源),数据**扁平化**后
  由 `formBindingData` 推给卡片;断网时用上次成功的缓存并注明"离线"
· 卡片是后台进程 ⇒ 请求带 `authProbe: true`:401 **不能**触发全局"踢回
  登录页"(那会顺手把正在用应用的人踢下线,v62 那次事故同一个坑)
· 点卡片 → `postCardAction(router, fbModule)` → `EntryAbility` 读 Want 参数
  直接落到对应模块;补了 `onNewWant`(没有它,第二次点卡片"什么也没发生")
· 4×4 右上角 ↻ 走 `postAction(message)` → `onFormEvent` 重取数再推回

验证:`hvigorw assembleHap` BUILD SUCCESSFUL;打包后的 hap 里确认
`FbFormAbility`(type=form)与 `$profile:form_config` 都在,卡片代码进了
`ets/widgets.abc`。静态守卫 730 条全绿,反向验证 110 条注入全部能让守卫变红。
⚠️ 设备仍断连(`hdc list targets` 为空),真机尚未验收。
2026-10-20 15:27:32 +08:00

440 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 家书 FamilyBook · 资源模块(鸿蒙端)
*
* 对应 Web 端 `templates/resources/index.php`。
* 资源 = 证件 / 资料 / 附件的集合(户口本、房产证、学籍资料…),可共享给某个成员。
*
* 端上形态:**按类别分组的卡片列表**,卡片右侧显示归属人与附件数;
* 那一排「类别筛选胶囊」对应 Web 端页头的分类入口。
*/
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK, fbAlpha } from '../../common/Theme';
import { FbAppState, fbAppState } from '../../common/Store';
import { FbGroup, FbResourceItem } from '../../model/Types';
import { fbGroupResource } from '../../model/DemoData';
import { parseResources } from '../../data/FbParse';
import { loadModule } from '../../data/FbModuleLoad';
import { FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
import { FbModuleSettingsSheet } from '../../components/FbModuleSettingsSheet';
import { FbResourceSheet } from '../../components/FbResourceSheet';
import { FB_BACK } from '../../common/FbBack';
import { FB_SET_RESOURCES } from '../../model/FbSettingsMenus';
@ComponentV2
export struct FbResourcesPage {
/**
* 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。
* 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。
*/
@BuilderParam topBar: () => void;
@Param app: FbAppState = fbAppState();
/** 模块主题色(资源 = 靛蓝) */
private accent: string = '#5566A8';
/**
* 模块设置浮层是否打开(右上角三条横线点开的那个)。
*
* 🔴 用户 2026-10-16:「所有页面的设置按钮全部都改成和网页版一致,
* 就是**三个横线**那种设置按钮。」
* 网页版的做法:动作区最右一个 ☰ → 点开是**这个模块的设置菜单**。
*/
@Local settingsOpen: boolean = false;
@Local all: FbResourceItem[] = [];
/**
* 当前筛选范围:'' = 全部 / 'shared' = 共享给我的 / 'mine' = 我名下的。
*
* 🔴 v65:原来这里是「证件 / 资料」两个类别 —— 服务端 `resources` 表
* **没有类别列**(`app/Schema.php` 只有 title / content / owner_id),
* 解析时 kind 只能全填 `other` ⇒ 这两个胶囊点了永远是空列表。
* 改成按**权限/归属**筛:这两个维度服务端真的给(`perm` / `owner_id`)。
*/
@Local scopeFilter: string = '';
/** 资源浮层(新建 / 查看 / 编辑 / 删除)是否打开 */
@Local sheetOpen: boolean = false;
/** 浮层里那条(0 = 新建) */
@Local sheetId: number = 0;
@Local sheetTitle: string = '';
@Local sheetContent: string = '';
@Local sheetOwner: string = '';
@Local sheetCanWrite: boolean = true;
/**
* 页面出现:先读缓存、再拉接口(本地优先,见 FbModuleRepo 的说明)。
*
* ⚠️ 不在这里打开本地库 —— `EntryAbility.onCreate` 已统一打开。
*/
/**
* 🔴 v59e(用户 2026-10-18):系统返回手势 —— 先关本页的浮层,再交给 `Index` 换模块。
*
* 本页只有「模块设置」一个浮层,所以规则很短:开着就关掉(返回 true = 已消费)。
* ⚠️ 登记/注销成对出现,见 `common/FbBack.ets` 与 `Index.onBackPress`。
*/
aboutToDisappear(): void {
FB_BACK.unregister('resources');
}
private backOnce(): boolean {
if (this.sheetOpen) {
this.sheetOpen = false;
return true;
}
if (this.settingsOpen) {
this.settingsOpen = false;
return true;
}
return false;
}
aboutToAppear(): void {
FB_BACK.register('resources', (): boolean => this.backOnce());
this.load();
}
private async load(): Promise<void> {
await loadModule<FbResourceItem[]>('resources', parseResources,
(rows: FbResourceItem[]): void => {
this.all = rows;
},
(note: string): void => {
this.syncNote = note;
});
}
/** 顶部提示(同步失败 / 未登录时显示一行小字) */
@Local syncNote: string = '';
/**
* 设置浮层开着 ⇒ 让底部悬浮快捷栏让位。
*
* 🔴 悬浮栏在**更外层 Index 的 Stack** 里,层级比本页高 ⇒
* 浮层底部的按钮会被它盖住、点击被抢走(名册/日程页都踩过)。
*/
@Monitor('settingsOpen')
onSettingsChanged(): void {
fbAppState().sheetCount = this.settingsOpen ? 1 : 0;
}
/** 资源浮层开着 ⇒ 底部悬浮快捷栏同样让位(两个浮层不会同时开) */
@Monitor('sheetOpen')
onResourceSheetChanged(): void {
fbAppState().sheetCount = this.sheetOpen ? 1 : 0;
}
/** 打开「新增资源」 */
private openNew(): void {
this.sheetId = 0;
this.sheetTitle = '';
this.sheetContent = '';
this.sheetOwner = '';
this.sheetCanWrite = true;
this.sheetOpen = true;
}
/** 打开某条资源(只读共享进来的锁输入框,但内容照看) */
private openDetail(r: FbResourceItem): void {
this.sheetId = r.id;
this.sheetTitle = r.name;
this.sheetContent = r.content;
this.sheetOwner = r.ownerName;
/* `perm` 是服务端 `Resource::all` 里算好的:owner / write / read。
⚠️ 只有 owner 与 write 能改(服务端 `Resource::canWrite` 同一口径)。 */
this.sheetCanWrite = r.perm === 'owner' || r.perm === 'write';
this.sheetOpen = true;
}
build() {
// 🔴 外层 Stack:**模块设置浮层**要浮在页面之上
// (本页原来是 Scroll 直接作根,没有 Stack,这里补一层)
Stack({ alignContent: Alignment.Bottom }) {
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'),
/* 🔴 v65:接上回调。原来这颗按钮**没有 `onTap`** ——
用户说的"新增加资源"没完成就是这条。 */
onTap: (): void => {
this.openNew();
}
})
// 🔴 设置按钮 = 网页版那种**三条横线**(☰)—— 用户 2026-10-16。
// 点开是本模块的设置菜单(条目照网页版 `templates/<模块>/_settings_menu.php`)。
FbIconButton({
icon: $r('app.media.fb_ic_menu'),
tint: FbTint.muted,
onTap: (): void => {
this.settingsOpen = true;
}
})
}
.width('100%')
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
.alignItems(VerticalAlign.Center)
// ---- 筛选(全部 / 共享给我 / 我名下)----
Row({ space: 8 }) {
this.scopeChip('全部', '')
this.scopeChip('共享给我', 'shared')
this.scopeChip('我名下', 'mine')
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)
}
.width('100%')
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
}
.width('100%')
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
.padding({ bottom: FB_DOCK_RESERVE })
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
// 🔴 滚动方向上报(驱动底部悬浮栏随滚收起 / 唤回)—— 见 common/FbScroll.ets
.onDidScroll(fbOnScroll)
// 🔴 触摸方向兜底(内容不足一屏的页面滚不动,只能靠手指位移判断)
// 与上面的 `onDidScroll` 互补、互不冲突 —— 见 common/FbScroll.ets
.onTouch(fbOnTouch)
.align(Alignment.TopStart)
// ---- 模块设置浮层(右上角三条横线点开的那个)----
if (this.settingsOpen) {
Column()
.width('100%')
.height('100%')
.backgroundColor(FB_MASK)
.transition(fbMaskTransition())
.onClick((): void => {
this.settingsOpen = false;
})
FbModuleSettingsSheet({
// 🔴 环色 = 本模块主题色(用户 2026-10-19)
accent: this.accent,
title: '资源设置',
items: FB_SET_RESOURCES,
onClose: (): void => {
this.settingsOpen = false;
},
onPick: (action: string): void => {
/* v65:两条都接上了(原来都是空串 ⇒ 画灰、点了不响应) */
if (action === 'resourceShared') {
this.scopeFilter = 'shared';
this.settingsOpen = false;
return;
}
if (action === 'resourceMine') {
this.scopeFilter = 'mine';
this.settingsOpen = false;
return;
}
}
})
.transition(fbSheetTransition())
}
// ---- 资源浮层:新建 / 查看 / 编辑 / 删除(v65)----
if (this.sheetOpen) {
FbResourceSheet({
accent: this.accent,
editingId: this.sheetId,
editingTitle: this.sheetTitle,
editingContent: this.sheetContent,
ownerName: this.sheetOwner,
canWrite: this.sheetCanWrite,
onClose: (): void => {
this.sheetOpen = false;
},
onSaved: (): void => {
this.sheetOpen = false;
this.load();
},
onDeleted: (): void => {
this.sheetOpen = false;
this.load();
}
})
.transition(fbSheetTransition())
}
}
.width('100%')
.height('100%')
}
/** 一项资源卡(整卡可点 → 打开详情/编辑) */
@Builder
resourceCard(r: FbResourceItem) {
Row({ space: 12 }) {
// 图标底:共享来的用「共享」图标,自己名下的用「锁」
Stack({ alignContent: Alignment.Center }) {
Image(r.shared ? $r('app.media.fb_ic_share') : $r('app.media.fb_ic_lock'))
.width(19)
.height(19)
.renderMode(ImageRenderMode.Template)
.colorFilter(fbTint(this.accent))
}
.width(42)
.height(42)
.borderRadius(FbMetric.radiusS)
.backgroundColor(fbAlpha(this.accent, 0.08))
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(r.name)
.fontSize(FbMetric.fontBody)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
if (r.shared) {
FbTag({ text: r.perm === 'write' ? '可改' : '只读', tone: 'green' })
}
}
.width('100%')
.alignItems(VerticalAlign.Center)
// 正文预览:资源的正文就是"编号、有效期、放在哪" —— 比附件数有用得多
if (r.content !== '') {
Text(r.content)
.fontSize(10)
.fontColor(FbColors.muted)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
}
Row({ space: 10 }) {
Text('归属 ' + r.ownerName)
.fontSize(10)
.fontColor(FbColors.muted)
Text(r.updatedAt)
.fontSize(10)
.fontColor(FbColors.faint)
}
.alignItems(VerticalAlign.Center)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Image($r('app.media.fb_ic_forward'))
.width(13)
.height(13)
.renderMode(ImageRenderMode.Template)
.colorFilter(fbTint(FbTint.faint))
}
.width('100%')
.padding(14)
.backgroundColor(FbColors.surface)
.borderRadius(FbMetric.radius)
.margin({ bottom: 8 })
.alignItems(VerticalAlign.Center)
/* 🔴 v65:整卡可点 → 打开详情(原来列表项**没有 `onClick`** ⇒ "点不进去") */
.onClick((): void => {
this.openDetail(r);
})
}
/** 筛选胶囊(全部 / 共享给我 / 我名下) */
@Builder
scopeChip(label: string, key: string) {
Text(label)
.fontSize(FbMetric.fontSmall)
.fontColor(this.scopeFilter === key ? Color.White : FbColors.muted)
.backgroundColor(this.scopeFilter === key ? this.accent : FbColors.inputBg)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(FbMetric.radiusPill)
.onClick(() => {
this.scopeFilter = key;
})
}
@Builder
groupHeader(title: string, count: string) {
Row({ space: 8 }) {
Text(title)
.fontSize(FbMetric.fontSmall)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
Text(count)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
Blank()
}
.width('100%')
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
.backgroundColor(FbColors.paper)
}
private filtered(): FbResourceItem[] {
if (this.scopeFilter.length === 0) {
return this.all;
}
if (this.scopeFilter === 'shared') {
// 共享给我的 = 权限不是 owner 的那些
return this.all.filter((r: FbResourceItem): boolean => r.shared);
}
return this.all.filter((r: FbResourceItem): boolean => !r.shared);
}
private groups(): FbGroup<FbResourceItem>[] {
return fbGroupResource(this.filtered());
}
private sharedCount(): number {
return this.all.filter((r: FbResourceItem): boolean => r.shared).length;
}
}