v65/v66:六个空壳模块接线 + 桌面服务卡片
用户 2026-10-20:
「做几个和 syncCalendar 差不多的卡片。另外其他的功能也逐步完成……旅程的
新建旅程啊,车辆管理、出行方式、节点设置……资源也点不进去……任务也点不
进去,也没法完成……纪念日、新增纪念日……财务的显示也有问题……笔记现在
也点不进去,记笔记也记不了。」
v65 —— 六个模块(任务 / 资源 / 纪念日 / 笔记 / 账本 / 旅程)原本是**同一套
空壳模板**复制出来的:`FbPrimaryButton` 没有 `onTap`、`ListItem` 没有
`onClick`、设置菜单的 `action` 全是空串。这一版把它们全部接上真实接口:
· 任务:新建 / 编辑(`FbTaskEditSheet` + `FbTaskRepo`)、列表可点、勾完成
(走 `/tasks/<id>/status` —— 服务端 `PATCH` **显式忽略** status,走 Patch
会得到"看起来成功的假成功")、按看板筛选
· 资源:新增 / 查看 / 改 / 删(`FbResourceSheet` + `FbResourceRepo`);
过滤维度从**并不存在的**「证件 / 资料」改成按权限与归属
(`resources` 表没有 category 列,那个筛选一直是筛空的)
· 纪念日:新增 / 编辑(`FbDayEditSheet` + `FbDayRepo`);派生生日
(id=0、来自名册)只给提示不给编辑 —— 点开 `/days/0` 必然 404
· 笔记:记一条 / 打开 / 改(`FbNoteSheet` + `FbNoteRepo`),正文按 blocks 存
· 账本:记一笔(`FbLedgerEntrySheet` + `FbLedgerRepo`);修掉财务显示自相
矛盾 —— 资产/负债改回**服务端 summary**(原来端上按金额正负号重算,与
服务端"负债取绝对值"的口径打架,会算出 净资产 ≠ 资产 − 负债)
· 旅程:题图卡片(封面 + 标题 + 城市)+ 行程 + 新建旅程 + 新建节点
服务端同时修掉两处**静默数据丢失**(网页端因提交格式凑巧没暴露,只有走
API 的客户端会中招):
· 新建笔记传的是 `body`,而 `Note::create()` 只读 `blocks` ⇒ 新笔记只有标题
· 改笔记时 `blocks` 恒为空,而 `Note::update()` 是整组替换 ⇒ 只改标题就把
正文全删光;另修 `annivPeopleOf()`(`find()` 的 SQL 不含 people,原来那个
兜底恒为空 ⇒ 只改纪念日标题会清空关联人)
v66 —— 桌面服务卡片(用户选了「桌面服务卡片」这一项):
· `FbFormAbility`(FormExtensionAbility)+ `form_config.json` 三块尺寸:
2×2 今日概览 / 2×4 左右分栏 / 4×4 今日全景
· `FbCardData` 只打 `/home` 一个接口(与网页首页同源),数据**扁平化**后
由 `formBindingData` 推给卡片;断网时用上次成功的缓存并注明"离线"
· 卡片是后台进程 ⇒ 请求带 `authProbe: true`:401 **不能**触发全局"踢回
登录页"(那会顺手把正在用应用的人踢下线,v62 那次事故同一个坑)
· 点卡片 → `postCardAction(router, fbModule)` → `EntryAbility` 读 Want 参数
直接落到对应模块;补了 `onNewWant`(没有它,第二次点卡片"什么也没发生")
· 4×4 右上角 ↻ 走 `postAction(message)` → `onFormEvent` 重取数再推回
验证:`hvigorw assembleHap` BUILD SUCCESSFUL;打包后的 hap 里确认
`FbFormAbility`(type=form)与 `$profile:form_config` 都在,卡片代码进了
`ets/widgets.abc`。静态守卫 730 条全绿,反向验证 110 条注入全部能让守卫变红。
⚠️ 设备仍断连(`hdc list targets` 为空),真机尚未验收。
This commit is contained in:
@@ -123,6 +123,10 @@ interface RawDay {
|
||||
month?: number; day?: number; is_leap?: number;
|
||||
person_id?: number | null; person_name?: string | null;
|
||||
source?: string; born?: string | null;
|
||||
/** v65:关联人 id 数组(服务端列表/单条都会带) */
|
||||
person_ids?: number[] | null;
|
||||
/** v65:备注 */
|
||||
note?: string | null;
|
||||
}
|
||||
|
||||
interface RawTask {
|
||||
@@ -201,6 +205,10 @@ interface RawNote {
|
||||
id?: number; title?: string; is_shared?: number; updated_at?: string;
|
||||
author_name?: string | null; avatar?: string | null; avatar_color?: string | null;
|
||||
is_mine?: number; blocks?: RawNoteBlock[];
|
||||
/** v65:列表里服务端已经算好的摘要 */
|
||||
excerpt?: string | null;
|
||||
/** v65:标签数组(`Note::attachChildren` 挂上的) */
|
||||
tags?: string[] | null;
|
||||
}
|
||||
|
||||
interface RawNoteBlock {
|
||||
@@ -262,6 +270,12 @@ export function parseTrips(json: string): FbTripItem[] {
|
||||
dateRange: dateRangeText(s(r.starts_at), s(r.ends_at), dayCount),
|
||||
nodeCount: n(r.node_count),
|
||||
doneCount: 0,
|
||||
/* 题图 / 城市 / 简介 / 同行人数 —— 对齐网页版列表卡(`templates/trips/_list_body.php`)。
|
||||
⚠️ `cover_key` 存**原始键**,拼地址交给 `fbMediaUrl()`(见 FbTripItem 的说明)。 */
|
||||
coverKey: s(r.cover_key),
|
||||
city: s(r.city) !== '' ? s(r.city) : cities.join(' · '),
|
||||
summary: s(r.summary),
|
||||
attendeeCount: n(r.attendee_count),
|
||||
};
|
||||
out.push(it);
|
||||
}
|
||||
@@ -280,6 +294,7 @@ export function parseDays(json: string): FbAnniversaryItem[] {
|
||||
if (n(r.person_id) > 0) {
|
||||
people.push(chip(s(r.person_name), '#A84A28', n(r.person_id)));
|
||||
}
|
||||
const src: string = s(r.source) !== '' ? s(r.source) : 'custom';
|
||||
const it: FbAnniversaryItem = {
|
||||
id: n(r.id),
|
||||
title: s(r.title),
|
||||
@@ -288,6 +303,15 @@ export function parseDays(json: string): FbAnniversaryItem[] {
|
||||
kind: kindCn(s(r.kind)),
|
||||
people: people,
|
||||
yearly: s(r.repeat_type) !== 'once',
|
||||
source: src,
|
||||
month: m,
|
||||
day: d,
|
||||
repeatType: s(r.repeat_type) !== '' ? s(r.repeat_type) : 'solar',
|
||||
/* ⚠️ 服务端 `kind` 是**自由文本**(用户在设置里能自己加分类名)
|
||||
⇒ 这个原始值要留着回填,别只留映射后的类别码。 */
|
||||
rawKind: s(r.kind),
|
||||
note: s(r.note),
|
||||
personIds: idList(r.person_ids),
|
||||
};
|
||||
out.push(it);
|
||||
}
|
||||
@@ -339,6 +363,10 @@ function taskRow(t: RawTask, boardName: string, boardColor: string): FbTaskItem
|
||||
assignee: s(t.assignee_name),
|
||||
childDone: n(t.child_done),
|
||||
childTotal: n(t.child_total),
|
||||
/* v65:新建/编辑/搬看板要用到的三个**原始**字段(原来只派生了显示文案) */
|
||||
boardId: n(t.board_id),
|
||||
note: s(t.note),
|
||||
dueAt: due,
|
||||
};
|
||||
return it;
|
||||
}
|
||||
@@ -455,15 +483,18 @@ export function parseResources(json: string): FbResourceItem[] {
|
||||
const rows: RawResource[] = asArray<RawResource>(json);
|
||||
const out: FbResourceItem[] = [];
|
||||
for (const r of rows) {
|
||||
/* ⚠️ 服务端 `resources` 表**没有 category 列**(见 `app/Schema.php`)——
|
||||
只有 title / content / owner_id。所以端上按「归属人」分组/筛选,
|
||||
不按"证件/资料"那种并不存在的类别(v65 之前那两个胶囊永远筛出空列表)。 */
|
||||
const perm: string = s(r.perm);
|
||||
const it: FbResourceItem = {
|
||||
id: n(r.id),
|
||||
name: s(r.title),
|
||||
// 资源没有"类别"字段(服务端的 content 是自由文本),
|
||||
// ⇒ 用「谁的」来分类更贴近实际用法(Web 端列表也是按归属分组)
|
||||
kind: 'other',
|
||||
ownerName: s(r.owner_nick) !== '' ? s(r.owner_nick) : s(r.owner_name),
|
||||
fileCount: 0,
|
||||
shared: s(r.perm) !== 'owner',
|
||||
ownerId: n(r.owner_id),
|
||||
perm: perm !== '' ? perm : 'read',
|
||||
content: s(r.content),
|
||||
shared: perm !== 'owner',
|
||||
updatedAt: datePart(s(r.updated_at)),
|
||||
};
|
||||
out.push(it);
|
||||
@@ -502,19 +533,52 @@ export function parseNotes(json: string): FbNoteItem[] {
|
||||
const rows: RawNote[] = arr<RawNote>(root['items'] as RawNote[]);
|
||||
const out: FbNoteItem[] = [];
|
||||
for (const r of rows) {
|
||||
/* ⚠️ 服务端列表已带 `blocks` / `tags` / `excerpt`(`Note::attachChildren`),
|
||||
这里把它们都取出来 —— 原来只拿 blocks 算了个摘要,正文和标签全丢了。 */
|
||||
const body: string = noteBodyOf(r.blocks);
|
||||
const tags: string[] = [];
|
||||
for (const t of arr<string>(r.tags)) {
|
||||
tags.push(s(t));
|
||||
}
|
||||
const exc: string = s(r.excerpt);
|
||||
out.push({
|
||||
id: n(r.id),
|
||||
title: s(r.title),
|
||||
excerpt: firstTextOf(r.blocks),
|
||||
tags: [],
|
||||
excerpt: exc !== '' ? exc : body,
|
||||
tags: tags,
|
||||
updatedAt: datePart(s(r.updated_at)),
|
||||
shared: n(r.is_shared) !== 0,
|
||||
attachCount: 0,
|
||||
body: body,
|
||||
mine: n(r.is_mine) !== 0,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* `note_blocks` → 一段纯文本。
|
||||
*
|
||||
* ⚠️ 图片块的内容是**附件 key**(不是正文),拼进来会变成一串乱码 key
|
||||
* ⇒ 记成「[图片]」,用户一眼知道那里有张图。
|
||||
*/
|
||||
function noteBodyOf(blocks: RawNoteBlock[] | undefined): string {
|
||||
const rows: RawNoteBlock[] = arr<RawNoteBlock>(blocks);
|
||||
const parts: string[] = [];
|
||||
for (const b of rows) {
|
||||
const kind: string = s(b.kind);
|
||||
if (kind === 'image') {
|
||||
parts.push('[图片]');
|
||||
continue;
|
||||
}
|
||||
const txt: string = s(b.content).trim();
|
||||
if (txt !== '') {
|
||||
parts.push(txt);
|
||||
}
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
/** 通知 → FbNotificationItem[] */
|
||||
export function parseNotifications(json: string): FbNotificationItem[] {
|
||||
const root: Record<string, Object> | null = asObject(json);
|
||||
|
||||
Reference in New Issue
Block a user