Files
FamilyBook_harmonyos/entry/src/main/ets/common/FbShare.ets
T
laoyang 7858de27e3 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 个模块全报"抽屉里没找到"(像应用坏了,其实是探针过期)。
- 资源/笔记两页写死了早已不存在的条目(户口本/房产证/阳台改造清单)⇒ 改成
  结构性文案(副标题 + 常驻筛选胶囊),不随用户数据变化而恒红。
2026-10-18 23:12:57 +08:00

138 lines
5.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 家书 FamilyBook · 「把一个人分享出去」(鸿蒙端)
*
* 🔴 用户 2026-10-18 原话:
* 「再增加一个功能,就是下面有一个按钮,详情、编辑、关闭,还有分享。
* 分享这个可以选择,比如说微信或什么的,以文本的形式,姓名、电话、邮箱,
* 这几个以文本的形式分享给别人。」
*
* 为什么用**系统分享面板**(Share Kit)而不是自己做「复制 / 发微信」:
* · 用户要的是"可以选择,比如说微信或什么的" —— 那正是系统分享面板的职责,
* 它会把手机上**所有认领了纯文本**的应用都列出来(微信 / 短信 / 邮件 / 备忘录…)。
* · 自己去拉起微信 = 依赖微信的包名与应用跳转白名单,换台手机就失效;
* 而且要维护"哪些应用在装"的判断,纯属自找。
*
* ⚠️ 分享内容是**纯文本**(`general.plain-text`),不是 vCard:
* 用户说得很清楚"以文本的形式,姓名、电话、邮箱"。vCard(.vcf)虽然
* "更像通讯录",但很多聊天窗口里发出去只是一张卡片,反而看不见内容。
* ⚠️ Share Kit 在**没有 Huawei 分享服务**的设备上可能不可用(`systemShare`
* 的 syscap 是 `SystemCapability.Collaboration.SystemShare`)
* ⇒ 必须兜一层**剪贴板**:弹不出面板时把同一段文本复制走并告诉用户,
* "分享没反应"是最让人恼火的一种失败。
*/
import { systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor } from '@kit.ArkData';
import { pasteboard } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN: number = 0x0000;
const TAG: string = 'FbShare';
/**
* 分享一个人需要的字段。
*
* ⚠️ 只收**纯字符串**(不收 `FbRosterPerson`):
* 这个工具被"简要卡"和"详情卡"两处用,两处的数据类型不同(一个来自列表、
* 一个来自 `/people/<id>`),让它们各自拼好再传进来最省事,
* 也避免 `common/` 反向依赖 `data/`(本项目踩过循环 import)。
*/
export interface FbSharePerson {
/** 显示名(昵称优先) */
name: string;
/** 电话(主号在前) */
phones: string[];
/** 邮箱('' = 不显示) */
email: string;
}
/**
* 拼分享文本:**姓名 / 电话 / 邮箱** —— 用户点名的就是这三项。
*
* 例:
* ```
* 杨永全
* 电话:13800000000 / 0532-88888888
* 邮箱:yyq@example.com
* ```
* ⚠️ 第一行**只放名字**(不带"姓名:"前缀):在微信里发出去,第一行就是
* "这个人是谁",比"姓名:杨永全"更像一条通讯录信息。
* ⚠️ 空项**整行不出现**:`邮箱:` 后面空着会让对方以为漏了。
* ⚠️ **不要"顺手多给两行"**(单位 / 职务 / 生日…):用户原话只列了
* 「姓名、电话、邮箱,这几个」。这是一段会被发到第三方应用里的文本,
* 多写的每一个字段都是用户没想到会外发的信息。
*/
export function fbPersonShareText(p: FbSharePerson): string {
const lines: string[] = [];
lines.push(p.name);
if (p.phones.length > 0) {
const out: string[] = [];
for (const s of p.phones) {
if (s !== '') {
out.push(s);
}
}
if (out.length > 0) {
lines.push('电话:' + out.join(' / '));
}
}
if (p.email !== '') {
lines.push('邮箱:' + p.email);
}
return lines.join('\n');
}
/**
* 拉起系统分享面板。
*
* @returns `''` = 已拉起(用户看到面板了);非空 = **没能拉起**,
* 返回的是给用户看的一句话(此时内容已复制到剪贴板)。
*
* ⚠️ 为什么要返回"一句话"而不是布尔:失败时用户必须知道"内容去哪了",
* 只说"失败"他会以为白点了。
*/
export async function fbShareText(ctx: common.UIAbilityContext,
text: string, title: string): Promise<string> {
try {
const record: systemShare.SharedRecord = {
utd: uniformTypeDescriptor.UniformDataType.PLAIN_TEXT,
content: text,
title: title,
label: 'text',
description: '',
};
const data: systemShare.SharedData = new systemShare.SharedData(record);
const controller: systemShare.ShareController = new systemShare.ShareController(data);
await controller.show(ctx, {
selectionMode: systemShare.SelectionMode.SINGLE,
// 纯文本推荐用缩略卡(DETAIL 那张大预览图是给图片/视频的)
previewMode: systemShare.SharePreviewMode.DEFAULT,
});
return '';
} catch (err) {
const e: BusinessError = err as BusinessError;
hilog.warn(DOMAIN, TAG, '系统分享不可用,退回剪贴板:%{public}s',
e.message !== undefined ? e.message : JSON.stringify(err));
return await fbCopyText(text);
}
}
/**
* 兜底:复制到剪贴板。
*
* @returns `''` = 复制成功;非空 = 失败提示
*/
export async function fbCopyText(text: string): Promise<string> {
try {
const pd: pasteboard.PasteData =
pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
await pasteboard.getSystemPasteboard().setData(pd);
return '已复制到剪贴板,可以粘贴到任何聊天窗口';
} catch (err) {
hilog.error(DOMAIN, TAG, '复制到剪贴板失败:%{public}s', JSON.stringify(err));
return '分享失败(本机没有可用的分享服务)';
}
}