HarmonyOS 通讯录管家 —— 基于 ArkTS + SQLite 的联系人管理应用

一个完整的 HarmonyOS(鸿蒙)原生通讯录管理应用,采用 ArkTS 语言开发,使用 @ohos.data.relationalStore(SQLite)进行数据持久化,实现了联系人的增删改查、分组管理、模糊搜索、生日智能提醒、快速拨号等核心功能。


运行效果
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

目录


项目亮点

  • 纯 ArkTS 原生开发:完全使用 HarmonyOS ArkTS 语言和 ArkUI 声明式框架,无第三方依赖
  • SQLite 本地持久化:基于 @ohos.data.relationalStore 实现完整的数据库 CRUD,数据存储在设备本地
  • 清晰的三层架构:数据类型定义层(ContactTypes.ets)→ 数据库操作层(ContactDB.ets)→ UI 展示层(Index.ets / ContactForm.ets),职责分明,易于维护
  • 智能生日提醒:自动计算 7 天内即将过生日的联系人,在主页顶部以醒目横幅提示
  • ArkTS 严格模式适配:解决了 arkts-no-any-unknownarkts-no-untyped-obj-literalsarkts-no-utility-types 等编译限制问题,可作为 ArkTS 类型系统实践参考

技术栈

类别技术说明
开发语言ArkTSHarmonyOS 原生应用开发语言
UI 框架ArkUI声明式 UI 开发框架
数据库@ohos.data.relationalStoreSQLite 关系型数据库
路由导航router页面间跳转与参数传递
UI 组件promptActionToast 提示与确认对话框
系统能力Want / UIAbilityContext拉起系统拨号界面
构建工具HvigorHarmonyOS 应用构建工具
目标平台HarmonyOS(Stage 模型)API 12+,SDK 6.1.1

项目结构

ArkTS-SQLite/
├── entry/src/main/ets/
│   ├── entryability/
│   │   └── EntryAbility.ets              # 应用入口 Ability
│   ├── model/
│   │   ├── ContactTypes.ets              # 数据类型定义 + 工具函数
│   │   └── ContactDB.ets                 # SQLite 数据库操作层
│   └── pages/
│       ├── Index.ets                     # 主页面(联系人列表 + 搜索 + 生日提醒)
│       └── ContactForm.ets               # 表单页面(新增 / 编辑联系人)
├── entry/src/main/resources/             # 资源文件(颜色、字符串、图片等)
└── build-profile.json5                   # 项目构建配置

核心文件说明:

文件职责核心内容
ContactTypes.ets数据层定义 ContactContactGroupContactInsertParam 等接口,提供分组常量和工具函数
ContactDB.ets数据库层封装 SQLite 建表、增删改查、模糊搜索、生日查询等全部数据库操作
Index.ets主页面联系人分组列表、搜索栏、生日提醒横幅、快速拨号、空状态展示
ContactForm.ets表单页面新增/编辑联系人的完整表单,含输入验证、分组选择、生日日期选择器

功能特性

1. 联系人管理(CRUD)

  • 新增:填写姓名、电话、邮箱、分组、生日、备注,支持收藏标记
  • 编辑:通过路由传递联系人 JSON 数据,表单自动回填,修改后更新数据库
  • 删除:弹出确认对话框,确认后从数据库删除并实时刷新列表
  • 查询:按收藏降序 → 分组升序 → 姓名升序排列展示

2. 分组管理

预设 5 个分组(带 emoji 图标),主页按分组聚合展示:

分组图标ID
家人👪1
朋友🤝2
同事💼3
同学🎓4
其他📍5

3. 搜索功能

  • 支持按姓名或电话号码模糊匹配(SQL LIKE 语义)
  • 使用 RdbPredicatesbeginWrap / contains / or / endWrap 组合复杂查询条件
  • 搜索结果仍按分组聚合展示,带清除按钮

4. 生日智能提醒

  • 自动计算 7 天内即将过生日的联系人
  • 主页顶部显示红色提醒横幅,带 Badge 数量标记
  • 点击"查看"弹出对话框,列出具体联系人生日信息
  • 联系人卡片中对生日临近者显示蛋糕 🎂 标记

5. 快速拨号

  • 联系人卡片右侧电话按钮
  • 通过 ohos.want.action.dial Intent 拉起系统拨号界面

6. 表单验证

  • 姓名:必填,不超过 20 字符
  • 电话:必填,正则验证 [\d\-+\s()]{5,20}
  • 邮箱:非必填,如有则验证格式
  • 未保存修改离开时弹出确认提示

数据库设计

数据库文件:contact_manager.db,安全级别 S1

contact 表(联系人)

字段类型约束说明
idINTEGERPRIMARY KEY AUTOINCREMENT主键,自增
nameTEXTNOT NULL姓名
phoneTEXTNOT NULL电话号码
emailTEXTDEFAULT ‘’邮箱地址
group_idINTEGERDEFAULT 5分组 ID,默认"其他"
birthdayTEXTDEFAULT ‘’生日,格式 yyyy-MM-dd
is_favoriteINTEGERDEFAULT 0是否收藏(0 否 / 1 是)
remarkTEXTDEFAULT ‘’备注
created_atTEXTDEFAULT ‘’创建时间

contact_group 表(分组)

字段类型约束说明
idINTEGERPRIMARY KEY AUTOINCREMENT主键,自增
nameTEXTNOT NULL分组名称
iconTEXTDEFAULT ‘’分组图标

核心代码解析

1. 数据库初始化与建表

使用 relationalStore.getRdbStore() 获取数据库实例,通过 executeSql() 执行建表语句:

async init(): Promise<void> {
  if (this.store) return;
  const config: relationalStore.StoreConfig = {
    name: DB_NAME,
    securityLevel: relationalStore.SecurityLevel.S1,
  };
  this.store = await relationalStore.getRdbStore(this.context, config);
  await this.store.executeSql(CREATE_TABLE_GROUP);
  await this.store.executeSql(CREATE_TABLE_CONTACT);
  await this.initGroups(); // 首次启动时插入预设分组
}

2. 联系人 CRUD 操作

插入联系人 —— 将表单数据映射为数据库字段,自动填充创建时间:

async insertContact(contact: ContactInsertParam): Promise<number> {
  if (!this.store) throw new Error('数据库未初始化');
  const now = new Date();
  const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
  const row: relationalStore.ValuesBucket = {
    name: contact.name,
    phone: contact.phone,
    email: contact.email || '',
    group_id: contact.groupId || 5,
    birthday: contact.birthday || '',
    is_favorite: contact.isFavorite || 0,
    remark: contact.remark || '',
    created_at: dateStr,
  };
  return await this.store.insert(TABLE_CONTACT, row);
}

查询并转换结果集 —— 将 ResultSet 逐行读取并映射为 Contact 对象:

private resultSetToContacts(resultSet: relationalStore.ResultSet): Contact[] {
  const contacts: Contact[] = [];
  try {
    while (resultSet.goToNextRow()) {
      contacts.push({
        id: resultSet.getLong(resultSet.getColumnIndex('id')),
        name: resultSet.getString(resultSet.getColumnIndex('name')),
        phone: resultSet.getString(resultSet.getColumnIndex('phone')),
        email: resultSet.getString(resultSet.getColumnIndex('email')) || '',
        groupId: resultSet.getLong(resultSet.getColumnIndex('group_id')),
        birthday: resultSet.getString(resultSet.getColumnIndex('birthday')) || '',
        isFavorite: resultSet.getLong(resultSet.getColumnIndex('is_favorite')),
        remark: resultSet.getString(resultSet.getColumnIndex('remark')) || '',
        createdAt: resultSet.getString(resultSet.getColumnIndex('created_at')) || '',
      });
    }
  } catch (e) {
    console.error('resultSetToContacts error:', JSON.stringify(e));
  } finally {
    resultSet.close();
  }
  return contacts;
}

3. 模糊搜索实现

使用 RdbPredicates 的组合查询方法,实现姓名或电话的模糊匹配:

async searchContacts(keyword: string): Promise<Contact[]> {
  if (!this.store) throw new Error('数据库未初始化');
  const predicates = new relationalStore.RdbPredicates(TABLE_CONTACT);
  predicates.beginWrap();
  predicates.contains('name', keyword);     // 姓名包含关键词
  predicates.or();                           // OR 条件
  predicates.contains('phone', keyword);    // 电话包含关键词
  predicates.endWrap();
  predicates.orderByDesc('is_favorite');
  predicates.orderByAsc('name');
  const resultSet: relationalStore.ResultSet = await this.store.query(predicates, [
    'id', 'name', 'phone', 'email', 'group_id',
    'birthday', 'is_favorite', 'remark', 'created_at'
  ]);
  return this.resultSetToContacts(resultSet);
}

4. 生日智能提醒

查询所有联系人后,在应用层过滤 7 天内即将过生日的联系人:

async queryUpcomingBirthdays(): Promise<Contact[]> {
  if (!this.store) throw new Error('数据库未初始化');
  const all = await this.queryAllContacts();
  return all.filter(c => {
    if (!c.birthday) return false;
    const parts = c.birthday.split('-');
    if (parts.length < 3) return false;
    const today = new Date();
    const bd = new Date(today.getFullYear(), parseInt(parts[1]) - 1, parseInt(parts[2]));
    const diffDays = Math.ceil((bd.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
    return diffDays >= 0 && diffDays <= 7;
  });
}

5. 声明式 UI 列表渲染

使用 ForEach 按分组聚合展示联系人,每个联系人卡片包含头像、信息和操作按钮:

ForEach(this.groupedContacts, (item: GroupWithContacts) => {
  // 分组标题
  ListItem() {
    Row() {
      Text(`${item.group.icon} ${item.group.name}`)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
    }
  }
  // 该分组下的联系人列表
  ForEach(item.contacts, (contact: Contact) => {
    ListItem() {
      this.buildContactItem(contact)
    }
  })
})

6. 路由传参与表单回填

通过 router.pushUrl 传递 JSON 序列化的联系人数据,表单页面解析后自动回填:

// 主页面 —— 传递参数
onEditContact(contact: Contact): void {
  const param: RouterParams = { mode: 'edit', contact: JSON.stringify(contact) };
  router.pushUrl({ url: 'pages/ContactForm', params: param });
}

// 表单页面 —— 解析参数并回填
aboutToAppear(): void {
  const params = router.getParams() as Record<string, string>;
  if (params) {
    this.mode = params['mode'] || 'add';
    if (this.mode === 'edit' && params['contact']) {
      const contactData = JSON.parse(params['contact']) as Contact;
      this.contactName = contactData.name;
      this.contactPhone = contactData.phone;
      // ... 其他字段回填
    }
  }
}

ArkTS 编译避坑指南

在开发过程中遇到并解决了多个 ArkTS 严格模式下的编译问题,总结如下:

错误码错误描述解决方案
arkts-no-any-unknown禁止使用 any / unknown 类型为所有变量添加显式类型标注,如 const count: number = ...
arkts-no-untyped-obj-literals对象字面量必须对应显式声明的接口先声明接口(如 RouterParams),再用接口类型约束变量
arkts-no-utility-types不支持 OmitPick 等 TypeScript 工具类型手动定义等效接口(如 ContactInsertParam
arkts-no-aliases-by-index不支持索引访问类型使用 ForEach 回调参数直接声明类型,如 (group: ContactGroup)
arkts-no-type-querytypeof 仅允许在表达式上下文中使用避免在类型位置使用 typeof,改用显式接口声明
Button 多子组件Button 组件只能有一个子组件Row / Column 包裹多个子组件

经验总结:ArkTS 的类型系统比 TypeScript 更严格,开发时应始终使用显式类型标注,避免依赖类型推断;不使用 any、不使用工具类型、不使用 typeof 在类型位置。


运行环境

项目要求
开发工具DevEco Studio 5.0+
HarmonyOS SDKAPI 12+,SDK 6.1.1
目标设备HarmonyOS 手机(phone)
编译模式Stage 模型
语言ArkTS(TypeScript 超集)

快速开始

  1. 使用 DevEco Studio 打开项目
  2. 连接 HarmonyOS 设备或启动模拟器
  3. 点击 Run 按钮运行应用
  4. 应用首次启动会自动创建数据库和预设分组数据

总结与展望

本项目是一个典型的 HarmonyOS 原生应用实践,通过通讯录管理场景覆盖了以下核心知识点:

  • ArkTS 语言特性:接口定义、异步编程(async/await)、类型系统
  • ArkUI 声明式 UI:组件嵌套、状态管理(@State)、ForEach 列表渲染、自定义 Builder
  • SQLite 数据持久化:relationalStore 建表、增删改查、复合条件查询
  • 页面路由:pushUrl 传参、getParams 接收参数、back 返回
  • 系统能力调用:Want Intent 拉起系统拨号
  • ArkTS 编译限制适配:严格模式下的类型系统实践

后续可扩展方向:

  • 接入 @ohos.contacts 系统通讯录能力,实现与系统通讯录的数据同步
  • 添加联系人头像拍照/相册选择功能
  • 实现数据备份与恢复(利用已声明的 EntryBackupAbility
  • 接入分布式数据管理,实现多设备间通讯录同步
  • 添加更多分组管理功能(自定义分组、分组排序)

项目技术栈:HarmonyOS / ArkTS / ArkUI / SQLite / relationalStore / Stage 模型

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐