鸿蒙原生ArkTS通讯录管家实战开发详解
文章摘要: 《鸿蒙原生ArkTS通讯录管家实战开发详解》介绍了一款基于鸿蒙系统的轻量级通讯录管理应用。项目采用ArkTS语言开发,实现了联系人增删改查、分组管理、生日提醒等完整功能。技术亮点包括:利用关系型数据库实现本地持久化、声明式UI响应式特性、全局Builder函数设计模式等。应用采用三层架构设计,包含数据层、业务层和UI层,核心代码集中在单个文件中便于维护。文章详细解析了数据模型设计、配色
鸿蒙原生ArkTS通讯录管家实战开发详解
一、项目概述
1.1 项目背景
在移动互联网时代,通讯录管理是每一部智能手机最基础也最核心的功能之一。从最简单的联系人与手机号映射,到支持分组管理、生日提醒、收藏标记、快速拨号,用户对通讯录应用的需求随着使用场景的丰富而不断升级。然而,系统自带的通讯录应用往往功能固化、交互形式单一,难以满足特定场景下的个性化需求。
正是在这一背景下,通讯录管家(AddressBook) 应运而生。这是一款基于鸿蒙原生系统、采用ArkTS语言开发的轻量级通讯录管理应用,致力于为用户提供简洁、高效、功能完整的联系人管理体验。应用充分利用了鸿蒙系统的关系型数据库(RDB)能力和声明式UI框架的响应式特性,在不到1200行代码的体量内,实现了涵盖联系人增删改查、分组管理、搜索过滤、生日提醒、收藏标记、一键拨号等完整功能。
1.2 技术亮点
通讯录管家项目虽然体量不大,但在技术上有多个值得关注的亮点:
- 关系型数据库本地持久化:基于
@ohos.data.relationalStore模块实现了完整的SQLite数据库操作,支持建表、增删改查、分组统计和条件筛选。 - 单向数据流与响应式UI:ArkTS的
@State装饰器与全局Builder函数的组合使用,构建了清晰的数据流动链路。 - 全局Builder函数设计模式:所有UI组件均定义为全局
@Builder函数而非组件内方法,通过参数回调彻底解除了对this的依赖,避免了ArkTS对Function.bind的限制。 - 交互式表单校验:输入验证与实时错误提示联动,用户在提交前即可获得明确的修正指引。
- 侧滑操作栏:通过
Swiper组件实现联系人卡片的侧滑操作,提供收藏、编辑、拨号、删除四个快捷入口。
1.3 目标用户
通讯录管家面向的用户群体非常广泛:
- 普通手机用户:需要一个轻量、清爽的通讯录管理工具,替代系统自带应用完成日常的联系人管理。
- 商务人士:需要通过分组功能管理客户、同事关系,利用生日提醒功能维护重要的人际关系。
- 鸿蒙应用学习者:希望通过一个完整的实战项目学习ArkTS的声明式UI开发、关系型数据库操作和组件化设计模式。
- 开发者参考:需要快速了解鸿蒙RDB API与ArkTS Builder函数结合使用的开发范式的技术开发者。
二、项目架构总览
2.1 整体架构
通讯录管家采用"数据层-业务层-UI层"三层架构,层次清晰、职责分明:
数据层(ContactDatabase):封装了所有与关系型数据库相关的操作,包括数据库初始化、联系人增删改查、分组统计、今日生日查询等。该层对外暴露异步方法(返回Promise),与上层完全解耦。
业务层(AddressBook主组件):管理应用的核心状态(联系人列表、分组信息、表单状态等),协调数据层与UI层的交互,处理用户操作事件(搜索、分组切换、表单提交、收藏切换、删除等)。
UI层(全局Builder函数):由10个独立的@Builder函数组成,每个函数负责一个独立的UI区域。所有函数通过参数接收数据和回调,不持有任何状态,做到纯粹的展示层职责。
2.2 文件结构
通讯录管家的核心代码完全集中在一个文件中——AddressBook.ets,文件结构如下(按代码顺序):
├── 接口定义(Contact、GroupInfo、Colors、SwipeActionData)
├── 常量定义(GROUPS分组、COLORS配色、AVATAR_COLORS头像色)
├── 工具函数(getAvatarColor)
├── ContactDatabase 类(数据库CRUD封装)
│ ├── init() → 初始化数据库
│ ├── getAllContacts() → 获取全部联系人
│ ├── getContactsByGroup() → 按分组获取联系人
│ ├── searchContacts() → 按关键字搜索
│ ├── addContact() → 添加联系人
│ ├── updateContact() → 更新联系人
│ ├── deleteContact() → 删除联系人
│ ├── getGroupStats() → 分组统计
│ ├── getTodayBirthdayContacts() → 今日生日查询
│ └── parseResultSet() → 结果集解析
├── 输入验证函数(validateName、validatePhone、validateBirthday)
├── AddressBook 主组件
│ └── build() → 页面组装
└── 全局 @Builder 函数(10个)
├── buildHeader()
├── buildSearchBar()
├── buildGroupTabs()
├── buildBirthdayBanner()
├── buildLoadingView()
├── buildEmptyView()
├── buildContactCard() / buildSwipeActions() / buildSwipeButton()
├── buildContactList()
├── buildFloatingButton()
├── buildErrorToast()
├── buildFormDialog()
└── buildDeleteConfirmDialog()
这种将完整应用写入单一文件的组织方式,在小体量项目中具有明显的优势:无需在多个文件间频繁跳转、所有状态和UI逻辑一目了然、降低了模块导入导出的心智负担。
三、数据模型与常量体系
3.1 Contact 接口
Contact是应用最核心的数据模型,定义了联系人的完整数据结构:
interface Contact {
id: number; // 数据库主键,自增
name: string; // 联系人姓名
phone: string; // 手机号
group: string; // 分组(家人/朋友/同事/同学/其他)
birthday: string; // 生日,格式为'MM-DD',空串表示未设置
isFavorite: number; // 是否收藏,值为0或1
remark: string; // 备注信息
}
这个模型的设计有几个值得关注的点:
- id使用number类型:鸿蒙RDB的自增主键返回的是number类型,与JavaScript的Number类型一致。
- birthday使用’MM-DD’格式:只存储月日而非完整日期。这是因为生日提醒只需要核对月日是否与当天匹配,不需要年份信息。这种设计既简化了存储,也避免了跨年问题。
- isFavorite使用number而非boolean:这是为了与SQLite的存储格式一致——SQLite没有原生的布尔类型,使用0/1整数表示布尔值是最通用的做法。
3.2 GroupInfo 接口
interface GroupInfo {
name: string; // 分组名称
count: number; // 该分组下的联系人数量
}
用于从数据库的分组统计查询中承载结果数据。
3.3 Colors 配色系统
通讯录管家的配色系统采用全局常量的方式统一定义,共13个颜色变量覆盖了应用的完整视觉需求:
const COLORS: Colors = {
primary: '#007AFF', // 主色调——iOS风格的蓝色
primaryLight: '#E8F2FF', // 主色浅色——用于标签背景
background: '#F2F4F8', // 页面背景——浅灰色
cardBg: '#FFFFFF', // 卡片背景——纯白
textPrimary: '#1A1A2E', // 主文字——深蓝黑
textSecondary: '#6B7280', // 次要文字——灰色
textTertiary: '#9CA3AF', // 辅助文字——浅灰色
headerBg: '#FFFFFF', // 头部背景——纯白
shadow: 'rgba(0, 0, 0, 0.08)', // 阴影色
danger: '#FF3B30', // 危险色——删除操作
success: '#34C759', // 成功色——拨号操作
warning: '#FF9500', // 警告色——收藏切换
birthday: '#FF6B9D', // 生日色——粉色横幅
};
这个配色方案采用了清新、明亮的蓝白基调,配合浅灰色页面背景和白色卡片,形成了典型的iOS风格视觉语言。关键操作按钮使用蓝、绿、橙、红等语义色,让用户通过颜色就能快速判断操作的类型。
3.4 头像颜色映射
const AVATAR_COLORS: Record<string, string> = {
'家人': '#FF6B9D', // 粉色——温馨
'朋友': '#34C759', // 绿色——活力
'同事': '#007AFF', // 蓝色——专业
'同学': '#FF9500', // 橙色——青春
'其他': '#AF52DE', // 紫色——中性
};
每个分组对应的头像颜色经过精心选择,符合该分组的视觉联想。例如家人使用温馨的粉色、同事使用专业的蓝色、朋友使用活力的绿色。颜色的差异化也帮助用户在浏览联系人列表时,通过头像色快速识别联系人所属的分组。
四、数据库层 ContactDatabase 深度解析
4.1 数据库初始化
class ContactDatabase {
private store: relationalStore.RdbStore | null = null;
private readonly DB_NAME: string = 'ContactManager.db';
private readonly TABLE_NAME: string = 'contacts';
async init(context: common.Context): Promise<void> {
if (this.store) return;
const config: relationalStore.StoreConfig = {
name: this.DB_NAME,
securityLevel: relationalStore.SecurityLevel.S1,
};
this.store = await relationalStore.getRdbStore(context, config);
await this.store.executeSql(this.CREATE_TABLE_SQL);
}
}
初始化过程做了两个关键操作:第一,通过getRdbStore获取或创建数据库实例;第二,执行建表SQL创建contacts表(如果表已存在则跳过)。securityLevel: S1表示安全等级为低级别,适用于不包含敏感个人数据的场景。
建表SQL如下:
CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT NOT NULL,
group_name TEXT DEFAULT '其他',
birthday TEXT DEFAULT '',
is_favorite INTEGER DEFAULT 0,
remark TEXT DEFAULT ''
)
这里使用了id INTEGER PRIMARY KEY AUTOINCREMENT,确保每条记录拥有唯一的自增主键。group_name字段默认值为’其他’,简化了添加联系人时的默认分组逻辑。
4.2 查询方法
数据库层提供了四种查询方法,覆盖了通讯录的核心查询场景:
获取全部联系人:getAllContacts()直接调用私有方法queryList(null),不传分组参数时查询所有记录并按姓名升序排列。
按分组获取联系人:getContactsByGroup(group)通过RdbPredicates的equalTo方法过滤分组字段。对于’全部’分组有特殊处理——不会添加过滤条件。
关键字搜索:searchContacts(keyword)是查询层最复杂的一个方法。它通过predicates.contains('name', keyword)和predicates.or().contains('phone', keyword)构建了一个"姓名或手机号包含关键字"的复合查询条件,实现了类似系统通讯录的搜索体验——输入姓名或手机号的任意部分都能匹配到对应的联系人。
今日生日查询:getTodayBirthdayContacts()使用JavaScript的Date对象获取当前月日,格式化为’MM-DD’字符串后,通过equalTo('birthday', monthDay)精确匹配数据库中的生日字段。这种方法简单直接,无需在SQL层面进行日期函数运算。
4.3 分组统计
async getGroupStats(): Promise<GroupInfo[]> {
this.ensureReady();
const sql: string = `SELECT group_name AS name, COUNT(*) AS count
FROM ${this.TABLE_NAME}
GROUP BY group_name ORDER BY group_name`;
const resultSet = await this.store!.querySql(sql);
// ...解析结果集...
// 补充 count 为 0 的分组
for (const g of GROUPS) {
if (g === '全部') continue;
let found: boolean = false;
for (const item of groups) {
if (item.name === g) { found = true; break; }
}
if (!found) {
groups.push({ name: g, count: 0 });
}
}
return groups;
}
这个方法使用SQL的GROUP BY语句进行分组计数。值得关注的是,它在数据库查询之后,还遍历了预定义的分组列表GROUPS,为没有任何联系人的分组补充count: 0的记录。这样确保UI层在展示分组标签时,每个分组都有对应的计数数据,不会出现"某个分组没有数据就不显示"的问题。
4.4 增删改操作
三种写操作的设计都遵循相同的模式:调用ensureReady()确保数据库已初始化→构造操作参数→执行对应方法→返回结果。
- addContact:构造
ValuesBucket对象,调用insert方法,返回新记录的自增ID。 - updateContact:使用
RdbPredicates按id定位记录,调用update方法批量更新所有字段。 - deleteContact:使用
RdbPredicates按id定位记录,调用delete方法删除。
这种"定位→操作"的范式是鸿蒙RDB的标准用法,与Android的SQLiteOpenHelper使用习惯类似,学习曲线平滑。
五、主组件 AddressBook 状态管理
5.1 状态变量设计
AddressBook主组件共定义了16个@State状态变量,管理着应用的全部运行时数据:
| 状态变量 | 类型 | 用途 |
|---|---|---|
| contactList | Contact[] | 当前显示的联系人列表 |
| groupStats | GroupInfo[] | 分组统计信息 |
| selectedGroup | string | 当前选中的分组 |
| searchText | string | 搜索输入框内容 |
| isLoading | boolean | 数据加载状态 |
| errorMsg | string | 错误提示信息 |
| showFormDialog | boolean | 表单对话框显示/隐藏 |
| isEditing | boolean | 当前是否为编辑模式 |
| editingContact | Contact/null | 正在编辑的联系人 |
| formName/Phone/… | string | 表单各字段值 |
| formNameError/… | string | 表单字段校验错误信息 |
| showDeleteConfirm | boolean | 删除确认对话框显示/隐藏 |
| deletingContact | Contact/null | 待删除的联系人 |
| birthdayContacts | Contact[] | 今日生日的联系人列表 |
| showBirthdayBanner | boolean | 生日横幅显示/隐藏 |
这16个状态变量的设计遵循了"最小状态原则"——每个变量只管理一个独立的变化维度,没有冗余或推导出的状态。例如,是否在编辑模式下通过isEditing布尔值管理,而不是通过editingContact !== null来推导。
5.2 数据加载流程
应用的启动数据加载由aboutToAppear生命周期方法触发:
aboutToAppear(): void {
const ctx: common.Context = getContext(this) as common.Context;
this.initDB(ctx);
}
async initDB(ctx: common.Context): Promise<void> {
this.isLoading = true;
this.errorMsg = '';
try {
await this.db.init(ctx);
await this.refreshView();
const todayBirthdays: Contact[] = await this.db.getTodayBirthdayContacts();
this.birthdayContacts = todayBirthdays;
this.showBirthdayBanner = todayBirthdays.length > 0;
} catch (err) {
this.errorMsg = '数据加载失败: ' + (err as BusinessError).message;
} finally {
this.isLoading = false;
}
}
加载流程分为三步:初始化数据库→加载联系人数据→检查今日生日。每一步都有完备的异常处理,任何失败都会通过errorMsg状态展示给用户,同时finally块确保加载状态被及时关闭。这种"先加载、后校验、终显示"的模式,在用户体验上表现为:用户打开应用时看到加载动画,随后迅速看到联系人列表,如果有今日寿星还会在列表中展示粉色的生日横幅。
5.3 数据刷新机制
refreshView方法是整个应用数据流动的核心枢纽:
async refreshView(): Promise<void> {
let contacts: Contact[];
if (this.searchText) {
contacts = await this.db.searchContacts(this.searchText);
} else {
contacts = await this.db.getContactsByGroup(this.selectedGroup);
}
this.contactList = contacts;
this.groupStats = await this.db.getGroupStats();
}
这个方法同时承担两个职责:加载联系人列表和刷新分组统计。联系人列表的加载策略根据搜索状态分为两条路径——有搜索关键字时走搜索查询,无关键字时按当前分组查询。每次contactList的更新都会触发ArkUI的响应式渲染,而groupStats的更新则驱动分组标签上的计数刷新。
六、UI 构建体系详解
6.1 页面整体布局
通讯录管家的主页面采用纵向滚动布局,从上到下依次排列10个UI区域:
Stack(层叠容器,承载所有UI层)
└── Column(主内容区,可滚动)
├── buildHeader() → 标题栏(联系人数量 + 分组名称 + 加载指示器)
├── buildSearchBar() → 搜索栏(搜索图标 + 输入框 + 清除按钮)
├── buildBirthdayBanner() → 生日横幅(条件渲染,有今日寿星时显示)
├── buildGroupTabs() → 分组标签(水平滚动标签栏,支持分组切换)
├── buildLoadingView() → 加载视图(数据加载中时显示)
├── buildEmptyView() → 空状态视图(列表为空时显示)
├── buildContactList() → 联系人列表(核心内容区)
├── buildFloatingButton() → 浮动按钮(右下角的"+"添加按钮)
├── buildErrorToast() → 错误提示(条件渲染)
├── buildFormDialog() → 添加/编辑表单(条件渲染 + Stack层叠)
└── buildDeleteConfirmDialog() → 删除确认(条件渲染 + Stack层叠)
这种布局方式有几个显著特点:
- Stack作为根容器:允许表单对话框和删除确认对话框覆盖在主内容区之上,同时通过半透明遮罩层实现模态效果。
- 条件渲染贯穿始终:生日横幅、加载视图、空状态、错误提示、表单对话框、删除确认对话框都是条件渲染的,只在对应的状态为true时才出现在UI树中。
- Flex布局权重管理:联系人列表使用
layoutWeight(1)自动填满剩余空间,确保不同屏幕尺寸下都能合理布局。
6.2 标题栏 buildHeader
标题栏采用Row + Column的组合布局,左侧显示应用名称和联系人统计信息,右侧显示加载指示器:
@Builder
function buildHeader(count: number, selectedGroup: string, loading: boolean) {
Row() {
Column() {
Text('📞 通讯录管家')
.fontSize(22).fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(String(count) + ' 位联系人 · ' + selectedGroup)
.fontSize(13).fontColor(COLORS.textSecondary)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
if (loading) {
LoadingProgress().width(24).height(24).color(COLORS.primary)
}
}
.width('100%')
.padding({ left: 20, right: 20, top: 48, bottom: 16 })
.backgroundColor(COLORS.headerBg)
}
这个Builder的设计体现了全局函数模式的核心思想:通过参数接收所有需要的数据(联系人数量、当前分组、加载状态),通过.layoutWeight(1)让左侧Column占据所有可用空间,右侧的加载指示器只有在加载状态为true时才渲染。这种"数据驱动视图"的理念贯穿整个应用。
6.3 搜索栏 buildSearchBar
搜索栏是典型的数据输入组件组合:
@Builder
function buildSearchBar(
searchText: string,
onChange: (val: string) => void,
onClear: () => void
) {
Row() {
Text('🔍').fontSize(16).margin({ left: 12, right: 8 })
TextInput({ placeholder: '搜索姓名或手机号...', text: searchText })
.layoutWeight(1).fontSize(14)
.fontColor(COLORS.textPrimary)
.placeholderColor(COLORS.textTertiary)
.backgroundColor(Color.Transparent).height(40)
.onChange((val: string) => onChange(val))
if (searchText.length > 0) {
Text('✕').fontSize(16).fontColor(COLORS.textTertiary)
.margin({ right: 12 })
.onClick(() => onClear())
}
}
.width('100%').height(44)
.backgroundColor(COLORS.cardBg).borderRadius(12)
.margin({ left: 16, right: 16, top: 12, bottom: 4 })
.shadow({ radius: 4, color: COLORS.shadow, offsetY: 2 })
}
这里有三个设计细节值得关注:第一,搜索图标使用Emoji字符而非图片资源,减少了项目体积;第二,清除按钮(✕)只有在输入内容不为空时才渲染,这是通过if (searchText.length > 0)条件渲染实现的;第三,输入框设置为透明背景,将圆角和阴影效果交给外层Row组件,实现了"圆角搜索条"的视觉效果。
6.4 分组标签 buildGroupTabs
分组标签栏使用Scroll + Row实现水平滚动的标签组:
@Builder
function buildGroupTabs(
groups: string[],
selectedGroup: string,
getCount: (group: string) => string,
onSelect: (group: string) => void
) {
Scroll() {
Row() {
ForEach(groups, (group: string) => {
Column() {
Text(group).fontSize(13)
.fontColor(selectedGroup === group ? COLORS.primary : COLORS.textSecondary)
.fontWeight(selectedGroup === group ? FontWeight.Medium : FontWeight.Regular)
if (group !== '全部') {
Text(getCount(group)).fontSize(10)
.fontColor(selectedGroup === group ? COLORS.primary : COLORS.textTertiary)
.margin({ top: 2 })
}
}
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.borderRadius(20)
.backgroundColor(selectedGroup === group ? COLORS.primaryLight : 'transparent')
.onClick(() => onSelect(group))
})
}
.padding({ left: 8, right: 8 })
}
.scrollBar(BarState.Off)
.width('100%')
}
这个组件的交互设计很细腻:选中的标签使用浅蓝色背景和蓝色文字,未选中的标签使用透明背景和灰色文字;每个分组标签下方显示该分组的联系人数量("全部"分组除外);通过Scroll容器支持标签水平滚动,当分组较多时不会挤压单个标签的空间。getCount回调函数由主组件传入,实现了从全局Builder函数到主组件状态的数据读取。
6.5 生日横幅 buildBirthdayBanner
生日横幅是应用中最具"温度感"的功能:
@Builder
function buildBirthdayBanner(
birthdayContacts: Contact[],
onDismiss: () => void
) {
Row() {
Text('🎂').fontSize(24).margin({ right: 12 })
Column() {
Text('🎉 今日寿星').fontSize(15)
.fontWeight(FontWeight.Medium).fontColor('#FFFFFF')
Text(birthdayContacts.map((c: Contact) => c.name).join('、')
+ ' — 送出生日祝福吧!')
.fontSize(12).fontColor('rgba(255,255,255,0.85)')
.margin({ top: 2 }).maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text('✕').fontSize(14).fontColor('rgba(255,255,255,0.7)')
.margin({ left: 8 }).onClick(() => onDismiss())
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor('#FF6B9D')
.margin({ left: 16, right: 16, top: 8 })
.borderRadius(12)
}
生日横幅使用粉色背景(与家庭分组颜色一致),左侧显示蛋糕Emoji,中间显示寿星姓名和祝福语,右侧提供关闭按钮。横幅的内容是动态生成的——birthdayContacts.map(c => c.name).join('、')将多个寿星的姓名用顿号连接。如果当天有多个联系人过生日,横幅会一并展示所有寿星姓名,如"张三、李四、王五——送出生日祝福吧!"。
6.6 联系人卡片 buildContactCard
联系人卡片是应用中最复杂的UI组件,包含了头像、姓名、收藏标记、生日标记、手机号、备注、分组标签和拨号按钮:
@Builder
function buildContactCard(
contact: Contact,
onEdit: (c: Contact) => void,
onDelete: (c: Contact) => void,
onFavorite: (c: Contact) => void,
onCall: (phone: string) => void
) {
Row() {
// 头像:取姓名首字 + 分组颜色
Text(contact.name.charAt(0))
.fontSize(18).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
.width(44).height(44).textAlign(TextAlign.Center)
.borderRadius(22)
.backgroundColor(getAvatarColor(contact.group))
.margin({ right: 12 })
// 中间信息区
Column() {
Row() {
Text(contact.name).fontSize(16).fontWeight(FontWeight.Medium)
.fontColor(COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
if (contact.isFavorite) { Text('⭐').fontSize(12).margin({ left: 4 }) }
if (contact.birthday) { Text('🎂').fontSize(12).margin({ left: 4 }) }
}
.alignItems(VerticalAlign.Center)
Text(contact.phone).fontSize(14).fontColor(COLORS.textSecondary)
.margin({ top: 3 })
if (contact.remark) {
Text(contact.remark).fontSize(12).fontColor(COLORS.textTertiary)
.margin({ top: 2 }).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
// 分组标签(非"其他"分组时显示)
if (contact.group !== '其他') {
Text(contact.group).fontSize(11).fontColor(COLORS.primary)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(COLORS.primaryLight).borderRadius(10)
.margin({ right: 8 })
}
// 拨号按钮
Text('📞').fontSize(18).width(36).height(36)
.textAlign(TextAlign.Center).borderRadius(18)
.backgroundColor(COLORS.primaryLight)
.onClick(() => onCall(contact.phone))
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor(COLORS.cardBg).borderRadius(12)
.margin({ left: 16, right: 16, top: 6, bottom: 6 })
.shadow({ radius: 3, color: COLORS.shadow, offsetY: 1 })
}
这个卡片的设计充满了细节:
- 头像:使用联系人姓名的第一个字符作为头像文字,圆形背景的颜色根据分组自动匹配。这是"无头像联系人"场景下最优雅的占位方案,无需用户上传头像也能提供差异化的视觉标识。
- 信息区:姓名最大显示一行,超出部分使用省略号截断。收藏标记和生日标记通过条件渲染显示在姓名右侧。手机号和备注分别使用不同字号和颜色形成清晰的视觉层次。
- 分组标签:只有在分组不是默认的"其他"时才显示,避免所有卡片都带标签造成视觉冗余。标签使用浅蓝色背景和蓝色文字,与蓝白配色方案保持一致。
- 拨号按钮:点击后通过
call.makeCall系统能力发起拨号请求。注意这里的onCall函数是作为参数传入的,Builder函数本身不持有任何能力。
6.7 联系人列表 buildContactList
联系人列表使用List + ForEach + ListItem的组合构建高效的滚动物化列表:
@Builder
function buildContactList(
contacts: Contact[],
onEdit: (c: Contact) => void,
onDelete: (c: Contact) => void,
onFavorite: (c: Contact) => void,
onCall: (phone: string) => void
) {
List() {
ForEach(contacts, (contact: Contact) => {
ListItem() {
buildContactCard(contact, onEdit, onDelete, onFavorite, onCall)
}
})
}
.width('100%')
.layoutWeight(1)
.margin({ top: 4, bottom: 80 })
}
使用List而非Scroll + Column的主要优势在于性能:List组件内置了列表项的回收复用机制,当列表项数量较大时(如数百个联系人),只有可见区域的列表项会被渲染,大幅减少了内存占用和渲染开销。bottom: 80的下边距为底部的浮动按钮预留了空间,避免最后一个联系人被浮动按钮遮挡。
6.8 浮动按钮 buildFloatingButton
浮动按钮(FAB)使用绝对定位固定在页面右下角:
@Builder
function buildFloatingButton(onClick: () => void) {
Column() {
Text('+').fontSize(28).fontColor('#FFFFFF').fontWeight(FontWeight.Lighter)
}
.width(56).height(56)
.backgroundColor(COLORS.primary).borderRadius(28)
.shadow({ radius: 8, color: 'rgba(0,122,255,0.4)', offsetY: 4 })
.position({ bottom: 32, right: 24 })
.onClick(() => onClick())
}
圆形按钮使用borderRadius(28)实现(宽高56的一半),配合position绝对定位固定在右下角。阴影使用了带透明度的蓝色(与主色一致),模拟了按钮浮在页面之上的视觉效果。FontWeight.Lighter让加号看起来更纤细、更精致。
6.9 错误提示 buildErrorToast
错误提示使用条件渲染和自动消失的模式:
@Builder
function buildErrorToast(msg: string, onDismiss: () => void) {
Column() {
Text(msg)
.fontSize(14).fontColor('#FFFFFF')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor('rgba(0,0,0,0.8)')
.borderRadius(8)
}
.width('100%')
.padding({ left: 24, right: 24 })
.position({ top: 100 })
.onClick(() => onDismiss())
}
错误提示显示在页面上部(position({ top: 100 })),使用半透明黑色背景的圆角文字块。用户点击即可关闭。虽然当前版本没有实现自动定时关闭,但这种设计已经能够满足错误信息的展示需求。
6.10 表单对话框 buildFormDialog
表单对话框是通讯录管家中最复杂的UI组件,包含姓名、手机号、分组、生日、备注五个输入字段和两个操作按钮:
@Builder
function buildFormDialog(
isEditing: boolean,
formName: string,
formPhone: string,
formGroup: string,
formBirthday: string,
formRemark: string,
formNameError: string,
formPhoneError: string,
formBirthdayError: string,
onNameChange: (v: string) => void,
onPhoneChange: (v: string) => void,
onGroupChange: (g: string) => void,
onBirthdayChange: (v: string) => void,
onRemarkChange: (v: string) => void,
onSubmit: () => void,
onCancel
更多推荐




所有评论(0)