写点什么

【小而美】HarmonyOS 官方模板优秀案例 (第 5 期:工具行业 · 日历应用)

  • 2025-08-28
    北京
  • 本文字数:8833 字

    阅读完需:约 29 分钟

🚀🚀🚀🚀

鸿蒙生态为开发者提供海量的 HarmonyOS 模板/组件,助力开发效率原地起飞

★一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板 ★

工具行业群英荟萃,是小而美应用的主要聚集赛道

本期介绍的案例是其中一类:日常刚需的日历应用

👉覆盖 20+行业,本帖下方以汇总形式持续更新中,点击收藏!一键三连!常看常新!


【第 5 期】工具行业· 日历应用

一、 概述

1. 行业洞察

1) 行业诉求:

  • 日历类应用,未来竞争将聚焦于 AI 驱动的个性化体验、场景化生态构建及文化适配能力正从单一工具进化为连接工作、生活、社交的 “时间操作系统”。

    商业模式是日历类应用的重要场景诉求,目前免费增值为主,差异化变现破局,如何结合小艺做个性化推荐是差异化的根本。

    满足用户的进阶需求:社交协作,隐私保护。


2) 行业常用三方 SDK

说明:“以上三方库及链接仅为示例,三方库由三方开发者独立提供,以其官方内容为准”

SDK 链接:

岳鹰全景监控 SDK 穿山甲广告 SDK 友盟SDK 腾讯微信 SDK 腾讯优量汇 极光 SDK 高德地图 百度地图 腾讯地图定位 高德地图定位


2. 优秀案例概览(下载模板

基于以上行业分析,本期将介绍鸿蒙生态市场生活服务类行业模板——日历应用模板,为行业提供常用功能的开发案例,模板主要分为万年历、黄历、和我的三大模块。

  • Stage 开发模型 + 声明式 UI 开发范式。

    分层架构设计+ 组件化拆分,支持开发者在开发时既可以选择完整使用模板,也可以根据需求单独选用其中的业务组件。

    本模板已集成华为账号等服务,只需做少量配置和定制即可快速实现华为账号的登录。


本模板主要页面及核心功能如下所示:

​日历模板 |-- 万年历 |    |-- 日历选择 |    |-- 吉日查询 |    |-- 日期计算 |    |-- 节日节气 |    └-- 宜忌展示 |-- 黄历 |    |-- 日期切换 |    |-- 宜忌展示 |    |-- 五行、冲煞  |    |-- 彭祖百忌 └-- 我的 |     |-- 个人信息 |     └-- 设置 |       └-- 主题切换 |       └-- 隐私协议 |       └-- 用户协议  
复制代码


二、 应用架构设计

1. 分层模块化设计

  • 产品定制层:专注于满足不同设备或使用场景的个性化需求,作为应用的入口,是用户直接互动的界面。

① 本实践暂时只支持直板机,为单 HAP 包形式,包含路由根节点、底部导航栏等。

  • 基础特性层:用于存放相对独立的功能 UI 和业务逻辑实现。

① 本实践的基础特性层将应用底部导航栏的每个选项拆分成一个独立的业务功能模块。

② 每个功能模块都具备高内聚、低耦合、可定制的特点,支持产品的灵活部署。

  • 公共能力层:存放公共能力,包括公共 UI 组件、数据管理、外部交互和工具库等共享功能。

① 本实践的公共能力层分为公共基础能力和行业组件,均打包为 HAR 包被基础特性层的业务模块引用。

② 公共基础能力包含账号管理、动态布局等工具,公共类型定义,网络库,以及弹窗、加载等公共组件。

③ 可分可合组件将包含行业特点、可完全自闭环的能力抽出独立的组件模块,支持开发者在开发中单独集成使用,详见业务组件设计章节。

2. 业务组件设计

为支持开发者单独获取特定场景的页面和功能,本模板将功能完全自闭环的部分能力抽离出独立的行业组件模块,不依赖公共基础能力包,开发者可以单独集成,开箱即用,降低使用难度。


三、 行业场景技术方案

1. 一键搜题

1) 场景说明

支持华为账号一键登录及其他方式(账号密码登录)。

用户登录后展示昵称和头像,点击用户信息栏可进入用户主页,查看并编辑个人信息和历史动态。

支持添加重要提醒(日程、生日、纪念日、代办),更新提醒,删除提醒。

2) 技术方案

  • 华为账号一键登录

① 通过Account Kit实现华为账号一键登录,并获取用户手机号,关联应用已有用户。

  • 头像修改

① 通过 Scenario Fusion Kit 提供的选择头像Button快速拉起头像选择页面,供用户完成华为账号头像或其他头像的选择与展示。

  • 重要提醒

① 通过 @kit.CalendarKit 提供的提供日历与日程管理能力将应用中的工作、生活中与时间相关的日程服务与系统日历进行集成,从而实现日程管理、事件创建、查询等功能。

  • 主题切换

① 通过全局主题对象,控制全局的主题颜色切换,并使用持久化存储当前主题选择。


3) 代码参考

  • 部分核心代码参见华为账号一键登录实现章节。


2.黄历

1) 场景说明

  • 支持根据日期查看当日黄历信息。

  • 切换日期查询其他日期黄历。

  • 根据选择日期查看今日宜今日忌。

  • 支持根据选择的黄历查看白话文。

2) 技术方案

根据万年历选择日期进行对应日期黄历的展示。

通过日历选择组件暴露的句柄,感知当前选择的日期,并通过句柄同步修改万年历对应的日期。


3)代码参考

  • 部分核心代码参见黄历实现章节。


3.万年历

1) 场景说明

支持日历查看,日期切换,设置周首日。

支持查看今日宜,今日忌。

支持实用工具查询(吉日查询,日期计算,节日节气)。

支持查看城市限行。

支持查看历史上的今天。

2) 技术方案

  • 日历查看

① 通过使用Swiper组件结合计算每月的日期实现日期轮播查看。

  • 头像修改

① 通过 Scenario Fusion Kit 提供的选择头像Button快速拉起头像选择页面,供用户完成华为账号头像或其他头像的选择与展示。

  • 实用工具

① 使用工具结合DatePicker日期选择器,实现日期选择并根据条件计算。

  • 城市限行

② 通过申请位置权限,或者当前城市的限行车牌尾号,并进行展示。


3)代码参考

  • 部分核心代码参见万年历实现章节。


四、 模板代码

1. 工程结构(下载模板

详细代码结构如下所示:

Application├──├──commons│   ├──common                                // 公共能力层│     ├──src/main/ets                        // 基础能力│     │  └──components                       // 公共组件│     │  └──dividerTmp                       // 下划线公共组件│     │  └──https                            // 网络请求库│     │  └──models                           // 公共接口常量│     │  └──quickLogin                       // 华为账号一键登录│     │  └──style                            // 公共样式│     │  └──utils                            // 工具类│     │  └──viewmodels                       // 接口层│     └──Index.ets                           // 对外接口类│  ├──router_module                          // 全局路由组件├──├──components                             // 公共组件│   ├──base_apis                             // 通用组件(模态框,弹窗,选择器等)│   ├──base_calendar                         // 日历组件│   ├──calendar_almanac                      // 黄历组件│   ├──calendar_events                       // 重要提醒组件│   ├──date_calculation                      // 日期计算组件│   ├──festival_solar                        // 节日节气组件│   ├──login_info                            // 登录组件组件│   ├──vip_center                            // 开通会员组件│   ├──traffic_restriction                   // 城市限行组件│   ├──yiji_query                            // 宜忌查询组件├──features                                  // 基础特性层│  ├──almanac/src/main/ets                   // 黄历│  │  ├──pages                               // 首页入口│     │  ├──AlmanacView                      // 黄历入口│  ├──almanac/src/main/resources             // 资源文件目录│  ├──almanac/Index.ets                      // 对外接口类│  ├──perpetual/src/main/ets                 // 万年历│  │  ├──components                          // 万年历组件│  │  ├──pages                               │     │  ├──PerpetualCalendar                // 万年历组件入口│  ├──perpetual/src/main/resources           // 资源文件目录│  ├──perpetual/Index.ets                    // 对外接口类│  ├──mine/src/main/ets                      // 我的(包含一键登录)│  │  └──pages                               // 我的入口页│     │  ├──MinePage                         // 登录│  │  └──components                          // 我的页面入口│  └──mine/src/main/resources                // 资源文件目录└─product/entry/src/main      ├─ets   │  ├─widget   │  │  ├──pages               │  │      ├──WidgetCard.ets       // 服务卡片       │  ├─entryability   │  │      ├──EntryAbility.ets             // 应用程序入口   │  ├─page   │  │  ├──Index.ets                        // 入口   │  │  ├──PrivacyPage.ets                  // 隐私协议      │  │  ├──SafePage.ets                     // 隐私协议弹窗     │  │  ├──SplashPage.ets                   // 闪屏页           │  │  ├──TabContainer.ets                 // tab页入口   └─resources
复制代码


2. 关键代码解读

本篇代码非应用的全量代码,只包括应用的部分能力的关键代码。

若需获取全量代码,请查看模板集成章节。

1) 个人信息

  • 华为账号一键登录

    getQuickLoginAnonymousPhone() {    // 创建授权请求,并设置参数    const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();    // 获取手机号需要传如下scope,传参数之前需要先申请对应scope权限,才能返回对应数据    authRequest.scopes = ['quickLoginAnonymousPhone'];    authRequest.permissions = ['serviceauthcode'];    // 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面    authRequest.forceAuthorization = false;    // 用于防跨站点请求伪造    authRequest.state = util.generateRandomUUID();    try {      const controller = new authentication.AuthenticationController(getContext(this));      controller.executeRequest(authRequest).then((response) => {        const authorizationWithHuaweiIDResponse = response as authentication.AuthorizationWithHuaweiIDResponse;        const state = authorizationWithHuaweiIDResponse.state;        if (state !== undefined && authRequest.state !== state) {          hilog.error(0x0000, 'testTag', `Failed to authorize. The state is different, response state: ${state}`);          return;        }        hilog.info(0x0000, 'testTag', 'Succeeded in authentication.');        const authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse.data!;        const code = authorizationWithHuaweiIDCredential.authorizationCode;        const unionID = authorizationWithHuaweiIDCredential.unionID;        const openID = authorizationWithHuaweiIDCredential.openID;        const anonymousPhone = authorizationWithHuaweiIDCredential?.extraInfo?.quickLoginAnonymousPhone as string;        if (anonymousPhone) {          hilog.info(0x0000, 'testTag', 'Succeeded in authentication.');          this.quickLoginAnonymousPhone = anonymousPhone;          return;        } else {          this.quickLoginAnonymousPhone = '123xxxxxx456'        }        // 开发者处理code、unionID、openID        this.authorizationCode = code      }).catch((err: BusinessError) => {        this.dealAllPhoneError(err);      });    } catch (error) {      this.dealAllPhoneError(error);    }  }
复制代码


  • 重要提醒

class CalendarManage {  context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;  private static _instance: CalendarManage
static get instance() { if (!CalendarManage._instance) { CalendarManage._instance = new CalendarManage() } return CalendarManage._instance }
public getCalendarPermission(): Promise<string> { const permissions: Permissions[] = ['ohos.permission.READ_CALENDAR', 'ohos.permission.WRITE_CALENDAR']; let atManager = abilityAccessCtrl.createAtManager(); return new Promise((resolve, reject) => { atManager.requestPermissionsFromUser(this.context, permissions).then((result: PermissionRequestResult) => { resolve('success') }).catch((error: BusinessError) => { reject('failed') console.error(`get Permission error, error. Code: ${error.code}, message: ${error.message}`); }) }) }
/* * 添加提醒到日历 * */ private async calendarEvent(calendar: calendarManager.Calendar, calendarInfo: UserEventItem): Promise<CalendarInfo> { const event: calendarManager.Event = { title: calendarInfo.content, type: calendarManager.EventType.NORMAL, id: calendarInfo.eventId, isLunar:calendarInfo.date[0].isLunar, startTime: new Date(dayjs(calendarInfo.date[0].date).format('YYYY-MM-DD') + ' ' + calendarInfo.date[0].time).getTime(), endTime: new Date(dayjs(calendarInfo.date[1].date).format('YYYY-MM-DD') + ' ' + calendarInfo.date[1].time).getTime(), reminderTime: CalendarManage.getReminderTime(calendarInfo.remindList), recurrenceRule: { recurrenceFrequency: repeatMap[calendarInfo.repeatType], }, }; return new Promise(async (resolve, reject) => { if (calendarInfo.eventId) { calendar.updateEvent(event).then(() => { resolve({ status: 'success', }) }).catch((err: BusinessError) => { console.error(`Failed to update event. Code: ${err.code}, message: ${err.message}`); }); } else { calendar.addEvent(event).then((data: number) => { console.info(`Succeeded in adding event, id -> ${data}`); resolve({ status: 'success', data: data, }) }).catch((err: BusinessError) => { resolve({ status: 'failed', }) }); } }) } /* * 根据提醒参数创建日历参数 * */ public async calendarEventCreate(calendarInfo: UserEventItem, operationType?: string): Promise<CalendarInfo> { if (calendarInfo.remindList[0] === '不提醒') { return { status: 'not need calendar', } } let permission = await this.getCalendarPermission() if (permission !== 'success') { return { status: 'permission failed', } } let calendar: calendarManager.Calendar | undefined = undefined; // 指定日历账户信息 const calendarAccount: calendarManager.CalendarAccount = { name: '日历模板', type: calendarManager.CalendarType.LOCAL, // 日历账户显示名称,该字段如果不填,创建的日历账户在界面显示为空字符串。 displayName: '日历模板', }; let calendarMgr: calendarManager.CalendarManager | null = calendarManager.getCalendarManager(this.context); // 创建日历账户 try { calendar = await calendarMgr?.createCalendar(calendarAccount) let res: CalendarInfo if (operationType === 'delete') { res = await this.calendarEventDelete(calendar, calendarInfo) } else { res = await this.calendarEvent(calendar, calendarInfo) } return res } catch (e) { return { status: 'calendar operation failed', } } } /* * 删除已经添加到日历的提醒 * */ private async calendarEventDelete(calendar: calendarManager.Calendar, calendarInfo: UserEventItem): Promise<CalendarInfo> { try { await calendar.deleteEvent(calendarInfo.eventId) return { status: 'success', } } catch (e) { return { status: 'failed', } } }}
复制代码


2)黄历

  • 重要提醒

/** * 抛出句柄 */export class CalendarController {     public static vm: CalendarVM = CalendarVM.instance;        public setSelectDate(date: Date) {       CalendarController.vm.changeDate(date)     }        public getTodayYiJi() {       CalendarController.vm.getTodayYiJi()     }}
复制代码


  • 切换日期

/** * 切换日期 */  public changeDate(date: Date) {    let gap = (date.getFullYear() - this.curDate.year()) * 12 + date.getMonth() - this.curDate.month()    this.dateListSource.clearData()    let i = -2    while (i <= 2) {      let month = this.getDateList(i + gap)      this.dateListSource.pushData(month)      i++    }    this.curIndex = 2    this.selectDate = dayjs(date) }
复制代码


  • 获取今日宜和忌

/** * 获取今日宜和忌 */  public getTodayYiJi() {    const todayLunar = Lunar.fromDate(new Date(this.selectDate.format('YYYY-MM-DD')));    const yi = todayLunar.getDayYi();    const ji = todayLunar.getDayJi();    this.todayYiJi = {      yi,      ji,    }  }
复制代码


3)万年历

  • 展示日历

/** * 展示日历 */  Swiper(this.swiperController) {        LazyForEach(this.vm.dateListSource, (item: DateModelList) => {          Grid() {            ForEach(item, (item: DateModel) => {              GridItem() {              }            },(item: DateModel) => JSON.stringify(item));          }          .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')          .maxCount(7)          .columnsGap(0)          .rowsGap(0)          .padding({ bottom: 20 })        },(item: DateModelList) => JSON.stringify(item));}
复制代码


  • 获取位置权限

/** * 获取位置权限 */  getCurrentLocation() {    this.permissionRequestUtils.locationPermissionRequest().then(async (res) => {      if (res === 'success') {        this.locationPermission = res        this.permissionRequestUtils.getCurrentLocation().then((res: string) => {          this.location = res        }).catch((err:BusinessError) => {          this.location = '北京'        })      } else {        this.dealError()      }    }).catch(() => {      this.dealError()    })}
复制代码


3. 模板集成

本模板提供了两种代码集成方式,供开发者自由选用。

1) 整体集成(下载模板

开发者可以选择直接基于模板工程开发自己的应用工程。

  • 模板代码获取:

① 通过 IDE 插件创建模板工程,开发指导

② 通过生态市场下载源码, 下载模板

③ 通过开源仓访问源码,仓库地址

  • 打开模板工程,根据 README 说明中的快速入门章节,将自己的应用信息配置在模板工程内,即可运行并查看模板效果。



  • 对接开发者自己的服务器接口,转换数据结构,展示真实的云侧数据。

将 commons/lib_common/src/main/ets/httprequest/HttpRequestApi.ets 文件中的 mock 接口替换为真实的服务器接口。


 

在 commons/lib_common/src/main/ets/httprequest/HttpRequest.ets 文件中将云侧开发者自定义的数据结构转换为端侧数据结构。


 

根据自己的业务内容修改模板,进行定制化开发。


2) 按需集成

若开发者已搭建好自己的应用工程,但暂未实现其中的部分场景能力,可以选择取用其中的业务组件,集成在自己的工程中。

  • 组件代码获取:

① 通过 IDE 插件下载组件源码。开发指导

② 通过生态市场下载组件源码。 下载地址

  • 下载组件源码,根据 README 中的说明,将组件包配置在自己的工程中。



  • 根据 API 参考和示例代码,将组件集成在自己的对应场景中。


以上是第五期“工具行业-日历应用”行业优秀案例的内容,更多行业敬请期待~

欢迎下载使用行业模板“点击下载”,若您有体验和开发问题,或者迫不及待想了解 XX 行业的优秀案例,欢迎在评论区留言,小编会快马加鞭为您解答~

同时诚邀您添加下方二维码加入“组件模板活动社群”,精彩上新 &活动不错过!


👉本系列持续更新,欢迎收藏本帖!

第一期:HarmonyOS官方模板优秀案例 | 便捷生活行业· 购物中心

第二期:HarmonyOS官方模板优秀案例 | 新闻行业· 综合新闻

第三期:HarmonyOS官方模板优秀案例 | 教育行业· 教育备考

第四期:HarmonyOS官方模板优秀案例 | 餐饮行业· 美食菜谱

第五期:HarmonyOS官方模板优秀案例 | 工具行业· 日历应用

第六期:……(小编加急整理中,敬请期待)


👉HarmonyOS 组件模板相关推荐

  • 【活动 ing】HarmonyOS 组件/模板集成创新活动,报名时间截止 2025 年 8 月 30 日,点击查看

  • ·鸿蒙应用开发者激励计划 2025,点击查看


2025-08-28 17:181

评论

发布
暂无评论

Go 语言中排序的 3 种方法

AlwaysBeta

Go

如何搭建企业个人直播流媒体服务器,使用OBS推送PC桌面流到SRS

平平无奇爱好科技

PoseiSwap 更新质押系统,并将在 8 月18 日开启“Trident ”快照

BlockChain先知

干货丨云耀云服务器L实例需要配置升级教程

平平无奇爱好科技

干货丨使用Portainer进行Docker可视化管理

YG科技

轻松实现Matomo网站数据的高效分析极简教程

YG科技

中小企业如何使用Superset进行数据分析

YG科技

9 种方法使用 Amazon CodeWhisperer 快速构建应用

亚马逊云科技 (Amazon Web Services)

人工智能

mac系统综合清理优化 MacCleaner 3 Pro激活中文

胖墩儿不胖y

mac系统清理优化软件 优化工具

关于SpringBoot,我有话要讲(Filter和Controller中的异常处理)

java易二三

Java 程序员 Spring Boot 计算机

google borg(k8s亲爹) 论文读后感

摸鱼编程

k8s Google borg

中小企业预算有限,如何使用Odoo构建企业ERP?

平平无奇爱好科技

Presto 设计与实现(四):动态代码生成 ByteBuddy

冰心的小屋

数据湖 bytebuddy presto presto 设计与实现

快手公布自研大模型最新进展:“快手AI对话”已开放内测

Geek老T

AI Codec 大语言模型

如何使用Prestashop三步轻松搭建自己的电商网站

平平无奇爱好科技

如何通过华为云云耀云服务器L实例自建企业Nextcloud云存储

平平无奇爱好科技

干货丨如何使用GitLab进行团队及项目管理

YG科技

数字化转型与架构-规划篇回顾

数字随行

数字化转型

步步向前,曙光已现:百度的大模型之路

脑极体

百度 文心

ARTS 打卡第 9 天

自由

ARTS 打卡计划

浅谈冥想(69/100)

hackstoic

冥想

ARTS打卡 第一周

一期一会

Python 数据库 ARTS 打卡计划 前端框架 算法题

java 程序启动后cpu高怎么办?

摸鱼编程

JVM JIT jfr pgo

从新学习String和StringBuilder,让面试官虎躯一震

摸鱼编程

Java 面试 string StringBuilder

如何三步实现高性能WordPress网站的部署

平平无奇爱好科技

PoseiSwap 更新质押系统,并将在 8 月18 日开启“Trident ”快照

西柚子

【小而美】HarmonyOS官方模板优秀案例
(第5期:工具行业 · 日历应用)_HarmonyOS_HarmonyOS_InfoQ精选文章