From 2ff90519faa2e03afce5d35213d6349e58929490 Mon Sep 17 00:00:00 2001 From: laoyang Date: Tue, 20 Oct 2026 20:18:38 +0800 Subject: [PATCH] =?UTF-8?q?v68=EF=BC=9A=E8=B4=A2=E5=8A=A1=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=EF=BC=88=E6=94=B9=E4=B8=80=E7=AC=94/=E5=88=A0?= =?UTF-8?q?=E4=B8=80=E7=AC=94/=E8=B4=A6=E6=9C=AC=E6=94=B9=E5=90=8D/?= =?UTF-8?q?=E5=85=B1=E4=BA=AB=E7=BB=99=E8=B0=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 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]`),真机未验收 --- .../ets/components/FbLedgerEntrySheet.ets | 95 ++++- .../ets/components/FbLedgerSettingsSheets.ets | 385 ++++++++++++++++++ entry/src/main/ets/data/FbLedgerRepo.ets | 82 +++- entry/src/main/ets/data/FbParse.ets | 4 + entry/src/main/ets/model/DemoData.ets | 24 +- entry/src/main/ets/model/FbSettingsMenus.ets | 4 +- entry/src/main/ets/model/Types.ets | 12 +- .../main/ets/pages/ledger/FbLedgerPage.ets | 126 +++++- .../main/ets/pages/people/FbPeoplePage.ets | 15 +- 9 files changed, 703 insertions(+), 44 deletions(-) create mode 100644 entry/src/main/ets/components/FbLedgerSettingsSheets.ets diff --git a/entry/src/main/ets/components/FbLedgerEntrySheet.ets b/entry/src/main/ets/components/FbLedgerEntrySheet.ets index b216d08..0cbcd20 100644 --- a/entry/src/main/ets/components/FbLedgerEntrySheet.ets +++ b/entry/src/main/ets/components/FbLedgerEntrySheet.ets @@ -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//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('已有账目的金额修改 / 删除还在网页端 —— 接口只开放了"记一笔"。') - .fontSize(FbMetric.fontTiny) - .fontColor(FbColors.faint) - .width('100%') - .maxLines(3) + 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) @@ -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,8 +326,11 @@ export struct FbLedgerEntrySheet { } 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()); + 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 : '没记上,请稍后再试。'; @@ -280,4 +338,21 @@ export struct FbLedgerEntrySheet { } this.onSaved(); } + + /** 删掉这一笔(服务端 `DELETE /ledger//entries`,id 走 body + query) */ + private async remove(): Promise { + 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(); + } } diff --git a/entry/src/main/ets/components/FbLedgerSettingsSheets.ets b/entry/src/main/ets/components/FbLedgerSettingsSheets.ets new file mode 100644 index 0000000..1b71132 --- /dev/null +++ b/entry/src/main/ets/components/FbLedgerSettingsSheets.ets @@ -0,0 +1,385 @@ +/** + * 家书 FamilyBook · 账本设置的两个浮层(鸿蒙端,v68) + * + * 对应 `FB_SET_LEDGER` 里原来那两条**灰条**: + * 「账本名字」→ `FbLedgerRenameSheet`(PATCH /ledger/) + * 「共享给谁」→ `FbLedgerShareSheet`(GET/POST/DELETE /ledger//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 { + 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 { + 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 { + 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 { + 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 { + 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) + } +} diff --git a/entry/src/main/ets/data/FbLedgerRepo.ets b/entry/src/main/ets/data/FbLedgerRepo.ets index 72f5365..7109fbb 100644 --- a/entry/src/main/ets/data/FbLedgerRepo.ets +++ b/entry/src/main/ets/data/FbLedgerRepo.ets @@ -3,19 +3,25 @@ * * 对应服务端 `app/ApiHandler.php` 的 `ledger()`: * POST /api/v1/ledger 新建账本(name) - * POST /api/v1/ledger//entries 记一笔(kind / amount / title / note / person_id) + * PATCH /api/v1/ledger/ 账本改名(name) ← v68 * DELETE /api/v1/ledger/ 删账本 + * POST /api/v1/ledger//entries 记一笔(kind/amount/title/note/person_id) + * PATCH /api/v1/ledger//entries 改一笔(**id 在 body 里**) ← v68 + * DELETE /api/v1/ledger//entries 删一笔(**id 在 body/query**)← v68 + * GET /api/v1/ledger//shares 共享给了谁 ← v68 + * POST /api/v1/ledger//shares 共享给某人(person_id/perm)← v68 + * DELETE /api/v1/ledger//shares 取消共享(person_id) ← v68 * - * 🔴 用户 2026-10-20:「还有这个财务的显示也有问题。」 - * ⇒ 端上「记一笔」按钮**没有 `onTap`**、账本切换**不重拉明细**、 - * 资产/负债是**端上自己按正负号重新加**的(与净资产口径不一致)。见 `FbLedgerPage`。 + * 🔴 用户 2026-10-20:「还有这个财务的显示也有问题。」(v65 已修显示口径) + * v68 补的是**动作**:设置里「共享给谁」「账本名字」原来是灰条, + * 账目也没有"改一笔/删一笔" —— 模型里这些方法早就有,只差路由与端上入口。 * * ⚠️ **金额传「元」的字符串**:服务端 `Ledger::toCents()` 负责转分 * (App 与网页表单同一口径)。这里**不要**先转分再传 —— 会双重换算,差 100 倍。 * - * ⚠️ **没有"改一笔"和"删一笔"的接口**(`Api::` 路由只解析到第 3 段, - * `/ledger//entries/` 的第 4 段会被丢掉)。所以端上只做"记一笔", - * 改了金额请去网页端操作 —— 这一条要显式写在界面上,别让用户以为存不上。 + * 🔴 **被改/被删的那一笔,id 只能放 body 或 query,不能拼进路径**: + * `Api::dispatch()` 只解析 `///` **三段**, + * `/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 { + const body: Record = {}; + 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 { + /* ⚠️ 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 = {}; + 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 { + const body: Record = {}; + 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 { + 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 { + const body: Record = {}; + 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 { + const path: string = '/api/v1/ledger/' + ledgerId.toString() + '/shares?person_id=' + + personId.toString(); + const body: Record = {}; + body['person_id'] = personId; + const opts: FbApiOptions = { method: 'DELETE', token: token, body: body }; + return await fbApi(path, opts); +} diff --git a/entry/src/main/ets/data/FbParse.ets b/entry/src/main/ets/data/FbParse.ets index 3e1496a..1f1e027 100644 --- a/entry/src/main/ets/data/FbParse.ets +++ b/entry/src/main/ets/data/FbParse.ets @@ -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)), }); } diff --git a/entry/src/main/ets/model/DemoData.ets b/entry/src/main/ets/model/DemoData.ets index 6a8fc28..c7f22e7 100644 --- a/entry/src/main/ets/model/DemoData.ets +++ b/entry/src/main/ets/model/DemoData.ets @@ -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' } ]; // ============================================================ diff --git a/entry/src/main/ets/model/FbSettingsMenus.ets b/entry/src/main/ets/model/FbSettingsMenus.ets index 5336eb0..766ac12 100644 --- a/entry/src/main/ets/model/FbSettingsMenus.ets +++ b/entry/src/main/ets/model/FbSettingsMenus.ets @@ -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) diff --git a/entry/src/main/ets/model/Types.ets b/entry/src/main/ets/model/Types.ets index ff081df..9a479b2 100644 --- a/entry/src/main/ets/model/Types.ets +++ b/entry/src/main/ets/model/Types.ets @@ -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; } diff --git a/entry/src/main/ets/pages/ledger/FbLedgerPage.ets b/entry/src/main/ets/pages/ledger/FbLedgerPage.ets index 456c2da..35931fa 100644 --- a/entry/src/main/ets/pages/ledger/FbLedgerPage.ets +++ b/entry/src/main/ets/pages/ledger/FbLedgerPage.ets @@ -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//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/` 与 `/ledger//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 { diff --git a/entry/src/main/ets/pages/people/FbPeoplePage.ets b/entry/src/main/ets/pages/people/FbPeoplePage.ets index 3fb7796..fdaec3e 100644 --- a/entry/src/main/ets/pages/people/FbPeoplePage.ets +++ b/entry/src/main/ets/pages/people/FbPeoplePage.ets @@ -1482,7 +1482,14 @@ export struct FbPeoplePage { // ============================================================ /** - * 家谱的说明条("以我为中心" + 图谱/列表切换 + 补关系按钮)。 + * 家谱的说明条("以我为中心" + 图谱/列表切换)。 + * + * 🔴 v68:这里原来还有一颗「补关系」按钮,**从来没有 `onTap`** —— + * 点了一点反应都没有(全仓库扫过,只剩这一颗)。 + * 它要的是"选一个还没有亲属关系的人 → 给他挂上父/母/配偶", + * 而服务端 `relations` 表**只有读**(没有写接口),网页版也没有这个功能 + * ⇒ 与其留一个永远点不动的按钮,先撤掉(免得被当成"坏了")。 + * 要做得先加 `POST /api/v1/persons//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 })