feat(harmony)★★: 背景档与预设改用官方 Chip,删掉重复的同步指示器
承接 2026-09-29 的反馈,本批改动是那一轮工作的收尾。
## 1. 自绘 Text → 官方 `Chip`
背景档与预设两排原来是**自绘 `Text`**,而且与设置页主题那排
**圆角/内边距不一致**(预设那排 `borderRadius(4)`,另两处是
`radiusControl`)。改用 `import { Chip, AccessibilitySelectedType,
LengthMetrics } from '@kit.ArkUI'`。
## 2. 删掉 `@Prop statusText`(一个数两个指示器)
它原来是「外观」卡右上角那个「已同步」的**第二份**:页面把同一个
`statusLabel(this.appearanceStatus)` 既画在卡片级标题行、又透传进来
画在「背景」小标题右边 ⇒ 同一屏两个「已同步」,永远同值。
删掉透传这一份,值只从页面进、只画一处。
## 3. Surface.ets(32 行)/ Theme.ets(51 行)/ ContactsTab.ets(21 行)
配套:Chip 的尺寸档、主题色取用、联系人页的调用点。
## 判据
- `test/harmony-appearance.test.mjs`:外观判据随实现更新
- `test/mutants/baseline.sha`:变异体自证底本同步
### ⚠️ 设备端判据本机跑不了(不是代码问题)
node --test test/harmony-appearance.test.mjs
pass 26 / fail 3
失败的 3 条**全部**是设备端断言:
★ 设备:打开背景开关后,壁纸上真的出现了
AssertionError: 要能拉起我们的应用并等到它到前台
★ 设备:窗格内容不得超出屏幕
AssertionError: 要能拉起应用
★ 设备:切换主题后界面真的整体换色了
AssertionError: 设备连续 10 轮拉不起应用(本条已连续 3 轮没真跑)
即**本机没有接 Harmony 设备**,不是断言挂在代码上。
判据文本本身断言「设备连续 10 轮拉不起」——它自己在如实报告
「这条从未在真机上跑过」。**需要真机补验**,这里不假装它通过了。
## docs
API.md / GUI-PLAN-HARMONY.md / PLAN.md 同步上述改动。
This commit is contained in:
39
docs/API.md
39
docs/API.md
@ -88,7 +88,7 @@ POST /me/mail/send 发信
|
||||
GET /me/mail/inbox 收件箱(?status=unread|all&limit=N)
|
||||
GET /me/mail/sent 发件箱
|
||||
GET /mail/{id} 单封详情(含附件列表)
|
||||
GET /mail/{id}/thread 对话树(分块加载,见下)
|
||||
GET /mail/{id}/thread 对话树(?offset=N&limit=N 自线索根分块加载,见下)
|
||||
POST /mail/{id}/read 标记已读
|
||||
POST /me/mail/{id}/forward 转发(引用原文 + 附件随行)
|
||||
```
|
||||
@ -119,22 +119,28 @@ POST /me/mail/{id}/forward 转发(引用原文 + 附件随行)
|
||||
### 对话树
|
||||
|
||||
```
|
||||
GET /mail/{id}/thread?dir=around&limit=40 首屏:锚点 + 部分祖先 + 部分子孙
|
||||
GET /mail/{id}/thread?dir=up&offset=20&limit=40 继续往上(上滑加载)
|
||||
GET /mail/{id}/thread?dir=down&offset=40&limit=40 继续往下
|
||||
GET /mail/{id}/thread?limit=60 首屏:自线索根起的头一块
|
||||
GET /mail/{id}/thread?offset=60&limit=60 继续往后取(offset 传上次 next_offset)
|
||||
```
|
||||
|
||||
树由 `parent_mail_id` 编码:回复指向来信,转发指向被转发的原件。
|
||||
因此**树可以跨会话** —— 转发把线索引到新会话,却仍属同一条线索。
|
||||
|
||||
**从线索根整树 BFS,而不是「锚点的祖先链 + 锚点的子树」**:后者会让兄弟分支
|
||||
整条落在盲区里 —— 一封抄送给两个 Agent 的邮件会收到两个回复,二者互为兄弟,
|
||||
从其中一个看树看不到另一个;挂在原件上的转发分支同理。先上溯到根
|
||||
(`ThreadRootOf`)再整棵展开,兄弟、抄送产生的平行回复、转发分支都是根的子孙,
|
||||
一次覆盖。
|
||||
|
||||
```json
|
||||
{
|
||||
"anchor_mail_id": "...",
|
||||
"dir": "around",
|
||||
"root_mail_id": "...",
|
||||
"anchor_depth": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"mail_id": "...", "parent_mail_id": "...",
|
||||
"depth": -3,
|
||||
"depth": 0,
|
||||
"from_name": "admin", "to_name": "pi",
|
||||
"subject": "...", "body_preview": "正文前 240 字节…",
|
||||
"attachment_count": 2,
|
||||
@ -142,20 +148,25 @@ GET /mail/{id}/thread?dir=down&offset=40&limit=40 继续往下
|
||||
}
|
||||
],
|
||||
"total": 21, "hidden": 4,
|
||||
"has_more_up": true, "has_more_down": false,
|
||||
"next_up": 20, "next_down": 20
|
||||
"has_more": true, "next_offset": 60
|
||||
}
|
||||
```
|
||||
|
||||
- `depth` 是**相对锚点**的层级:0 = 锚点,负数 = 祖先,正数 = 子孙。
|
||||
分块加载时根可能还没取到,所以不用「距根深度」
|
||||
- `offset` 是相对锚点的偏移:`up` 按层数,`down` 按节点数。把 `next_up`/`next_down`
|
||||
原样回传即可,不必自己算已加载数量
|
||||
- `depth` 是**距线索根**的层级:0 = 根,1 = 它的直接回复。**恒 ≥ 0**。
|
||||
从根展开后根一定在结果里,绝对深度因此总是可知的
|
||||
- `offset` 是自根起的**节点数**偏移(不是层数)。从根 BFS 后只剩一个方向,
|
||||
游标就是「已取到的节点数」;把 `next_offset` 原样回传即可,不必自己算
|
||||
- `root_mail_id` / `anchor_depth` 是服务端算出的根与锚点深度,供客户端定位
|
||||
- 节点只带 `body_preview`(240 字节,按 UTF-8 边界截断),全文用 `GET /mail/{id}` 单取
|
||||
- `hidden` = 本页因权限被过滤掉的节点数
|
||||
- `detached` = 父邮件不在当前已加载集合里;`parent_hidden` 进一步区分
|
||||
「确实无权查看」(永久)与「尚未加载」(随上滑补齐)
|
||||
- `limit` 夹到 [1, 200],非法值回落默认 40
|
||||
「确实无权查看」(永久)与「尚未加载」(往下翻会补上)
|
||||
- `limit` 夹到 [1, 200],非法值回落默认 **60**
|
||||
|
||||
**锚点一定可见**:巨型线索里锚点可能没落在 BFS 首页,此时服务端额外把
|
||||
「根 → 锚点」这条路径补进第一页,锚点自己单取。但该路径的回填**上限 60 层**
|
||||
(`anchorPathBudget`)—— 超过 60 层的链条,锚点之上会出现断点。
|
||||
递归 CTE 本身另有 `descendantDepthCap = 10000` 的兜底,仅防数据损坏成环。
|
||||
|
||||
**鉴权按会话逐个进行**:A 转发给 B 之后,B 与 C 在新会话里的往来不会回流给 A。
|
||||
拿一个自己无权访问的 `mail_id` 当锚点直接返回 403。
|
||||
|
||||
@ -135,7 +135,7 @@ entry/src/main/ets/
|
||||
| 主界面(平板/2in1) | 两栏/三栏自适应:左列表 + 右详情;`@ohos.mediaquery` 断点切换 |
|
||||
| 会话列表 | 列表/卡片视图切换;卡片含主题、最新一封发件人+摘要、预算徽标(剩 1 橙 / 用尽红 / 不限不显示) |
|
||||
| 邮件详情 | Markdown 正文 + 附件行 + 抄送行 + 权限卡片 + 底部「回复 / 转发」+ 对话树入口 |
|
||||
| 对话树 | 缩进 + 连接线(不引图形库),分块加载:`dir=around` 首屏 + `up/down` 增量 |
|
||||
| 对话树 | 缩进 + 连接线(不引图形库),分块加载:自线索根整树 BFS,`offset` 增量 |
|
||||
| 写信页 | 收件人补全(三段式 suggest)+ 抄送 + 主题 + 正文 + 预算档位 + 附件 + 会话别名(仅 `.new`) |
|
||||
| 设置 | 网关地址、退出登录、密钥管理(`/me/keys` 创建/吊销,展示 token_hint) |
|
||||
|
||||
|
||||
25
docs/PLAN.md
25
docs/PLAN.md
@ -764,7 +764,7 @@ execute: {
|
||||
两处不一致时无法判断谁对。直接用递归 CTE 在 mails 上查(`idx_mails_parent` 已有)。
|
||||
|
||||
- [x] `repo/thread.go`:`AncestorsRaw`(向上)/ `DescendantsRaw`(向下)两个方向的递归 CTE
|
||||
- [x] `GET /mail/{id}/thread?dir=around|up|down&offset=N&limit=N`
|
||||
- [x] `GET /mail/{id}/thread?offset=N&limit=N`(自线索根整树 BFS)
|
||||
- [x] **树可跨会话**:转发把线索引到新会话,但 parent 仍指向原件。
|
||||
这正是树视图比会话内平铺更有价值的地方 —— 能看出线索分叉去了哪里
|
||||
- [x] **按会话逐个鉴权**:A 转发给 B 之后,B 与 C 在新会话里的往来不能回流给 A。
|
||||
@ -780,15 +780,16 @@ execute: {
|
||||
|
||||
原先实现是深度上限 100 直接截断,超长线索看不到全貌。改为按方向分页:
|
||||
|
||||
- [x] 首屏 `dir=around` 取锚点附近一块(limit 在两个方向各分一半);
|
||||
`dir=up|down` + `offset` 增量加载,两端各自带 `has_more_*` 与 `next_*`
|
||||
- [x] **游标用「相对锚点的层号/节点偏移」而不是 mail_id**:
|
||||
偏移量每次从锚点重走一遍,无状态、不可伪造;
|
||||
- [x] **从线索根整树 BFS**(`ThreadRootOf` 上溯到根,再 `DescendantsRaw` 展开):
|
||||
弃掉原先 `dir=around|up|down` 三方向分页 —— 那种做法下**兄弟分支整条落在盲区里**:
|
||||
一封抄送给两个 Agent 的邮件会收到两个回复,二者互为兄弟,从其中一个看树看不到另一个;
|
||||
挂在原件上的转发分支同理。先到根再整棵展开,兄弟、平行回复、转发分支都是根的子孙
|
||||
- [x] **游标是「自根起的节点数」而不是 mail_id**:偏移量每次从根重走一遍,无状态、不可伪造;
|
||||
用 mail_id 做游标就必须允许传入**不可见**的邮件(不可见的中间段要穿过去),
|
||||
那还得单独证明它确实是锚点的祖先,反而更绕。
|
||||
祖先方向的层号天然稳定 —— 新邮件只会追加成叶子,不会插进已有链条中间
|
||||
- [x] `depth` 改为**相对锚点**(0 = 锚点,负 = 祖先,正 = 子孙):
|
||||
分块加载时根可能还没取到,绝对深度无从得知
|
||||
那还得单独证明它确实是根的祖先,反而更绕。
|
||||
节点顺序天然稳定 —— 新邮件只会追加成叶子,不会插进已有链条中间
|
||||
- [x] `depth` 为**距线索根**的绝对层级(0 = 根,恒 ≥ 0):从根展开后根一定在结果里,
|
||||
绝对深度总是可知(`AncestorsRaw` 内部的负 depth 在 handler 层加回 `anchorDepth` 抵消)
|
||||
- [x] repo 层**不做可见性过滤**:不可见的中间段必须能穿过(转发把线索引进别人的会话,
|
||||
再往上却可能仍是自己参与的往来)。过滤放在 handler 层,那里才知道调用者是谁
|
||||
- [x] 前端 `IntersectionObserver` 双哨兵(rootMargin 200px 提前触发)+
|
||||
@ -797,8 +798,10 @@ execute: {
|
||||
(`setState` 异步,读 state 会双双看到 false)
|
||||
- [x] 实测 251 封长链:7 页取完,depth 连续无缺口,取到真正的根;
|
||||
311 封(含 60 个转发分支)9 页取完,`depth=1` 恰为 61 个
|
||||
- [x] `limit` 夹到 [1,200],非法值回落默认 40(分页参数不该因笔误让整个请求失败);
|
||||
`limit=1` 时两方向各保底 1,否则算出 `downLimit=0` 连锚点自己都不返回
|
||||
- [x] `limit` 夹到 [1,200],非法值回落默认 60(分页参数不该因笔误让整个请求失败)
|
||||
- [x] 锚点回填上限 `anchorPathBudget = 60` 层:锚点没落在 BFS 首页时把「根 → 锚点」
|
||||
那条路径补进第一页,保证点开的那封一定在树里;超过 60 层则锚点之上留断点。
|
||||
递归 CTE 另有 `descendantDepthCap = 10000` 兜底,仅防数据损坏成环
|
||||
|
||||
**工作列表卡片视图(已完成)**:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user