From 2b0401052c5a72f43e7457ed1849ed0dbf45dc16 Mon Sep 17 00:00:00 2001 From: laoyang Date: Sun, 18 Oct 2026 20:58:40 +0800 Subject: [PATCH] =?UTF-8?q?v0.8.2=20=E5=A4=B4=E5=83=8F=E7=82=B9=E8=BF=9B?= =?UTF-8?q?=E6=88=91=E7=9A=84=E8=AF=A6=E6=83=85=20/=20=E6=9C=AA=E8=AF=BB?= =?UTF-8?q?=E6=95=B0=E6=8E=A5=E7=9C=9F=E5=AE=9E=E6=95=B0=E6=8D=AE=20/=20?= =?UTF-8?q?=E5=90=8D=E5=86=8C=E5=9C=86=E8=A7=92=E5=8D=A1=EF=BC=88=E5=AE=B9?= =?UTF-8?q?=E5=99=A8=E8=A6=81=E6=8D=A2=E6=88=90=E9=A1=B5=E9=9D=A2=E5=BA=95?= =?UTF-8?q?=E8=89=B2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 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 里也有两处。 --- entry/src/main/ets/common/Store.ets | 16 +- entry/src/main/ets/common/Theme.ets | 9 + entry/src/main/ets/components/TopBar.ets | 159 +++++++++++++++++- entry/src/main/ets/data/FbPersonRepo.ets | 31 ++++ entry/src/main/ets/pages/home/FbHomePage.ets | 15 ++ .../main/ets/pages/people/FbPeoplePage.ets | 53 +++--- 6 files changed, 257 insertions(+), 26 deletions(-) diff --git a/entry/src/main/ets/common/Store.ets b/entry/src/main/ets/common/Store.ets index b622575..40bbd38 100644 --- a/entry/src/main/ets/common/Store.ets +++ b/entry/src/main/ets/common/Store.ets @@ -42,8 +42,20 @@ export class FbAppState { /** 当前会话(接口接上之前先用演示数据;`loggedIn=false` 时应跳登录页) */ @Trace session: FbSession = FB_DEMO_SESSION; - /** 未读通知数(顶栏铃铛角标) */ - @Trace unread: number = 2; + /** + * 未读通知数(顶栏铃铛角标)。 + * + * 🔴🔴 v59b(用户 2026-10-18):「提醒按钮,明明没提醒,怎么还显示个 2?」 + * 这个 **写死的 2** 就是答案:它原来只是个占位初值,而全工程没有任何地方 + * 用真实数据填过它(只有通知页的「全部已读」会清成 0) + * ⇒ 没进过通知页的人**永远看到 2**。 + * + * ⚠️ 改成 **0**:未读数在拿到真实值之前必须是"没有未读", + * 而不是"假装有 2 条"。真实值由 `/home` 的 `unread` 写入 + * (见 FbHomePage 里那段说明),进通知页后由那边继续维护。 + * 宁可短暂少显示,也不能凭空多出一个用户会去追问的数字。 + */ + @Trace unread: number = 0; /** * 状态栏高度(vp)。 diff --git a/entry/src/main/ets/common/Theme.ets b/entry/src/main/ets/common/Theme.ets index c02e5fc..ff66c71 100644 --- a/entry/src/main/ets/common/Theme.ets +++ b/entry/src/main/ets/common/Theme.ets @@ -58,6 +58,15 @@ export class FbMetric { static readonly radius: number = 16; /** 小圆角(内嵌块 / 输入框) */ static readonly radiusS: number = 10; + /** + * 列表项圆角(**一行一张卡**那种,如名册的人员行)。 + * + * 🔴 为什么单独一档(2026-10-18 用户「名册太丑」): + * 列表项**一行一张卡**、上下密排;用 16(区块卡的圆角)会显得"胖"、 + * 相邻两卡的圆角挤在一起很吵;用 10(内嵌小块的圆角)在 12vp 的行内边距下 + * 又显得方、像"被裁过的白条"。14 是这两个之间的甜点。 + */ + static readonly radiusCard: number = 14; /** 胶囊圆角 */ static readonly radiusPill: number = 999; diff --git a/entry/src/main/ets/components/TopBar.ets b/entry/src/main/ets/components/TopBar.ets index 2297306..c508578 100644 --- a/entry/src/main/ets/components/TopBar.ets +++ b/entry/src/main/ets/components/TopBar.ets @@ -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 { + 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%' }) + } + /** * 同步面板(点顶栏图标弹出)。 * diff --git a/entry/src/main/ets/data/FbPersonRepo.ets b/entry/src/main/ets/data/FbPersonRepo.ets index e243679..a7f3b4d 100644 --- a/entry/src/main/ets/data/FbPersonRepo.ets +++ b/entry/src/main/ets/data/FbPersonRepo.ets @@ -509,6 +509,37 @@ export class FbPersonRepo { return out; } + /** + * 按 id 取**一个人**(名册里那份,非详情接口)。 + * + * 🔴 v59(用户 2026-10-18):「点击头像进入我的详情页面。」 + * 头像点开的是**当前登录者自己**的详情,而"我"**不一定在自己的名册里** —— + * 名册筛的是"我挑出来的那一小撮",本人常常不在其中。 + * 不加这个查询,"点头像 → 详情"就会**静默没有任何反应**(`rawById` 里查不到)。 + * + * ⚠️ 与 `list()` 的差别只有两点:不 JOIN `person_roster`(不要求"在我名册里"), + * 以及带 `kind='member'`(排除观影那边可能写进来的非成员行)。 + * 字段清单必须与 `list()` **完全一致**,否则 `rowToPerson` 会读出 undefined + * (它按列名取值)⇒ 到头来没人看得懂为什么"详情里少了几行"。 + */ + async byId(id: number): Promise { + if (id <= 0) { + return null; + } + const sql: string = + 'SELECT p.id, p.name, p.nickname, p.gender, p.birthday, p.avatar, p.avatar_color,' + + ' p.note, p.email, p.org, p.title, p.slogan, p.addr_city,' + + ' 0 AS sort_order, NULL AS phones,' + + ' (SELECT GROUP_CONCAT(t.tag, "\\u0001") FROM person_tags t' + + ' WHERE t.person_id = p.id) AS tags' + + ' FROM persons p WHERE p.id = ? LIMIT 1'; + const rows: FbRow[] = await this.db.queryRows(sql, [id]); + if (rows.length === 0) { + return null; + } + return this.rowToPerson(rows[0]); + } + /** * 读名册的元信息(人 + 标签云 + 我的 id)。 * diff --git a/entry/src/main/ets/pages/home/FbHomePage.ets b/entry/src/main/ets/pages/home/FbHomePage.ets index 0b6ba3c..23a7bb6 100644 --- a/entry/src/main/ets/pages/home/FbHomePage.ets +++ b/entry/src/main/ets/pages/home/FbHomePage.ets @@ -133,6 +133,21 @@ export struct FbHomePage { this.statOverdue = d.stat.overdue; this.memories = d.memories; this.recentDays = d.recentDays; + /* 🔴🔴 v59b(用户 2026-10-18):「提醒按钮,明明没提醒,怎么还显示个 2?」 + * + * 真根因:`app.unread` **从来没接过数据** —— 它只是 `FbAppState` 里的 + * 一个写死的初值 `@Trace unread: number = 2;`,而全工程**只有一处** + * 给它赋值(通知页的「全部已读」把它改成 0)。 + * ⇒ 于是"从没进过通知页"的用户**永远看到 2**;进了通知页又变成 + * "永远是 0"(把真实未读也盖掉了)。两个方向都错。 + * + * `/home` 本来就返回权威的 `unread`(服务端 `Notification::unreadCount`, + * 与通知页同一个来源)—— 这里把它同步进全局状态,铃铛角标就真了。 + * + * ⚠️ 首页是启动后必到的页面(默认模块),所以这一个赋值点就够 + * "进应用看到真数字";进通知页后由那边继续维护(标记已读会变)。 + */ + this.app.unread = d.unread; }, (note: string): void => { this.syncNote = note; diff --git a/entry/src/main/ets/pages/people/FbPeoplePage.ets b/entry/src/main/ets/pages/people/FbPeoplePage.ets index 1b9a615..be36032 100644 --- a/entry/src/main/ets/pages/people/FbPeoplePage.ets +++ b/entry/src/main/ets/pages/people/FbPeoplePage.ets @@ -802,15 +802,21 @@ export struct FbPeoplePage { // 见 `build()` 的说明。 .layoutWeight(1) .width('100%') - // 🔴 **白底铺满整屏、不内缩、不圆角**(用户 2026-10-15 反馈"最下面还有一条 - // 有背景色的东西,不是从最底下滑上来的")。 - // - // 之前是"左右 margin 内缩 + 圆角"的**卡片形态** ⇒ - // ① 列表底部与屏幕底之间留出页面底色; - // ② 左右两条底色也会在滚到底时露出来。 - // ⇒ 白底**通栏到底**,不再做卡片。 - // ⚠️ 别再用 `margin`/`borderRadius`/`clip` —— 那三个就是"卡片感"的来源。 - .backgroundColor(FbColors.surface) + /* 🔴🔴 v59b(用户 2026-10-18):「名册页面,不好看,太丑了,应该是 + * 每个人都是圆角矩形,往中间收一点,距离左右都有点距离, + * 每个人中间都有一点空隙。这样是很多个圆角矩形平铺下来的。」 + * + * 🔴 真凶:卡片**早就**是圆角矩形了(v51 就做了 `borderRadius` + + * `margin`),但**容器也是纯白** ⇒ **白卡压在白底上**, + * 圆角和左右收边**全都看不见** —— 看上去就是一整块通栏白。 + * (这解释了为什么"改过一轮还是丑":改的是卡片的形状, + * 而问题在于它没有可对比的背景。) + * + * ⇒ 容器改成**页面底色(宣纸米色)**,白卡才"浮"得出来; + * 外层那个兜底白(见 build 末尾)也一并改掉,否则滚到底 + * 会从米色突然切回白色。 + */ + .backgroundColor(FbColors.paper) // 🔴🔴 **这里绝对不能再加 `padding({bottom})`** // (用户 2026-10-15:「它的内容是从悬浮框上面滚出来的」)。 // @@ -831,9 +837,10 @@ export struct FbPeoplePage { } .width('100%') .layoutWeight(1) - // 🔴 外层也铺白底:内容不足一屏时,列表白底铺不到屏幕底, - // 这一层就是"兜底的白" —— 不能让页面底色从底部露出来。 - .backgroundColor(FbColors.surface) + /* 兜底底色 = **页面底色**(原来是纯白)。 + ⚠️ 必须与列表容器同色:列表内容不足一屏时,露出来的就是这一层; + 两层不同色会在底部切出一条突兀的色带(v51 踩过同类问题)。 */ + .backgroundColor(FbColors.paper) } /** @@ -916,14 +923,22 @@ export struct FbPeoplePage { }) } .width('100%') - .padding({ left: 14, right: 14, top: 11, bottom: 11 }) - // 🔴 用户 2026-10-16:「人员列表项:每一个人员是不是都可以做成一个 - // **圆角矩形**的形式?」⇒ 每条**独立一张圆角卡**(白底 + 间距), - // 不再靠 Divider 连成一大块。 + .padding({ left: 14, right: 14, top: 12, bottom: 12 }) + /* 🔴 v59b(用户 2026-10-18):「每个人都是圆角矩形,往中间收一点, + * 距离左右都有点距离,每个人中间都有一点空隙。这样是很多个 + * 圆角矩形平铺下来的。」 + * + * ⚠️ 形状其实**早就是**圆角卡了(v51),用户说"丑"的真凶在容器 —— + * 列表容器也是纯白 ⇒ 白卡压白底 ⇒ 圆角/收边**全看不见**。 + * 已把容器改成页面底色(见 rosterView 的 `backgroundColor`), + * 白卡这才"浮"得出来。 + * ⚠️ 这里的三个数也一起放大了一档(user 明确要"收一点 + 有空隙"): + * 左右 16(= 全站 pagePad,与页头对齐)、卡间 10(原来 8,偏挤)、 + * 圆角 14(原来 10,在 12 的行内边距下 10 显得方)。 + */ .backgroundColor(FbColors.surface) - .borderRadius(FbMetric.radiusS) - // ⚠️ 间距放这里(@Builder 不能链式)—— 左右留边 + 卡间空隙 - .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 8 }) + .borderRadius(FbMetric.radiusCard) + .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 10 }) .alignItems(VerticalAlign.Center) // 🔴 点整行 = **拨打这个人的电话**(用户 2026-10-13 / 10-15 明确要求)。 // 规则(2026-10-15 用户确认):**有主号打主号**;