HarmonyOS6 - 鸿蒙读取系统联系人实战案例

HarmonyOS6 - 鸿蒙读取系统联系人实战案例

开发环境为:

开发工具:DevEco Studio 6.0.1 Release

API版本是:API21

本文所有代码都已使用模拟器测试成功!

1. 需求

首先我们设定如下需求:

  1. 点击添加,就弹框显示手机联系人列表
  2. 勾选联系人,勾选完毕后,点击确认,勾选的联系人数据,就显示到APP中这个页面中
  3. 这个页面中,可以多选联系人,点击删除,就删除了联系人数据
  4. 联系人数据存储在APP本地数据库中
  5. 应用重启后联系人数据依然需要存在

2. 分析

针对上面需求,我的分析思路如下:

  1. 页面结构设计

设计联系人列表页面的整体布局结构,包括顶部导航栏、联系人列表区域和底部操作栏。

  1. 数据模型定义

定义联系人数据接口(FriendsItem)和电话信息接口(PhoneItem),用于规范数据类型。

  1. 数据库操作实现
  • 封装RDB数据库工具类(RdbTools)
  • 实现数据增删改查功能
  • 添加防重复逻辑,通过手机号判断联系人是否已存在
  1. 核心功能开发

(1)联系人展示

  • 页面加载时从数据库查询并显示所有联系人
  • 使用List组件渲染联系人列表
  • 添加数据加载完成提示

(2)联系人添加

  • 集成系统联系人选择组件
  • 批量添加联系人功能
  • 实时反馈添加结果(新增成功/重复提醒)

(3)多选删除功能

  • 长按联系人进入多选模式
  • 显示勾选框供用户选择
  • 实现全选/取消全选功能
  • 批量删除选中的联系人
  1. 用户体验优化
  • 添加多选模式下的视觉反馈(选中背景色变化)
  • 操作结果提示(弹窗消息)
  • 底部按钮动态切换(添加/取消、选择/删除)
  • 手势交互(长按触发多选)
  1. 异步处理优化
  • 解决异步操作顺序问题,确保日志正确显示
  • 使用async/await确保数据处理的顺序性
  • 优化Promise链式调用,避免状态混乱
  1. 状态管理
  • 使用@State管理组件状态
  • 维护多选模式状态
  • 管理选中联系人ID集合
  • 确保UI状态与数据同步

3. 编码

根据以上思路,我们开始编写代码

首先新建页面【ContactsDetail.ets】,代码如下:

js 复制代码
import { router } from "@kit.ArkUI";
import { contact } from "@kit.ContactsKit";
import { myTools } from "../common/MyTools";
import { ObjectModel } from "../common/rdb/ObjectModel";
import RdbTools from "../common/rdb/RdbTools";

export interface FriendsItem {
  id?: number;
  name?: string;
  phone?: string;
}

export interface PhoneItem {
  labelId?: number;
  labelName?: string;
  phoneNumber?: string;
}

const TAG = 'ContactsDetail'

/**
 * 联系人列表(可添加系统联系人)
 */
interface PhoneItemInterface {
  isNew: boolean;
  phone: string;
}

@Entry
@Component
struct ContactsDetail {
  // 联系人数据集合
  @State friendsList: FriendsItem[] = [];
  // 选中的联系人ID集合
  @State selectedIds: Set<number> = new Set<number>();
  // 是否处于多选模式
  @State isMultiSelectMode: boolean = false;

  //新增联系人信息到RDB数据库
  async addDataToRDB(friend: FriendsItem): Promise<PhoneItemInterface> {
    return new Promise(async (resolve) => {
      //新增之前,先查询数据库中是否已经存在该手机号
      await RdbTools.queryData(friend.phone).then(async res => {
        console.info(TAG, '查询手机号[' + friend.phone + ']结果=' + JSON.stringify(res))
        if (res.length > 0) {
          console.info(TAG, '查询手机号[' + friend.phone + ']数据库已存在,无需新增!')
          resolve({ isNew: false, phone: friend.phone as string }); // 重复添加
        } else {
          console.info(TAG, '查询手机号[' + friend.phone + ']数据库不存在,需要新增!')
          //数据保存到RDB数据库
          await RdbTools.insertData({
            id: friend.id,
            name: friend.name,
            phone: friend.phone,
          } as ObjectModel)
          //加到列表集合中
          this.friendsList.push(friend);
          resolve({ isNew: true, phone: friend.phone as string }); // 新添加
        }
      })
    });
  }

  //查询RDB数据库中的所有联系人数据
  async queryAllDB() {
    await RdbTools.queryData('').then(res => {
      console.info(TAG, '查询数据库所有联系人信息=' + JSON.stringify(res))
      if (res) {
        this.friendsList = res
        console.info(TAG, 'this.friendsList=' + JSON.stringify(this.friendsList))
        myTools.alertMsg('数据加载完毕!')
      }
    })
  }

  aboutToAppear(): void {
    //需要等待RDB数据库初始化完毕后才能查询数据库,所以这里进入页面后,1秒之后才查询数据库
    setTimeout(() => {
      this.queryAllDB();
    }, 1000)
  }

  // 选择联系人
  async selectContacts() {
    // 选择联系人时的筛选条件 (是否多选)
    let contactSelectionOptions: contact.ContactSelectionOptions = { isMultiSelect: true };
    // 调用通讯录选择组件,让用户选择需要传入APP的通讯录联系人
    let promise = contact.selectContacts(contactSelectionOptions);

    await promise.then(async (data) => {
      // 用户选择确认之后,会在此处收到回调
      console.info(TAG, `selectContacts success: data->${JSON.stringify(data)}`);
      if (data && data.length > 0) {
        // 用于收集结果的数组
        const chongfuPhone: string[] = [];
        const newPhone: string[] = [];

        // 使用for循环按顺序处理每个联系人,并等待每个处理完成
        for (const contactItem of data) {
          let phoneItemArray: Array<PhoneItem> = JSON.parse(JSON.stringify(contactItem.phoneNumbers));
          let friend: FriendsItem = {
            id: contactItem.id,
            name: contactItem.name?.fullName,
            phone: phoneItemArray[0].phoneNumber
          };

          // 等待每个联系人处理完成
          const result = await this.addDataToRDB(friend);

          // 根据结果分类
          if (result.isNew) {
            newPhone.push(result.phone);
          } else {
            chongfuPhone.push(result.phone);
          }
        }

        console.info(TAG, `处理完成后的this.friendsList=${JSON.stringify(this.friendsList)}`);
        console.info(TAG, `处理完成后的newPhone.length=${newPhone.length}`);
        console.info(TAG, `处理完成后的chongfuPhone.length=${chongfuPhone.length}`);

        //弹框提示新增结果
        if (newPhone.length > 0) {
          if (chongfuPhone.length > 0) {
            myTools.alertMsg('新增' + newPhone.length + '个联系人成功!重复手机号有:' +
            chongfuPhone.toString(), 5000);
          } else {
            myTools.alertMsg('新增' + newPhone.length + '个联系人成功!', 5000);
          }
        } else {
          if (chongfuPhone.length > 0) {
            myTools.alertMsg('新增0个联系人成功!重复手机号有:' +
            chongfuPhone.toString(), 5000);
          }
        }
      }
    }).catch((err: BusinessError) => {
      console.error(TAG, `selectContacts fail: err->${JSON.stringify(err)}`);
    });
  }

  //删除RDB数据库中的联系人
  async deletePhone(item: FriendsItem) {
    await RdbTools.deleteData(item.id).then(() => {
      this.queryAllDB();
    });
  }

  // 批量删除选中的联系人
  async deleteSelectedContacts() {
    if (this.selectedIds.size === 0) {
      myTools.alertMsg('请先选择要删除的联系人!');
      return;
    }

    myTools.alertDialogRollback('请确认:', `确定删除选中的${this.selectedIds.size}个联系人吗?`, async () => {
      try {
        // 批量删除选中的联系人
        const deletePromises = Array.from(this.selectedIds).map(async id => {
          await RdbTools.deleteData(id).then(() => {
          });
        }
        );
        await Promise.all(deletePromises);
        // 删除成功后更新列表
        await this.queryAllDB();
        // 清空选中集合
        this.selectedIds.clear();
        // 退出多选模式
        this.isMultiSelectMode = false;

        myTools.alertMsg('删除成功!');
      } catch (error) {
        console.error(TAG, '批量删除失败:', error);
        myTools.alertMsg('删除失败,请重试!');
      }
    }, () => {
      // 取消操作
    });
  }

  // 切换多选模式
  toggleMultiSelectMode() {
    this.isMultiSelectMode = !this.isMultiSelectMode;
    if (!this.isMultiSelectMode) {
      // 退出多选模式时清空选中状态
      this.selectedIds.clear();
    }
  }

  // 取消多选模式
  cancelMultiSelectMode() {
    this.isMultiSelectMode = false;
    this.selectedIds.clear();
  }

  // 选中/取消选中联系人
  toggleSelectContact(item: FriendsItem) {
    if (!item.id) {
      return;
    }

    if (this.selectedIds.has(item.id)) {
      this.selectedIds.delete(item.id);
    } else {
      this.selectedIds.add(item.id);
    }

    // 如果所有项都取消选中,退出多选模式
    if (this.selectedIds.size === 0) {
      this.isMultiSelectMode = false;
    }
  }

  // 全选/取消全选
  toggleSelectAll() {
    if (this.selectedIds.size === this.friendsList.length) {
      // 如果已经全选,则取消全选
      this.selectedIds.clear();
    } else {
      // 全选
      this.selectedIds = new Set(this.friendsList.filter(item => item.id).map(item => item.id!));
    }
  }

  // 勾选框组件
  @Builder
  checkboxBuilder(item: FriendsItem) {
    Checkbox()
      .select(this.selectedIds.has(item.id!))
      .selectedColor('#007DFF')
      .width(24)
      .height(24)
      .margin({ right: 12 })
      .onChange((checked: boolean) => {
        if (checked) {
          this.selectedIds.add(item.id!);
        } else {
          this.selectedIds.delete(item.id!);
        }
      })
  }

  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Image($r('app.media.back'))
          .width(24)
          .height(24)
          .margin({ left: 16 })
          .onClick(() => {
            if (this.isMultiSelectMode) {
              // 如果处于多选模式,点击返回退出多选模式
              this.cancelMultiSelectMode();
            } else {
              router.back();
            }
          })

        Text(this.isMultiSelectMode ? `已选中${this.selectedIds.size}项` : '常用联系人')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .fontColor('#000000')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)

        // 多选模式下的操作按钮
        if (this.isMultiSelectMode) {
          Text(this.selectedIds.size === this.friendsList.length ? '取消全选' : '全选')
            .fontSize(16)
            .fontColor('#007DFF')
            .margin({ right: 16 })
            .onClick(() => {
              this.toggleSelectAll();
            })
        } else {
          // 非多选模式下的占位
          Row()
            .width('50%')
            .height(24)
            .margin({ right: 16 })
        }
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Start)

      // 内容区域
      List() {
        ForEach(this.friendsList, (item: FriendsItem, index: number) => {
          ListItem() {
            Row() {
              // 多选模式下显示勾选框
              if (this.isMultiSelectMode) {
                this.checkboxBuilder(item)
              }

              Column() {
                Text(item.name)
                  .fontSize(16)
                  .fontColor('#333333')
                  .margin({ bottom: 4 })
                Text(item.phone)
                  .fontSize(14)
                  .fontColor('#999999')
              }
              .alignItems(HorizontalAlign.Start)

              Blank()

              // 非多选模式下显示箭头图标
              if (!this.isMultiSelectMode) {
                Image($r('app.media.next'))
                  .width(20)
                  .height(20)
                  .objectFit(ImageFit.Cover)
              }
            }
            .width('100%')
            .padding({
              top: 16,
              bottom: 16,
              left: 20,
              right: 20
            })
            .backgroundColor(this.isMultiSelectMode && this.selectedIds.has(item.id!) ? '#F0F8FF' : '#FFFFFF')
            .onClick(() => {
              if (this.isMultiSelectMode) {
                // 多选模式下点击切换选中状态
                this.toggleSelectContact(item);
              }
            })
            .gesture(
              LongPressGesture({ duration: 500 })
                .onAction(() => {
                  // 长按进入多选模式并选中当前项
                  if (!this.isMultiSelectMode) {
                    this.isMultiSelectMode = true;
                  }
                  if (item.id) {
                    this.toggleSelectContact(item);
                  }
                })
            )
          }
        })
      }
      .width('95%')
      .borderRadius(8)
      .margin({ top: 12 })
      .backgroundColor('#FFFFFF')
      .divider({
        strokeWidth: 1,
        startMargin: 20,
        endMargin: 20,
        color: '#ffe9f0f0'
      })
      .layoutWeight(1)

      // 底部Tab栏
      Row() {
        // 左侧按钮:多选模式下显示"取消",非多选模式下显示"添加"
        Column() {
          if (this.isMultiSelectMode) {
            // 多选模式下的取消按钮
            Text('×')  // 取消符号
              .fontSize(24)
              .fontColor('#333333')
            Text('取消')
              .fontSize(12)
              .fontColor('#333333')
              .margin({ top: 4 })
          } else {
            // 非多选模式下的添加按钮
            Text('+')
              .fontSize(24)
              .fontColor('#333333')
            Text('添加')
              .fontSize(12)
              .fontColor('#333333')
              .margin({ top: 4 })
          }
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .height('100%')
        .onClick(() => {
          if (this.isMultiSelectMode) {
            // 多选模式下点击取消按钮,退出多选模式
            this.cancelMultiSelectMode();
          } else {
            // 非多选模式下点击添加按钮,添加联系人
            let isContactsAvailable = canIUse('SystemCapability.Applications.Contacts');
            if (isContactsAvailable) {
              this.selectContacts();
            } else {
              console.info(TAG, 'The device does not support the ability to select contacts.');
            }
          }
        })

        // 右侧按钮:多选模式下显示"删除",非多选模式下显示"选择"
        Column() {
          if (this.isMultiSelectMode) {
            // 多选模式下的删除按钮
            Text('\u{1F5D1}')  // Unicode 垃圾箱符号
              .fontSize(22)
              .fontColor(this.selectedIds.size > 0 ? '#FF3B30' : '#999999')
            Text(`删除(${this.selectedIds.size})`)
              .fontSize(12)
              .fontColor(this.selectedIds.size > 0 ? '#FF3B30' : '#999999')
              .margin({ top: 4 })
          } else {
            // 非多选模式下的选择按钮
            Text('\u{2705}')  // Unicode 对勾符号
              .fontSize(22)
              .fontColor('#333333')
            Text('选择')
              .fontSize(12)
              .fontColor('#333333')
              .margin({ top: 4 })
          }
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .height('100%')
        .onClick(() => {
          if (this.isMultiSelectMode) {
            // 多选模式下执行批量删除
            if (this.selectedIds.size > 0) {
              this.deleteSelectedContacts();
            } else {
              myTools.alertMsg('请先选择要删除的联系人!');
            }
          } else {
            // 非多选模式下进入多选模式
            this.toggleMultiSelectMode();
          }
        })
      }
      .width('100%')
      .height(70)
      .backgroundColor('#F5F5F5')
      .padding({ top: 8, bottom: 8 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

RdbTools工具类代码如下:

js 复制代码
import { relationalStore } from "@kit.ArkData"
import Logger from "../Logger";
import { myTools } from "../MyTools";
import { ObjectModel } from "./ObjectModel";

/**
 * 操作本地RDB数据库的工具类
 */
export default class RdbTools {

  private static rdbStore: relationalStore.RdbStore;

  static setStore(store: relationalStore.RdbStore) {
    RdbTools.rdbStore = store;
  }

  static getStore(): relationalStore.RdbStore {
    return RdbTools.rdbStore;
  }

  // 表名
  static tableName: string = 'phone_book'
  //数据库文件名
  static rdbName: string = 'phoneBook.db'
  // SQL 语法:(SQL语法的数据类型关键词不一样,可通过AI生成SQL语句)
  // 解释说明:
  //   CREATE TABLE IF NOT EXISTS     如果表不存在才创建新的表
  //   INTEGER -> number              INTEGER 整数型   FLOAT 浮点数
  //   PRIMARY KEY                    主键(唯一标识)
  //   AUTOINCREMENT                  自增
  //   TEXT -> string                 字符串型
  //   NOT NULL                       不允许空
  static sqlCreate: string = `CREATE TABLE IF NOT EXISTS ${RdbTools.tableName} (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        phone TEXT NOT NULL
      )`

  // 创建数据库文件
  static async initDataBase() {
    try {
      // 获取操作数据库的管理对象(如果数据库文件不存在,会自动创建数据库文件)
      const store = RdbTools.rdbStore;
      // 执行创建表的语句 execute 执行
      store.executeSql(RdbTools.sqlCreate)
      Logger.debug('数据库文件创建成功!')
    } catch (error) {
      Logger.error('创建数据库文件,发生异常=' + JSON.stringify(error))
    }
  }

  // 新增单条数据
  static async insertData(item: ObjectModel) {
    Logger.debug('新增数据=' + JSON.stringify(item))
    try {
      // 获取操作数据库的对象
      const store = RdbTools.rdbStore;
      Logger.debug('store=' + JSON.stringify(store))
      // 添加一条数据
      const id = await store.insert(RdbTools.tableName, item)
      Logger.debug('新增单条数据成功,id=' + id);
    } catch (error) {
      myTools.alertMsg('新增数据,发生异常:' + JSON.stringify(error))
      Logger.error('新增单条数据,发生异常=' + JSON.stringify(error))
    }
  }

  // 新增多条数据
  static async insertArrayData(item: Array<ObjectModel>) {
    try {
      // 获取操作数据库的对象
      const store = RdbTools.rdbStore;
      // 添加一条数据
      const id = await store.batchInsert(RdbTools.tableName, item)
      Logger.debug('新增多条数据成功,id=' + id)
    } catch (error) {
      myTools.alertMsg('批量新增数据,发生异常:' + JSON.stringify(error))
      Logger.error('新增多条数据,发生异常=' + JSON.stringify(error))
    }
  }

  //删除数据
  static async deleteData(id?: number, ids?: Array<number>) {
    Logger.debug('需要删除的id=' + id)
    Logger.debug('需要删除的ids=' + ids)
    try {
      // 获取操作数据库对象
      const store = RdbTools.rdbStore;
      const predicates = new relationalStore.RdbPredicates(RdbTools.tableName)
      // 注意!!!:记得添加 predicates 限定条件,否者会删除所有数据
      // predicates.equalTo('id', 2)        // equalTo 删除一条
      if (id) {
        predicates.equalTo('id', id) // equalTo 删除一条
      } else {
        predicates.in('id', ids) //  in      批量删除
      }
      const affectedRows = await store.delete(predicates)
      Logger.debug('删除数据,受影响行数=' + affectedRows)
      if (affectedRows == 0) {
        myTools.alertMsg('数据删除失败!')
      } else {
        myTools.alertMsg('数据删除成功!')
      }
    } catch (error) {
      myTools.alertMsg('删除数据,发生异常:' + JSON.stringify(error))
      Logger.error('删除数据,发生异常=' + JSON.stringify(error))
    }
  }

  //更新数据
  static async updateData(item: ObjectModel) {
    if (!item.id) {
      AlertDialog.show({
        message: 'ID缺失,无法修改!'
      })
      return;
    }
    try {
      // 获取操作数据库对象
      const store = RdbTools.rdbStore;
      // 谓词(条件)
      const predicates = new relationalStore.RdbPredicates(RdbTools.tableName)
      // 注意!!!:记得添加 predicates 限定条件,否者修改全部
      predicates.equalTo('id', item.id)
      const affectedRows = await store.update(item, predicates)
      Logger.debug('修改数据 - 受影响行数:' + affectedRows)
    } catch (error) {
      myTools.alertMsg('更新数据,发生异常:' + JSON.stringify(error))
      Logger.debug('更新数据,发生异常=' + JSON.stringify(error))
    }
  }

  // 查询数据
  static async queryData(phone?: string) {
    // 准备一个数组,用于存储数据库提取的数据
    const list: ObjectModel [] = []
    try {
      Logger.debug('>>>>>>>>>>>>> 1')
      // 获取操作数据库的对象
      const store = RdbTools.rdbStore;
      Logger.debug('>>>>>>>>>>>>> 2')
      // 谓词(条件)
      const predicates = new relationalStore.RdbPredicates(RdbTools.tableName)
      Logger.debug('>>>>>>>>>>>>> 3')
      predicates.orderByDesc('id') // 倒序(由大到小,常用于排序)
      Logger.debug('>>>>>>>>>>>>> 4')
      if (phone) {
        Logger.debug('>>>>>>>>>>>>> 5')
        predicates.like('phone', '%' + phone + '%') //  模糊匹配查询
      }
      // predicates.orderByAsc('id')   // 正序(小到大,常用于排序)
      // predicates.equalTo('id', 1)    // 等于(常用于详情页)
      // predicates.in('id', [1, 3, 5]) // 查找多项(常用批量删除)
      // predicates.like('title', '%哈%') //  模糊匹配(常用于搜索)
      // resultSet 结果集
      Logger.debug('>>>>>>>>>>>>> 6')
      const resultSet = await store.query(predicates)
      Logger.debug('>>>>>>>>>>>>> 7')
      // resultSet.goToNextRow()   指针移动到下一行
      Logger.debug('查询RDB数据,结果集数量条数=' + resultSet.rowCount)
      while (resultSet.goToNextRow()) {
        // 移动指针的时候提取数据,按列下标提取数据
        list.push({
          // resultSet.getColumnIndex()        根据列名称获取下标(索引)
          id: resultSet.getLong(resultSet.getColumnIndex('id')),
          name: resultSet.getString(resultSet.getColumnIndex('name')),
          phone: resultSet.getString(resultSet.getColumnIndex('phone')),
        })
      }
      Logger.debug('查询RDB数据,结果集=' + JSON.stringify(list))
    } catch (error) {
      myTools.alertMsg('查询数据,发生异常:' + JSON.stringify(error))
      Logger.error('查询数据,发生异常=' + JSON.stringify(error))
    }
    return list;
  }
}

EntryAbility.ets文件中需要【onWindowStageCreate】方法中初始化数据库,代码如下:

js 复制代码
relationalStore.getRdbStore(this.context, {
    name: RdbTools.rdbName, // 数据库文件名
    securityLevel: relationalStore.SecurityLevel.S1 // 数据库安全级别
}, (err, store) => {
    if (err) {
        Logger.error(`Failed to get RdbStore. Code:${err.code}, message:${err.message}`);
        return;
    }
    Logger.info('Succeeded in getting RdbStore.');
    //保存store, 方便后面我们对数据库的操作
    RdbTools.setStore(store)
    RdbTools.initDataBase()
});

4. 演示

然后使用真机测试即可,最终成果演示视频如下:

鸿蒙读取系统联系人

最后

  • 希望本文对你有所帮助!
  • 本人如有任何错误或不当之处,请留言指出,谢谢!
相关推荐
小学生波波3 小时前
HarmonyOS6 - 鸿蒙双向滚动课程表案例
list·scroll·鸿蒙开发·课程表·harmonyos6
小学生波波1 天前
HarmonyOS6 - 鸿蒙CustomDialog封装信息提示框
arkts·鸿蒙·鸿蒙开发·harmonyos6·信息提示框
小学生波波1 天前
HarmonyOS - 鸿蒙开发百度地图案例
地图·百度地图·路线规划·鸿蒙开发·harmonyos6·鸿蒙地图·打点
小学生波波1 天前
HarmonyOS6 - 鸿蒙电商页面实战案例
登录页面·arkts·鸿蒙系统·电商·harmonyos6
熊猫钓鱼>_>2 天前
【开源鸿蒙跨平台开发先锋训练营】DAY 2 React Native for OpenHarmony 开发笔记与实战指南
react native·开源·harmonyos·arkts·openharmony·gitcode·atomgit
小学生波波3 天前
最新版鸿蒙开发工具DevEco Studio保姆级安装教程
arkts·鸿蒙系统·安装教程·deveco studio·鸿蒙开发·harmonyos6
编程乐学3 天前
鸿蒙非原创--DevEcoStudio开发的奶茶点餐APP
华为·harmonyos·deveco studio·鸿蒙开发·奶茶点餐·鸿蒙大作业
小学生波波3 天前
HarmonyOS6 - 弹框选择年份和月份实战案例
鸿蒙·鸿蒙开发·弹窗组件·harmonyos6·选择年份
小学生波波3 天前
HarmonyOS6 - Progress进度条组件案例
arkts·鸿蒙系统·鸿蒙开发·progress·harmonyos6·进度条组件