🚀🚀🚀🚀
鸿蒙生态为开发者提供海量的 HarmonyOS 模板/组件,助力开发效率原地起飞
★一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板 ★
工具行业群英荟萃,是小而美应用的主要聚集赛道
本期介绍的案例是其中一类:日常刚需的日历应用
👉覆盖 20+行业,本帖下方以汇总形式持续更新中,点击收藏!一键三连!常看常新!
【第 5 期】工具行业· 日历应用
一、 概述
1. 行业洞察
1) 行业诉求:
2) 行业常用三方 SDK
说明:“以上三方库及链接仅为示例,三方库由三方开发者独立提供,以其官方内容为准”
SDK 链接:
岳鹰全景监控 SDK 穿山甲广告 SDK 友盟SDK 腾讯微信 SDK 腾讯优量汇 极光 SDK 高德地图 百度地图 腾讯地图定位 高德地图定位
2. 优秀案例概览(下载模板)
基于以上行业分析,本期将介绍鸿蒙生态市场生活服务类行业模板——日历应用模板,为行业提供常用功能的开发案例,模板主要分为万年历、黄历、和我的三大模块。
Stage 开发模型 + 声明式 UI 开发范式。
分层架构设计+ 组件化拆分,支持开发者在开发时既可以选择完整使用模板,也可以根据需求单独选用其中的业务组件。
本模板已集成华为账号等服务,只需做少量配置和定制即可快速实现华为账号的登录。
本模板主要页面及核心功能如下所示:
日历模板
|-- 万年历
| |-- 日历选择
| |-- 吉日查询
| |-- 日期计算
| |-- 节日节气
| └-- 宜忌展示
|-- 黄历
| |-- 日期切换
| |-- 宜忌展示
| |-- 五行、冲煞
| |-- 彭祖百忌
└-- 我的
| |-- 个人信息
| └-- 设置
| └-- 主题切换
| └-- 隐私协议
| └-- 用户协议
复制代码
二、 应用架构设计
1. 分层模块化设计
① 本实践暂时只支持直板机,为单 HAP 包形式,包含路由根节点、底部导航栏等。
① 本实践的基础特性层将应用底部导航栏的每个选项拆分成一个独立的业务功能模块。
② 每个功能模块都具备高内聚、低耦合、可定制的特点,支持产品的灵活部署。
① 本实践的公共能力层分为公共基础能力和行业组件,均打包为 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 插件创建模板工程,开发指导。
② 通过生态市场下载源码, 下载模板。
③ 通过开源仓访问源码,仓库地址。
将 commons/lib_common/src/main/ets/httprequest/HttpRequestApi.ets 文件中的 mock 接口替换为真实的服务器接口。
在 commons/lib_common/src/main/ets/httprequest/HttpRequest.ets 文件中将云侧开发者自定义的数据结构转换为端侧数据结构。
根据自己的业务内容修改模板,进行定制化开发。
2) 按需集成
若开发者已搭建好自己的应用工程,但暂未实现其中的部分场景能力,可以选择取用其中的业务组件,集成在自己的工程中。
① 通过 IDE 插件下载组件源码。开发指导
② 通过生态市场下载组件源码。 下载地址
以上是第五期“工具行业-日历应用”行业优秀案例的内容,更多行业敬请期待~
欢迎下载使用行业模板“点击下载”,若您有体验和开发问题,或者迫不及待想了解 XX 行业的优秀案例,欢迎在评论区留言,小编会快马加鞭为您解答~
同时诚邀您添加下方二维码加入“组件模板活动社群”,精彩上新 &活动不错过!
👉本系列持续更新,欢迎收藏本帖!
第一期:HarmonyOS官方模板优秀案例 | 便捷生活行业· 购物中心
第二期:HarmonyOS官方模板优秀案例 | 新闻行业· 综合新闻
第三期:HarmonyOS官方模板优秀案例 | 教育行业· 教育备考
第四期:HarmonyOS官方模板优秀案例 | 餐饮行业· 美食菜谱
第五期:HarmonyOS官方模板优秀案例 | 工具行业· 日历应用
第六期:……(小编加急整理中,敬请期待)
👉HarmonyOS 组件模板相关推荐
评论