Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbLedgerSettingsSheets.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

386 lines
12 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 · 账本设置的两个浮层(鸿蒙端,v68)
*
* 对应 `FB_SET_LEDGER` 里原来那两条**灰条**:
* 「账本名字」→ `FbLedgerRenameSheet`(PATCH /ledger/<id>)
* 「共享给谁」→ `FbLedgerShareSheet`(GET/POST/DELETE /ledger/<id>/shares)
*
* 🔴 用户 2026-10-20:「财务的显示也有问题」,v65 修的是**显示口径**
* (端上按正负号重加 vs 服务端负债取绝对值);这一批补的是**动作**:
* 模型 `Ledger::rename/setShare/unshare/shares` 一直都有,只是没有路由,
* 端上就只能把这两条画成灰的。
*
* ⚠️ 只有**账本主人**能改共享(服务端也是这个判断,403 会说原话)——
* 界面先把"只有主人"写出来,别让被共享的人点了才收到 403。
*/
import { FbColors, FbMetric, FB_MASK } from '../common/Theme';
import { FbSheetClose } from './FbSheetClose';
import { FbSheetRing } from './FbSheetRing';
import { FbPersonPickSheet } from './FbPersonPickSheet';
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { FbApiResult } from '../data/FbHttp';
import { renameLedger, fetchLedgerShares, shareLedger, unshareLedger } from '../data/FbLedgerRepo';
/** 一条共享记录(服务端 `Ledger::shares()`:s.* + p.name / avatar) */
interface FbLedgerShareRow {
personId: number;
name: string;
perm: string;
}
/** 服务端原始行(`Api::ok(['items' => …])`)—— 字段名照抄数据库列名 */
interface RawShareRow {
person_id?: number;
name?: string | null;
perm?: string | null;
}
interface RawShareList {
items?: RawShareRow[];
}
@ComponentV2
export struct FbLedgerRenameSheet {
@Param accent: string = '#5E7A3C';
@Param ledgerId: number = 0;
@Param currentName: string = '';
@Event onClose: () => void = (): void => {};
@Event onSaved: () => void = (): void => {};
@Local name: string = '';
@Local saving: boolean = false;
@Local err: string = '';
aboutToAppear(): void {
this.name = this.currentName;
}
build() {
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)
Text('只改名字,账目不动')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
FbSheetClose({
onClose: (): void => {
this.onClose();
},
color: this.accent
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
TextInput({ text: this.name, placeholder: '如:家里的账 / 装修账' })
.placeholderColor(FbColors.faint)
.placeholderFont({ size: FbMetric.fontTiny })
.fontSize(FbMetric.fontBody)
.fontColor(FbColors.ink)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.height(44)
.width('100%')
.padding({ left: 10, right: 10 })
.onChange((v: string): void => {
this.name = v;
this.err = '';
})
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(this.accent)
.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 nm: string = this.name.trim();
if (nm === '') {
this.err = '名字不能空着。';
return;
}
this.saving = true;
const res: FbApiResult = await renameLedger(FB_TOKEN_STORE.get(), this.ledgerId, nm);
this.saving = false;
if (!res.ok) {
this.err = res.error !== '' ? res.error : '没改成,请稍后再试。';
return;
}
this.onSaved();
}
}
@ComponentV2
export struct FbLedgerShareSheet {
@Param accent: string = '#5E7A3C';
@Param ledgerId: number = 0;
@Param ledgerName: string = '';
/** 只有主人能改 —— 服务端也是这个判断 */
@Param isOwner: boolean = true;
@Event onClose: () => void = (): void => {};
/** 共享关系变了(页面可以提示一句) */
@Event onChanged: () => void = (): void => {};
@Local rows: FbLedgerShareRow[] = [];
@Local roster: FbRosterPerson[] = [];
@Local picking: boolean = false;
@Local busy: boolean = false;
@Local note: string = '';
aboutToAppear(): void {
this.reload();
}
private async reload(): Promise<void> {
const res: FbApiResult = await fetchLedgerShares(FB_TOKEN_STORE.get(), this.ledgerId);
if (!res.ok) {
this.note = res.error !== '' ? res.error : '共享列表没拉到。';
return;
}
/* ⚠️ `FbApiResult.data` 是**原始 JSON 片段**(不是已解析的对象),
而且列表在信封里是 `{items:[…]}` —— 两层都要拆。 */
let parsed: RawShareList = {};
try {
parsed = JSON.parse(res.data) as RawShareList;
} catch (e) {
this.note = '共享列表格式看不懂。';
return;
}
const rawRows: RawShareRow[] = parsed.items !== undefined ? parsed.items : [];
const raw: FbLedgerShareRow[] = [];
for (const it of rawRows) {
raw.push({
personId: it.person_id !== undefined ? it.person_id : 0,
name: it.name !== undefined && it.name !== null ? it.name : '(无名)',
perm: it.perm !== undefined && it.perm !== null ? it.perm : 'read'
});
}
this.rows = raw;
this.note = '';
}
private async openPicker(): Promise<void> {
if (this.roster.length === 0) {
try {
this.roster = await FbPersonRepo.inst().list();
} catch (e) {
this.roster = [];
}
}
this.picking = true;
}
private async addShares(ids: number[]): Promise<void> {
this.picking = false;
if (ids.length === 0) {
return;
}
this.busy = true;
let bad: string = '';
for (const id of ids) {
const res: FbApiResult = await shareLedger(FB_TOKEN_STORE.get(), this.ledgerId, id, 'read');
if (!res.ok && bad === '') {
bad = res.error;
}
}
this.busy = false;
this.note = bad !== '' ? bad : '';
await this.reload();
this.onChanged();
}
private async drop(personId: number): Promise<void> {
this.busy = true;
const res: FbApiResult = await unshareLedger(FB_TOKEN_STORE.get(), this.ledgerId, personId);
this.busy = false;
if (!res.ok) {
this.note = res.error !== '' ? res.error : '没取消成。';
return;
}
await this.reload();
this.onChanged();
}
build() {
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: 10 }) {
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('共享给谁')
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
Text('「' + this.ledgerName + '」· 被共享的人只能看')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.maxLines(1)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.flexShrink(1)
FbSheetClose({
onClose: (): void => {
this.onClose();
},
color: this.accent
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
if (!this.isOwner) {
Text('这本账不是你建的 —— 只有账本主人能改共享关系。')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.width('100%')
.maxLines(3)
}
if (this.rows.length === 0) {
Text('还没共享给任何人')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.width('100%')
} else {
ForEach(this.rows, (r: FbLedgerShareRow) => {
Row({ space: 8 }) {
Text(r.name)
.fontSize(FbMetric.fontBody)
.fontColor(FbColors.ink)
.layoutWeight(1)
.maxLines(1)
Text(r.perm === 'write' ? '可改' : '只看')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
if (this.isOwner) {
Text('取消')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.padding({ left: 8, right: 4, top: 4, bottom: 4 })
.onClick((): void => {
this.drop(r.personId);
})
}
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.alignItems(VerticalAlign.Center)
}, (r: FbLedgerShareRow): string => 'lsh_' + r.personId.toString())
}
if (this.note !== '') {
Text(this.note)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.width('100%')
.maxLines(3)
}
if (this.isOwner) {
Text(this.busy ? '处理中…' : '共享给…')
.fontSize(FbMetric.fontBody)
.fontWeight(FontWeight.Medium)
.fontColor(this.busy ? FbColors.faint : '#FFFFFF')
.textAlign(TextAlign.Center)
.width('100%')
.height(44)
.borderRadius(FbMetric.radiusS)
.backgroundColor(this.accent)
.onClick((): void => {
this.openPicker();
})
}
}
.width('100%')
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
.alignItems(HorizontalAlign.Start)
}
}
.width('92%')
/* 选人浮层:复用名册那颗(多选 + 确定),选完逐个人 POST */
if (this.picking) {
FbPersonPickSheet({
people: this.roster,
selected: [],
title: '共享给谁',
accent: this.accent,
onConfirm: (ids: number[]): void => {
this.addShares(ids);
},
onClose: (): void => {
this.picking = false;
}
})
}
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.Transparent)
}
}