【小而美】HarmonyOS官方模板优秀案例(第5期:工具行业 · 日历应用)
在commons/lib_common/src/main/ets/httprequest/HttpRequest.ets文件中将云侧开发者自定义的数据结构转换为端侧数据结构。将commons/lib_common/src/main/ets/httprequest/HttpRequestApi.ets文件中的mock接口替换为真实的服务器接口。为支持开发者单独获取特定场景的页面和功能,本模板将功能完
鸿蒙生态为开发者提供海量的HarmonyOS模板/组件,助力开发效率原地起飞
★ 一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板 ★
工具行业群英荟萃,是小而美应用的主要聚集赛道
本期介绍的案例是其中一类:日常刚需的日历应用
覆盖20+行业,本帖以汇总形式持续更新中,点击收藏!一键三连!常看常新!
【第5期】工具行业 · 日历应用
- 概述
- 行业洞察
- 行业诉求:
- 日历类应用,未来竞争将聚焦于 AI 驱动的个性化体验、场景化生态构建及文化适配能力正从单一工具进化为连接工作、生活、社交的 “时间操作系统”。
- 商业模式是日历类应用的重要场景诉求,目前免费增值为主,差异化变现破局,如何结合小艺做个性化推荐是差异化的根本。
- 满足用户的进阶需求:社交协作,隐私保护。
- 行业常用三方SDK
分类 |
三方库名称 |
功能 |
SDK链接 |
登录认证 |
中国移动一键登录SDK/易盾一键登录SDK/创蓝闪验/极光安全认证/阿里云号码认证SDK/中国电信一键登录SDK |
登录 |
|
分享 |
友盟/ShareSDK/微信分享/QQ分享/新浪微博SDK/MobTech ShareSDK |
统计/推送/分享 |
|
支付 |
支付宝支付/微信支付/银联支付 |
支付 |
|
数据分析 |
友盟移动统计SD/神策数据SDK |
数据收集、处理、分析、运用 |
|
性能监控 |
腾讯Bugly SDK/听云SDK/岳鹰全景监控SDK |
异常上报和运营统计 |
|
地图 |
高德地图SDK |
地图 |
|
推送 |
个推/华为推送/极光PUSH/阿里推送SDK |
消息推送 |
|
媒体 |
阿里云视频播放器SDK |
音视频 |
说明:“以上三方库及链接仅为示例,三方库由三方开发者独立提供,以其官方内容为准”
- 案例概览(下载模板)
基于以上行业分析,本期将介绍鸿蒙生态市场生活服务类行业模板——日历应用模板,为行业提供常用功能的开发案例,模板主要分为万年历、黄历、和我的三大模块。
- Stage开发模型 + 声明式UI开发范式。
- 分层架构设计 + 组件化拆分,支持开发者在开发时既可以选择完整使用模板,也可以根据需求单独选用其中的业务组件。
- 本模板已集成华为账号等服务,只需做少量配置和定制即可快速实现华为账号的登录。
本模板主要页面及核心功能如下所示:
日历模板 |-- 万年历 | |-- 日历选择 | |-- 吉日查询 | |-- 日期计算 | |-- 节日节气 | └-- 宜忌展示 |-- 黄历 | |-- 日期切换 | |-- 宜忌展示 | |-- 五行、冲煞 | |-- 彭祖百忌 └-- 我的 | |-- 个人信息 | └-- 设置 | └-- 主题切换 | └-- 隐私协议 | └-- 用户协议 |
- 应用架构设计
- 分层模块化设计
- 产品定制层:专注于满足不同设备或使用场景的个性化需求,作为应用的入口,是用户直接互动的界面。
- 本实践暂时只支持直板机,为单HAP包形式,包含路由根节点、底部导航栏等。
- 基础特性层:用于存放相对独立的功能UI和业务逻辑实现。
- 本实践的基础特性层将应用底部导航栏的每个选项拆分成一个独立的业务功能模块。
- 每个功能模块都具备高内聚、低耦合、可定制的特点,支持产品的灵活部署。
- 公共能力层:存放公共能力,包括公共UI组件、数据管理、外部交互和工具库等共享功能。
- 本实践的公共能力层分为公共基础能力和可分可合组件,均打包为HAR包被上层业务组件引用。
- 公共基础能力包含日志、文件处理等工具类,公共类型定义,网络库,以及弹窗、加载等公共组件。
- 可分可合组件将包含行业特点、可完全自闭环的能力抽出独立的组件模块,支持开发者在开发中单独集成使用,详见业务组件设计章节。
为支持开发者单独获取特定场景的页面和功能,本模板将功能完全自闭环的部分能力抽离出独立的行业组件模块,不依赖公共基础能力包,开发者可以单独集成,开箱即用,降低使用难度。
- 行业场景技术方案
- 个人信息
- 场景说明
支持华为账号一键登录及其他方式(账号密码登录)。
用户登录后展示昵称和头像,点击用户信息栏可进入用户主页,查看并编辑个人信息和历史动态。
支持添加重要提醒(日程、生日、纪念日、代办),更新提醒,删除提醒。
- 技术方案
- 华为账号一键登录
- 通过Account Kit实现华为账号一键登录,并获取用户手机号,关联应用已有用户。
- 头像修改
- 通过Scenario Fusion Kit提供的选择头像Button快速拉起头像选择页面,供用户完成华为账号头像或其他头像的选择与展示。
- 重要提醒
- 通过@kit.CalendarKit提供的提供日历与日程管理能力将应用中的工作、生活中与时间相关的日程服务与系统日历进行集成,从而实现日程管理、事件创建、查询等功能。
- 主题切换
- 通过全局主题对象,控制全局的主题颜色切换,并使用持久化存储当前主题选择。
- 代码参考
- 部分核心代码参见华为账号一键登录实现章节。
- 黄历
- 场景说明
- 支持根据日期查看当日黄历信息。
- 切换日期查询其他日期黄历。
- 根据选择日期查看今日宜今日忌。
- 支持根据选择的黄历查看白话文。
- 技术方案
根据万年历选择日期进行对应日期黄历的展示。
通过日历选择组件暴露的句柄,感知当前选择的日期,并通过句柄同步修改万年历对应的日期。
- 代码参考
- 部分核心代码参见黄历实现章节。
- 万年历
- 场景说明
支持日历查看,日期切换,设置周首日。
支持查看今日宜,今日忌。
支持实用工具查询(吉日查询,日期计算,节日节气)。
支持查看城市限行。
支持查看历史上的今天。
- 技术方案
- 日历查看
- 通过使用Swiper组件结合计算每月的日期实现日期轮播查看。
- 头像修改
- 通过Scenario Fusion Kit提供的选择头像Button快速拉起头像选择页面,供用户完成华为账号头像或其他头像的选择与展示。
- 实用工具
- 使用工具结合DatePicker日期选择器,实现日期选择并根据条件计算。
- 城市限行
- 通过申请位置权限,或者当前城市的限行车牌尾号,并进行展示。
- 代码参考
- 部分核心代码参见万年历实现章节。
- 模板代码
- 工程结构(下载模板)
详细代码结构如下所示:
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 |
- 关键代码解读
本篇代码非应用的全量代码,只包括应用的部分能力的关键代码。
若需获取全量代码,请查看模板集成章节。
- 个人信息
- 华为账号一键登录
- 重要提醒
typescript 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); } } |
typescript 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', } } } } |
- 黄历
- 重要提醒
- 切换日期
- 获取今日宜和忌
typescript /** * 抛出句柄 */ export class CalendarController { public static vm: CalendarVM = CalendarVM.instance;
public setSelectDate(date: Date) { CalendarController.vm.changeDate(date) }
public getTodayYiJi() { CalendarController.vm.getTodayYiJi() } } |
typescript /** * 切换日期 */ 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) } |
typescript /** * 获取今日宜和忌 */ 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, } } |
- 万年历
- 展示日历
- 获取位置权限
typescript /** * 展示日历 */ 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)); } |
typescript /** * 获取位置权限 */ 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() }) } |
- 模板集成
本模板提供了两种代码集成方式,供开发者自由选用。
- 整体集成(下载模板)
开发者可以选择直接基于模板工程开发自己的应用工程。
- 模板代码获取:
- 打开模板工程,根据README说明中的快速入门章节,将自己的应用信息配置在模板工程内,即可运行并查看模板效果。
- 对接开发者自己的服务器接口,转换数据结构,展示真实的云侧数据。
将commons/lib_common/src/main/ets/httprequest/HttpRequestApi.ets文件中的mock接口替换为真实的服务器接口。
在commons/lib_common/src/main/ets/httprequest/HttpRequest.ets文件中将云侧开发者自定义的数据结构转换为端侧数据结构。
根据自己的业务内容修改模板,进行定制化开发。
- 按需集成
若开发者已搭建好自己的应用工程,但暂未实现其中的部分场景能力,可以选择取用其中的业务组件,集成在自己的工程中。
- 组件代码获取:
- 下载组件源码,根据README中的说明,将组件包配置在自己的工程中。
- 根据API参考和示例代码,将组件集成在自己的对应场景中。
以上是第五期“工具行业-日历应用”行业优秀案例的内容,更多行业敬请期待~
欢迎下载使用行业模板“点击下载”,若您有体验和开发问题,或者迫不及待想了解XX行业的优秀案例,欢迎在评论区留言,小编会快马加鞭为您解答~
同时诚邀您添加下方二维码加入“组件模板活动社群”,精彩上新&活动不错过!
本系列持续更新,欢迎大家收藏本帖!
期数 |
帖子 |
链接 |
第1期 |
HarmonyOS官方模板优秀案例 | 便捷生活行业 · 购物中心 |
|
第2期 |
HarmonyOS官方模板优秀案例 | 新闻行业 · 综合新闻 |
|
第3期 |
HarmonyOS官方模板优秀案例 | 教育行业 · 教育备考 |
|
第4期 |
HarmonyOS官方模板优秀案例 | 餐饮行业 · 美食菜谱 |
|
第5期 |
HarmonyOS官方模板优秀案例 | 工具行业 · 日历应用 |
|
第6期 |
小编加急整理中,敬请期待 |
HarmonyOS组件模板相关推荐
更多推荐
所有评论(0)