Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbLedgerSettingsSheets.ets
T

386 lines
12 KiB
Plaintext
Raw Normal View History

/**
* 家书 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)
}
}