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` 为空),真机尚未验收。
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 「记一笔」浮层(鸿蒙端)
|
||||
*
|
||||
* 对应网页版 `templates/ledger/` 的记一笔表单。
|
||||
*
|
||||
* 🔴 用户 2026-10-20:「还有这个财务的显示也有问题。」
|
||||
* ⇒ 端上「记一笔」按钮原来**没有 `onTap`** —— 点了什么都不发生。
|
||||
*
|
||||
* ⚠️ 金额输入的是**元**(可带两位小数),服务端 `Ledger::toCents()` 转分。
|
||||
* 端上**不做**元→分换算(双重换算会差 100 倍)。
|
||||
*
|
||||
* ⚠️ 类别键必须与 `Ledger::KINDS` 一字不差 —— 写错了服务端会落到 `other`,
|
||||
* 而且**负债**(贷款/信用卡/欠款)落到资产会让净资产算错。所以这里照抄那张表。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, FB_MASK, fbAlpha } from '../common/Theme';
|
||||
import { FbSheetClose } from './FbSheetClose';
|
||||
import { FbSheetRing } from './FbSheetRing';
|
||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { addLedgerEntry } from '../data/FbLedgerRepo';
|
||||
import { FbApiResult } from '../data/FbHttp';
|
||||
|
||||
/** 类别键(顺序与 `Ledger::KINDS` 一致) */
|
||||
const KIND_KEYS: string[] = ['deposit', 'fund', 'stock', 'insurance', 'house', 'car',
|
||||
'cash', 'loan', 'credit', 'owe', 'other'];
|
||||
const KIND_NAMES: string[] = ['存款', '基金', '股票', '保险', '房子', '车子',
|
||||
'现金', '贷款', '信用卡', '欠款', '其它'];
|
||||
/** 哪些是负债(`Ledger::isDebt` 同一份判断) */
|
||||
const DEBT_KEYS: string[] = ['loan', 'credit', 'owe'];
|
||||
|
||||
@ComponentV2
|
||||
export struct FbLedgerEntrySheet {
|
||||
/** 模块主题色(财务 = 橄榄绿) */
|
||||
@Param accent: string = '#5E7A3C';
|
||||
/** 记到哪本账 */
|
||||
@Param ledgerId: number = 0;
|
||||
@Param ledgerName: string = '';
|
||||
|
||||
@Event onClose: () => void = (): void => {};
|
||||
@Event onSaved: () => void = (): void => {};
|
||||
|
||||
@Local kind: string = 'deposit';
|
||||
@Local amount: string = '';
|
||||
@Local title: string = '';
|
||||
@Local note: string = '';
|
||||
@Local saving: boolean = false;
|
||||
@Local err: string = '';
|
||||
|
||||
private kindNameOf(k: string): string {
|
||||
const i: number = KIND_KEYS.indexOf(k);
|
||||
return i >= 0 ? KIND_NAMES[i] : '其它';
|
||||
}
|
||||
|
||||
/** 下标 → 类别中文名(越界给「其它」,不抛错) */
|
||||
private kindLabel(idx: number): string {
|
||||
return idx >= 0 && idx < KIND_NAMES.length ? KIND_NAMES[idx] : '其它';
|
||||
}
|
||||
|
||||
private isDebt(): boolean {
|
||||
return DEBT_KEYS.indexOf(this.kind) >= 0;
|
||||
}
|
||||
|
||||
build() {
|
||||
/* 🔴🔴 根必须 `Stack`:遮罩与卡片要"叠",不能当 `Column` 子项"排"
|
||||
(排会把卡片挤出屏幕 —— 2026-10-20 真机事故,见 pitfalls.md) */
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FB_MASK)
|
||||
.onClick((): void => {
|
||||
this.onClose();
|
||||
})
|
||||
|
||||
Column() {
|
||||
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accent }) {
|
||||
Column({ space: 12 }) {
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('记一笔')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
Text(this.ledgerName !== '' ? ('记到「' + this.ledgerName + '」') : '记到当前账本')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.maxLines(1)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
.flexShrink(1)
|
||||
|
||||
FbSheetClose({
|
||||
onClose: (): void => {
|
||||
this.onClose();
|
||||
},
|
||||
color: this.accent
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 12 }) {
|
||||
// ---- 类别 ----
|
||||
Column({ space: 6 }) {
|
||||
Text('类别')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Scroll() {
|
||||
Row({ space: 8 }) {
|
||||
ForEach(KIND_KEYS, (k: string, idx: number) => {
|
||||
Text(this.kindLabel(idx))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.kind === k ? '#FFFFFF' : FbColors.muted)
|
||||
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
||||
.backgroundColor(this.kind === k ? this.accent : FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick((): void => {
|
||||
this.kind = k;
|
||||
})
|
||||
}, (k: string): string => 'lk_' + k)
|
||||
}
|
||||
.padding({ right: 4 })
|
||||
}
|
||||
.scrollable(ScrollDirection.Horizontal)
|
||||
.scrollBar(BarState.Off)
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ---- 金额 ----
|
||||
Row({ space: 8 }) {
|
||||
Text('金额')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(34)
|
||||
TextInput({ text: this.amount, placeholder: '如 12500 或 860.50' })
|
||||
.type(InputType.NUMBER_DECIMAL)
|
||||
.placeholderColor(FbColors.faint)
|
||||
.placeholderFont({ size: FbMetric.fontTiny })
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(FbColors.ink)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.height(44)
|
||||
.layoutWeight(1)
|
||||
.padding({ left: 10, right: 10 })
|
||||
.onChange((v: string): void => {
|
||||
this.amount = v;
|
||||
this.err = '';
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 负债提示:用户容易把欠款填成正数又不知道对不对
|
||||
if (this.isDebt()) {
|
||||
Text('「' + this.kindNameOf(this.kind) + '」算负债 —— 填正数就行,'
|
||||
+ '净资产会自动减去它(服务端按绝对值计入总负债)。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
}
|
||||
|
||||
// ---- 名称 ----
|
||||
Row({ space: 8 }) {
|
||||
Text('名称')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(34)
|
||||
TextInput({ text: this.title, placeholder: '如 工资卡 / 房贷' })
|
||||
.placeholderColor(FbColors.faint)
|
||||
.placeholderFont({ size: FbMetric.fontTiny })
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.height(42)
|
||||
.layoutWeight(1)
|
||||
.padding({ left: 10, right: 10 })
|
||||
.onChange((v: string): void => {
|
||||
this.title = v;
|
||||
this.err = '';
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 备注 ----
|
||||
Row({ space: 8 }) {
|
||||
Text('备注')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(34)
|
||||
TextArea({ text: this.note, placeholder: '想记的细节(可空)' })
|
||||
.placeholderColor(FbColors.faint)
|
||||
.placeholderFont({ size: FbMetric.fontTiny })
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.height(72)
|
||||
.layoutWeight(1)
|
||||
.padding({ left: 10, right: 10, top: 8, bottom: 8 })
|
||||
.onChange((v: string): void => {
|
||||
this.note = v;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Top)
|
||||
|
||||
Text('已有账目的金额修改 / 删除还在网页端 —— 接口只开放了"记一笔"。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.constraintSize({ maxHeight: 400 })
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.align(Alignment.TopStart)
|
||||
|
||||
if (this.err !== '') {
|
||||
Text(this.err)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.red)
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
}
|
||||
|
||||
Text(this.saving ? '记入中…' : '记下来')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(this.saving ? FbColors.faint : '#FFFFFF')
|
||||
.textAlign(TextAlign.Center)
|
||||
.width('100%')
|
||||
.height(44)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(fbAlpha(this.accent, this.saving ? 0.35 : 0.98))
|
||||
.onClick((): void => {
|
||||
this.save();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.hitTestBehavior(HitTestMode.Transparent)
|
||||
}
|
||||
|
||||
private async save(): Promise<void> {
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
const amt: string = this.amount.trim();
|
||||
if (amt === '') {
|
||||
this.err = '金额得填一个数。';
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
this.err = '';
|
||||
const res: FbApiResult = await addLedgerEntry(FB_TOKEN_STORE.get(), this.ledgerId,
|
||||
this.kind, amt, this.title.trim(), this.note.trim());
|
||||
this.saving = false;
|
||||
if (!res.ok) {
|
||||
this.err = res.error !== '' ? res.error : '没记上,请稍后再试。';
|
||||
return;
|
||||
}
|
||||
this.onSaved();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user