v0.8.2 头像点进我的详情 / 未读数接真实数据 / 名册圆角卡(容器要换成页面底色)

用户 2026-10-18 夜三条:

①「右上角,显示的应该是登录者的头像,点击头像进入我的详情页面。」
   头像**本来就是登录者本人的**(v59 已接 session.me.avatarUrl);
   真正缺的是**点击** —— TopBar 里那个 FbAvatar **根本没有 onClick**,
   只有一句写着"点开账号菜单"的注释,而"账号菜单"这个东西在代码里并不存在。
   ⇒ 接 openMyDetail() → bindSheet 弹 FbPersonSheet(复用名册那个人详情组件)。
   🔴 数据必须走 repo.byId(meId)(本次为它新增 byId),不能从名册列表那批里找:
      **"我"常常不在自己的名册里**(名册是"我挑出来的那一小撮"),
      从列表里查会**静默落空**、点了没反应 —— 最难查的那种失败。
   ⚠️ 三种状态都说清楚:加载中 / 拿到了 / 拿不到,绝不留空白浮层。

②「提醒按钮,明明没提醒,怎么还显示个 2?」
   🔴 真根因:`@Trace unread: number = 2;` 是**写死的占位初值**,
      而全工程只有一处给它赋值(通知页「全部已读」清 0)。
      没进过通知页的人**永远看到 2**;进了通知页又"永远 0"(把真实未读也盖掉)
      —— 两个方向都错。
   ⇒ ① 初值改 0(拿到真值前必须是"没有未读",不能凭空多出 2);
     ② 首页把 /home 的真实 unread 写进全局状态。
        (服务端 Notification::unreadCount,与通知页同源;/home 本来就返回它、
          FbParse 也早就解析了 —— 只是**没人往全局状态里写**。)

③「名册页面,不好看,太丑了,应该是每个人都是圆角矩形,往中间收一点,
   距离左右都有点距离,每个人中间都有一点空隙。」
   🔴🔴 真凶**不是卡片形状**(v51 就做了 borderRadius + margin)——
      而是**列表容器也是纯白** ⇒ 白卡压白底 ⇒ 圆角与左右收边**全看不见**,
      看上去就是一整块通栏白。这解释了"改过一轮还是丑":上次改的是卡片,
      问题在"背景没有对比"。
   ⇒ 容器 + 外层兜底改 FbColors.paper(页面底色),卡片保持白;
     圆角 radiusS(10) → 新增 radiusCard(14)(列表项专用档:16 显胖、10 显方);
     左右各 pagePad(16)、卡间 10(原 8 偏挤)、行内边距 11→12。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 347/0(本批新增 18 条,
另**收紧 2 条**旧判据 —— M 组"圆角卡"那条原来锚"逐字相邻两行"、且**测错了对象**:
没查容器背景,所以对"白卡压白底"这个真实缺陷毫无把关能力);
_fb_v59b_falsify.py 8 项注入 ⇒ 8/0。

⚠️ 本轮反向验证头一轮漏了 3 条(都是"判据太宽"),已逐条收紧并记入 pitfalls:
  · 删掉头像 onClick 后仍绿 —— openMyDetail 这个标识符在**方法定义**里也出现;
  · 容器改回白底后仍绿 —— 本页有**两处** FbColors.paper(容器 / 兜底);
  · /home unread 改成写死 2 后仍绿 —— Notification::unreadCount 在 PHP 里也有两处。
This commit is contained in:
2026-10-18 20:58:40 +08:00
parent 77b494a274
commit 2b0401052c
6 changed files with 257 additions and 26 deletions
+154 -5
View File
@@ -21,7 +21,13 @@ import { FbAppState, fbAppState } from '../common/Store';
import { FbSyncState, SYNC_DONE, SYNC_FAIL, SYNC_RUNNING, fbSyncState } from '../common/FbSyncStore';
import { fbModuleOf } from '../model/Modules';
import { FbAvatar } from './Widgets';
import { FbPersonSheet } from './FbPersonSheet';
// ⚠️ `inst()` 拿单例(本工程所有 repo 都是这个约定,见 FbPersonRepo 文件头);
// `FB_TOKEN_STORE` 在 **FbTokenStore.ets** 里(不是在 FbDeviceStore)。
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { syncAll } from '../data/FbSyncAll';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
@ComponentV2
@@ -41,6 +47,15 @@ export struct FbTopBar {
/** 同步面板是否打开 */
@Local syncSheet: boolean = false;
/** 「我的详情」浮层是否打开(点头像触发) */
@Local myDetailSheet: boolean = false;
/** 「我的详情」里的人(null = 还没取到) */
@Local myDetail: FbRosterPerson | null = null;
/** 「我的详情」是否在加载 */
@Local myDetailLoading: boolean = false;
/** 「我的详情」的兜底提示(拿不到资料时显示) */
@Local myDetailNote: string = '';
build() {
// 🔴 顶栏横向镜像 —— 「质感握姿」(用户 2026-10-16 第 4 条):
// 左手握时家书图标在**左**,右手握时它跑到**右**,方便单手点它开抽屉。
@@ -177,12 +192,22 @@ export struct FbTopBar {
// 与左边的同步图标、右边的头像各拉开一段(用户:太近了)
.margin({ left: FbMetric.gapTopActions, right: FbMetric.gapTopActions })
// 头像(点开账号菜单)
// 头像 —— 点它打开**我自己的详情**
//
// 🔴 v59(用户 2026-10-18):「右上角的头像应该是真实的。」
// ⇒ 传 `imageUrl`(真实头像图;服务端 `avatar_url()` 给的)。
// 没传过头像的人拿到空串 ⇒ `FbAvatar` 自带色块字回落,
// 所以这里不需要额外判断("两者都要有"是 FbAvatar 的既有设计)。
// 🔴🔴 v59b(用户 2026-10-18):「右上角,显示的应该是登录者的头像,
// **点击头像进入我的详情页面**。」
//
// ⚠️ 原来这里**根本没有 onClick**(只有一句写着"点开账号菜单"的注释)——
// 用户点了没任何反应,而代码里连一个"账号菜单"都不存在。
// ⇒ 现在接的是**详情浮层**(与名册里点某个人同一个组件、同一套数据源)。
//
// 🔴 头像**已经是登录者本人的**(`session.me.avatarUrl` 由 `/login` 与
// `/me` 两处给,见 FbLogin/FbSession)—— 用户这条要求上一轮就已满足,
// 真正缺的是"点了能进去"。
//
// ⚠️ 数据取自**本地库**(`FbPersonRepo.inst().byId`),不是名册列表里那份:
// "我"常常**不在自己的名册**里(名册是"我挑出来的那一小撮"),
// 从名册那批里找会静默落空 ⇒ 点了没反应(最难查的那种失败)。
FbAvatar({
name: this.app.session.me.name,
color: this.app.session.me.avatarColor,
@@ -192,6 +217,32 @@ export struct FbTopBar {
? this.app.session.me.avatarUrl
: ''
})
// ⚠️ `FbAvatar` 的布局尺寸在画布内(`Image` 自己撑圆),外层要显式
// 给一个可点区域 —— 32 的头像直接当热区在真机上偏小(手指容易点空)。
.width(40)
.height(40)
.margin({ left: FbMetric.gapTopActions })
.onClick((): void => {
this.openMyDetail();
})
// 「我的详情」浮层 —— 复用名册那个人详情组件(同一个组件、同一套字段)
//
// ⚠️ 用 `bindSheet` 而不是自己在 `build` 里叠一层遮罩:
// 这条顶栏在**每个模块页**都会渲染(`topBarSlot`),
// 自己叠遮罩要处理"盖住悬浮栏 / 盖住抽屉 / 手势返回"一堆事;
// bindSheet 是系统能力,这些它都管(同步面板也是这么做的)。
//
// ⚠️ `onDisappear` 必须复位 `myDetailSheet`(点遮罩/下滑关闭都会走它),
// 否则下次点头像**打不开**(状态位还是 true 但面板已经不在)。
.bindSheet(this.myDetailSheet, this.myDetailPanel(), {
height: SheetSize.FIT_CONTENT,
dragBar: true,
showClose: false,
backgroundColor: FbColors.surface,
onDisappear: (): void => {
this.myDetailSheet = false;
}
})
}
.width('100%')
// 🔴 高度 = **状态栏高度 + 顶栏本身高度**(用户 2026-10-13 要"全沉浸")。
@@ -245,6 +296,104 @@ export struct FbTopBar {
});
}
/**
* 打开「我」的详情浮层(点右上角头像触发)。
*
* 🔴 v59b(用户 2026-10-18):「点击头像进入我的详情页面。」
*
* 为什么**先打开浮层、再补数据**(而不是等网络回来才开):
* 本地优先是本应用的立身之本 —— 点一下要**立刻**有东西出来,
* 断网时也必须能看。本地库那份已经够画详情的前几段,
* 网络回来后再原地补全(`fetchDetail`)。
*
* ⚠️ 失败时**不弹错误**:`me.id` 拿不到(未登录/数据还没同步下来)就只弹一句
* "资料还没同步下来",而不是让用户对着一个空白浮层猜。
*/
private async openMyDetail(): Promise<void> {
const meId: number = this.app.session.me.id;
if (meId <= 0) {
this.myDetailNote = '还没拿到你的资料,请先同步一次';
this.myDetailSheet = true;
return;
}
/* ⚠️ 先开浮层(`myDetailSheet = true`)再 await:
否则网络慢时用户点了一下**几十毫秒内毫无反应**,会以为按钮坏了。
`loading` 由浮层自己显示,观感上"点开就有东西"。 */
this.myDetailLoading = true;
this.myDetail = null;
this.myDetailSheet = true;
try {
const local: FbRosterPerson | null = await FbPersonRepo.inst().byId(meId);
this.myDetail = local;
this.myDetailLoading = false;
// 本地有 ⇒ 顺手用网络那份补全(口径与名册里点某人完全一致)
const token: string = FB_TOKEN_STORE.get();
if (token !== '' && local !== null) {
const full: FbRosterPerson | null = await FbPersonRepo.inst().fetchDetail(meId, token);
// ⚠️ 浮层可能已经被关掉、或期间换了人 ⇒ 只在"还开着同一个人"时才覆盖
if (full !== null && this.myDetailSheet
&& this.myDetail !== null && this.myDetail.id === meId) {
this.myDetail = full;
}
}
} catch (err) {
const e: BusinessError = err as BusinessError;
hilog.warn(0x0000, 'FamilyBook', '取我的详情失败:%{public}s',
e.message !== undefined ? e.message : JSON.stringify(err));
this.myDetailLoading = false;
}
}
/**
* 「我的详情」面板(点头像弹出)。
*
* ⚠️ 三种状态都要有话说(用户 2026-10-18 报过"点了没反应"这类问题):
* · 加载中 ⇒ 「正在读取…」(不要让浮层空着)
* · 拿到了 ⇒ 复用 `FbPersonSheet`(姓名/称呼/电话/生日/单位/标签…)
* · 拿不到 ⇒ 显式一句 `myDetailNote`(而不是空白)
*/
@Builder
myDetailPanel() {
Column() {
if (this.myDetailLoading) {
Row({ space: 8 }) {
LoadingProgress().width(18).height(18).color(FbColors.brand)
Text('正在读取我的资料…')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.muted)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 28, bottom: 28 })
} else if (this.myDetail !== null) {
FbPersonSheet({
person: this.myDetail,
onClose: (): void => {
this.myDetailSheet = false;
},
// ⚠️ 这里**不给编辑入口**:编辑要跳名册页那套编辑浮层(含标签/电话增删),
// 而顶栏在**每个模块页**都存在 —— 在这里接编辑会变成
// "在日程页里编辑家庭成员的资料",职责与页面上下文都不对。
// 要看/改详细资料,从名册进(那是它的主场)。
onEdit: (): void => {
this.myDetailSheet = false;
}
})
} else {
Text(this.myDetailNote !== ''
? this.myDetailNote
: '暂时拿不到你的资料,下拉同步一次再试')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.muted)
.textAlign(TextAlign.Center)
.width('100%')
.padding({ top: 28, bottom: 28, left: 24, right: 24 })
}
}
.width('100%')
.constraintSize({ maxHeight: '82%' })
}
/**
* 同步面板(点顶栏图标弹出)。
*