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]`),真机未验收
This commit is contained in:
2026-10-20 20:18:38 +08:00
parent ec2cb14c4c
commit 2ff90519fa
9 changed files with 703 additions and 44 deletions
@@ -17,7 +17,7 @@ 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 { addLedgerEntry, updateLedgerEntry, deleteLedgerEntry } from '../data/FbLedgerRepo';
import { FbApiResult } from '../data/FbHttp';
/** 类别键(顺序与 `Ledger::KINDS` 一致) */
@@ -35,6 +35,19 @@ export struct FbLedgerEntrySheet {
/** 记到哪本账 */
@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 => {};
@@ -45,6 +58,28 @@ export struct FbLedgerEntrySheet {
@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);
@@ -77,7 +112,7 @@ export struct FbLedgerEntrySheet {
Column({ space: 12 }) {
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('记一笔')
Text(this.isEdit() ? '改这一笔' : '记一笔')
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
@@ -213,11 +248,30 @@ export struct FbLedgerEntrySheet {
.width('100%')
.alignItems(VerticalAlign.Top)
Text('已有账目的金额修改 / 删除还在网页端 —— 接口只开放了"记一笔"。')
if (this.isEdit()) {
if (!this.confirmingDelete) {
Text('删掉这一笔')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.fontColor(FbColors.red)
.width('100%')
.maxLines(3)
.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)
@@ -235,7 +289,8 @@ export struct FbLedgerEntrySheet {
.maxLines(3)
}
Text(this.saving ? '记入中…' : '记下来')
Text(this.saving ? (this.isEdit() ? '保存中…' : '记入中…')
: (this.isEdit() ? '保存改动' : '记下来'))
.fontSize(FbMetric.fontBody)
.fontWeight(FontWeight.Medium)
.fontColor(this.saving ? FbColors.faint : '#FFFFFF')
@@ -271,7 +326,10 @@ export struct FbLedgerEntrySheet {
}
this.saving = true;
this.err = '';
const res: FbApiResult = await addLedgerEntry(FB_TOKEN_STORE.get(), this.ledgerId,
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) {
@@ -280,4 +338,21 @@ export struct FbLedgerEntrySheet {
}
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();
}
}
@@ -0,0 +1,385 @@
/**
* 家书 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)
}
}
+75 -7
View File
@@ -3,19 +3,25 @@
*
* 对应服务端 `app/ApiHandler.php` 的 `ledger()`:
* POST /api/v1/ledger 新建账本(name)
* POST /api/v1/ledger/<id>/entries 记一笔(kind / amount / title / note / person_id)
* PATCH /api/v1/ledger/<id> 账本改名(name) ← v68
* DELETE /api/v1/ledger/<id> 删账本
* POST /api/v1/ledger/<id>/entries 记一笔(kind/amount/title/note/person_id)
* PATCH /api/v1/ledger/<id>/entries 改一笔(**id 在 body 里**) ← v68
* DELETE /api/v1/ledger/<id>/entries 删一笔(**id 在 body/query**)← v68
* GET /api/v1/ledger/<id>/shares 共享给了谁 ← v68
* POST /api/v1/ledger/<id>/shares 共享给某人(person_id/perm)← v68
* DELETE /api/v1/ledger/<id>/shares 取消共享(person_id) ← v68
*
* 🔴 用户 2026-10-20:「还有这个财务的显示也有问题。」
* ⇒ 端上「记一笔」按钮**没有 `onTap`**、账本切换**不重拉明细**、
* 资产/负债是**端上自己按正负号重新加**的(与净资产口径不一致)。见 `FbLedgerPage`。
* 🔴 用户 2026-10-20:「还有这个财务的显示也有问题。」(v65 已修显示口径)
* v68 补的是**动作**:设置里「共享给谁」「账本名字」原来是灰条,
* 账目也没有"改一笔/删一笔" —— 模型里这些方法早就有,只差路由与端上入口。
*
* ⚠️ **金额传「元」的字符串**:服务端 `Ledger::toCents()` 负责转分
* (App 与网页表单同一口径)。这里**不要**先转分再传 —— 会双重换算,差 100 倍。
*
* ⚠️ **没有"改一笔"和"删一笔"的接口**(`Api::` 路由只解析到第 3 段,
* `/ledger/<id>/entries/<eid>` 的第 4 段会被丢掉)。所以端上只做"记一笔",
* 改了金额请去网页端操作 —— 这一条要显式写在界面上,别让用户以为存不上。
* 🔴 **被改/被删的那一笔,id 只能放 body 或 query,不能拼进路径**:
* `Api::dispatch()` 只解析 `/<res>/<id>/<sub>` **三段**,
* `/ledger/3/entries/17` 的第 4 段会被直接丢掉(会落到"整本账本"上)。
*/
import { fbApi, FbApiResult, FbApiOptions } from './FbHttp';
@@ -33,3 +39,65 @@ export async function addLedgerEntry(token: string, ledgerId: number, kind: stri
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
return await fbApi('/api/v1/ledger/' + ledgerId.toString() + '/entries', opts);
}
/** 改一笔(金额/名称/备注/收支/经手人)。`entryId` 必须在 body 里,见文件头说明。 */
export async function updateLedgerEntry(token: string, ledgerId: number, entryId: number,
kind: string, amountYuan: string, title: string, note: string): Promise<FbApiResult> {
const body: Record<string, Object> = {};
body['id'] = entryId;
body['kind'] = kind;
body['amount'] = amountYuan;
body['title'] = title;
body['note'] = note;
const opts: FbApiOptions = { method: 'PATCH', token: token, body: body };
return await fbApi('/api/v1/ledger/' + ledgerId.toString() + '/entries', opts);
}
/** 删一笔 */
export async function deleteLedgerEntry(token: string, ledgerId: number,
entryId: number): Promise<FbApiResult> {
/* ⚠️ DELETE 也带 body:服务端的 `Api::body()` 认 JSON body;
同时也放进 query(`?id=`)—— 某些代理会丢 DELETE 的 body。 */
const path: string = '/api/v1/ledger/' + ledgerId.toString() + '/entries?id=' +
entryId.toString();
const body: Record<string, Object> = {};
body['id'] = entryId;
const opts: FbApiOptions = { method: 'DELETE', token: token, body: body };
return await fbApi(path, opts);
}
/** 账本改名 */
export async function renameLedger(token: string, ledgerId: number,
name: string): Promise<FbApiResult> {
const body: Record<string, Object> = {};
body['name'] = name;
const opts: FbApiOptions = { method: 'PATCH', token: token, body: body };
return await fbApi('/api/v1/ledger/' + ledgerId.toString(), opts);
}
/** 共享给了谁(服务端回 `{items:[…]}`,每项含 person_id / name / perm) */
export async function fetchLedgerShares(token: string, ledgerId: number): Promise<FbApiResult> {
const opts: FbApiOptions = { token: token, authProbe: true };
return await fbApi('/api/v1/ledger/' + ledgerId.toString() + '/shares', opts);
}
/** 共享给某人(perm = read / write) */
export async function shareLedger(token: string, ledgerId: number, personId: number,
perm: string): Promise<FbApiResult> {
const body: Record<string, Object> = {};
body['person_id'] = personId;
body['perm'] = perm;
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
return await fbApi('/api/v1/ledger/' + ledgerId.toString() + '/shares', opts);
}
/** 取消某人的共享 */
export async function unshareLedger(token: string, ledgerId: number,
personId: number): Promise<FbApiResult> {
const path: string = '/api/v1/ledger/' + ledgerId.toString() + '/shares?person_id=' +
personId.toString();
const body: Record<string, Object> = {};
body['person_id'] = personId;
const opts: FbApiOptions = { method: 'DELETE', token: token, body: body };
return await fbApi(path, opts);
}
+4
View File
@@ -174,6 +174,8 @@ interface RawTaskBoard {
interface RawLedgerBook {
id?: number; name?: string; my_perm?: string | null; entry_count?: number;
share_perm?: string | null;
/** v68:服务端补的"是不是我建的"(`my_perm` 只有 read/write,分不出主人) */
is_owner?: boolean | number | null;
}
interface RawLedgerEntry {
@@ -390,6 +392,7 @@ export function parseLedger(json: string): FbLedgerBook[] {
name: s(r.name),
netCent: 0, // 概览接口不返回净资产,进详情页时再取 summary
myPerm: s(r.my_perm) !== '' ? s(r.my_perm) : 'read',
isOwner: b(r.is_owner),
entryCount: n(r.entry_count),
};
out.push(bk);
@@ -421,6 +424,7 @@ export function parseLedgerEntries(json: string): FbLedgerDetail {
amountCent: n(r.amount_cents),
// 「挂名的人」优先用昵称(我习惯叫他什么),没有就用本名
ownerName: s(r.person_nick) !== '' ? s(r.person_nick) : s(r.person_name),
note: s(r.note),
updatedAt: datePart(s(r.updated_at)),
});
}
+12 -12
View File
@@ -246,21 +246,21 @@ export const FB_DEMO_TASK_BOARDS: string[] = ['家务', '家人健康', '孩子'
// ============================================================
export const FB_DEMO_LEDGER_BOOKS: FbLedgerBook[] = [
{ id: 1, name: '家庭总账', netCent: 86420000, myPerm: 'owner', entryCount: 9 },
{ id: 2, name: '我的私房账', netCent: 1260000, myPerm: 'owner', entryCount: 3 },
{ id: 3, name: '爸妈的养老账', netCent: 4580000, myPerm: 'write', entryCount: 5 }
{ id: 1, name: '家庭总账', netCent: 86420000, myPerm: 'write', isOwner: true, entryCount: 9 },
{ id: 2, name: '我的私房账', netCent: 1260000, myPerm: 'write', isOwner: true, entryCount: 3 },
{ id: 3, name: '爸妈的养老账', netCent: 4580000, myPerm: 'write', isOwner: false, entryCount: 5 }
];
export const FB_DEMO_LEDGER_ENTRIES: FbLedgerEntry[] = [
{ id: 1, name: '工资卡', kind: 'deposit', amountCent: 18600000, ownerName: '我', updatedAt: '2026-10-01' },
{ id: 2, name: '公积金', kind: 'deposit', amountCent: 24300000, ownerName: '我', updatedAt: '2026-10-01' },
{ id: 3, name: '指数基金', kind: 'fund', amountCent: 34800000, ownerName: '我', updatedAt: '2026-09-30' },
{ id: 4, name: '重疾险', kind: 'insurance', amountCent: 500000, ownerName: '爱人', updatedAt: '2026-08-12' },
{ id: 5, name: '自住房', kind: 'house', amountCent: 180000000, ownerName: '我们', updatedAt: '2026-06-20' },
{ id: 6, name: '代步车', kind: 'car', amountCent: 8200000, ownerName: '我', updatedAt: '2026-06-20' },
{ id: 7, name: '房贷余额', kind: 'debt', amountCent: -142600000, ownerName: '我们', updatedAt: '2026-10-01' },
{ id: 8, name: '信用卡待还', kind: 'debt', amountCent: -3860000, ownerName: '爱人', updatedAt: '2026-10-08' },
{ id: 9, name: '车位', kind: 'house', amountCent: 12000000, ownerName: '我们', updatedAt: '2026-06-20' }
{ id: 1, name: '工资卡', kind: 'deposit', amountCent: 18600000, note: '', ownerName: '我', updatedAt: '2026-10-01' },
{ id: 2, name: '公积金', kind: 'deposit', amountCent: 24300000, note: '', ownerName: '我', updatedAt: '2026-10-01' },
{ id: 3, name: '指数基金', kind: 'fund', amountCent: 34800000, note: '', ownerName: '我', updatedAt: '2026-09-30' },
{ id: 4, name: '重疾险', kind: 'insurance', amountCent: 500000, note: '', ownerName: '爱人', updatedAt: '2026-08-12' },
{ id: 5, name: '自住房', kind: 'house', amountCent: 180000000, note: '', ownerName: '我们', updatedAt: '2026-06-20' },
{ id: 6, name: '代步车', kind: 'car', amountCent: 8200000, note: '', ownerName: '我', updatedAt: '2026-06-20' },
{ id: 7, name: '房贷余额', kind: 'debt', amountCent: -142600000, note: '', ownerName: '我们', updatedAt: '2026-10-01' },
{ id: 8, name: '信用卡待还', kind: 'debt', amountCent: -3860000, note: '', ownerName: '爱人', updatedAt: '2026-10-08' },
{ id: 9, name: '车位', kind: 'house', amountCent: 12000000, note: '', ownerName: '我们', updatedAt: '2026-06-20' }
];
// ============================================================
+2 -2
View File
@@ -61,8 +61,8 @@ export const FB_SET_DAYS: FbSettingItem[] = [
/** 账本(templates/ledger/_settings_menu.php) */
export const FB_SET_LEDGER: FbSettingItem[] = [
{ icon: 'share', label: '共享给谁', hint: '对方就能记能看', action: '' },
{ icon: 'edit', label: '账本名字', hint: '改这本账本的名字', action: '' }
{ icon: 'share', label: '共享给谁', hint: '对方就能看这本账', action: 'ledgershare' },
{ icon: 'edit', label: '账本名字', hint: '改这本账本的名字', action: 'ledgername' }
];
/** 任务(templates/tasks/_settings_menu.php)
+11 -1
View File
@@ -686,8 +686,16 @@ export interface FbLedgerBook {
name: string;
/** 净资产(分) */
netCent: number;
/** 我的权限:owner / read / write */
/**
* 我的权限:**只有 `read` / `write` 两档**(服务端 `Ledger::find` 就是这么给的)。
*
* 🔴 v68:这里原来写的是 `owner / read / write` —— 服务端**从来不会**给 `owner`,
* 于是端上"是不是我建的账本"永远判成 false(共享浮层会一直说"只有主人能改")。
* 现在用服务端补的 `isOwner` 布尔。
*/
myPerm: string;
/** 这本账是不是我建的(只有主人能改共享关系) */
isOwner: boolean;
/** 账目条数 */
entryCount: number;
}
@@ -703,6 +711,8 @@ export interface FbLedgerEntry {
amountCent: number;
/** 挂名的人 */
ownerName: string;
/** 备注(v68:改一笔时要回填,原来类型里没有 ⇒ 编辑框永远是空的) */
note: string;
/** 记录/盘点时间 */
updatedAt: string;
}
@@ -26,6 +26,7 @@ import { fbOnScroll, fbOnTouch, fbOnHScroll } from '../../common/FbScroll';
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
import { FbModuleSettingsSheet } from '../../components/FbModuleSettingsSheet';
import { FbLedgerEntrySheet } from '../../components/FbLedgerEntrySheet';
import { FbLedgerRenameSheet, FbLedgerShareSheet } from '../../components/FbLedgerSettingsSheets';
import { FB_BACK } from '../../common/FbBack';
import { FB_SET_LEDGER } from '../../model/FbSettingsMenus';
@@ -77,6 +78,20 @@ export struct FbLedgerPage {
@Local debtNow: number = 0;
/** 「记一笔」浮层是否打开 */
@Local entryOpen: boolean = false;
/**
* 正在编辑哪一笔(0 = 记新的)。
*
* 🔴 v68:服务端补了 `PATCH/DELETE /ledger/<id>/entries`(id 走 body),
* 端上点一行就能改/删 —— 这四组初值就是点进来的那一行。
*/
@Local editId: number = 0;
@Local editKind: string = 'deposit';
@Local editAmount: string = '';
@Local editTitle: string = '';
@Local editNote: string = '';
/** 设置里两条浮层(账本改名 / 共享给谁) */
@Local renameOpen: boolean = false;
@Local shareOpen: boolean = false;
/**
* 页面出现:**两级加载**。
@@ -95,6 +110,14 @@ export struct FbLedgerPage {
}
private backOnce(): boolean {
if (this.shareOpen) {
this.shareOpen = false;
return true;
}
if (this.renameOpen) {
this.renameOpen = false;
return true;
}
if (this.entryOpen) {
this.entryOpen = false;
return true;
@@ -164,7 +187,24 @@ export struct FbLedgerPage {
/** 「记一笔」浮层开着 ⇒ 底部悬浮快捷栏让位(与设置浮层不会同时开) */
@Monitor('entryOpen')
onEntrySheetChanged(): void {
fbAppState().sheetCount = this.entryOpen ? 1 : 0;
this.recountSheets();
}
/* v68:改名 / 共享两个浮层同样要让底部悬浮快捷栏让位 */
@Monitor('renameOpen')
onRenameChanged(): void {
this.recountSheets();
}
@Monitor('shareOpen')
onShareChanged(): void {
this.recountSheets();
}
/** 任一浮层开着 ⇒ 悬浮栏让位(三者互斥,最多一个) */
private recountSheets(): void {
const n: number = (this.entryOpen || this.renameOpen || this.shareOpen) ? 1 : 0;
fbAppState().sheetCount = n;
}
build() {
@@ -434,9 +474,18 @@ export struct FbLedgerPage {
this.settingsOpen = false;
},
onPick: (action: string): void => {
/* 「共享给谁」「账本名字」**都没有 API**(服务端只开了
建账本 / 记一笔 / 删账本),所以这里保持"暂未开放"的灰态,
不接一个点了报错的入口。 */
/* v68:这两条原来都是灰的(服务端没有路由)。
现在服务端补齐了 `PATCH /ledger/<id>` 与 `/ledger/<id>/shares`。 */
if (action === 'ledgershare') {
this.settingsOpen = false;
this.shareOpen = true;
return;
}
if (action === 'ledgername') {
this.settingsOpen = false;
this.renameOpen = true;
return;
}
}
})
.transition(fbSheetTransition())
@@ -448,17 +497,59 @@ export struct FbLedgerPage {
accent: this.accent,
ledgerId: this.currentId(),
ledgerName: this.currentBook().name,
editId: this.editId,
editKind: this.editKind,
editAmount: this.editAmount,
editTitle: this.editTitle,
editNote: this.editNote,
onClose: (): void => {
this.entryOpen = false;
this.editId = 0;
},
onSaved: (): void => {
this.entryOpen = false;
this.editId = 0;
// 重拉明细:金额与汇总都在里面,端上拼不出来
this.loadEntries(this.currentId());
}
})
.transition(fbSheetTransition())
}
// ---- 账本改名(v68:设置里那条原来是灰的)----
if (this.renameOpen) {
FbLedgerRenameSheet({
accent: this.accent,
ledgerId: this.currentId(),
currentName: this.currentBook().name,
onClose: (): void => {
this.renameOpen = false;
},
onSaved: (): void => {
this.renameOpen = false;
this.load(); // 名字在 /ledger 列表里,重拉一次
}
})
.transition(fbSheetTransition())
}
// ---- 共享给谁(v68)----
if (this.shareOpen) {
FbLedgerShareSheet({
accent: this.accent,
ledgerId: this.currentId(),
ledgerName: this.currentBook().name,
// 主人才能改共享:`myPerm` 是服务端 `Ledger::find` 给的
isOwner: this.currentBook().isOwner,
onClose: (): void => {
this.shareOpen = false;
},
onChanged: (): void => {
this.load();
}
})
.transition(fbSheetTransition())
}
}
.width('100%')
.height('100%')
@@ -506,6 +597,31 @@ export struct FbLedgerPage {
}
}
.width('100%')
.onClick((): void => {
/* v68:点一行 = 改这一笔(金额 / 名称 / 备注 / 类别),按住不动的"删除"
放在浮层底部(二次确认)—— 与观影/旅程同一套交互习惯。 */
this.editId = e.id;
this.editKind = e.kind;
this.editAmount = this.amountYuanText(e.amountCent);
this.editTitle = e.name;
this.editNote = e.note;
this.entryOpen = true;
})
}
/**
* 分 → 「元」输入框文本(`1250050` → `12500.50`)。
*
* ⚠️ 不能复用 `fbMoney()`:那个带千分位和「¥」,塞回 `TextInput` 会被
* `Ledger::toCents()` 当成非法数字(它只吃数字与小数点)。
*/
private amountYuanText(cent: number): string {
const neg: boolean = cent < 0;
const c: number = Math.abs(cent);
const y: number = Math.floor(c / 100);
const f: number = c % 100;
const fs: string = f < 10 ? ('0' + f.toString()) : f.toString();
return (neg ? '-' : '') + y.toString() + '.' + fs;
}
private isCurrent(id: number): boolean {
@@ -524,7 +640,7 @@ export struct FbLedgerPage {
if (hit !== undefined) {
return hit;
}
return { id: 0, name: '账本', netCent: 0, myPerm: 'owner', entryCount: 0 };
return { id: 0, name: '账本', netCent: 0, myPerm: 'read', isOwner: false, entryCount: 0 };
}
private netCent(): number {
@@ -1482,7 +1482,14 @@ export struct FbPeoplePage {
// ============================================================
/**
* 家谱的说明条("以我为中心" + 图谱/列表切换 + 补关系按钮)。
* 家谱的说明条("以我为中心" + 图谱/列表切换)。
*
* 🔴 v68:这里原来还有一颗「补关系」按钮,**从来没有 `onTap`** ——
* 点了一点反应都没有(全仓库扫过,只剩这一颗)。
* 它要的是"选一个还没有亲属关系的人 → 给他挂上父/母/配偶",
* 而服务端 `relations` 表**只有读**(没有写接口),网页版也没有这个功能
* ⇒ 与其留一个永远点不动的按钮,先撤掉(免得被当成"坏了")。
* 要做得先加 `POST /api/v1/persons/<id>/relations`,那是独立一批活。
*
* ⚠️ 从 `kinView` 里抽出来的原因:**列表模式**要把它**一起放进滚动的 Scroll**
* (这样它才跟着内容滚走),而**图谱模式**它是固定的一层。
@@ -1511,12 +1518,6 @@ export struct FbPeoplePage {
.padding(2)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusPill)
FbPrimaryButton({
label: '补关系',
bg: '#8A6B4B',
icon: $r('app.media.fb_ic_plus')
})
}
.width('100%')
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })