v0.8.3 头像显示 / 主题色标签 / 右侧贴边 / 简要卡片 / 浮层分离
用户 2026-10-18 第二批五条: ①「右上角的个人头像没显示。」 🔴 真根因:`/me` 与 `/login` 返回的 `avatar_url` 是**相对地址** (`media.php?k=avatars%2Fxxx.jpg` —— 不带域名、不带令牌), 那是给**浏览器**用的(同源 + cookie);`Image()` 要完整 URL ⇒ 加载失败。 真机日志:`GetAsset failed: media.php?k=avatars%2F20261002_...jpg` ⇒ 新增 `fbMediaUrl(raw, token)` 归一化,**三种形态都认** (绝对地址 / media.php 相对式 / 裸 key)—— 少认一种就会出现 "有的头像能显示、有的不能",那类 bug 最难查。 ⚠️ 不能复用 `fbImageSrc()`:那是"附件 key → 本地缓存"的通道, 把整条 URL 当 key 传进去会得到一个必然 404 的地址。 ②「名册、家谱这两个标签,用的不是主题色。」 🔴 两个调用点**各硬编码 `'#A84A28'`**(品牌砖红),而名册模块主题色是 `#4A7C82`(青)⇒ 与整页不在一个色系。 ⇒ `segBtn` 不再由调用方传色,内部取 `fbThemeVars(fbModuleOf('people').color).brand`; 顺手消掉"两处各写一遍字面量"这个漂移源。 ③「名册列表,左侧有空隙了……怎么右侧不是呢?右侧还是紧贴屏幕。」 🔴 实测(像素扫描):左米色 50~72px、**右米色 0px**。 根因是 `Row` 内 `layoutWeight(1)` 的子项默认 `min-width: auto` (按内容最小宽度)⇒ 长名字/多标签把**卡片布局盒撑破** ⇒ 右边越界贴屏幕 (左边先被 margin 占位、所以还留着空隙)。 ⇒ 弹性子项开 `flexShrink(1)`,姓名那行连同 `width('100%')`。 ④「点击列表,不打电话了,容易误触。而是弹出一个卡片,卡片中是头像、名称、 电话号码、邮箱等简要信息,下面三个按钮,详情、编辑和删除。…… 列表页面中,也不要详情按钮了。」 ⇒ 新建 `FbPersonQuickSheet`(@ComponentV2): 头像 / 姓名+称谓 / 电话(主号 + 其他)/ 邮箱 / 单位 / 生日 + 三个等宽按钮「详情 / 编辑 / 删除」。 ⇒ 点整行走 `openQuick()`(**不再拨号**);列表里的「详情」按钮**删除**。 ⇒ 删除走 `AlertDialog` **二次确认**(服务端物理删除、无回收站 ⇒ 误触不可逆), 文案带姓名("确定把「老杨」…")—— 误触时最需要"是不是点错了人"这个信息。 ⚠️ 两级浮层分开两个状态(`quickPerson` / `detailPerson`);详情关掉**回到简要卡**, 而不是一路退回列表。 ⑤「这个卡片,弹出来之后,是白色的,和下面的列表中的白色,都融合在一起, 根本看不出是弹框。」 🔴 原来只靠 `#00000066` 遮罩区分 —— 而本应用**列表本身就是大面积白底**, 白卡压在"被压暗的白"上边界仍发飘。 ⇒ **三层分离**:① 遮罩(宿主)② 卡片**阴影**(抬起来)③ 极浅**描边** (同色背景上仍有一条可见界线;只靠阴影在浅底上不够稳)。 ⚠️ 三个浮层(简要卡 / 详情 / 编辑)**同手段** —— 否则从简要卡切到编辑会突然"变平"。 验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 377/0(新增 28 条, 另修 3 条旧判据);_fb_v59c_falsify.py 16 项注入 ⇒ 16/0。 ⚠️ 本轮反向验证又抓出 2 条假绿 + 1 条"没真注入",已逐条收紧(记入 pitfalls): · `flexShrink(1)` 在本页有**两处**(姓名 Text / 整个 Column)⇒ "出现过"型判据 删掉关键那处仍绿;改成锚"相邻关系"(`.layoutWeight(1)` 之后 220 字符内); · `showAlertDialog` 被 `if (false)` 包起来(确认框永不弹)⇒ 字符串还在、判据仍绿; 改成查因果(窗口内有 `value: '删除'` + `this.doDelete(who)`); · 有一条注入只改了注释、没删那行代码 ⇒ 报 [MISS],那是"注入没打到点上"。
This commit is contained in:
@@ -15,16 +15,19 @@
|
||||
import { FbColors, FbMetric, fbTint, FbTint } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbKinGraph, FbKinPerson, FbRosterItem } from '../../model/Types';
|
||||
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../../model/Modules';
|
||||
import { parseKin } from '../../data/FbParse';
|
||||
import { loadModule } from '../../data/FbModuleLoad';
|
||||
import { FbAvatar, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FbPersonSheet } from '../../components/FbPersonSheet';
|
||||
import { FbPersonQuickSheet } from '../../components/FbPersonQuickSheet';
|
||||
import { FbPersonEditSheet } from '../../components/FbPersonEditSheet';
|
||||
import { FbKinGraphView } from '../../components/FbKinGraph';
|
||||
import { FbTagChip } from '../../components/FbTagChip';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
import { FbDb } from '../../data/FbDb';
|
||||
import { FbPersonRepo, FbRosterPerson, FbRosterSyncResult, FbTagCount } from '../../data/FbPersonRepo';
|
||||
import { FbApiResult } from '../../data/FbHttp';
|
||||
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { call } from '@kit.TelephonyKit';
|
||||
@@ -226,6 +229,29 @@ export struct FbPeoplePage {
|
||||
*/
|
||||
@Local editPerson: FbRosterPerson | null = null;
|
||||
|
||||
/**
|
||||
* 「简要卡片」里的人(点列表整行弹出)。
|
||||
*
|
||||
* 🔴 v59c(用户 2026-10-18):「点击列表,不打电话了,容易误触。而是弹出
|
||||
* 一个卡片,卡片中是头像、名称、电话号码、邮箱等简要信息,下面三个按钮,
|
||||
* 详情、编辑和删除。」
|
||||
*
|
||||
* ⚠️ 与 `detailPerson`(详情浮层)**分开两个状态**,不要合并:
|
||||
* 两者是"概览 → 深入"的两级,且**在同一个 Stack 里叠**(详见渲染处),
|
||||
* 合并成一个变量就分不清"现在该显示哪一级"(从详情返回时要回简要卡)。
|
||||
*/
|
||||
@Local quickPerson: FbRosterPerson | null = null;
|
||||
|
||||
/**
|
||||
* 「简要卡片」用哪份数据。
|
||||
*
|
||||
* 优先用**已拉到的详情那份**(`detailPerson`,字段更全、含工作经历与补充电话);
|
||||
* 没有就退回列表那份(`quickPerson`)。
|
||||
* ⚠️ 这是为了"从详情返回简要卡"时状态一致 —— 详见 `quickSource()` 的实现与
|
||||
* `onBackFromDetail()`。
|
||||
*/
|
||||
@Local quickFromDetail: FbRosterPerson | null = null;
|
||||
|
||||
/**
|
||||
* 🔴 浮层开合时**让底部悬浮栏让位**(用户 2026-10-15 实测的真 bug)。
|
||||
*
|
||||
@@ -423,6 +449,73 @@ export struct FbPeoplePage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 「删除」——**破坏性操作,必须先二次确认**。
|
||||
*
|
||||
* 🔴 v59c(用户 2026-10-18):简要卡上加了「删除」按钮。
|
||||
* 这是本项目第一个"从浮层直接删人"的入口(原来只能进编辑浮层再删),
|
||||
* 所以确认这一步**不能省**:
|
||||
* · 一次误触就真的把人删了(服务端是物理删除,没有回收站);
|
||||
* · 而且这个人可能还在观影/日程里被引用 —— 后果不可逆。
|
||||
* ⇒ 弹系统 `AlertDialog`(比自绘确认框稳,也不占额外的浮层层级)。
|
||||
*
|
||||
* ⚠️ 文案里带上**姓名**:"删除「老杨」?"比"确定删除吗?"安全得多 ——
|
||||
* 用户能立刻看出"是不是点错了人"(这正是误触最需要的信息)。
|
||||
*/
|
||||
private confirmDelete(): void {
|
||||
const who: FbRosterPerson | null = this.quickSource();
|
||||
if (who === null) {
|
||||
return;
|
||||
}
|
||||
const name: string = who.shown !== '' ? who.shown : who.name;
|
||||
this.getUIContext().showAlertDialog({
|
||||
title: '删除联系人',
|
||||
message: '确定把「' + name + '」从名册里删除吗?此操作无法撤销。',
|
||||
autoCancel: true,
|
||||
primaryButton: {
|
||||
value: '取消',
|
||||
action: (): void => {
|
||||
}
|
||||
},
|
||||
secondaryButton: {
|
||||
value: '删除',
|
||||
fontColor: FbColors.red,
|
||||
action: (): void => {
|
||||
this.doDelete(who);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 真正执行删除(确认之后) */
|
||||
private async doDelete(who: FbRosterPerson): Promise<void> {
|
||||
const token: string = FB_TOKEN_STORE.get();
|
||||
if (token === '') {
|
||||
this.syncNote = '未登录,无法删除';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res: FbApiResult = await this.repo.remove(who.id, token);
|
||||
if (!res.ok) {
|
||||
hilog.warn(DOMAIN, TAG, '删除联系人失败:%{public}s', res.error);
|
||||
this.syncNote = '删除失败:' + res.error;
|
||||
return;
|
||||
}
|
||||
// 关闭浮层 + 重新拉名册(那人整条会从本地库消失)
|
||||
this.quickPerson = null;
|
||||
this.quickFromDetail = null;
|
||||
this.detailPerson = null;
|
||||
this.editPerson = null;
|
||||
this.syncNote = '已删除 ' + (who.shown !== '' ? who.shown : who.name);
|
||||
await this.syncInBackground();
|
||||
} catch (err) {
|
||||
const e: BusinessError = err as BusinessError;
|
||||
hilog.error(DOMAIN, TAG, '删除联系人抛异常:%{public}s',
|
||||
e.message !== undefined ? e.message : JSON.stringify(err));
|
||||
this.syncNote = '删除失败:' + (e.message !== undefined ? e.message : '网络异常');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉家谱(`/api/v1/kin`)—— 先缓存、再网络。
|
||||
*
|
||||
@@ -461,7 +554,14 @@ export struct FbPeoplePage {
|
||||
FbPersonSheet({
|
||||
person: this.detailPerson,
|
||||
onClose: (): void => {
|
||||
/* 🔴 v59c:详情是**从简要卡进来的** ⇒ 关掉后回到简要卡,
|
||||
而不是一路退回列表(用户会觉得"我刚才那层被吞掉了")。
|
||||
见 `quickFromDetail` 的说明。 */
|
||||
const back: FbRosterPerson | null = this.detailPerson;
|
||||
this.detailPerson = null;
|
||||
if (back !== null && this.quickPerson !== null) {
|
||||
this.quickFromDetail = back;
|
||||
}
|
||||
},
|
||||
onEdit: (): void => {
|
||||
// 关详情、开编辑(把同一个人带过去)
|
||||
@@ -471,6 +571,49 @@ export struct FbPeoplePage {
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 简要卡片(点列表整行弹出)----
|
||||
//
|
||||
// 🔴 v59c(用户 2026-10-18):点行不再拨号,改弹这张卡。
|
||||
// ⚠️ 与详情浮层**用同一个遮罩色**(`#00000066`)——
|
||||
// 两级浮层的"压暗程度"必须一致,否则用户会觉得"换了一层世界"。
|
||||
if (this.quickPerson !== null && this.detailPerson === null) {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor('#00000066')
|
||||
.onClick((): void => {
|
||||
this.quickPerson = null;
|
||||
this.quickFromDetail = null;
|
||||
})
|
||||
|
||||
FbPersonQuickSheet({
|
||||
person: this.quickSource(),
|
||||
onClose: (): void => {
|
||||
this.quickPerson = null;
|
||||
this.quickFromDetail = null;
|
||||
},
|
||||
onDetail: (): void => {
|
||||
// 关简要卡、开详情(把同一个人带过去)
|
||||
const who: FbRosterPerson | null = this.quickSource();
|
||||
this.quickPerson = null;
|
||||
this.quickFromDetail = null;
|
||||
if (who !== null) {
|
||||
this.openDetail(who.id);
|
||||
}
|
||||
},
|
||||
onEdit: (): void => {
|
||||
const who: FbRosterPerson | null = this.quickSource();
|
||||
this.quickFromDetail = null;
|
||||
if (who !== null) {
|
||||
this.editPerson = who;
|
||||
}
|
||||
},
|
||||
onDelete: (): void => {
|
||||
this.confirmDelete();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 编辑浮层 ----
|
||||
if (this.editPerson !== null) {
|
||||
Column()
|
||||
@@ -616,9 +759,19 @@ export struct FbPeoplePage {
|
||||
peopleHeader() {
|
||||
Column() {
|
||||
// ---- 分段切换(名册 / 家谱)----
|
||||
//
|
||||
// 🔴 v59c(用户 2026-10-18):「名册、家谱这两个标签,用的不是主题色。」
|
||||
// ⇒ 原来两个都硬编码 `'#A84A28'`(**品牌砖红**),而名册的主题色是
|
||||
// `FB_MODULES` 里那个 `#4A7C82`(青)—— 页面里其它地方
|
||||
// (如列表项那个"详情"按钮的底色/字色)用的都是主题色粉调,
|
||||
// 于是这一处显得"跟整个页面不是一个色系"。
|
||||
// ⚠️ 用 `fbThemeVars(...).brand` 走统一算法,而不是直接把 `#4A7C82`
|
||||
// 抄进来:`fbThemeVars` 会把色相拉到一个**可读的饱和度/亮度区间**
|
||||
// (见其实现),直接抄模块色在选中态当底、白字压上去会偏亮发飘。
|
||||
// ⚠️ 两处都必须传同一个值 —— 之前正是"两处各写一遍字面量"才漂的。
|
||||
Row({ space: 4 }) {
|
||||
this.segBtn('名册', 'roster', '#A84A28')
|
||||
this.segBtn('家谱', 'kin', '#A84A28')
|
||||
this.segBtn('名册', 'roster')
|
||||
this.segBtn('家谱', 'kin')
|
||||
}
|
||||
.padding(3)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
@@ -882,6 +1035,15 @@ export struct FbPeoplePage {
|
||||
.fontColor(FbColors.ink)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.maxLines(1)
|
||||
/* 🔴🔴 内容必须**允许收缩**(`flexShrink`),否则会把卡片撑破 ——
|
||||
* 这就是用户 2026-10-18 报的「左侧有空隙、右侧紧贴屏幕」。
|
||||
* 原理:Row 里的子项默认 `min-width: auto`(= 按内容最小宽度),
|
||||
* 长名字 / 多个标签会把这一行顶得比卡片可用宽度更宽;
|
||||
* 卡片本身有 `margin(left/right: 16)`,但**布局盒被内容撑出去**,
|
||||
* 于是右边越界贴到屏幕缘(左边因为 margin 先占位,所以还有空隙)。
|
||||
* ⇒ 给两个可能变长的 Row 都开 `flexShrink(1)` + 给 Text 让路。
|
||||
* (本项目第 N 次踩这个"flex 子项不收缩"的坑,见 pitfalls。) */
|
||||
.flexShrink(1)
|
||||
if (p.isAdmin) {
|
||||
FbTag({ text: '管理员', tone: 'brand' })
|
||||
}
|
||||
@@ -890,6 +1052,7 @@ export struct FbPeoplePage {
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
|
||||
Row({ space: 8 }) {
|
||||
if (p.tags.length > 0) {
|
||||
@@ -907,20 +1070,17 @@ export struct FbPeoplePage {
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
/* ⚠️ 这一列是弹性项,必须允许收缩 —— 否则它的最小宽度(由内容决定)
|
||||
会把整行顶宽、把卡片撑破(右侧紧贴屏幕,用户 2026-10-18 报的那个)。 */
|
||||
.flexShrink(1)
|
||||
|
||||
// 「详情」按钮(用户要求:右侧加一个按钮写着"详情")
|
||||
// ⚠️ 为什么不是那个灰箭头:箭头**只是装饰**(用户说的"灰色小横线"就是它),
|
||||
// 而"点行"这个动作被用户指定成**拨号**了 ——
|
||||
// 所以详情必须有个**显式的按钮**,否则没有任何入口。
|
||||
Text('详情')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor('#4A7C82')
|
||||
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
|
||||
.backgroundColor('#4A7C8215')
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
this.openDetail(p.id);
|
||||
})
|
||||
/* 🔴 v59c(用户 2026-10-18):「列表页面中,也不要详情按钮了。」
|
||||
* ⇒ 原来这里有个显式的「详情」按钮(v51 加的,当时"点行=拨号"
|
||||
* 占用了整行点击,详情只能另给入口)。
|
||||
* 现在**点行改成弹简要卡片**(卡片里有详情/编辑/删除三个按钮)
|
||||
* ⇒ 整行点击已经是入口,这个按钮就是冗余的,且它正是把卡片
|
||||
* 右侧撑破的元凶之一(固定宽度 + 不收缩)。
|
||||
*/
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
|
||||
@@ -940,39 +1100,50 @@ export struct FbPeoplePage {
|
||||
.borderRadius(FbMetric.radiusCard)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
// 🔴 点整行 = **拨打这个人的电话**(用户 2026-10-13 / 10-15 明确要求)。
|
||||
// 规则(2026-10-15 用户确认):**有主号打主号**;
|
||||
// 一个号码都没有时改成"打开详情"(点了没反应比什么都让人困惑)。
|
||||
//
|
||||
// ⚠️ 为什么不用"挑最近打过的号":
|
||||
// HarmonyOS **没有"读通话记录列表"的公开 API**(只有个
|
||||
// `hasMatchedCallLog`——判断某号码 6 小时内打过没,还要 `CHECK_CALL_LOG`
|
||||
// 敏感权限),**无法按时间排出"最近那个"**。已与用户确认走"主号优先"。
|
||||
// ⚠️ `phones` 由服务端保证「主号排第一」(见 FbRosterPerson.phones)。
|
||||
// ⚠️ 用 `p.phones` 而不是 `p.phone`:后者只是 `phones[0]` 的便捷别名,
|
||||
// 这里显式用数组,语义更清楚(也方便将来做"多号选择")。
|
||||
/* 🔴🔴 v59c(用户 2026-10-18):「点击列表,不打电话了,容易误触。
|
||||
* 而是弹出一个卡片,卡片中是头像、名称、电话号码、邮箱等简要信息,
|
||||
* 下面三个按钮,详情、编辑和删除。」
|
||||
*
|
||||
* ⇒ 点整行**不再拨号**(拨号容易误触:列表里手指一滑就拨出去了),
|
||||
* 改成弹「简要卡片」(见 `quickSheet` 那一组状态与 `quickPanel`)。
|
||||
* 电话号码在卡片里也是可点的(那时是**确认过**的点击,不算误触)。
|
||||
*/
|
||||
.onClick((): void => {
|
||||
if (p.phones.length > 0) {
|
||||
this.dial(p.phones[0], p.name);
|
||||
} else {
|
||||
this.openDetail(p.id);
|
||||
}
|
||||
this.openQuick(p);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开某人的详情浮层(点「详情」按钮、或对没手机号的人点整行)。
|
||||
* 打开某人的「简要卡片」(点列表整行触发)。
|
||||
*
|
||||
* 🔴 v52(用户 2026-10-15):**先秒开、再补全**。
|
||||
* 本地库里那份只有"列表要用的字段",而"全功能"要求详情能看到
|
||||
* **工作经历 / 补充电话 / 私人备注 / 各级地址** —— 这些只在
|
||||
* `GET /people/<id>` 里有。
|
||||
* ⇒ ① 立即用本地那份把浮层打开(不等网络,点开就有内容);
|
||||
* ② 同时发一个详情请求,回来就**原地替换**(字段更多、更准)。
|
||||
* ⚠️ 断网/失败时保持 ① 的显示,不弹错误 ——
|
||||
* "详情少几项"比"点了没反应"好得多。
|
||||
* ⚠️ 用 `this.detailToken` 防止"快速连点两个人,前一个的响应把后一个覆盖"。
|
||||
* 🔴 v59c(用户 2026-10-18):「点击列表,不打电话了,容易误触。而是弹出
|
||||
* 一个卡片,卡片中是头像、名称、电话号码、邮箱等简要信息,下面三个按钮,
|
||||
* 详情、编辑和删除。」
|
||||
*
|
||||
* ⚠️ 与 `openDetail` 是**两级**:
|
||||
* 简要卡(概览 + 三个动作)→ 点「详情」才进完整浮层。
|
||||
* 从列表那份(`rawById`)直接起 —— 它已经有头像/姓名/电话/邮箱/标签,
|
||||
* 够画这张卡(**不必等网络**,点开就有东西)。
|
||||
*/
|
||||
private openQuick(p: FbRosterItem): void {
|
||||
const raw: FbRosterPerson | undefined = this.rawById.get(p.id);
|
||||
if (raw === undefined) {
|
||||
return;
|
||||
}
|
||||
this.quickFromDetail = null;
|
||||
this.quickPerson = raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简要卡要用哪份数据(详情那份优先)。
|
||||
*
|
||||
* ⚠️ 抽成方法而不是在 `@Builder` 参数里写三目:括号里塞多行表达式会让
|
||||
* ArkTS 编译器解析错乱(本项目踩过,报的却是无关的 `comma operator`)。
|
||||
*/
|
||||
private quickSource(): FbRosterPerson | null {
|
||||
return this.quickFromDetail !== null ? this.quickFromDetail : this.quickPerson;
|
||||
}
|
||||
|
||||
/**
|
||||
* 名册里**所有标签名**(给编辑浮层的"点选"用)。
|
||||
*
|
||||
@@ -987,6 +1158,19 @@ export struct FbPeoplePage {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开某人的**详情浮层**(简要卡里点「详情」)。
|
||||
*
|
||||
* 🔴 v52(用户 2026-10-15):**先秒开、再补全**。
|
||||
* 本地库里那份只有"列表要用的字段",而"全功能"要求详情能看到
|
||||
* **工作经历 / 补充电话 / 私人备注 / 各级地址** —— 这些只在
|
||||
* `GET /people/<id>` 里有。
|
||||
* ⇒ ① 立即用本地那份把浮层打开(不等网络,点开就有内容);
|
||||
* ② 同时发一个详情请求,回来就**原地替换**(字段更多、更准)。
|
||||
* ⚠️ 断网/失败时保持 ① 的显示,不弹错误 ——
|
||||
* "详情少几项"比"点了没反应"好得多。
|
||||
* ⚠️ 用 `this.detailToken` 防止"快速连点两个人,前一个的响应把后一个覆盖"。
|
||||
*/
|
||||
private openDetail(id: number): void {
|
||||
const raw: FbRosterPerson | undefined = this.rawById.get(id);
|
||||
if (raw === undefined) {
|
||||
@@ -1277,8 +1461,18 @@ export struct FbPeoplePage {
|
||||
// 共用小件
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 分段切换的一个按钮(名册 / 家谱)。
|
||||
*
|
||||
* 🔴 v59c(用户 2026-10-18):「名册、家谱这两个标签,用的不是主题色。」
|
||||
* ⇒ 颜色**不再由调用方传字面量**,改成方法内部自己取名册模块的主题色。
|
||||
* 之前两个调用点各写一遍 `'#A84A28'`(品牌砖红,与名册的青色主题不符),
|
||||
* 而且"两处各写一遍"本身就是漂移源(改一处忘一处)。
|
||||
* ⚠️ 取色走 `fbThemeVars(名册模块色).brand`(统一算法),
|
||||
* 不是直接抄 `FB_MODULES` 里那个 `#4A7C82` —— 见调用点上方那段说明。
|
||||
*/
|
||||
@Builder
|
||||
segBtn(label: string, key: string, color: string) {
|
||||
segBtn(label: string, key: string) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(this.subView === key ? FontWeight.Medium : FontWeight.Normal)
|
||||
@@ -1286,13 +1480,19 @@ export struct FbPeoplePage {
|
||||
.textAlign(TextAlign.Center)
|
||||
.height(30)
|
||||
.layoutWeight(1)
|
||||
.backgroundColor(this.subView === key ? color : Color.Transparent)
|
||||
.backgroundColor(this.subView === key ? this.themeBrand() : Color.Transparent)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
this.subView = key;
|
||||
})
|
||||
}
|
||||
|
||||
/** 名册模块的主题色(选中态实底)。取色规则见 `segBtn` 的说明。 */
|
||||
private themeBrand(): string {
|
||||
const m: FbModuleDef | undefined = fbModuleOf('people');
|
||||
return fbThemeVars(m !== undefined ? m.color : '#A84A28').brand;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前该显示的人(**标签筛选 + 关键词搜索**之后的结果)。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user