Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbLedgerEntrySheet.ets
T
laoyang 2ff90519fa v68:财务补齐(改一笔/删一笔/账本改名/共享给谁)
用户 2026-10-20:「还有这个财务的显示也有问题」——v65 修的是**显示口径**
(端上按正负号重加 vs 服务端负债取绝对值);这一批补的是**动作**:
设置里「共享给谁」「账本名字」一直是灰条,账目也改不了删不掉。

## 根因:不是端上没写,是服务端没有路由
`Ledger::rename / setShare / unshare / shares / updateEntry / removeEntry`
**一直都有**,`ApiHandler::ledger()` 却只开了「建账本 / 记一笔 / 删账本」
⇒ 端上只能把这两条画成"暂未开放"。

## 端上做了什么
- 点一行账目 → 同一个浮层进**编辑态**(类别 / 金额 / 名称 / 备注 +
  删除二次确认)。`aboutToAppear` 里把 @Param 拷进 @Local,
  点第二行才不会还是上一笔的内容。
- 设置「账本名字」→ 改名浮层;「共享给谁」→ 共享浮层(列表 + 选人 +
  逐个人取消)。列表解析走 `{items:[…]}`(`FbApiResult.data` 是原始 JSON)。
- 三个浮层共用 `recountSheets()` 让悬浮栏让位(原来只有"记一笔"有)。

## 顺手修的两处「对着服务端抄错」
1. `FbLedgerBook.myPerm` 的注释写 `owner / read / write`,
   而服务端**从来不给 `owner`**(主人也是 `write`)⇒ 端上"是不是我建的"永远 false。
   现在用服务端新补的 `isOwner` 布尔。
2. `FbLedgerEntry` 没有 `note` 字段 ⇒ 编辑时备注框永远是空的。补上类型与解析。

## 顺带
- 撤掉名册家谱那颗「补关系」按钮:从来没有 `onTap`,服务端 `relations`
  只有读接口、网页版也没这个功能 ⇒ 留个永远点不动的按钮只会被当成"坏了"。

## 验证
- `hvigorw assembleHap` → BUILD SUCCESSFUL
- `_fb_arkts_guard.js` → 827 / 0(新增 groupV68 29 条)
- `_fb_v59d_falsify.py` → 139 / 0(v68 新增 9 条注入全部变红)
- ⚠️ 设备仍断连(`hdc list targets` → `[Empty]`),真机未验收
2026-10-20 20:18:38 +08:00

359 lines
14 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 · 「记一笔」浮层(鸿蒙端)
*
* 对应网页版 `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, updateLedgerEntry, deleteLedgerEntry } 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 = '';
/**
* 编辑模式:> 0 = 改这一条(`0` = 新记一笔)。
*
* 🔴 v68(用户 2026-10-20「财务的显示也有问题」的后续):
* 原来这里只有「记一笔」,底部还写着"改/删去网页端"——因为服务端没有路由。
* v68 服务端补了 `PATCH/DELETE /ledger/<id>/entries`(id 走 body),
* 端上就地把**改一笔 / 删一笔**做完。
*/
@Param editId: number = 0;
@Param editKind: string = 'deposit';
@Param editAmount: string = '';
@Param editTitle: string = '';
@Param editNote: 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 = '';
/** 删除的二次确认(删账目不可逆,必须先问一句) */
@Local confirmingDelete: boolean = false;
/**
* 编辑态要**把 @Param 拷进 @Local**。
*
* ⚠️ 不能在字段初始化里写 `@Local amount: string = this.editAmount`:
* V2 的成员初始化顺序不保证,`@Param` 那时可能还没赋值(拿到空串)。
* 浮层每次打开都是新实例 ⇒ 在 `aboutToAppear` 拷一次就够。
*/
aboutToAppear(): void {
if (this.editId > 0) {
if (this.editKind !== '') { this.kind = this.editKind; }
this.amount = this.editAmount;
this.title = this.editTitle;
this.note = this.editNote;
}
}
private isEdit(): boolean {
return this.editId > 0;
}
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(this.isEdit() ? '改这一笔' : '记一笔')
.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)
if (this.isEdit()) {
if (!this.confirmingDelete) {
Text('删掉这一笔')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.width('100%')
.textAlign(TextAlign.Center)
.onClick((): void => {
this.confirmingDelete = true;
})
} else {
Text('再点一次就删掉了(不可恢复)')
.fontSize(FbMetric.fontTiny)
.fontColor('#FFFFFF')
.textAlign(TextAlign.Center)
.width('100%')
.height(38)
.borderRadius(FbMetric.radiusS)
.backgroundColor('#C0533F')
.onClick((): void => {
this.remove();
})
}
}
}
.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 ? (this.isEdit() ? '保存中…' : '记入中…')
: (this.isEdit() ? '保存改动' : '记下来'))
.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 = this.isEdit()
? await updateLedgerEntry(FB_TOKEN_STORE.get(), this.ledgerId, this.editId,
this.kind, amt, this.title.trim(), this.note.trim())
: 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();
}
/** 删掉这一笔(服务端 `DELETE /ledger/<id>/entries`,id 走 body + query) */
private async remove(): Promise<void> {
if (this.saving) {
return;
}
this.saving = true;
const res: FbApiResult = await deleteLedgerEntry(FB_TOKEN_STORE.get(), this.ledgerId,
this.editId);
this.saving = false;
if (!res.ok) {
this.err = res.error !== '' ? res.error : '没删掉,请稍后再试。';
this.confirmingDelete = false;
return;
}
this.onSaved();
}
}