v0.8.4 简要卡观感 / 分享 / 名册搜索 / 全局返回手势(v59e)

八条需求的落地与真因:

1. 浮层与白底"糊在一起"、看不清 —— 根因不是配色,是**遮罩从来没画出来过**:
   ArkUI 的八位色值是 #AARRGGBB(alpha 在前),而全应用写的是 '#00000066'
   (本意"黑 40%")⇒ alpha = 0x00 ⇒ 全透明。同型还有 brand + '1A'(拼到了蓝通道)
   与 shadow('#00000033')。⇒ 17 个文件统一改走 fbAlpha() / FB_MASK / FB_SHADOW /
   FB_HAIRLINE;真机像素采样复验:遮罩 (148,146,140)、卡片白、浅底 (237,242,243)。
2. 简要卡外圈加**主题色描边环**(外层铺色 + padding,不是 border —— border 会被
   内层圆角切出白缝)。
3. 简要卡**去掉删除按钮**,删除挪到完整详情卡最下面(Divider + 红字块)。
   顺手修一处静默失败:confirmDelete() 原来只取 quickSource(),
   从家谱直接开详情时 quickPerson 为 null ⇒ 点删除毫无反应。
4. 信息行间距 10 → 16(每行再各加 3vp 上下内边距,实测行距 134/118px)。
5. 主电话整行可点**直接拨号**(call.makeCall,落到系统拨号盘)。
6. 按钮改为 详情 / 编辑 / 分享 / 关闭;分享走系统分享面板,
   内容为纯文本「姓名 / 电话 / 邮箱」三项(用户点名的就是这三项,
   不多带单位),面板拉不起来时兜底复制到剪贴板并提示。
7. 名册搜索"完全不能用"有两个独立真因:
   ① 搜索框是 List 的第一个 item,无结果时整个 List 被空态替换 ⇒ 输入框被卸载、
      焦点丢失 ⇒ 输不进第二个字。改:List 无条件渲染,空态自身做成 ListItem。
   ② rowToPerson 把 initials 写死成空串,服务端下发的拼音首字母从未落库 ⇒
      拼音搜索恒 0 结果。改:FB_DB_VERSION 6→7 + PRAGMA 探测补列 + 同步时落库;
      匹配同时遍历全部电话(原来只比主号)。
   另加搜索框「清空」(24×24 命中区)。
8. 全局返回手势:应用是单页 + 模块分发、没有路由栈 ⇒ 自建 FB_BACK 登记表
   (倒序询问)+ moduleHistory + applyModule()(不记历史,否则 A/B 两页来回弹)。
   9 个页面登记/注销;Index.onBackPress 先关浮层 → 回上一模块 → 才 return false
   交给系统退桌面(onBackPress 必须声明 boolean,返回 void 会被当成没接管)。

验证:
- 静态守卫 417 / 0(新增 ⑤阴影·描边·色环 ⑥透明度色 ⑦返回手势 ⑧搜索 ⑨分享)
- 反向验证 10 / 0(9 条注入全部能让守卫变红;其中"遮罩换纯黑"那条暴露了
  一条存在型假绿,已改成数个数 + 禁止 Color.Black)
- 建表/迁移列一致性守卫 12 / 0
- 真机全模块遍历 14 / 14;真机逐项复核 12 / 0 与 7 / 0

工具侧顺带修好两个过期的探针:
- 遍历探针仍用"应用内横滑"打开抽屉,而该手势早已按用户要求去掉、入口改为顶栏图标
  ⇒ 一次全量遍历里 12 个模块全报"抽屉里没找到"(像应用坏了,其实是探针过期)。
- 资源/笔记两页写死了早已不存在的条目(户口本/房产证/阳台改造清单)⇒ 改成
  结构性文案(副标题 + 常驻筛选胶囊),不随用户数据变化而恒红。
This commit is contained in:
2026-10-18 23:12:57 +08:00
parent 1c1a859464
commit 7858de27e3
26 changed files with 1213 additions and 257 deletions
+2 -2
View File
@@ -24,7 +24,7 @@
* 自己用 Stack + 位移动画做,行为完全可控。
*/
import { FbColors, FbIcon, FbMetric, fbTint, FbTint } from '../common/Theme';
import { FbColors, FbIcon, FbMetric, fbTint, FbTint, fbAlpha } from '../common/Theme';
import { FbAppState, fbAppState } from '../common/Store';
import { FbModuleDef, FB_MODULES, fbDefaultNavModules, fbThemeVars } from '../model/Modules';
import { FB_DEFAULT_SLOGAN } from '../model/DemoData';
@@ -229,7 +229,7 @@ export struct FbDrawer {
private washOf(hex: string): string {
// 与 Theme.fbPalette 的 wash 同口径:压饱和、抬明度到 0.94
const t = fbThemeVars(hex);
return t.brand + '1A';
return fbAlpha(t.brand, 0.10);
}
build() {
@@ -36,7 +36,7 @@
*/
import { hilog } from '@kit.PerformanceAnalysisKit';
import { FbColors, FbMetric } from '../common/Theme';
import { FbColors, FbMetric, FB_MASK } from '../common/Theme';
import { FbCalEventFull, FbCalEventInput, FbCalRepo } from '../data/FbCalRepo';
import { FbCalBook } from '../model/Types';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
@@ -780,7 +780,7 @@ export struct FbEventEditSheet {
.height('100%')
.justifyContent(FlexAlign.Center)
.padding({ left: 32, right: 32 })
.backgroundColor('#00000066')
.backgroundColor(FB_MASK)
.onClick((): void => {
this.confirmDel = false;
})
@@ -242,7 +242,7 @@ export struct FbMovieDetailSheet {
Column()
.width('100%')
.height('100%')
.backgroundColor('#000000E6')
.backgroundColor('#E6000000')
.onClick((): void => {
this.viewerOpen = false;
})
@@ -22,7 +22,7 @@
*/
import { hilog } from '@kit.PerformanceAnalysisKit';
import { FbColors, FbMetric } from '../common/Theme';
import { FbColors, FbMetric, FB_HAIRLINE, FB_MASK, FB_SHADOW } from '../common/Theme';
import { FbRosterPerson, FbJobRow, FbContactRow } from '../data/FbPersonRepo';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { FbApiOptions, FbApiResult, fbApi, fbApiBase } from '../data/FbHttp';
@@ -1036,8 +1036,8 @@ export struct FbPersonEditSheet {
/* 🔴 v59c:与 `FbPersonSheet` / `FbPersonQuickSheet` 同一套"三层分离"
(用户 2026-10-18:「白色和下面列表的白色融合在一起,看不出是弹框」)。
⚠️ 三个浮层必须**同手段**,否则从简要卡 → 编辑切过去会突然"变平"。 */
.shadow({ radius: 24, color: '#00000033', offsetX: 0, offsetY: 6 })
.border({ width: 1, color: '#00000014' })
.shadow({ radius: 24, color: FB_SHADOW, offsetX: 0, offsetY: 6 })
.border({ width: 1, color: FB_HAIRLINE })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
@@ -1045,7 +1045,7 @@ export struct FbPersonEditSheet {
// 遮罩盖满整屏(点空白处关掉 = 取消)
.justifyContent(FlexAlign.Center)
.padding({ left: 32, right: 32 })
.backgroundColor('#00000066')
.backgroundColor(FB_MASK)
.onClick((): void => {
this.confirmDel = false;
})
@@ -1,30 +1,42 @@
/**
* 家书 FamilyBook · 名册「简要卡片」(鸿蒙端)
*
* 🔴🔴 用户 2026-10-18 原话:
* 🔴 用户 2026-10-18 原话(第一版):
* 「点击列表,不打电话了,容易误触。而是弹出一个卡片,卡片中是头像、
* 名称、电话号码、邮箱等简要信息,下面三个按钮,详情、编辑和删除。」
* 🔴 用户 2026-10-18 原话(第二版,本文件当前形态):
* 「点击按钮后弹出这个简单框,不要加删除按钮了,删除按钮太容易误触。
* 应该在详情页面的最下面加一个删除按钮。」
* 「电话和单位什么的距离太近了,再拉远一点。」
* 「上面的电话点击之后就可以直接拨打。同时……下面有一个按钮,
* 详情、编辑、关闭,还有分享。」
* 「弹出的这个框,现在颜色和下面的白色都混到一起了……能不能在这个边框外面
* 加一圈这个颜色,也就是这个主题色。」
*
* 为什么要单独一个组件(而不是在名册页里再写一个 `@Builder`):
* · 它是**一张自己的卡**(与"完整详情"是两级),有自己的按钮组与布局;
* · 名册页那个文件已经 1300+ 行,再往里塞会失控;
* · 将来"从日程里点参会人"也可能要弹同一张卡 ⇒ 组件化才复用得上。
* ⇒ 于是本卡片相比第一版有四处变化,逐条对应在下面的代码注释里:
* ① 「删除」**移走**(挪到 `FbPersonSheet` 的最下面,见那边的 `onDelete`);
* ② 信息行**拉开间距**(`INFO_GAP`,从 10 → 16);
* ③ 主电话那一行**可点即拨**(发 `onCall` 事件,真正拨号在页面侧);
* ④ 底下四个按钮:详情 / 编辑 / 分享 / 关闭。
*
* 🔴 关于「弹框与下面列表都是白的、看不出是弹框」(用户同一条反馈里提的):
* 本卡片的解法是**三层分离**(缺一层就会"糊在一起"):
* ① 宿主给**深色遮罩**(见调用点,`#00000066`)—— 把下面的列表压暗;
* ② 卡片自己**加阴影**(`shadow`)—— 与遮罩拉开"浮起来"的层次;
* ③ 卡片底色用**纸白**、并给一圈**极浅描边** —— 即使在同色背景上,
* 边缘也有一条可见的界线(只靠阴影在浅底上是不够的)。
* 🔴 「卡片和下面的白色糊在一起」的三层解法(缺一层就会"看不清"):
* ⚠️ 只做①(很多地方只做遮罩)在"背景也是大面积白"时效果最弱:
* 白卡压在"被压暗的白"上,边界仍然发飘 —— 用户说的就是这种观感。
* ① 宿主给**深色遮罩**(见调用点,`#00000066`)—— 把下面的列表压暗;
* ② 卡片外面**一圈主题色描边环**(用户这次点名要的)—— 它是**实色**,
* 不依赖光照假设,白底白遮罩上也一眼看得出边界在哪;
* ③ 卡片自己**加阴影** —— 与遮罩拉开"浮起来"的层次。
* ⚠️ ② 的实现是"外层 `Column` 铺主题色 + 内边距 2 + 内层白卡",
* **不是**给白卡加 `border`:`border` 是画在盒子边界上的,
* 会把卡片内部撑窄 2vp,且圆角处颜色会顺着内圆角走(看着像"白卡描了彩边")。
* 铺一层再垫一张,才是真正的"外面加一圈"。
*
* ⚠️ 「删除」是**破坏性操作**,必须二次确认(见调用点的 `AlertDialog`)。
* 卡片这里**只负责发事件**,不自己做确认也不自己调接口 ——
* 与 `FbPersonSheet` 的做法一致(浮层之间不互相引用,免得成环)。
* ⚠️ 本组件**只发事件、不自己做动作**(不拨号、不删除、不弹确认):
* 拨号要 AbilityContext、删除要二次确认 + 删完刷新列表、分享要拼文本,
* 这些都是**页面**的职责。浮层之间也不互相引用(免得成环)。
*/
import { FbColors, FbMetric, fbTint, FbTint } from '../common/Theme';
import { FbColors, FbMetric, FB_SHADOW, fbTint, FbTint, fbAlpha } from '../common/Theme';
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
import { FbRosterPerson } from '../data/FbPersonRepo';
import { FbAvatar, FbTag } from './Widgets';
@@ -35,14 +47,32 @@ import { FB_TOKEN_STORE } from '../data/FbTokenStore';
const CARD_SHADOW: number = 24;
/**
* 「删除」用的红 —— 与 `resources/base/element/color.json` 的 `red` **同值**。
* 主题色描边环的宽度(vp)。
*
* ⚠️ 不能直接用 `FbColors.red`(那是 `Resource`):本文件要拿它拼
* `RED + '1A'`(透明度后缀)做底色,`Resource` 拼不了字符串。
* ⇒ 凡是要参与**颜色运算**的地方都用 hex(本项目已有 `FbTint` 这组先例)。
* ⚠️ 2vp 是"看得见但不喧哗"的那一档:1vp 在深色遮罩上仍显得细弱,
* 3vp 以上就变成"彩色相框",抢了内容和按钮的注意力。
*/
const RING_W: number = 2;
/**
* 信息行之间的间距(vp)。
*
* 🔴 用户 2026-10-18:「电话和单位什么的距离太近了,再拉远一点。」
* ⇒ 原来是 10(`Column({space: 10})`),现在 16,并且每行自己还有
* 上下 3vp 的内边距(见 `infoRow` 的 `padding`)⇒ 实际行距约 22vp。
*/
const INFO_GAP: number = 16;
/**
* 「关闭」按钮的底色 —— 与 `resources/base/element/color.json` 的
* `input_bg`(`FbColors.inputBg`)**同值**。
*
* ⚠️ 为什么不用 `FbColors.inputBg`:那是 `Resource`,而 `actionBtn` 的
* `bg/fg` 参数必须是 hex 字符串(要为「编辑/分享」做 `fbAlpha(bg, 0.10)` 的颜色运算)。
* ⇒ 与 `FbTint` 同一套处理方式:需要"参与运算"的颜色,一律在端上抄一份 hex。
* ⚠️ color.json 改色时这里要同步改(只有这一个值,风险可控)。
*/
const RED: string = '#C0533F';
const INPUT_BG: string = '#F2EDE2';
@ComponentV2
export struct FbPersonQuickSheet {
@@ -67,10 +97,24 @@ export struct FbPersonQuickSheet {
};
/**
* 「删除」:只发事件。
* ⚠️ 二次确认与调接口都在页面侧 —— 那里才知道"删完怎么刷新列表"。
* 「分享」:把这个人以**纯文本**(姓名 / 电话 / 邮箱)分享出去。
*
* ⚠️ 文本的拼接与系统分享面板都在页面侧(`common/FbShare.ets`)——
* 本组件只管"用户点了这一下"。
*/
@Event onDelete: () => void = (): void => {
@Event onShare: () => void = (): void => {
};
/**
* 「拨号」:点主电话那一行触发。
*
* 🔴 用户 2026-10-18:「上面的电话点击之后就可以直接拨打。」
* ⚠️ 传号码出去(而不是让页面自己去 person 里取):
* 将来"其他电话"也想点的时候,这里不用改签名。
* ⚠️ 实际动作是 `call.makeCall()` = **拉起拨号盘并填好号码**,
* 最后一下由用户按(合规要求,也是"不误触"的保险)。
*/
@Event onCall: (phone: string) => void = (phone: string): void => {
};
/** 名册模块的主题色(按钮配色与整个名册页同一个色系) */
@@ -147,7 +191,13 @@ export struct FbPersonQuickSheet {
return org !== '' ? org : title;
}
/** 一行「图标 + 标签 + 值」 */
/**
* 一行「图标 + 标签 + 值」。
*
* ⚠️ 上下各留 3vp 的内边距:`Column({space})` 的间距是**盒子之间**的距离,
* 行内的文字本身贴着盒子边,只调 space 会觉得"怎么拉还是挤"。
* 两者一起给才是真正的"拉开"。
*/
@Builder
infoRow(icon: Resource, label: string, value: string) {
Row({ space: 10 }) {
@@ -168,22 +218,68 @@ export struct FbPersonQuickSheet {
.layoutWeight(1)
}
.width('100%')
.padding({ top: 3, bottom: 3 })
.alignItems(VerticalAlign.Center)
}
/**
* 一个动作按钮。
* 可点即拨的电话行(主号专用)。
*
* ⚠️ 三个按钮等宽平分(`layoutWeight(1)`)—— 用户要"下面三个按钮",
* 等宽是最不容易出错、也最像"选择"的排法。
* 🔴 用户 2026-10-18:「上面的电话点击之后就可以直接拨打。」
*
* ⚠️ 视觉上必须**明确"这一行能点"**(否则用户不会去点):
* 号码用主题色、右侧挂一个电话小图标、整行有浅色底与圆角 ——
* 与上面那些"只是文字"的行形成对比。
* ⚠️ 点击区域是**整行**(不是只有号码文字):手指在手机上点单行文字
* 命中率很低,"点歪了没反应"会被当成 bug。
*/
@Builder
callRow(phone: string) {
Row({ space: 10 }) {
Image($r('app.media.fb_ic_user'))
.width(16)
.height(16)
.colorFilter(fbTint(this.brand()))
Text('电话')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.faint)
.width(52)
Text(phone)
.fontSize(FbMetric.fontSmall)
.fontColor(this.brand())
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
/* ⚠️ 右侧不挂图标:本工程的 `media/` 里**没有电话图标**
(只有 fb_ic_user / notes / people / days / share 这一批)。
与其为了一个小提示去加一张新图(还要跟着主题染两份色),
不如用一枚文字胶囊 —— 它更直白:"这里可以拨打"。 */
Text('拨打')
.fontSize(10)
.fontColor(FbTint.white)
.backgroundColor(this.brand())
.borderRadius(FbMetric.radiusPill)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
}
.width('100%')
.padding({ left: 8, right: 8, top: 8, bottom: 8 })
.backgroundColor(fbAlpha(this.brand(), 0.08))
.borderRadius(FbMetric.radiusS)
.alignItems(VerticalAlign.Center)
.onClick((): void => {
this.onCall(phone);
})
}
/**
* 一个动作按钮。
*
* ⚠️ `fg`/`bg` 必须是 **hex 字符串**(不是 `Resource`):
* 这里要拿它做**颜色运算**(`bg + '1A'` 加透明度后缀),
* `Resource` 参与不了字符串拼接。红色那个因此用与
* `color.json` 的 `red` **同值**的字面量(见 `RED`)。
* 这里要拿它做**颜色运算**(`fbAlpha(bg, 0.10)` 加透明度后缀),
* `Resource` 参与不了字符串拼接。
* ⚠️ 四个按钮等宽平分(`layoutWeight(1)`)—— 等宽是最不容易出错、
* 也最像"选择"的排法。
*/
@Builder
actionBtn(label: string, fg: string, bg: string, fire: () => void) {
@@ -203,108 +299,116 @@ export struct FbPersonQuickSheet {
build() {
Column() {
Column({ space: 14 }) {
// ---- 头像 + 姓名 + 称谓 ----
Row({ space: 14 }) {
FbAvatar({
name: this.person !== null ? this.person.name : '',
color: this.person !== null ? this.person.avatarColor : '#A84A28',
dia: 54,
fontSizeVp: 22,
imageUrl: this.avatarSrc()
})
Column({ space: 5 }) {
Row({ space: 8 }) {
Text(this.title())
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.maxLines(1)
.flexShrink(1)
if (this.person !== null && this.person.label !== '') {
FbTag({ text: this.person.label, tone: 'muted' })
/* 🔴 ② 主题色描边环(用户 2026-10-18 点名要的那一圈)
⚠️ 它是"外层铺色 + 内边距",不是 `border` —— 见文件头说明。 */
Column() {
Column({ space: 14 }) {
// ---- 头像 + 姓名 + 称谓 ----
Row({ space: 14 }) {
FbAvatar({
name: this.person !== null ? this.person.name : '',
color: this.person !== null ? this.person.avatarColor : '#A84A28',
dia: 54,
fontSizeVp: 22,
imageUrl: this.avatarSrc()
})
Column({ space: 5 }) {
Row({ space: 8 }) {
Text(this.title())
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.maxLines(1)
.flexShrink(1)
if (this.person !== null && this.person.label !== '') {
FbTag({ text: this.person.label, tone: 'muted' })
}
}
.alignItems(VerticalAlign.Center)
.width('100%')
if (this.person !== null && this.person.name !== this.title()) {
Text('本名 ' + this.person.name)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
}
}
.alignItems(VerticalAlign.Center)
.width('100%')
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.flexShrink(1)
}
.width('100%')
.alignItems(VerticalAlign.Center)
if (this.person !== null && this.person.name !== this.title()) {
Text('本名 ' + this.person.name)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
// ---- 简要信息(电话 / 邮箱 / 单位 / 生日;有才显示)----
//
// 🔴 用户 2026-10-18:「电话和单位什么的距离太近了,再拉远一点。」
// ⇒ `INFO_GAP`(16)+ 每行自己的上下 3vp 内边距。
Column({ space: INFO_GAP }) {
if (this.mainPhone() !== '') {
// ③ 主电话:**可点即拨**(其余电话仍只是文字,避免误触)
this.callRow(this.mainPhone())
}
if (this.otherPhones() !== '') {
this.infoRow($r('app.media.fb_ic_user'), '其他', this.otherPhones())
}
if (this.person !== null && this.person.email !== '') {
this.infoRow($r('app.media.fb_ic_notes'), '邮箱',
this.person !== null ? this.person.email : '')
}
if (this.jobText() !== '') {
this.infoRow($r('app.media.fb_ic_people'), '单位', this.jobText())
}
if (this.birthdayText() !== '') {
this.infoRow($r('app.media.fb_ic_days'), '生日', this.birthdayText())
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.flexShrink(1)
}
.width('100%')
.alignItems(VerticalAlign.Center)
// ---- 简要信息(电话 / 邮箱 / 单位 / 生日;有才显示)----
Column({ space: 10 }) {
if (this.mainPhone() !== '') {
this.infoRow($r('app.media.fb_ic_user'), '电话', this.mainPhone())
}
if (this.otherPhones() !== '') {
this.infoRow($r('app.media.fb_ic_user'), '其他', this.otherPhones())
}
if (this.person !== null && this.person.email !== '') {
this.infoRow($r('app.media.fb_ic_notes'), '邮箱',
this.person !== null ? this.person.email : '')
}
if (this.jobText() !== '') {
this.infoRow($r('app.media.fb_ic_people'), '单位', this.jobText())
}
if (this.birthdayText() !== '') {
this.infoRow($r('app.media.fb_ic_days'), '生日', this.birthdayText())
}
}
.width('100%')
// ---- 三个动作:详情 / 编辑 / 删除 ----
//
// ⚠️ 「删除」用红色(语义色),其余两个是主题色的深浅两档:
// 同一组按钮里,"哪个是破坏性的"必须一眼看出来。
Row({ space: 10 }) {
this.actionBtn('详情', FbTint.white, this.brand(), (): void => {
this.onDetail();
})
this.actionBtn('编辑', this.brand(), this.brand() + '1A', (): void => {
this.onEdit();
})
this.actionBtn('删除', RED, RED + '1A', (): void => {
this.onDelete();
})
}
.width('100%')
// ---- 关闭 ----
Text('关闭')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.muted)
.textAlign(TextAlign.Center)
.width('100%')
.height(38)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.onClick((): void => {
this.onClose();
})
// ---- 四个动作:详情 / 编辑 / 分享 / 关闭 ----
//
// 🔴 「删除」**不在这里**(用户 2026-10-18:太容易误触)——
// 它挪到了完整详情卡片的最下面(`FbPersonSheet` 的 `onDelete`)。
// ⚠️ 「关闭」也做成**同等大小的按钮**(而不是一行文字链接):
// 四个动作同一个视觉权重,用户不用在"哪个是按钮"上犹豫。
// 它是灰色(`inputBg` + 次要字色)—— 语义上"这一下什么都不改"。
Row({ space: 8 }) {
this.actionBtn('详情', FbTint.white, this.brand(), (): void => {
this.onDetail();
})
this.actionBtn('编辑', this.brand(), fbAlpha(this.brand(), 0.10), (): void => {
this.onEdit();
})
this.actionBtn('分享', this.brand(), fbAlpha(this.brand(), 0.10), (): void => {
this.onShare();
})
this.actionBtn('关闭', FbTint.muted, INPUT_BG, (): void => {
this.onClose();
})
}
.width('100%')
}
/* 🔴 卡片本体:见文件头「三层分离」的说明。
⚠️ 宽度 **100%**(不是 92%):92% 由**外面那圈环**负责,
这里再写 92% 就成了 0.92×0.92 ≈ 84.6%,卡片会比详情卡窄一圈。
⚠️ 四角圆角 + 居中 —— 与 `FbPersonSheet` 同一套观感
(用户 2026-10-16 定:"圆角浮窗式、不占满宽、浮在屏幕中间")。 */
.width('100%')
.constraintSize({ maxHeight: '72%' })
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
.backgroundColor(FbColors.surface)
/* ⚠️ 内层圆角要**比外层小一档**(14 vs 16):两圈同心圆角差值
= 环宽 2,否则环在内侧会显厚、外侧会显薄(看着像描边歪了)。 */
.borderRadius(FbMetric.radius)
/* ③ 阴影:把卡片从"被压暗的列表"上抬起来 */
.shadow({ radius: CARD_SHADOW, color: FB_SHADOW, offsetX: 0, offsetY: 6 })
.alignItems(HorizontalAlign.Start)
}
/* 🔴 卡片本体:见文件头「三层分离」的说明。
⚠️ `width('92%')` + 四角圆角 + 居中 —— 与 `FbPersonSheet` 同一套观感
(用户 2026-10-16 定:"圆角浮窗式、不占满宽、浮在屏幕中间")。 */
.width('92%')
.constraintSize({ maxHeight: '72%' })
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
.backgroundColor(FbColors.surface)
.borderRadius(FbMetric.radius)
/* ② 阴影:把卡片从"被压暗的列表"上抬起来 */
.shadow({ radius: CARD_SHADOW, color: '#00000033', offsetX: 0, offsetY: 6 })
/* ③ 极浅描边:同色背景上仍有一条可见界线(只靠阴影在浅底上不够) */
.border({ width: 1, color: '#00000014' })
.alignItems(HorizontalAlign.Start)
.padding(RING_W)
.backgroundColor(this.brand())
.borderRadius(FbMetric.radius + RING_W)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
@@ -313,4 +417,4 @@ export struct FbPersonQuickSheet {
不设的话这一层会把空白处也吃掉,点空白关不掉(很别扭)。 */
.hitTestBehavior(HitTestMode.Transparent)
}
}
}
@@ -24,7 +24,7 @@
* 只是缺那几项详情专属字段,界面不会空。
*/
import { FbColors, FbMetric, fbTint, FbTint } from '../common/Theme';
import { FbColors, FbMetric, fbTint, FbTint, FB_HAIRLINE, FB_SHADOW, fbAlpha } from '../common/Theme';
import { FbRosterPerson, FbJobRow, FbContactRow } from '../data/FbPersonRepo';
import { FbAvatar, FbTag } from './Widgets';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
@@ -39,6 +39,15 @@ import { fbImageSrc } from '../data/FbAttachQueue';
*/
const FB_SHEET_BOTTOM_GAP: number = 16;
/**
* 「删除」用的红 —— 与 `resources/base/element/color.json` 的 `red` **同值**。
*
* ⚠️ 不能直接用 `FbColors.red`(那是 `Resource`):本文件要拿它拼
* `fbAlpha(RED, 0.08)`(透明度后缀)做底色,`Resource` 拼不了字符串。
* ⚠️ color.json 改色时这里要同步改(只有这一个值,风险可控)。
*/
const RED: string = '#C0533F';
@ComponentV2
export struct FbPersonSheet {
@@ -56,6 +65,33 @@ export struct FbPersonSheet {
*/
@Event onEdit: () => void = (): void => {
};
/**
* 「删除」回调。
*
* 🔴🔴 用户 2026-10-18 原话:
* 「点击按钮后弹出这个简单框,不要加删除按钮了,删除按钮太容易误触。
* 应该在**详情页面的最下面**加一个删除按钮。」
*
* ⇒ 「删除」从"简要卡"(`FbPersonQuickSheet`)挪到了这里,并且放在
* **整张卡的最下面**、与编辑/关闭之间隔一条分隔线 ——
* 这是"危险区"的通用做法:多一层遮挡、手指不会顺手扫到。
*
* ⚠️ 与别的事件一样:**只发事件**。二次确认与调接口都在页面侧
* (那里才知道"删完怎么刷新列表")。
*/
@Event onDelete: () => void = (): void => {
};
/**
* 是否显示「删除」按钮。
*
* ⚠️ 顶栏那个「我的详情」(`TopBar.myDetailPanel`)要传 `false`:
* 删除自己既没意义、又很危险(服务端是物理删除,删完连账号都进不去)。
* 默认 `true` = 名册里的详情卡(那里的删除是正常的)。
*/
@Param canDelete: boolean = true;
/**
* 嵌入模式:宿主**自己已经是浮层**(如 `bindSheet`)时为 true。
*
@@ -481,6 +517,41 @@ export struct FbPersonSheet {
}
.width('100%')
.margin({ top: 18 })
/* ---- 危险区:删除联系人 ----
*
* 🔴 用户 2026-10-18:「应该在详情页面的最下面加一个删除按钮。」
* ⇒ 位置在**编辑/关闭之下**、并且与它们之间隔一条分隔线:
* 手指从上往下扫时不会顺手点到;想删必须**再往下够**一次。
* ⚠️ 视觉上只给文字 + 图标(不做成大红块):
* 它是"偶尔用一次"的操作,大红按钮会抢走"编辑"的注意力。
* ⚠️ 确认框在页面侧(`FbPeoplePage.confirmDelete`)—— 见 `onDelete` 的说明。 */
if (this.canDelete) {
Divider()
.color(FbColors.line)
.margin({ top: 14 })
Row({ space: 6 }) {
Image($r('app.media.fb_ic_trash'))
.width(14)
.height(14)
/* ⚠️ PNG 图标必须 colorFilter(fillColor 只对 SVG 生效) */
.colorFilter(fbTint(RED))
Text('删除联系人')
.fontSize(FbMetric.fontSmall)
.fontColor(RED)
.fontWeight(FontWeight.Medium)
}
.width('100%')
.height(42)
.justifyContent(FlexAlign.Center)
.backgroundColor(fbAlpha(RED, 0.08))
.borderRadius(FbMetric.radiusS)
.margin({ top: 10 })
.onClick((): void => {
this.onDelete();
})
}
}
// 🔴 用户 2026-10-16:「它应该弹出一个**圆角的、浮在上面的卡片**……
// **不要让它左右充满整个屏幕,下面也不要挨着屏幕**,
@@ -512,8 +583,8 @@ export struct FbPersonSheet {
* ⚠️ 只加阴影是不够的:浅色底上的投影很淡,真机上常常看不出来;
* 描边才是"稳"的那一手(它不依赖光照假设)。
*/
.shadow({ radius: 24, color: '#00000033', offsetX: 0, offsetY: 6 })
.border({ width: 1, color: '#00000014' })
.shadow({ radius: 24, color: FB_SHADOW, offsetX: 0, offsetY: 6 })
.border({ width: 1, color: FB_HAIRLINE })
.alignItems(HorizontalAlign.Start)
}
/* 🔴 embedded(宿主自己就是 sheet)时**不能再铺满 + 居中**:
+4
View File
@@ -413,6 +413,10 @@ export struct FbTopBar {
`AceDrag: Drag gesture has been canceled` —— 起来立刻被关**,
界面上看就是"点了没反应"(极易误判成"点击没接上")。 */
embedded: true,
/* 🔴 不给「删除」:这是"我自己的详情"。
删除自己既没意义又危险(服务端是物理删除,删完账号都进不去)。
名册里的详情卡才显示删除(那里 `canDelete` 取默认值 true)。 */
canDelete: false,
onClose: (): void => {
this.myDetailSheet = false;
},