当前位置: 首页 > news >正文

HarmonyOS实战开发-如何实现一个简单的健康生活应用(上)

介绍

本篇Codelab介绍了如何实现一个简单的健康生活应用,主要功能包括:

  1. 用户可以创建最多6个健康生活任务(早起,喝水,吃苹果,每日微笑,刷牙,早睡),并设置任务目标、是否开启提醒、提醒时间、每周任务频率。
  2. 用户可以在主页面对设置的健康生活任务进行打卡,其中早起、每日微笑、刷牙和早睡只需打卡一次即可完成任务,喝水、吃苹果需要根据任务目标量多次打卡完成。
  3. 主页可显示当天的健康生活任务完成进度,当天所有任务都打卡完成后,进度为100%,并且用户的连续打卡天数加一。
  4. 当用户连续打卡天数达到3、7、30、50、73、99天时,可以获得相应的成就。成就在获得时会以动画形式弹出,并可以在“成就”页面查看。
  5. 用户可以查看以前的健康生活任务完成情况。

本应用的运行效果如下图所示:

相关概念

  • @Observed 和 @ObjectLink:@Observed适用于类,表示类中的数据变化由UI页面管理;@ObjectLink应用于被@Observed装饰类的对象。
  • @Consume 和 @Provide:@Provide作为数据提供者,可以更新子节点的数据,触发页面渲染。@Consume检测到@Provide数据更新后,会发起当前视图的重新渲染。
  • Flex:一个功能强大的容器组件,支持横向布局,竖向布局,子组件均分和流式换行布局。
  • List:List是很常用的滚动类容器组件之一,它按照水平或者竖直方向线性排列子组件, List的子组件必须是ListItem,它的宽度默认充满List的宽度。
  • TimePicker:TimePicker是选择时间的滑动选择器组件,默认以00:00至23:59的时间区创建滑动选择器。
  • Toggle:组件提供勾选框样式、状态按钮样式及开关样式。
  • 后台代理提醒:使用后台代理提醒能力后,应用可以被冻结或退出,计时和弹出提醒的功能将被后台系统服务代理。
  • 关系型数据库(Relational Database,RDB):一种基于关系模型来管理数据的数据库。

环境搭建

软件要求

  • DevEco Studio版本:DevEco Studio 3.1 Release。
  • OpenHarmony SDK版本:API version 9。

硬件要求

  • 开发板类型:润和RK3568开发板。
  • OpenHarmony系统:3.2 Release。

环境搭建

完成本篇Codelab我们首先要完成开发环境的搭建,本示例以RK3568开发板为例,参照以下步骤进行:

  1. 获取OpenHarmony系统版本:标准系统解决方案(二进制)。以3.2 Release版本为例:

2.搭建烧录环境。

  1. 完成DevEco Device Tool的安装
  2. 完成RK3568开发板的烧录

3.搭建开发环境。

  1. 开始前请参考工具准备,完成DevEco Studio的安装和开发环境配置。
  2. 开发环境配置完成后,请参考使用工程向导创建工程(模板选择“Empty Ability”),选择JS或者eTS语言开发。

  3. 工程创建完成后,选择使用真机进行调测。

代码结构解读

本篇Codelab只对核心代码进行讲解。

├─entry/src/main/ets                 // 代码区
│  ├─common
│  │  ├─constants
│  │  │  └─CommonConstants.ets       // 公共常量
│  │  ├─database
│  │  │  ├─rdb                       // 数据库
│  │  │  │  ├─RdbHelper.ets
│  │  │  │  ├─RdbHelperImp.ets
│  │  │  │  ├─RdbUtil.ets
│  │  │  │  └─TableHelper.ets
│  │  │  └─tables                    // 数据库接口
│  │  │     ├─DayInfoApi.ets
│  │  │     ├─GlobalInfoApi.ets
│  │  │     └─TaskInfoApi.ets
│  │  └─utils
│  │     ├─BroadCast.ets             // 通知
│  │     ├─GlobalContext.ets         // 全局上下文
│  │     ├─HealthDataSrcMgr.ets      // 数据管理单例
│  │     ├─Logger.ets                // 日志类
│  │     └─Utils.ets                 // 工具类
│  ├─entryability
│  │  └─EntryAbility.ets             // 程序入口类
│  ├─model                           // model
│  │  ├─AchieveModel.ets
│  │  ├─DatabaseModel.ets            // 数据库model
│  │  ├─Mine.ets
│  │  ├─NavItemModel.ets             // 菜单栏model
│  │  ├─RdbColumnModel.ets           // 数据库表数据
│  │  ├─TaskInitList.ets
│  │  └─WeekCalendarModel.ets        // 日历model
│  ├─pages
│  │  ├─AdvertisingPage.ets          // 广告页
│  │  ├─MainPage.ets                 // 应用主页面
│  │  ├─MinePage.ets                 // 我的页面
│  │  ├─SplashPage.ets               // 启动页
│  │  ├─TaskEditPage.ets             // 任务编辑页面
│  │  └─TaskListPage.ets             // 任务列表页面
│  ├─service
│  │  └─ReminderAgent.ets            // 后台提醒
│  ├─view
│  │  ├─dialog                       // 弹窗组件
│  │  │  ├─AchievementDialog.ets     // 成就弹窗
│  │  │  ├─CustomDialogView.ets      // 自定义弹窗
│  │  │  ├─TaskDetailDialog.ets      // 打卡弹窗
│  │  │  ├─TaskDialogView.ets        // 任务对话框
│  │  │  ├─TaskSettingDialog.ets     // 任务编辑相关弹窗
│  │  │  └─UserPrivacyDialog.ets
│  │  ├─home                         // 主页面相关组件
│  │  │  ├─AddBtnComponent.ets       // 添加任务按钮组件
│  │  │  ├─HomeTopComponent.ets      // 首页顶部组件
│  │  │  ├─TaskCardComponent.ets     // 任务item组件件
│  │  │  └─WeekCalendarComponent.ets // 日历组件
│  │  ├─task                         // 任务相关组件
│  │  │  ├─TaskDetailComponent.ets   // 任务编辑详情组件
│  │  │  ├─TaskEditListItem.ets      // 任务编辑行内容
│  │  │  └─TaskListComponent.ets     // 任务列表组件
│  │  ├─AchievementComponent.ets     // 成就页面
│  │  ├─BadgeCardComponent.ets       // 勋章卡片组件
│  │  ├─BadgePanelComponent.ets      // 勋章面板组件
│  │  ├─HealthTextComponent.ets      // 自定义text组件
│  │  ├─HomeComponent.ets            // 首页页面
│  │  ├─ListInfo.ets                 // 用户信息列表
│  │  ├─TitleBarComponent.ets        // 成就标题组件
│  │  └─UserBaseInfo.ets             // 用户基本信息
│  └─viewmodel                       // viewmodel
│     ├─AchievementInfo.ets          // 成就信息
│     ├─AchievementMapInfo.ets       // 成就map信息
│     ├─AchievementViewModel.ets     // 成就相关模块
│     ├─BroadCastCallBackInfo.ets    // 通知回调信息
│     ├─CalendarViewModel.ets        // 日历相关模块
│     ├─CardInfo.ets                 // 成就卡片信息
│     ├─ColumnInfo.ets               // 数据库表结构
│     ├─CommonConstantsInfo.ets      // 公共常量信息
│     ├─DayInfo.ets                  // 每日信息
│     ├─GlobalInfo.ets               // 全局信息
│     ├─HomeViewModel.ets            // 首页相关模块
│     ├─PublishReminderInfo.ets      // 发布提醒信息
│     ├─ReminderInfo.ets             // 提醒信息
│     ├─TaskInfo.ets                 // 任务信息
│     ├─TaskViewModel.ets            // 任务设置相关模块
│     ├─WeekCalendarInfo.ets         // 日历信息
│     └─WeekCalendarMethodInfo.ets   // 日历方法信息
└─entry/src/main/resources           // 资源文件夹

应用架构分析

本应用的基本架构如下图所示,数据库为其他服务提供基础的用户数据,主要业务包括:用户可以查看和编辑自己的健康任务并进行打卡、查看成就。UI层提供了承载上述业务的UI界面。

应用主页面

本节将介绍如何给应用添加一个启动页,设计应用的主界面,以及首页的界面开发和数据展示。

启动页

首先我们需要给应用添加一个启动页,启动页里我们需要用到一个定时器来实现启动页展示固定时间后跳转应用主页的功能,效果图如下:

打开应用时会进入此页面,具体实现逻辑是:

通过修改/entry/src/main/ets/entryability里的loadContent路径可以改变应用的入口文件,我们需要把入口文件改为我们写的SplashPage启动页面。

// EntryAbility.ets
windowStage.loadContent('pages/SplashPage', (err, data) => {    if (err.code) {...}    Logger.info('windowStage','Succeeded in loading the content. Data: ' + JSON.stringify(data));
});

在SplashPage启动页的文件里通过首选项来实现是否需要弹“权限管理”的弹窗,如果需要弹窗的情况下,用户点击同意权限后通过首选项对用户的操作做持久化保存。相关代码如下:

// SplashPage.ets
import data_preferences from '@ohos.data.preferences';
onConfirm() {let preferences = data_preferences.getPreferences(this.context, H_STORE);preferences.then((res) => {res.put(IS_PRIVACY, true).then(() => {res.flush();Logger.info('SplashPage','isPrivacy is put success');}).catch((err: Error) => {Logger.info('SplashPage','isPrivacy put failed. Cause:' + err);});})this.jumpAdPage();
}
exitApp() {this.context.terminateSelf();
}
jumpAdPage() {setTimeout(() => {router.replaceUrl({ url: 'pages/AdvertisingPage' });}, Const.LAUNCHER_DELAY_TIME);
}
aboutToAppear() {let preferences = data_preferences.getPreferences(this.context, H_STORE);preferences.then((res) => {res.get(IS_PRIVACY, false).then((isPrivate) => {if (isPrivate === true) {this.jumpAdPage();} else {this.dialogController.open();}});});
}

APP功能入口

我们需要给APP添加底部菜单栏,用于切换不同的应用模块,由于各个模块之间属于完全独立的情况,并且不需要每次切换都进行界面的刷新,所以我们用到了Tabs,TabContent组件。

本应用一共有首页(HomeIndex),成就(AchievementIndex)和我的(MineIndex)三个模块,分别对应Tabs组件的三个子组件TabContent。

// MainPage.ets
TabContent() {HomeIndex({ homeStore: $homeStore, editedTaskInfo: $editedTaskInfo, editedTaskID: $editedTaskID }).borderWidth({ bottom: 1 }).borderColor($r('app.color.primaryBgColor'))
}
.tabBar(this.TabBuilder(TabId.HOME))
.align(Alignment.Start)
TabContent() {AchievementIndex()
}
.tabBar(this.TabBuilder(TabId.ACHIEVEMENT))
TabContent() {MineIndex().borderWidth({ bottom: 1 }).borderColor($r('app.color.primaryBgColor'))
}
.tabBar(this.TabBuilder(TabId.MINE))

首页

首页包含了任务信息的所有入口,包含任务列表的展示,任务的编辑和新增,上下滚动的过程中顶部导航栏的渐变,日期的切换以及随着日期切换界面任务列表跟着同步的功能,效果图如下:

具体代码实现我们将在下边分模块进行说明:

  1. 导航栏背景渐变

Scroll滚动的过程中,在它的onScroll方法里我们通过计算它Y轴的偏移量来改变当前界面的@State修饰的naviAlpha变量值,进而改变顶部标题的背景色,代码实现如下:

// HomeComponent.ets
// 视图滚动的过程中处理导航栏的透明度
onScrollAction() {  this.yOffset = this.scroller.currentOffset().yOffset;  if (this.yOffset > Const.DEFAULT_56) {    this.naviAlpha = 1; } else {    this.naviAlpha = this.yOffset / Const.DEFAULT_56;}
}

2.日历组件

日历组件主要用到的是一个横向滑动的Scroll组件。

// WeekCalendarComponent.etsbuild() {    Row() {      Column() {        Row() {...}             Scroll(this.scroller) {          Row() {            ForEach(this.homeStore.dateArr, (item: WeekDateModel, index?: number) => {              Column() {                Text(item.weekTitle)                  .fontColor(sameDate(item.date, this.homeStore.showDate) ? $r('app.color.blueColor') : $r('app.color.titleColor'))                                 Divider().color(sameDate(item.date, this.homeStore.showDate) ? $r('app.color.blueColor') : $r('app.color.white'))                Image(this.getProgressImg(item))                               } .onClick(() => WeekCalendarMethods.calenderItemClickAction(item, index, this.homeStore))            })          }       }...               .onScrollEdge((event) => this.onScrollEdgeAction(event))      }...       }...    }

手动滑动页面时,我们通过在onScrollEnd方法里计算Scroll的偏移量来实现分页的效果,同时Scroll有提供scrollPage()方法可供我们点击左右按钮的时候来进行页面切换。

// WeekCalendarComponent.ets
import display from '@ohos.display';
...
// scroll滚动停止时通过判断偏移量进行分页处理
onScrollEndAction() {if (this.isPageScroll === false) {let page = Math.round(this.scroller.currentOffset().xOffset / this.scrollWidth);page = (this.isLoadMore === true) ? page + 1 : page;if (this.scroller.currentOffset().xOffset % this.scrollWidth != 0 || this.isLoadMore === true) {let xOffset = page * this.scrollWidth;this.scroller.scrollTo({ xOffset, yOffset: 0 } as ScrollTo);this.isLoadMore = false;}this.currentPage = this.homeStore.dateArr.length / Const.WEEK_DAY_NUM - page - 1;Logger.info('HomeIndex', 'onScrollEnd: page ' + page + ', listLength ' + this.homeStore.dateArr.length);let dayModel: WeekDateModel = this.homeStore.dateArr[Const.WEEK_DAY_NUM * page+this.homeStore.selectedDay];Logger.info('HomeIndex', 'currentItem: ' + JSON.stringify(dayModel) + ', selectedDay  ' + this.homeStore.selectedDay);this.homeStore!.setSelectedShowDate(dayModel!.date!.getTime());}this.isPageScroll = false;
}

我们在需要在Scroll滑动到左边边缘的时候去请求更多的历史数据以便Scroll能一直滑动,通过Scroll的onScrollEdge方法我们可以判断它是否已滑到边缘位置。

// WeekCalendarComponent.ets
onScrollEdgeAction(side: Edge) {if (side === Edge.Top && this.isPageScroll === false) {Logger.info('HomeIndex', 'onScrollEdge: currentPage ' + this.currentPage);if ((this.currentPage + 2) * Const.WEEK_DAY_NUM >= this.homeStore.dateArr.length) {Logger.info('HomeIndex', 'onScrollEdge: load more data');let date: Date = new Date(this.homeStore.showDate);date.setDate(date.getDate() - Const.WEEK_DAY_NUM);this.homeStore.getPreWeekData(date, () => {});this.isLoadMore = true;}}
}

homeStore主要是请求数据库的数据并对数据进行处理进而渲染到界面上。

// HomeViewModel.ets
public getPreWeekData(date: Date, callback: Function) {let weekCalendarInfo: WeekCalendarInfo = getPreviousWeek(date);// 请求数据库数据DayInfoApi.queryList(weekCalendarInfo.strArr, (res: DayInfo[]) => {// 数据处理...  this.dateArr = weekCalendarInfo.arr.concat(...this.dateArr);})
}

同时我们还需要知道怎么根据当天的日期计算出本周内的所有日期数据。

// WeekCalendarModel.ets
export function getPreviousWeek(showDate: Date): WeekCalendarInfo {Logger.debug('WeekCalendarModel', 'get week date by date: ' + showDate.toDateString());let weekCalendarInfo: WeekCalendarInfo = new WeekCalendarInfo();let arr: Array<WeekDateModel> = [];let strArr: Array<string> = [];let currentDay = showDate.getDay() - 1;// 由于date的getDay()方法返回的是0-6代表周日到周六,我们界面上展示的周一-周日为一周,所以这里要将getDay()数据偏移一天let currentDay = showDate.getDay() - 1;if (showDate.getDay() === 0) {currentDay = 6;}// 将日期设置为当前周第一天的数据(周一)showDate.setDate(showDate.getDate() - currentDay);for (let index = WEEK_DAY_NUM; index > 0; index--) {let tempDate = new Date(showDate);tempDate.setDate(showDate.getDate() - index);let dateStr = dateToStr(tempDate);strArr.push(dateStr);arr.push(new WeekDateModel(WEEK_TITLES[tempDate.getDay()], dateStr, tempDate));}Logger.debug('WeekCalendarModel', JSON.stringify(arr));weekCalendarInfo.arr = arr;weekCalendarInfo.strArr = strArr;return weekCalendarInfo;
}
  1. 悬浮按钮

由于首页右下角有一个悬浮按钮,所以首页整体我们用了一个Stack组件,将右下角的悬浮按钮和顶部的title放在滚动组件层的上边。

// HomeComponent.ets
build() {  Stack() {    Scroll(this.scroller) {      Column() {     ...   // 上部界面组件Column() {          ForEach(this.homeStore.getTaskListOfDay(), (item: TaskInfo) => {            TaskCard({taskInfoStr: JSON.stringify(item),clickAction: (isClick: boolean) => this.taskItemAction(item, isClick)})...}, (item: TaskInfo) => JSON.stringify(item))} }   }}.onScroll(() => {this.onScrollAction()})// 悬浮按钮AddBtn({ clickAction: () => {this.editTaskAction()} }) // 顶部title Row() {Text($r('app.string.EntryAbility_label')).titleTextStyle().fontSize($r('app.float.default_24')).padding({ left: Const.THOUSANDTH_66 })}.width(Const.THOUSANDTH_1000).height(Const.DEFAULT_56).position({ x: 0, y: 0 }).backgroundColor(`rgba(${WHITE_COLOR_0X},${WHITE_COLOR_0X},${WHITE_COLOR_0X},${this.naviAlpha})`)CustomDialogView()
} 
.allSize() 
.backgroundColor($r('app.color.primaryBgColor'))

4.界面跳转及传参

首页任务列表长按时需要跳转到对应的任务编辑界面,同时点击悬浮按钮时需要跳转到任务列表页面。

页面跳转需要在头部引入router。

// HomeComponent.ets import router from '@ohos.router';
  1. 任务item的点击事件代码如下
  2. // HomeComponent.ets taskItemAction(item: TaskInfo, isClick: boolean): void { if (!this.homeStore.checkCurrentDay()) { return; } if (isClick) { // 点击任务打卡 let callback: CustomDialogCallback = { confirmCallback: (taskTemp: TaskInfo) => { this.onConfirm(taskTemp) }, cancelCallback: () => { } }; this.broadCast.emit(BroadCastType.SHOW_TASK_DETAIL_DIALOG, [item, callback]); } else { // 长按编辑任务 let editTaskStr: string = JSON.stringify(TaskMapById[item.taskID - 1]); let editTask: ITaskItem = JSON.parse(editTaskStr); ... router.pushUrl({ url: 'pages/TaskEditPage', params: { params: JSON.stringify(editTask) } }); } }

任务创建与编辑

本节将介绍如何创建和编辑健康生活任务。

功能概述

用户点击悬浮按钮进入任务列表页,点击任务列表可进入对应任务编辑的页面中,对任务进行详细的设置,之后点击完成按钮编辑任务后将返回首页。实现效果如下图:

任务列表与编辑任务

这里主要为大家介绍添加任务列表页的实现、任务编辑的实现、以及具体弹窗设置和编辑完成功能的逻辑实现。

任务列表页

任务列表页由包括上部分的标题、返回按钮以及正中间的任务列表组成。实现效果如图:

使用Navigation以及List组件构成元素,ForEach遍历生成具体列表。这里是Navigation构成页面导航:

// TaskListPage.ets
Navigation() {Column() {// 页面中间的列表TaskList() }.width(Const.THOUSANDTH_1000).justifyContent(FlexAlign.Center)
}
.size({ width: Const.THOUSANDTH_1000, height: Const.THOUSANDTH_1000 })
.title(Const.ADD_TASK_TITLE)
.titleMode(NavigationTitleMode.Mini)

列表右侧有一个判断是否开启的文字标识,点击某个列表需要跳转到对应的任务编辑页里。具体的列表实现如下:

// TaskListComponent.ets@Component
export default struct TaskList {...build() {List({ space: Const.LIST_ITEM_SPACE }) {ForEach(this.taskList, (item: ITaskItem) => {ListItem() {Row() {Row() {Image(item?.icon)...Text(item?.taskName).fontSize(Const.DEFAULT_20).fontColor($r('app.color.titleColor'))}.width(Const.THOUSANDTH_500)Blank()...// 状态改变if (item?.isOpen) {Text($r('app.string.already_open'))...}Image($r('app.media.ic_right_grey'))...}...}....onClick(() => {router.pushUrl({url: 'pages/TaskEditPage',params: {params: formatParams(item),}})})...}, (item: ITaskItem) => JSON.stringify(item))}...}
}

任务编辑页

任务编辑页由上方的“编辑任务”标题以及返回按钮,主体内容的List配置项和下方的完成按钮组成,实现效果如图:

由于每一个配置项功能不相同,且逻辑复杂,故将其拆分为五个独立的组件。

这是任务编辑页面,由Navigation和一个自定义组件TaskDetail构成:

// TaskEditPage.ets
Navigation() {Column() {TaskDetail()}.width(Const.THOUSANDTH_1000).height(Const.THOUSANDTH_1000)
}
.size({ width: Const.THOUSANDTH_1000, height: Const.THOUSANDTH_1000 })
.title(Const.EDIT_TASK_TITLE)
.titleMode(NavigationTitleMode.Mini)

自定义组件由List以及其子组件ListItem构成:

// TaskDetailComponent.ets
List({ space: Const.LIST_ITEM_SPACE }) {ListItem() {TaskChooseItem()}...ListItem() {TargetSetItem()}...ListItem() {OpenRemindItem()}...ListItem() {RemindTimeItem()}...ListItem() {FrequencyItem()}...
}
.width(Const.THOUSANDTH_940)

其中做了禁用判断,需要任务打开才可以点击编辑:

// TaskDetailComponent.ets
.enabled(this.settingParams?.isOpen
)

一些特殊情况的禁用,如每日微笑、每日刷牙的目标设置不可编辑:

// TaskDetailComponent.ets
.enabled(this.settingParams?.isOpen&& this.settingParams?.taskID !== taskType.smile&& this.settingParams?.taskID !== taskType.brushTeeth
)

提醒时间在开启提醒打开之后才可以编辑:

// TaskDetailComponent.ets
.enabled(this.settingParams?.isOpen && this.settingParams?.isAlarm)

设置完成之后,点击完成按钮,会向数据库更新现在进行改变的状态信息,并执行之后的逻辑判断:

// TaskDetailComponent.ets
addTask(taskInfo, context).then((res: number) => {GlobalContext.getContext().setObject('taskListChange', true);// 成功的状态,成功后跳转首页router.back({url: 'pages/MainPage', params: {editTask: this.backIndexParams(),}})Logger.info('addTaskFinished', JSON.stringify(res));
}).catch((error: Error) => {// 失败的状态,失败后弹出提示,并打印错误日志prompt.showToast({message: Const.SETTING_FINISH_FAILED_MESSAGE})Logger.error('addTaskFailed', JSON.stringify(error));
})

任务编辑弹窗

弹窗由封装的自定义组件CustomDialogView注册事件,并在点击对应的编辑项时进行触发,从而打开弹窗。

CustomDialogView引入实例并注册事件:

// TaskDialogView.ets
targetSettingDialog: CustomDialogController = new CustomDialogController({ builder: TargetSettingDialog(),autoCancel: true,alignment: DialogAlignment.Bottom,offset: { dx: Const.ZERO, dy: Const.MINUS_20 }
});
...// 注册事件
this.broadCast.on(BroadCastType.SHOW_TARGET_SETTING_DIALOG, () => {this.targetSettingDialog.open();
})

点击对应的编辑项进行触发:

// TaskDetailComponent.ets
.onClick(() => {this.broadCast.emit(BroadCastType.SHOW_TARGET_SETTING_DIALOG);
})

自定义弹窗的实现:

任务目标设置的弹窗较为特殊,故单独拿出来说明。

因为任务目标设置有三种类型:

  • 早睡早起的时间
  • 喝水的量度
  • 吃苹果的个数

如下图所示:

故根据任务的ID进行区分,将同一弹窗复用:

// TaskSettingDialog.ets
if ([taskType.getup, taskType.sleepEarly].indexOf(this.settingParams?.taskID) > Const.HAS_NO_INDEX) {TimePicker({selected: new Date(`${new Date().toDateString()} 8:00:00`),}).height(Const.THOUSANDTH_800).useMilitaryTime(true).onChange((value: TimePickerResult) => {this.currentTime = formatTime(value);})
} else {TextPicker({ range: this.settingParams?.taskID === taskType.drinkWater ? this.drinkRange : this.appleRange }).width(Const.THOUSANDTH_900,).height(Const.THOUSANDTH_800,).onChange((value) => {this.currentValue = value?.split(' ')[0];})
}

弹窗确认的时候将修改好的值赋予该项设置,如不符合规则,将弹出提示:

// TaskSettingDialog.ets
// 校验规则
compareTime(startTime: string, endTime: string) {if (returnTimeStamp(this.currentTime) < returnTimeStamp(startTime) ||returnTimeStamp(this.currentTime) > returnTimeStamp(endTime)) {prompt.showToast({message: Const.CHOOSE_TIME_OUT_RANGE})return false;}return true;
}
// 设置修改项
setTargetValue() {if (this.settingParams?.taskID === taskType.getup) {if (!this.compareTime(Const.GET_UP_EARLY_TIME, Const.GET_UP_LATE_TIME)) {return;}this.settingParams.targetValue = this.currentTime;return;}if (this.settingParams?.taskID === taskType.sleepEarly) {if (!this.compareTime(Const.SLEEP_EARLY_TIME, Const.SLEEP_LATE_TIME)) {return;}this.settingParams.targetValue = this.currentTime;return;}this.settingParams.targetValue = this.currentValue;
}

其余弹窗实现基本类似,这里不再赘述。

后台代理提醒

健康生活App中提供了任务提醒功能,我们用系统提供的后台代理提醒reminderAgent接口完成相关的开发。

说明: 后台代理提醒接口需要在module.json5中申请ohos.permission.PUBLISH_AGENT_REMINDER权限,代码如下:

// module.json5
"requestPermissions": [{"name": "ohos.permission.PUBLISH_AGENT_REMINDER"}
]

后台代理提醒entry\src\main\ets\service\ReminderAgent.ts文件中提供了发布提醒任务、查询提醒任务、删除提醒任务三个接口供任务编辑页面调用,跟随任务提醒的开关增加、更改、删除相关后台代理提醒,代码如下:

// ReminderAgent.ets
import reminderAgent from '@ohos.reminderAgentManager';
import notification from '@ohos.notificationManager';
import preferences from '@ohos.data.preferences';
import Logger from '../common/utils/Logger';
import { CommonConstants as Const } from '../common/constants/CommonConstants';
import ReminderInfo from '../viewmodel/ReminderInfo';
import PublishReminderInfo from '../viewmodel/PublishReminderInfo';// 发布提醒
function publishReminder(params: PublishReminderInfo, context: Context) {if (!params) {Logger.error(Const.REMINDER_AGENT_TAG, 'publishReminder params is empty');return;}let notifyId: string = params.notificationId.toString();hasPreferencesValue(context, notifyId, (preferences: preferences.Preferences, hasValue: boolean) => {if (hasValue) {preferences.get(notifyId, -1, (error: Error, value: preferences.ValueType) => {if (typeof value !== 'number') {return;}if (value >= 0) {reminderAgent.cancelReminder(value).then(() => {processReminderData(params, preferences, notifyId);}).catch((err: Error) => {Logger.error(Const.REMINDER_AGENT_TAG, `cancelReminder err: ${err}`);});} else {Logger.error(Const.REMINDER_AGENT_TAG, 'preferences get value error ' + JSON.stringify(error));}});} else {processReminderData(params, preferences, notifyId);}});
}// 取消提醒
function cancelReminder(reminderId: number, context: Context) {if (!reminderId) {Logger.error(Const.REMINDER_AGENT_TAG, 'cancelReminder reminderId is empty');return;}let reminder: string = reminderId.toString();hasPreferencesValue(context, reminder, (preferences: preferences.Preferences, hasValue: boolean) => {if (!hasValue) {Logger.error(Const.REMINDER_AGENT_TAG, 'cancelReminder preferences value is empty');return;}getPreferencesValue(preferences, reminder);});
}// 可通知ID
function hasNotificationId(params: number) {if (!params) {Logger.error(Const.REMINDER_AGENT_TAG, 'hasNotificationId params is undefined');return;}return reminderAgent.getValidReminders().then((reminders) => {if (!reminders.length) {return false;}let notificationIdList: Array<number> = [];for (let i = 0; i < reminders.length; i++) {let notificationId = reminders[i].notificationId;if (notificationId) {notificationIdList.push(notificationId);}}const flag = notificationIdList.indexOf(params);return flag === -1 ? false : true;});
}function hasPreferencesValue(context: Context, hasKey: string, callback: Function) {let preferencesPromise = preferences.getPreferences(context, Const.H_STORE);preferencesPromise.then((preferences: preferences.Preferences) => {preferences.has(hasKey).then((hasValue: boolean) => {callback(preferences, hasValue);});});
}// 进程提醒数据
function processReminderData(params: PublishReminderInfo, preferences: preferences.Preferences, notifyId: string) {let timer = fetchData(params);reminderAgent.publishReminder(timer).then((reminderId: number) => {putPreferencesValue(preferences, notifyId, reminderId);}).catch((err: Error) => {Logger.error(Const.REMINDER_AGENT_TAG, `publishReminder err: ${err}`);});
}// 获取数据
function fetchData(params: PublishReminderInfo): reminderAgent.ReminderRequestAlarm {return {reminderType: reminderAgent.ReminderType.REMINDER_TYPE_ALARM,hour: params.hour || 0,minute: params.minute || 0,daysOfWeek: params.daysOfWeek || [],wantAgent: {pkgName: Const.PACKAGE_NAME,abilityName: Const.ENTRY_ABILITY},title: params.title || '',content: params.content || '',notificationId: params.notificationId || -1,slotType: notification.SlotType.SOCIAL_COMMUNICATION}
}function putPreferencesValue(preferences: preferences.Preferences, putKey: string, putValue: number) {preferences.put(putKey, putValue).then(() => {preferences.flush();}).catch((error: Error) => {Logger.error(Const.REMINDER_AGENT_TAG, 'preferences put value error ' + JSON.stringify(error));});
}function getPreferencesValue(preferences: preferences.Preferences, getKey: string) {preferences.get(getKey, -1).then((value: preferences.ValueType) => {if (typeof value !== 'number') {return;}if (value >= 0) {reminderAgent.cancelReminder(value).then(() => {Logger.info(Const.REMINDER_AGENT_TAG, 'cancelReminder promise success');}).catch((err: Error) => {Logger.error(Const.REMINDER_AGENT_TAG, `cancelReminder err: ${err}`);});}}).catch((error: Error) => {Logger.error(Const.REMINDER_AGENT_TAG, 'preferences get value error ' + JSON.stringify(error));});
}const reminder = {publishReminder: publishReminder,cancelReminder: cancelReminder,hasNotificationId: hasNotificationId
} as ReminderInfoexport default reminder;

实现打卡功能

首页会展示当前用户已经开启的任务列表,每条任务会显示对应的任务名称以及任务目标、当前任务完成情况。用户只可对当天任务进行打卡操作,用户可以根据需要对任务列表中相应的任务进行点击打卡。如果任务列表中的每个任务都在当天完成则为连续打卡一天,连续打卡多天会获得成就徽章。打卡效果如下图所示:

任务列表

使用List组件展示用户当前已经开启的任务,每条任务对应一个TaskCard组件,clickAction包装了点击和长按事件,用户点击任务卡时会触发弹起打卡弹窗,从而进行打卡操作;长按任务卡时会跳转至任务编辑界面,对相应的任务进行编辑处理。代码如下:

// HomeComponent.ets
// 任务列表
ForEach(this.homeStore.getTaskListOfDay(), (item: TaskInfo) => {TaskCard({taskInfoStr: JSON.stringify(item),clickAction: (isClick: boolean) => this.taskItemAction(item, isClick)}).margin({ bottom: Const.DEFAULT_12 }).height($r('app.float.default_64'))
}, (item: TaskInfo) => JSON.stringify(item))
...
CustomDialogView() // 自定义弹窗中间件

自定义弹窗中间件CustomDialogView

在组件CustomDialogView的aboutToAppear生命周期中注册SHOW_TASK_DETAIL_DIALOG的事件回调方法 ,当通过emit触发此事件时即触发回调方法执行。代码如下:

// CustomDialogView.ets
export class CustomDialogCallback {confirmCallback: Function = () => {};cancelCallback: Function = () => {};
}@Component
export struct CustomDialogView {@State isShow: boolean = false;@Provide achievementLevel: number = 0;@Consume broadCast: BroadCast;@Provide currentTask: TaskInfo = TaskItem;@Provide dialogCallBack: CustomDialogCallback = new CustomDialogCallback();// 成就对话框achievementDialog: CustomDialogController = new CustomDialogController({builder: AchievementDialog(),autoCancel: true,customStyle: true});// 任务时钟对话框taskDialog: CustomDialogController = new CustomDialogController({builder: TaskDetailDialog(),autoCancel: true,customStyle: true});aboutToAppear() {Logger.debug('CustomDialogView', 'aboutToAppear');// 成就对话框this.broadCast.on(BroadCastType.SHOW_ACHIEVEMENT_DIALOG, (achievementLevel: number) => {Logger.debug('CustomDialogView', 'SHOW_ACHIEVEMENT_DIALOG');this.achievementLevel = achievementLevel;this.achievementDialog.open();});// 任务时钟对话框this.broadCast.on(BroadCastType.SHOW_TASK_DETAIL_DIALOG,(currentTask: TaskInfo, dialogCallBack: CustomDialogCallback) => {Logger.debug('CustomDialogView', 'SHOW_TASK_DETAIL_DIALOG');this.currentTask = currentTask || TaskItem;this.dialogCallBack = dialogCallBack;this.taskDialog.open();});}aboutToDisappear() {Logger.debug('CustomDialogView', 'aboutToDisappear');}build() {}
}

点击任务卡片

点击任务卡片会emit触发 “SHOW_TASK_DETAIL_DIALOG” 事件,同时把当前任务,以及确认打卡回调方法传递下去。代码如下:

// HomeComponent.ets
// 任务卡片事件
taskItemAction(item: TaskInfo, isClick: boolean): void {...if (isClick) {// 点击任务打卡let callback: CustomDialogCallback = { confirmCallback: (taskTemp: TaskInfo) => {this.onConfirm(taskTemp)}, cancelCallback: () => {} };// 触发弹出打卡弹窗事件  并透传当前任务参数(item) 以及确认打卡回调this.broadCast.emit(BroadCastType.SHOW_TASK_DETAIL_DIALOG, [item, callback]);} else {// 长按编辑任务...}
}
// 确认打卡
onConfirm(task) {this.homeStore.taskClock(task).then((res: AchievementInfo) => {// 打卡成功后 根据连续打卡情况判断是否 弹出成就勋章  以及成就勋章级别if (res.showAchievement) {// 触发弹出成就勋章SHOW_ACHIEVEMENT_DIALOG 事件, 并透传勋章类型级别let achievementLevel = res.achievementLevel;if (achievementLevel) {this.broadCast.emit(BroadCastType.SHOW_ACHIEVEMENT_DIALOG, achievementLevel);} else {this.broadCast.emit(BroadCastType.SHOW_ACHIEVEMENT_DIALOG);}}})
}

打卡弹窗组件TaskDetailDialog

打卡弹窗组件根据当前任务的ID获取任务名称以及弹窗背景图片资源。

打卡弹窗组件由两个小组件构成,代码如下:

// TaskDetailDialog.ets
Column() {// 展示任务的基本信息TaskBaseInfo({taskName: TaskMapById[this.currentTask?.taskID - 1].taskName  // 根据当前任务ID获取任务名称});// 打卡功能组件 (任务打卡、关闭弹窗)TaskClock({confirm: () => {this.dialogCallBack.confirmCallback(this.currentTask);this.controller.close();},cancel: () => {this.controller.close();},showButton: this.showButton})
}
...

TaskBaseInfo组件代码如下:

// TaskDetailDialog.ets
@Component
struct TaskBaseInfo {taskName: string | Resource = '';build() {Column({ space: Const.DEFAULT_8 }) {Text(this.taskName).fontSize($r('app.float.default_22')).fontWeight(FontWeight.Bold).fontFamily($r('app.string.HarmonyHeiTi_Bold')).taskTextStyle().margin({left: $r('app.float.default_12')})}.position({ y: $r('app.float.default_267') })}
}

TaskClock组件代码如下:

// TaskDetailDialog.ets
@Component
struct TaskClock {confirm: Function = () => {};cancel: Function = () => {};showButton: boolean = false;build() {Column({ space: Const.DEFAULT_12 }) {Button() {Text($r('app.string.clock_in')).height($r('app.float.default_42')).fontSize($r('app.float.default_20')).fontWeight(FontWeight.Normal).textStyle()}.width($r('app.float.default_220')).borderRadius($r('app.float.default_24')).backgroundColor('rgba(255,255,255,0.40)').onClick(() => {GlobalContext.getContext().setObject('taskListChange', true);this.confirm();}).visibility(!this.showButton ? Visibility.None : Visibility.Visible)Text($r('app.string.got_it')).fontSize($r('app.float.default_14')).fontWeight(FontWeight.Regular).textStyle().onClick(() => {this.cancel();})}}
}

打卡接口调用

// HomeViewModel.ets
public async taskClock(taskInfo: TaskInfo) {let taskItem = await this.updateTask(taskInfo);let dateStr = this.selectedDayInfo?.dateStr;// 更新任务失败if (!taskItem) {return {achievementLevel: 0,showAchievement: false} as AchievementInfo;}// 更新当前时间的任务列表this.selectedDayInfo.taskList = this.selectedDayInfo.taskList.map((item) => {return item.taskID === taskItem?.taskID ? taskItem : item;});let achievementLevel: number = 0;if(taskItem.isDone) {// 更新每日任务完成情况数据let dayInfo = await this.updateDayInfo();... // 当日任务完成数量等于总任务数量时 累计连续打卡一天// 更新成就勋章数据 判断是否弹出获得勋章弹出及勋章类型if (dayInfo && dayInfo?.finTaskNum === dayInfo?.targetTaskNum) {achievementLevel = await this.updateAchievement(this.selectedDayInfo.dayInfo);}}...return {achievementLevel: achievementLevel,showAchievement: ACHIEVEMENT_LEVEL_LIST.includes(achievementLevel)} as AchievementInfo;
}
// HomeViewModel.ets
// 更新当天任务列表
updateTask(task: TaskInfo): Promise<TaskInfo> {return new Promise((resolve, reject) => {let taskID = task.taskID;let targetValue = task.targetValue;let finValue = task.finValue;let updateTask = new TaskInfo(task.id, task.date, taskID, targetValue, task.isAlarm, task.startTime,task.endTime, task.frequency, task.isDone, finValue, task.isOpen);let step = TaskMapById[taskID - 1].step; // 任务步长let hasExceed = updateTask.isDone;if (step === 0) { // 任务步长为0 打卡一次即完成该任务updateTask.isDone = true; // 打卡一次即完成该任务updateTask.finValue = targetValue;} else {let value = Number(finValue) + step; // 任务步长非0 打卡一次 步长与上次打卡进度累加updateTask.isDone = updateTask.isDone || value >= Number(targetValue); // 判断任务是否完成updateTask.finValue = updateTask.isDone ? targetValue : `${value}`;}TaskInfoTableApi.updateDataByDate(updateTask, (res: number) => { // 更新数据库if (!res || hasExceed) {Logger.error('taskClock-updateTask', JSON.stringify(res));reject(res);}resolve(updateTask);})})
}

为了帮助大家更深入有效的学习到鸿蒙开发知识点,小编特意给大家准备了一份全套最新版的HarmonyOS NEXT学习资源,获取完整版方式请点击→HarmonyOS教学视频

HarmonyOS教学视频:语法ArkTS、TypeScript、ArkUI等…视频教程

鸿蒙生态应用开发白皮书V2.0PDF:

获取完整版白皮书方式请点击→《鸿蒙生态应用开发白皮书V2.0PDF

在这里插入图片描述

鸿蒙 (Harmony OS)开发学习手册

一、入门必看

  1. 应用开发导读(ArkTS)
  2. .……

在这里插入图片描述


二、HarmonyOS 概念

  1. 系统定义
  2. 技术架构
  3. 技术特性
  4. 系统安全

在这里插入图片描述

三、如何快速入门?《鸿蒙基础入门学习指南》

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. .……

在这里插入图片描述


四、开发基础知识

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. .……

在这里插入图片描述


五、基于ArkTS 开发

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 7.网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. .……

在这里插入图片描述


更多了解更多鸿蒙开发的相关知识可以参考:《鸿蒙 (Harmony OS)开发学习手册

相关文章:

HarmonyOS实战开发-如何实现一个简单的健康生活应用(上)

介绍 本篇Codelab介绍了如何实现一个简单的健康生活应用&#xff0c;主要功能包括&#xff1a; 用户可以创建最多6个健康生活任务&#xff08;早起&#xff0c;喝水&#xff0c;吃苹果&#xff0c;每日微笑&#xff0c;刷牙&#xff0c;早睡&#xff09;&#xff0c;并设置任…...

React中使用antDesign框架

1.在React项目中使用Ant Design&#xff0c;首先需要安装Ant Design: npm install antd --save 2.按需引入Ant Design组件&#xff0c;以减小最终打包的大小。使用babel-plugin-import插件可以实现按需加载。首先安装插件&#xff1a; npm install babel-plugin-import --save-…...

Electron安全防护实战:应对常见安全问题及权限控制措施

Electron安全防护实战&#xff1a;应对常见安全问题及权限控制措施 引言常见安全问题及其危害提升 Electron 应用安全性的措施限制渲染进程权限防止XSS与内容注入加固应用更新流程严格管理硬件权限使用安全的第三方模块加密敏感数据存储实现进程间通信&#xff08;IPC&#xff…...

StringBuffer与StringBuilder

1.区别 (1). String : 不可变字符序列. (2). StringBuffer : 可变字符序列.线程安全&#xff0c;但效率低. (3). StringBuilder : 可变字符序列.线程不安全&#xff0c;但效率高. 既然StringBuffer与StringBuilder都是可变字符序列&#xff0c;但二者咋区分开呢&#xff1f…...

HCIP综合实验拓扑

实验要求 1.R5为ISP&#xff0c;只能进行IP地址配置&#xff0c;其所有地址均配为公有I地址; 2、R1和R5间使用PPP的PAP认证&#xff0c;R5为主认证方: R2与R5之间使用ppp的CHAP认证&#xff0c;R5为主认证方; R3与R5之间使用HDLC封装; 3R1、R2、R3构建一个MGRE环境&#xf…...

nuxt学习

一、遇到的问题 1、nuxt初始化失败问题解决方案 使用npm和pnpm初始化都失败 原因&#xff1a;主机连不上DNS服务器 解决方案 Step1: 打开文件夹 Windows:路径&#xff1a;C:\Windows\System32\drivers\etc Mac: 路径&#xff1a;/etc/hosts Step2: 使用记事本方式打开 …...

VS学习建议

Visual Studio&#xff08;简称VS&#xff09;是由微软公司开发的一款集成开发环境&#xff08;IDE&#xff09;&#xff0c;支持多种编程语言&#xff0c;主要用于Windows平台上的应用程序开发。学习使用Visual Studio涉及多个方面&#xff0c;以下是一些关键的学习内容&#…...

java汇总区间

给定一个 无重复元素 的 有序 整数数组 nums 。 返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说&#xff0c;nums 的每个元素都恰好被某个区间范围所覆盖&#xff0c;并且不存在属于某个范围但不属于 nums 的数字 x 。 列表中的每个区间范围 [a,b] 应该按…...

【笔记】OpenHarmony设备开发:搭建开发环境(Ubuntu 20.04,VirtualBox 7.0.14)

参考&#xff1a;搭建开发环境&#xff08;HarmonyOS Device&#xff09; Note&#xff1a;Windows系统虚拟机中Ubuntu系统安装完成后&#xff0c;根据指导完成Ubuntu20.04基础环境配置&#xff08;HarmonyOS Connect 开发工具系列课&#xff09; 系统要求 Windows系统要求&…...

计算机视觉新巅峰,微软牛津联合提出MVSplat登顶3D重建

开篇&#xff1a;探索稀疏多视图图像的3D场景重建与新视角合成的挑战 3D场景重建和新视角合成是计算机视觉领域的一项基础挑战&#xff0c;尤其是当输入图像非常稀疏&#xff08;例如&#xff0c;只有两张&#xff09;时。尽管利用神经场景表示&#xff0c;例如场景表示网络&a…...

halcon图像腐蚀

1、原理 使用结构元素在图像上移动&#xff0c;只有结构元素上的所有像素点都属于图像中时&#xff0c;才保留结构元素中心点所在的像素&#xff0c;常用于分离连接的两个物体、消除噪声。 2、halcon代码 dev_open_file_dialog (read_image, default, default, Selection) r…...

neo4j使用详解(六、cypher即时时间函数语法——最全参考)

Neo4j系列导航&#xff1a; neo4j及简单实践 cypher语法基础 cypher插入语法 cypher插入语法 cypher查询语法 cypher通用语法 cypher函数语法 6.时间函数-即时类型 表示具体的时刻的时间类型函数 6.1.date函数 年-月-日时间函数&#xff1a; yyyy-mm-dd 6.1.1.获取date da…...

Web 前端性能优化之一:性能模型及网页原理

一、RAIL 性能模型 RAIL性能模型指出了用户对不同延迟时间的感知度&#xff0c;以用户为中心的原则&#xff0c;就是要让用户满意网站或应用的性能体验。 RAIL &#xff1a;响应(Response)、动画(Animation)、空闲(Idle)、加载(Load) RAIL 性能模型 用户感知延迟的时间窗口 1…...

常用的主流好用的WEB自动化测试工具强烈推荐

在业务使用的自动化测试工具很多。有开源的&#xff0c;有商业化的&#xff0c;各有各得特色&#xff0c;各有各得优点&#xff01;下面我就介绍几个我用过的一款非常优秀的国产自动化测试工具。在现有的自动化软件当中&#xff0c;都是以元素的name、id、xpath、class、tag、l…...

分享几个非常不错嵌入式开源项目,一定不要错过

大家好&#xff0c;我是知微&#xff01; 经常有小伙伴后台私信我&#xff1a; 有没有好的开源项目推荐怎么样才能提升自己的编程能力 那么这篇文章就推荐几个还不错的开源项目&#xff0c;感兴趣的小伙伴可以学习一下&#xff01; 日志库EasyLogger https://github.com/ar…...

Golang基础-4

Go语言基础 介绍 基础 数组(array) 数组声明 元素访问与修改 数组遍历 关系运算 切片创建 多维数组 介绍 本文介绍Go语言中数组(array)操作(数组声明、元素访问与修改、数组遍历、关系运算、切片创建、多维数组)等相关知识。 基础 数组 数组是具有相同数据类型的…...

2024软件设计师备考讲义——UML(统一建模语言)

UML的概念 用例图的概念 包含 <<include>>扩展<<exted>>泛化 用例图&#xff08;也可称用例建模&#xff09;描述的是外部执行者&#xff08;Actor&#xff09;所理解的系统功能。用例图用于需求分析阶段&#xff0c;它的建立是系统开发者和用户反复…...

HTML——1.简介、基础、元素

一、简介 HTML&#xff08;HyperText Markup Language&#xff09;是一种用于创建网页的标记语言。它使用标记&#xff08;tag&#xff09;来描述网页的结构和内容。HTML被用于定义网页中的文本、图像、链接、多媒体以及其他元素的排列和呈现方式。 HTML文档是由一系列的HTML…...

Rust 标准库:std::env::args() 函数简介

std::env::args() 是 Rust 标准库中的一个函数&#xff0c;它属于 std::env 模块。这个函数用于获取并返回一个迭代器&#xff0c;该迭代器包含了程序运行时从命令行传入的所有参数。 当你运行一个 Rust 程序并从命令行传递参数时&#xff0c;例如&#xff1a; my_rust_progr…...

【Blockchain】GameFi | NFT

Blockchain GameFiGameFi顶级项目TheSandbox&#xff1a;Decentraland&#xff1a;Axie Infinity&#xff1a; NFTNFT是如何工作的同质化和非同质化区块链协议NFT铸币 GameFi GameFi是游戏和金融的组合&#xff0c;它涉及区块链游戏&#xff0c;对玩家提供经济激励&#xff0c…...

【Docker】搭建安全可控的自定义通知推送服务 - Bark

【Docker】搭建安全可控的自定义通知推送服务 - Bark 前言 本教程基于绿联的NAS设备DX4600 Pro的docker功能进行搭建。 简介 Bark是一款为Apple设备用户设计的开源推送服务应用&#xff0c;它允许开发者、程序员以及一般用户将信息快速推送到他们自己的iPhone、iPad等设备上…...

国内IP代理软件电脑版:深入解析与应用指南

随着互联网技术的快速发展&#xff0c;网络活动日益丰富多样&#xff0c;IP代理软件也因其独特的功能和优势&#xff0c;成为许多电脑用户不可或缺的工具。在国内&#xff0c;由于网络环境的复杂性和特殊性&#xff0c;选择一款稳定、高效的IP代理软件电脑版尤为重要。虎观代理…...

面向对象设计之开闭原则

设计模式专栏&#xff1a; http://t.csdnimg.cn/4Mt4u 目录 1.引言 2.如何理解“对扩展开放、对修改关闭” 3.修改代码就意味着违反开闭原则吗 4.如何做到“对扩展开放、对修改关闭” 5.如何在项目中灵活应用开闭原则 6.总结 1.引言 开闭原则(Open Closed Principle&…...

【项目技术介绍篇】若依项目代码文件结构介绍

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过大学刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0…...

实现DevOps需要什么?

实现DevOps需要什么&#xff1f; 硬性要求&#xff1a;工具上的准备 上文提到了工具链的打通&#xff0c;那么工具自然就需要做好准备。现将工具类型及对应的不完全列举整理如下&#xff1a; 代码管理&#xff08;SCM&#xff09;&#xff1a;GitHub、GitLab、BitBucket、SubV…...

Linux小程序: 手写自己的shell

注意&#xff1a; 本文章只是为了理解shell内部的工作原理&#xff0c; 所以并没有完成shell的所有工作&#xff0c; 只是完成了shell里的一小部分工作 #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include &l…...

javaSwing租户管理系统

简介 欢迎阅读本篇博客&#xff0c;今天我将为大家介绍一个基于Java Swing开发的租户管理系统。该系统具有登录、注册、添加租户、查询租户信息、修改租户信息、删除租户、修改密码、退出登录等功能模块&#xff0c;旨在提供一个便捷的租户管理解决方案。 一、项目介绍 该租…...

cesium实现竖立的圆

cesium中的圆是平行于地面的&#xff0c;想实现竖起来的圆可以使用ellipsoid&#xff0c;设置其中一个轴的radii值为一个很小的值&#xff0c;比如0.00001&#xff0c;则这个轴上的宽度就会非常小&#xff0c;看起来就是一个圆面。 一、画圆ellipse&#xff0c;此处也把画圆的代…...

汽车电子行业知识:智能汽车电子架构

文章目录 3.智能汽车电子架构3.1.汽车电子概念及发展3.2.汽车电子架构类型3.2.1.博世汽车电子架构3.2.2.联合电子未来汽车电子架构3.2.3.安波福汽车电子架构3.2.4.丰田汽车电子架构3.2.5.华为汽车电子架构 3.智能汽车电子架构 3.1.汽车电子概念及发展 汽车电子是车体汽车电子…...

LeetCode146:LRU缓存

leetCode&#xff1a;146. LRU 缓存 题目描述 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中&#x…...

chown wordpress/海南网站设计

大数据技术与架构点击右侧关注&#xff0c;大数据开发领域最强公众号&#xff01;暴走大数据点击右侧关注&#xff0c;暴走大数据&#xff01;By 大数据技术与架构场景描述&#xff1a;如果一个task在处理过程中挂掉了&#xff0c;那么它在内存中的状态都会丢失&#xff0c;所有…...

机构改革后政府网站建设方案/西安seo代运营

转自http://www.opsers.org/base/learning-linux-the-day-that-the-system-configuration-in-the-rhel6-disk-array-raid.html磁盘阵列全名是: Redundant Arrays of Inexpensive Disks, RAID &#xff0c;大概的意思是&#xff1a;廉价的磁盘冗余阵列。 RAID 可以通过一个技术(…...

7k7k传奇世界网页版/惠州seo怎么做

新人刚来&#xff0c;带给大家一些福利&#xff0c;希望大家多关注我的博客&#xff0c;我会不定期的发放一些资料免费给大家&#xff01;http://yunpan.cn/cjHYq4bUuJqub 访问密码 1d6b c语言视频http://yunpan.cn/cjHYWnaGtFKK2 访问密码 0afb armlinux 书籍和视频h…...

网站反向代理怎么做/百度公司怎么样

5 种使用 Python 代码轻松实现数据可视化的方法#故事数据可视化PYTHON数据可视化是数据科学家工作中的重要组成部分。在项目的早期阶段&#xff0c;你通常会进行探索性数据分析&#xff08;Exploratory Data Analysis&#xff0c;EDA&#xff09;以获取对数据的一些理解。创建可…...

网站开发过程中的功能需求分析/黑帽友情链接

文章目录背景Sparse eigenvectors(单个向量的稀疏化)初始问题&#xff08;low-rank的思想&#xff1f;&#xff09;等价问题最小化$\lambda$ 得到下列问题&#xff08;易推&#xff09;再来一个等价问题条件放松&#xff08;凸化&#xff09;A robustness interpretation收缩关…...

电脑网站建设/百度域名注册官网

位置问题&#xff1a; 在bootstrap中用 datetimepicker 时默认是在输入框下面弹出的&#xff0c; 但是遇到输入框在屏幕最下面时&#xff0c;日期选择框会有一部分在屏幕下面&#xff0c;显示不了&#xff0c;因此需要能够从上面弹出。 可以在初始化时&#xff1a; $(.form_dat…...