写点什么

【小而美】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:182269

评论

发布
暂无评论

活动报名:Voice Agent 开发者分享会丨RTE Meetup

声网

Sora之后,视频生成模型的中国牌局

脑极体

AI

年终总结报告ppt怎么做?用AI工具快速自动生成!

职场工具箱

职场 PPT 年终总结 AIGC AI生成PPT

Ape-DTS:开源 DTS 工具,助力自建 MySQL、PostgreSQL 迁移上云

小猿姐

MySQL 数据库 postgresql 数据传输 数据迁移

使用观测云排查数据库死锁故障

观测云

数据库

通义灵码,让梦想照进现实更快一点

阿里云云效

阿里云 云原生

CCS'24(全球网络安全四大顶级会议之一)收录云起无垠最新研究成果

云起无垠

苹果电脑玩王者荣耀用什么软件怎么操作?

阿拉灯神丁

游戏 王者荣耀 手游 CrossOver Mac下载 mac游戏模拟器

2025年度计划如何制定?10个年度计划模板推荐!

职场工具箱

项目管理 职场 可视化 年度计划 办公软件

又双叒叕出来了一款船新Copilot!腾讯终于发大招了!码农们又可以丝滑摸鱼啦~

左诗右码

富士胶片中国可持续发展报告获上海跨国公司地区总部优秀ESG报告

财见

天翼云携手华为共建魔乐社区,共创AI生态新篇章

极客天地

AutoMQ 如何在 AWS 上避免 Kafka 跨 AZ 网络传输费用

AutoMQ

kafak Java开发分析工具 AutoMQ AZs 好文翻译

【开源大屏】玩转开源积木BI,从0到1设计一个大屏

JEECG低代码

数据分析 数据可视化 BI 分析工具 数据大屏

币安移除铭文市场的背后:对区块链、加密市场及用户的影响

chainwiseweb3

区块链技术 dapp开发 BTC铭文 交易所系统 铭文系统

融云 IM 基于 Rust 的鸿蒙 SDK 开发实践

融云 RongCloud

智保未来:国泰产险的 AI 网关革新之旅

阿里巴巴云原生

阿里云 云原生

币安移除铭文市场的深度解读:背后原因及其对区块链行业的影响

chainwiseweb3

DAPP系统开发 区块链技术开发 dapp开发 铭文 铭文系统开发

同为 Binance Labs 投资 ,APX Finance 与 Astherus 合并意味几何?

股市老人

当前热门 DApp 模式解析:六大方向的趋势与创新

区块链软件开发推广运营

交易所开发 dapp开发 链游开发 公链开发 代币开发

技术同学如何提升业务洞察力

老张

软件开发 系统架构 技术视野 业务洞察力

AICon北京站HarmonyOS技术分论坛开启招募,欢迎开发者踊跃报名

最新动态

C语言之输入输出

不在线第一只蜗牛

C# 开发语言

通义灵码,让梦想照进现实更快一点

阿里巴巴云原生

阿里云 云原生

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