From 6f360fd6a1a3ab75ca722212879eb269c9645d9d Mon Sep 17 00:00:00 2001 From: laoyang Date: Wed, 28 Oct 2026 21:47:28 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E6=8A=98=E7=BA=BF=EF=BC=9A?= =?UTF-8?q?=E6=A6=82=E8=A7=88=E5=8D=A1=E7=94=BB=E5=87=80=E8=B5=84=E4=BA=A7?= =?UTF-8?q?=E8=B6=8B=E5=8A=BF=EF=BC=88=E9=85=8D=E6=9C=8D=E5=8A=A1=E7=AB=AF?= =?UTF-8?q?=20/timeline=EF=BC=89laoyang/FamilyBook#24?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户原话(2026-10-28):「鸿蒙端对齐网页端」。财务概览卡原来「折线图占位 (第二轮接图表能力后填)」,现在: 1) FbLedgerPoint 类型 + parseLedgerTimeline(读 /timeline 的 items) 2) loadEntries 里顺带拉 timeline(单独缓存键 ledger_timeline_) 3) timelinePath():时序点 → 绝对坐标折线 path(Path 组件画,Y 轴按 min..max 归一,不是从 0 起 —— 金额平稳时不贴底);<2 点时给占位文案。 编译 BUILD SUCCESSFUL;守卫 _fb_arkts_guard.js 1324/0;服务端冒烟 _i24_timeline_api.py 8/0 + 反证命中。 --- entry/src/main/ets/data/FbParse.ets | 26 +++++- entry/src/main/ets/model/Types.ets | 12 +++ .../main/ets/pages/ledger/FbLedgerPage.ets | 83 +++++++++++++++++-- 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/entry/src/main/ets/data/FbParse.ets b/entry/src/main/ets/data/FbParse.ets index c830ad6..162a6a9 100644 --- a/entry/src/main/ets/data/FbParse.ets +++ b/entry/src/main/ets/data/FbParse.ets @@ -17,7 +17,7 @@ import { FbAccount, FbAnniversaryItem, FbBookItem, FbBookNoteItem, FbCalEvent, FbHomeData, FbKinChip, FbMark, - FbLedgerBook, FbLedgerDetail, FbLedgerEntry, FbNoteItem, FbNoteBlock, FbNotificationItem, FbResourceItem, + FbLedgerBook, FbLedgerDetail, FbLedgerEntry, FbLedgerPoint, FbNoteItem, FbNoteBlock, FbNotificationItem, FbResourceItem, FbTaskItem, FbTripItem, FbWeather, FbStat, FbMemoryItem, FbRecentDay, FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskKid, FbTaskMeta, FbCatOpt, FbBoardOpt, } from '../model/Types'; @@ -230,6 +230,10 @@ interface RawLedgerSummary { asset?: number; debt?: number; net?: number; count?: number; } +interface RawLedgerPoint { + date?: string; net?: number; asset?: number; debt?: number; +} + interface RawResource { id?: number; title?: string; content?: string; owner_id?: number; owner_name?: string | null; avatar?: string | null; avatar_color?: string | null; @@ -515,6 +519,26 @@ export function parseLedgerEntries(json: string): FbLedgerDetail { return d; } +/** 财务折线时序 → FbLedgerPoint[](工单 #24;`GET /api/v1/ledger//timeline`) */ +export function parseLedgerTimeline(json: string): FbLedgerPoint[] { + const root: Record | null = asObject(json); + if (root === null) { + return []; + } + const rows: RawLedgerPoint[] = (root['items'] !== undefined && root['items'] !== null) + ? arr(root['items'] as RawLedgerPoint[]) : []; + const out: FbLedgerPoint[] = []; + for (const r of rows) { + out.push({ + date: s(r.date), + netCent: n(r.net), + assetCent: n(r.asset), + debtCent: n(r.debt), + }); + } + return out; +} + /** id 数组的容错转换(服务端可能给 null) */ function idList(v: number[] | null | undefined): number[] { const xs: number[] = arr(v); diff --git a/entry/src/main/ets/model/Types.ets b/entry/src/main/ets/model/Types.ets index 9ab38e2..58e55a0 100644 --- a/entry/src/main/ets/model/Types.ets +++ b/entry/src/main/ets/model/Types.ets @@ -901,6 +901,18 @@ export interface FbLedgerDetail { debtCent: number; } +/** 一个时序点(工单 #24 财务折线:`GET /api/v1/ledger//timeline`) */ +export interface FbLedgerPoint { + /** 日期 YYYY-MM-DD */ + date: string; + /** 净资产(分) */ + netCent: number; + /** 总资产(分) */ + assetCent: number; + /** 总负债(分) */ + debtCent: number; +} + // ============================================================ // 资源(resources) // ============================================================ diff --git a/entry/src/main/ets/pages/ledger/FbLedgerPage.ets b/entry/src/main/ets/pages/ledger/FbLedgerPage.ets index 441f7c9..8c4bb00 100644 --- a/entry/src/main/ets/pages/ledger/FbLedgerPage.ets +++ b/entry/src/main/ets/pages/ledger/FbLedgerPage.ets @@ -16,9 +16,9 @@ import { FbColors, FbMetric, fbTint, FbTint, FB_MASK, fbAlpha } from '../../common/Theme'; import { FbAppState, fbAppState } from '../../common/Store'; -import { FbGroup, FbLedgerBook, FbLedgerEntry, FbLedgerDetail} from '../../model/Types'; +import { FbGroup, FbLedgerBook, FbLedgerEntry, FbLedgerDetail, FbLedgerPoint } from '../../model/Types'; import { fbLedgerKindName, fbMoney } from '../../model/DemoData'; -import { parseLedger, parseLedgerEntries } from '../../data/FbParse'; +import { parseLedger, parseLedgerEntries, parseLedgerTimeline } from '../../data/FbParse'; import { loadModule, loadPath } from '../../data/FbModuleLoad'; import { FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets'; import { FB_DOCK_RESERVE } from '../../components/FloatDock'; @@ -76,6 +76,8 @@ export struct FbLedgerPage { @Local netNow: number = 0; @Local assetNow: number = 0; @Local debtNow: number = 0; + /** 净资产折线时序(分;工单 #24) */ + @Local timeline: FbLedgerPoint[] = []; /** 「记一笔」浮层是否打开 */ @Local entryOpen: boolean = false; /** @@ -153,7 +155,7 @@ export struct FbLedgerPage { }); } - /** 拉某本账的明细(含汇总) */ + /** 拉某本账的明细(含汇总)+ 折线时序(工单 #24) */ private async loadEntries(id: number): Promise { // ⚠️ 用 `loadPath`(路径带 id,没法登记在静态模块表里)。 const key: string = 'ledger_detail_' + id.toString(); @@ -170,6 +172,13 @@ export struct FbLedgerPage { (note: string): void => { this.syncNote = note; }); + // 🔴 折线时序单独拉(服务端 `timeline` 用的是盘点快照,与明细不是一个数据源) + const tkey: string = 'ledger_timeline_' + id.toString(); + await loadPath(tkey, '/api/v1/ledger/' + id.toString() + '/timeline', + parseLedgerTimeline, + (pts: FbLedgerPoint[]): void => { + this.timeline = pts; + }); } @@ -357,7 +366,7 @@ export struct FbLedgerPage { } .width('100%') - // 折线图占位(第二轮接图表能力后填) + // 净资产折线(工单 #24:真实曲线,不再空占位) Column({ space: 4 }) { Row({ space: 6 }) { Image($r('app.media.fb_ic_chart')) @@ -365,17 +374,35 @@ export struct FbLedgerPage { .height(13) .renderMode(ImageRenderMode.Template) .colorFilter(fbTint(FbTint.faint)) - Text('净资产变化(盘点成线)') + Text('净资产变化(盘点成线' + (this.timeline.length > 0 ? ' · ' + this.timeline.length.toString() + ' 点' : '') + ')') .fontSize(10) .fontColor(FbColors.faint) } .alignItems(VerticalAlign.Center) - Column() + // 有数据画折线;没数据保留占位底 + if (this.timeline.length > 1) { + Path() + .width('100%') + .height(56) + .commands(this.timelinePath()) + .fillOpacity(0) + .stroke(this.accent) + .strokeWidth(1.5) + .strokeLineJoin(LineJoinStyle.Round) + } else { + Column({ space: 4 }) { + Text(this.timeline.length === 1 ? '只有一笔,记下一笔才连得出线' : '记几笔、盘点几次,这里就会长出曲线') + .fontSize(10) + .fontColor(FbColors.faint) + .textAlign(TextAlign.Center) + } .width('100%') .height(56) .borderRadius(FbMetric.radiusS) .backgroundColor(FbColors.inputBg) + .justifyContent(FlexAlign.Center) + } } .width('100%') .alignItems(HorizontalAlign.Start) @@ -642,6 +669,50 @@ export struct FbLedgerPage { return this.books.length > 0 ? this.books[0].id : 0; } + /** + * 净资产折线 path(工单 #24)。 + * + * 时序点 → 一条从左到右的折线。Y 轴按 min..max 归一(不是从 0 起), + * 否则金额平稳时曲线会"贴底"看不到波动。单点/零点由 UI 分支兜底。 + * ⚠️ Path 的 commands 用「绝对坐标」的 SVG 子集:`M x y L x y ...`。 + */ + private timelinePath(): string { + const pts: FbLedgerPoint[] = this.timeline; + if (pts.length < 2) { + return ''; + } + const W: number = 320; // 虚拟画布宽(Path 会随 width('100%') 拉伸) + const H: number = 56; + const pad: number = 4; + let lo: number = pts[0].netCent; + let hi: number = pts[0].netCent; + for (const p of pts) { + if (p.netCent < lo) { + lo = p.netCent; + } + if (p.netCent > hi) { + hi = p.netCent; + } + } + // 全平(最高最低相等)→ 人为放宽 ±1,让线画在中线而不是除零 + if (hi - lo < 1) { + hi = lo + 1; + lo = lo - 1; + } + let cmd: string = ''; + for (let i: number = 0; i < pts.length; i++) { + const x: number = pad + (W - pad * 2) * (i / (pts.length - 1)); + const t: number = (pts[i].netCent - lo) / (hi - lo); + const y: number = H - pad - (H - pad * 2) * t; + if (i === 0) { + cmd = 'M ' + x.toFixed(1) + ' ' + y.toFixed(1); + } else { + cmd += ' L ' + x.toFixed(1) + ' ' + y.toFixed(1); + } + } + return cmd; + } + private currentBook(): FbLedgerBook { const hit: FbLedgerBook | undefined = this.books.find((b: FbLedgerBook): boolean => b.id === this.currentId()); if (hit !== undefined) {