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
+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)),
});
}