联系人详情卡:点手机号直接拨号 (#89)

用户:点开以后的弹窗,点击手机号可以直接拨打电话。

事实核查:详情卡一直没有这个能力。手机号是一行纯 Text
(this.infoRow("phone", ...) 无 onClick)。而 FbPersonSheet 文件头
注释里写着它"自带手机号拨号入口"—— 那句是错的,实测点了没反应。

对照:名册的简要卡(FbPersonQuickSheet)v59e 就做了可点即拨,
详情卡一直缺 —— 同一份数据两处体验不一致。

① FbPersonSheet 加 @Event onCall,默认实现为空
   (TopBar.myDetailPanel 等宿主没接时仍可点、只是不拨,不崩不误触发)。
   卡片**不自己拨号**:call.makeCall 需要 UIAbilityContext,
   组件自己拿容易在浮层销毁瞬间拿到 undefined。

② 手机号那一行改成 phoneRow(不复用 infoRow)
   纯展示行加 onClick 会出现"整行看着一样、只有手机号能点",
   用户不知道点哪。新行给明确交互外观:整行浅主题底 + 号码主题色加粗
   + 右侧「拨打」胶囊,与 FbPersonQuickSheet.phoneRow 同一套语言。
   补充电话(逗号拼接的多个号)仍是普通展示行 —— 没法一次拨、也不该猜拨哪个。

③ 两个宿主接上:FbHomePage 新增 dial()、FbPeoplePage 复用已有 dial()。
   实现刻意与名册页一致(不另写一套),并抄上关键约束:
   必须 call.makeCall 而非 startAbility(tel:) —— 后者是隐式 Intent 会弹
   「选择打开方式」(2026-10-15 真机误点进"同步日历");上下文必须是
   UIAbilityContext;call.makeCall 不需要权限、只填号不代拨。

验证(真机 5NC0225B27004718,详情卡「爸爸/杨培振」):
- UI 树:手机号[208,772]、号码 17706392406[208,818]、拨打胶囊[837,795];
- 点号码文字(342,842) → 拉起拨号盘 177 0639 2406 / 山东青岛 电信 / 老爸;
- 点「拨打」胶囊(893,819) → 同样拉到该号码;
- 全程**没有**「选择打开方式」框,证明走的是 call.makeCall 而非 tel: 隐式 Intent;
- 返回 App 后详情卡状态保持。
hvigorw assembleHap BUILD SUCCESSFUL。
This commit is contained in:
2026-11-06 11:47:47 +08:00
parent 6a5d351e79
commit 8f38cfa98b
3 changed files with 130 additions and 2 deletions
@@ -90,7 +90,27 @@ export struct FbPersonSheet {
};
/**
* 是否显示「删除」按钮。
* 点手机号 → 请宿主去拨号。
*
* 🔴 用户 2026-11-06:「点开以后的弹窗,点击手机号可以直接拨打电话。」
* ⇒ 首页「联系人」点开的就是这张卡,而它的手机号原来只是一行 `Text`,
* 点了没任何反应。名册页的简要卡(`FbPersonQuickSheet`)早有这个能力,
* 详情卡一直缺 —— 两处体验不一致。
*
* ⚠️ 与 `onCall` 同口径:卡片**不自己拨号**。`call.makeCall()` 需要
* `UIAbilityContext`,而 `getUIContext().getHostContext()` 只有页面侧
* 才拿得稳;组件自己拿容易在浮层销毁的瞬间拿到 undefined。
* ⇒ 只把号码发出来,真正的拨号由宿主调它自己的 `dial()`。
* (名册页 `FbPeoplePage.dial()` 里有完整说明:必须用 `call.makeCall`
* 而不是 `tel:` 隐式 Intent,否则弹「选择打开方式」。)
*
* ⚠️ 默认实现是**空**:`TopBar.myDetailPanel` 等宿主没接这个事件时,
* 号码仍然显示、仍然可点,只是点了不拨 —— 不会崩、也不会误触发。
*/
@Event onCall: (phone: string) => void = (phone: string): void => {
};
/** 是否显示「删除」按钮。
*
* ⚠️ 顶栏那个「我的详情」(`TopBar.myDetailPanel`)要传 `false`:
* 删除自己既没意义、又很危险(服务端是物理删除,删完连账号都进不去)。
@@ -332,6 +352,63 @@ export struct FbPersonSheet {
return start + ' ~ ' + end;
}
/**
* 手机号那一行(**可点即拨**)。
*
* 🔴 用户 2026-11-06:「点开以后的弹窗,点击手机号可以直接拨打电话。」
*
* ⚠️ 为什么不复用 `infoRow`:那是个纯展示行,加上 `onClick` 以后会出现
* "整行看着一样、只有手机号这一行能点"的情况,用户不知道该点哪。
* 这里给它**明确的交互外观**:号码用主题色、右侧一枚「拨打」胶囊、
* 整行浅色底 —— 与名册页简要卡(`FbPersonQuickSheet.phoneRow`)同一套语言,
* 两处点到同一个号码时观感一致。
* ⚠️ 补充电话(`phones[1..]`)是**逗号拼接**的一串,没法一次拨多个号,
* 所以那一行仍用普通 `infoRow`,只把主号做成可点。
*/
@Builder
phoneRow(phone: string) {
Row({ space: 10 }) {
Image(this.iconOf('phone'))
.width(15)
.height(15)
.renderMode(ImageRenderMode.Template)
.colorFilter(fbTint(FbTint.faint))
Column({ space: 2 }) {
Text('手机号')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
Text(phone)
.fontSize(FbMetric.fontSmall)
.fontColor(this.accentColor())
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
/* ⚠️ 不挂图标:本工程 `media/` 里没有电话图标(只有 user/notes/people/days/share
那一批)。为一枚小提示去加新图(还得跟主题染两份色)不划算,
文字胶囊更直白:"这里能拨"。 */
Text('拨打')
.fontSize(10)
.fontColor(FbTint.white)
.backgroundColor(this.accentColor())
.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.accentColor(), 0.08))
.borderRadius(FbMetric.radiusS)
.alignItems(VerticalAlign.Center)
.margin({ top: 4, bottom: 4 })
.onClick((): void => {
/* ⚠️ 只发事件,不自己拨 —— 理由见 `onCall` 的说明 */
this.onCall(phone);
})
}
/**
* 信息行(图标 + 标签 + 值)。
*
@@ -487,10 +564,15 @@ export struct FbPersonSheet {
// ⚠️ 每条都**只在有值时显示** —— 空行会让浮层显得"很多没填",
// 而实际上大部分人的手机/邮箱/地址本来就是空的。
if (this.person !== null && this.person.phone !== '') {
this.infoRow('phone', '手机号', this.person.phone)
/* 🔴 2026-11-06:**主号改成可点即拨**(用户要求)。
原来这里是 `this.infoRow('phone', '手机号', this.person.phone)` ——
一行纯文字,点了没有任何反应。 */
this.phoneRow(this.person.phone)
}
// 🔴 补充电话(服务端下发的 `phones`,主号已在上面单独显示)
if (this.person !== null && this.person.phones.length > 1) {
/* ⚠️ 这一行是**逗号拼接的多个号**,没法一次拨(也不该猜用户想拨哪个),
所以保持普通展示行、不做成可点。 */
this.infoRow('phone', '其他电话', this.otherPhones())
}
if (this.person !== null && this.person.org !== '') {
@@ -43,6 +43,9 @@ import { FbCalEvent, FbCalBook, FbCatOpt, FbHomeData, FbMark, FbMemoryItem, FbRe
FbTaskItem, FbBoardOpt, FbTaskDetail, FbTaskMeta, taskKey,
FbWeather, isEditableEvent, isEventWritable } from '../../model/Types';
import { common } from '@kit.AbilityKit';
/* 🔴 2026-11-06:首页联系人详情要能**点号码直接拨**(用户要求),
与名册页 `FbPeoplePage` 同一个系统 API。 */
import { call } from '@kit.TelephonyKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { parseHome } from '../../data/FbParse';
@@ -734,6 +737,35 @@ export struct FbHomePage {
this.pfDetail = null;
}
/**
* 拨号(点联系人详情里的手机号时)。
*
* 🔴 用户 2026-11-06:「点开以后的弹窗,点击手机号可以直接拨打电话。」
*
* ⚠️ 与名册页 `FbPeoplePage.dial()` **同一实现**,别在这里另写一套:
* 两处拨同一个号码,行为必须一致(弹不弹选择框、是否填到拨号盘)。
*
* ⚠️ 必须用 `call.makeCall(ctx, 号码)`,**不是** `startAbility(dial + tel: URI)`:
* 后者是隐式 Intent —— 系统里凡声明该 action 的应用都会成为候选,
* 于是弹「选择打开方式」框(2026-10-15 真机实测就误点进"同步日历");
* 而且各厂商对 `tel:` 处理不一致(有的拉拨号盘、有的直接拨)。
* `call.makeCall` 是系统直达 API,无应用选择框、跨机型一致、**不需要权限**。
* ⚠️ 它仍只是"打开拨号盘并填好号码",最后一下由用户按 —— 合规要求满足。
*
* ⚠️ 上下文必须是 **`UIAbilityContext`**(`startAbility`/`makeCall` 只在这个类型上);
* 用 `common.Context` 会编译报 "Property ... does not exist"。
*/
private dial(phone: string, who: string): void {
const ctx: common.UIAbilityContext | undefined =
this.getUIContext().getHostContext() as common.UIAbilityContext | undefined;
if (ctx === undefined) {
return;
}
call.makeCall(ctx, phone).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '拨号失败(%{public}s):%{public}s', who, JSON.stringify(err));
});
}
/**
* 这条纪念日是不是「派生生日」(不能编辑的那种)。
*
@@ -2293,6 +2325,12 @@ export struct FbHomePage {
与其在这里硬塞一份,不如交给名册(那是它的主场)。 */
this.closePersonDetail();
this.app.switchModule('people');
},
/* 🔴 用户 2026-11-06:「点开以后的弹窗,点击手机号可以直接拨打电话。」
⚠️ 卡片只发号码,真正拨号走本页 `dial()`(理由见那里的注释)。 */
onCall: (phone: string): void => {
const who: string = this.pfDetail !== null ? this.pfDetail.shown : '';
this.dial(phone, who);
}
})
.transition(fbSheetTransition())
@@ -618,6 +618,14 @@ export struct FbPeoplePage {
⚠️ 走的还是同一个 `confirmDelete()`(二次确认 + 调接口 + 刷新名册)。 */
onDelete: (): void => {
this.confirmDelete();
},
/* 🔴 用户 2026-11-06:「点开以后的弹窗,点击手机号可以直接拨打电话。」
⇒ 详情卡补上 `onCall`。这张卡原来只有纯文字,点了没反应;
简要卡(`FbPersonQuickSheet`)早就能拨 —— 两处不一致。
⚠️ 复用本页已有的 `dial()`(`call.makeCall`,见那里的长注释)。 */
onCall: (phone: string): void => {
const who: FbRosterPerson | null = this.detailPerson;
this.dial(phone, who !== null ? who.shown : '');
}
})
/* 🔴 用户 2026-10-19:「点击详情,框变大了,也应该有一个过渡动画。」