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

获取成就

本节将介绍成就页面。

功能概述

成就页面展示用户可以获取的所有勋章,当用户满足一定的条件时,将点亮本页面对应的勋章,没有得到的成就勋章处于熄灭状态。共有六种勋章,当用户连续完成任务打卡3天、7天、30天、50天、73天、99天时,可以获得对应的“连续xx天达成”勋章。

页面布局与 ArkTS 代码对应关系

效果如图所示:

标题部分TitleBar是一个横向容器Row里包含一个子组件Text。

// TitleBarComponent.ets
@Component
export struct TitleBar {build() {Row() {Text($r('app.string.achievement')).fontSize($r('app.float.default_24')).fontColor($r('app.color.white')).align(Alignment.Start).padding({left: Const.ACHIEVE_TITLE_BAR_LEFT,top: Const.ACHIEVE_TITLE_BAR_TOP})}.width(Const.FULL_WIDTH)}
}

每个勋章卡片BadgeCard是由一个竖向容器Column、一个图片子组件Image和一个文字子组件Text组成。

// BadgeCardComponent.ets
@Component
export struct BadgeCard {@Prop content: string = '';imgSrc: Resource = $r('app.string.empty');build() {Column({space: Const.DEFAULT_18}) {Image(this.imgSrc).width(Const.FULL_WIDTH).height(Const.ACHIEVE_CARD_IMG_HEIGHT).objectFit(ImageFit.Contain)Text($r('app.string.task_achievement_level', Number(this.content))).lineHeight($r('app.float.default_16')).fontSize($r('app.float.default_12')).fontColor($r('app.color.white'))}.width(ratio2percent(Const.ACHIEVE_SPLIT_RATIO)).padding({top: Const.ACHIEVE_CARD_TOP, bottom: Const.ACHIEVE_CARD_BOTTOM})}
}

整体的勋章面板使用Flex一个组件即可以实现均分和换行的功能。

// BadgePanelComponent.ets
@Component
export struct BadgePanel {@StorageProp(ACHIEVEMENT_LEVEL_KEY) successiveDays: number = 0;aboutToAppear() {Logger.debug('BadgePanel','aboutToAppear');getAchievementLevel();}build() {Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {ForEach(getBadgeCardItems(this.successiveDays), (item: CardInfo) => {BadgeCard({ content: item.titleContent, imgSrc: item.achievement})})}.width(Const.FULL_WIDTH)}
}

获取数据

进入界面第一次获取数据在aboutToAppear()声明周期中从数据库GlobalInfo表中获取存储的勋章数据, 通过@StorageProp装饰器刷新界面,其他的地方只要通过AppStorage更新勋章数据即可。

// BadgePanelComponent.ets
aboutToAppear() {Logger.debug('BadgePanel','aboutToAppear');getAchievementLevel();
}// AchieveModel.ets
export function getAchievementLevel() {GlobalInfoApi.query((res: GlobalInfo) => {let globalInfo: GlobalInfo = res;let achievementStr = globalInfo.achievements??'';let achievements = achievementStr.split(',');if (achievements.length > 0) {AppStorage.Set<Number>(ACHIEVEMENT_LEVEL_KEY, Number(achievements[achievements.length - 1]));}})
}// BadgePanelComponent.ets
@StorageProp(ACHIEVEMENT_LEVEL_KEY) successiveDays: number = 0;ForEach(getBadgeCardItems(this.successiveDays), (item: CardInfo) => {BadgeCard({ content: item.titleContent, imgSrc: item.achievement})
})// AchievementViewModel.ets
export function getBadgeCardItems(successiveDays: number): Array<CardInfo> {let badgeMileStones = ACHIEVEMENT_LEVEL_LIST;let cardItems: Array<CardInfo> = [];for (let i = 0; i < badgeMileStones.length; i++) {let onOrOff = successiveDays >= badgeMileStones[i] ? 'on' : 'off';let titleContent = String(badgeMileStones[i]);let cardInfo: CardInfo = new CardInfo();cardInfo.titleContent = titleContent;cardInfo.achievement = getAchievement(`${ onOrOff }_${ badgeMileStones[i] }`);cardItems.push(cardInfo);}return cardItems;
}

搭建关系型数据库

本节将介绍如何调用关系型数据库接口在本地搭建数据库,并读写相应的用户数据。

创建数据库

要使用关系型数据库存储用户数据,首先要进行数据库的创建,并提供基本的增、删、查、改接口。

导入关系型数据库模块:

import data_rdb from '@ohos.data.rdb';

关系型数据库提供以下两个基本功能:

获取RdbStore

首先要获取一个RdbStore来操作关系型数据库,代码如下:

// RdbHelperImp.ets
getRdb(context: Context): Promise<RdbHelper> {this.storeConfig = {name: this.mDatabaseName, securityLevel: dataRdb.SecurityLevel.S1};return new Promise<RdbHelper>((success, error) => {dataRdb.getRdbStore(context, this.storeConfig).then(dbStore => {this.rdbStore = dbStore;success(this);}).catch((err: Error) => {Logger.error(`initRdb err : ${JSON.stringify(err)}`);error(err);})})
}

封装增、删、改、查接口

关系型数据库接口提供的增、删、改、查操作均有callback和Promise两种异步回调方式,本Codelab使用了callback异步回调,其中插入数据使用了insert()接口,实现代码如下:

// RdbHelperImp.ets
insert(tableName: string, values: dataRdb.ValuesBucket | Array<dataRdb.ValuesBucket>): Promise<number> {return new Promise<number>((success, error) => {Logger.info(`insert tableName : ${tableName}, values : ${JSON.stringify(values)}`);if (!values) {Logger.info(`insert failed, values is undefined`);error(0);return;}if (values instanceof Array) {Logger.info(`insert values isArray = ${values.length}`);this.rdbStore.beginTransaction();this.saveArray(tableName, values).then(data => {Logger.info(`insert success, data : ${JSON.stringify(data)}`);success(data);this.rdbStore.commit();}).catch((err: Error) => {Logger.error(`insert failed, err : ${err}`);error(err);this.rdbStore.commit();})} else {this.rdbStore.insert(tableName, values).then(data => {Logger.info(`insert success id : ${data}`);success(data);his.rdbStore.commit();}).catch((err: Error) => {Logger.error(`insert failed, err : ${JSON.stringify(err)}`);error(err);this.rdbStore.commit();})}})
}

删除数据使用了delete()接口,实现代码如下:

// RdbHelperImp.ets
delete(rdbPredicates: dataRdb.RdbPredicates): Promise<number> {Logger.info(`delete rdbPredicates : ${JSON.stringify(rdbPredicates)}`);return this.rdbStore.delete(rdbPredicates);
}

更新数据使用了update()接口,实现代码如下:

// RdbHelperImp.ets
update(values: dataRdb.ValuesBucket, rdbPredicates: dataRdb.RdbPredicates): Promise<number> {return this.rdbStore.update(values, rdbPredicates);
}

查找数据使用了query()接口,实现代码如下:

// RdbHelperImp.ets
query(rdbPredicates: dataRdb.RdbPredicates, columns?: Array<string>): Promise<dataRdb.ResultSet> {Logger.info(`query rdbPredicates : ${JSON.stringify(rdbPredicates)}`);return this.rdbStore.query(rdbPredicates, columns);
}

数据库表结构

根据健康生活APP的使用场景和业务逻辑,定义了三个数据对象,并使用三张数据表来存储,分别是健康任务信息表、每日信息表和全局信息表。

健康任务信息表

目前健康生活应用提供了6个基本的健康任务,分别是早起、喝水、吃苹果、每日微笑、睡前刷牙和早睡。用户可以选择开启或关闭某个任务,开启的任务可以选择是否开启提醒,在指定的时间段内提醒用户进行打卡。任务也可以选择开启的频率,如只在周一到周五开启等。需要记录每项任务的目标值和实际完成值,在用户打卡后判断任务是否已经完成,并记录在数据库中。因此,需要创建一张存储每天的健康任务信息的表,表头如下:

每日信息表

在主页面,用户可以查看当天健康任务的完成进度,需要创建一张表记录当天开启的任务个数和已经完成的任务个数,表头如下:

全局信息表

用户连续多日打卡完成所有创建的任务可以获得相应的成就,因此,需要有一张表记录连续打卡天数和已达成的成就项。另外,考虑应用多日未打开的情况,需要记录应用第一次打开的日期和最后一次打开的日期以向数据库回填数据,表头如下:

创建数据表

根据6.2中设计的表结构,创建对应的数据表,实现对相应数据的读写操作。

健康任务信息数据表

在获取RdbStore后,需要使用executeSql接口执行SQL语句来创建相应的表结构和初始化数据,SQL语句如下:

CREATE TABLE IF NOT EXISTS taskInfo(id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, taskID INTEGER, targetValue TEXT NOT NULL, isAlarm BOOLEAN, startTime TEXT NOT NULL, endTime TEXT NOT NULL, frequency TEXT NOT NULL, isDone BOOLEAN, finValue TEXT NOT NULL, isOpen BOOLEAN
)

健康任务信息数据表需要提供插入数据的接口,以在用户当天第一次打开应用时创建当天的健康任务信息,实现代码如下:

// TaskInfoApi.ets
insertData(taskInfo: TaskInfo, callback: Function): void {// 根据输入数据创建待插入的数据行const valueBucket = generateBucket(taskInfo);RdbUtils.insert('taskInfo', valueBucket).then(result => {callback(result);});Logger.info('TaskInfoTable', `Insert taskInfo {${taskInfo.date}:${taskInfo.taskID}} finished.`);
}

其中generateBucket()代码如下:

// TaskInfoApi.ets
function generateBucket(taskInfo: TaskInfo): dataRdb.ValuesBucket {let valueBucket = {} as dataRdb.ValuesBucket;Const.TASK_INFO.columns?.forEach((item: string) => {if (item !== 'id') {switch (item) {case 'date':valueBucket[item] = taskInfo.date;break;case 'taskID':valueBucket[item] = taskInfo.taskID;break;case 'targetValue':valueBucket[item] = taskInfo.targetValue;break;case 'isAlarm':valueBucket[item] = taskInfo.isAlarm;break;case 'startTime':valueBucket[item] = taskInfo.startTime;break;case 'endTime':valueBucket[item] = taskInfo.endTime;break;case 'frequency':valueBucket[item] = taskInfo.frequency;break;case 'isDone':valueBucket[item] = taskInfo.isDone;break;case 'finValue':valueBucket[item] = taskInfo.finValue;break;case 'isOpen':valueBucket[item] = taskInfo.isOpen;break;default:break;}}});return valueBucket;
}

用户开启和关闭任务,改变任务的目标值、提醒时间、频率等,用户打卡后修改任务的实际完成值都是通过更新数据接口来实现的,代码如下:

// TaskInfoApi.ets
updateDataByDate(taskInfo: TaskInfo, callback: Function): void {const valueBucket = generateBucket(taskInfo);let tableName = Const.TASK_INFO.tableName;if (!tableName) {return;}let predicates = new dataRdb.RdbPredicates(tableName);// 根据date和taskID匹配要更新的数据行predicates.equalTo('date', taskInfo.date).and().equalTo('taskID', taskInfo.taskID);RdbUtils.update(valueBucket, predicates).then((result: number) => {callback(result);});Logger.info('TaskInfoTable', `Update data {${taskInfo.date}:${taskInfo.taskID}} finished.`);
}

用户可以查看当天和以前某日的健康任务信息,需要提供查找数据接口,实现代码如下:

// TaskInfoApi.ets
query(date: string, isOpen: boolean = true, callback: Function): void {let tableName = Const.TASK_INFO.tableName;if (!tableName) {return;}let predicates = new dataRdb.RdbPredicates(tableName);predicates.equalTo('date', date);// 如果isOpen为true,则只查找开启的任务 if (isOpen) {predicates.equalTo('isOpen', true);}predicates.orderByAsc('taskID');  // 查找结果按taskID排序RdbUtils.query(predicates).then(resultSet => {let count = resultSet.rowCount;// 查找结果为空则返回空数组,否则返回查找结果数组if (count === 0 || typeof count === 'string') {Logger.error('TaskInfoTable', `${date} query no results!`);const result: TaskInfo[] = [];callback(result);} else {resultSet.goToFirstRow();const result: TaskInfo[] = [];for (let i = 0; i < count; i++) {let tmp = new TaskInfo(0, '', 0, '', false, '', '', '', false, '');tmp.isOpen = resultSet.getDouble(resultSet.getColumnIndex('isOpen')) ? true : false;tmp.id = resultSet.getDouble(resultSet.getColumnIndex('id'));tmp.date = resultSet.getString(resultSet.getColumnIndex('date'));tmp.taskID = resultSet.getDouble(resultSet.getColumnIndex('taskID'));tmp.targetValue = resultSet.getString(resultSet.getColumnIndex('targetValue'));tmp.isAlarm = resultSet.getDouble(resultSet.getColumnIndex('isAlarm')) ? true : false;tmp.startTime = resultSet.getString(resultSet.getColumnIndex('startTime'));tmp.endTime = resultSet.getString(resultSet.getColumnIndex('endTime'));tmp.frequency = resultSet.getString(resultSet.getColumnIndex('frequency'));tmp.isDone = resultSet.getDouble(resultSet.getColumnIndex('isDone')) ? true : false;tmp.finValue = resultSet.getString(resultSet.getColumnIndex('finValue'));result[i] = tmp;resultSet.goToNextRow();}callback(result);}});
}

每日信息数据表

创建每日信息数据表的SQL语句如下:

CREATE TABLE IF NOT EXISTS dayInfo(date TEXT NOT NULL PRIMARY KEY, targetTaskNum INTEGER, finTaskNum INTEGER
)

在当天第一次打开应用时需要初始化每日信息数据,页面需要根据用户编辑任务和打卡的情况来更新当天目标任务个数和完成任务个数,所以需要提供插入数据和更新数据的接口,写法与上一条中相应接口类似,不再赘述。

页面需要查找对应日期的目标任务个数和完成任务个数用以在页面显示任务进度,因此需要查找数据的接口。且页面在打开时需要显示当周每天任务的完成情况,因此需要允许一次调用查找一周的每日任务信息。实现代码如下:

// DayInfoApi.ets
queryList(dates: string[], callback: Function): void {let predicates: dataRdb.RdbPredicates = new dataRdb.RdbPredicates(Const.DAY_INFO.tableName ? Const.DAY_INFO.tableName : '');predicates.in('date', dates);  // 匹配日期数组内的所有日期RdbUtils.query(predicates).then(resultSet => {let count = resultSet.rowCount;if (count === 0) {Logger.info('DayInfoTable', 'query no results.');let result: DayInfo[] = [];callback(result);} else {resultSet.goToFirstRow();let result: DayInfo[] = [];for (let i = 0; i < count; i++) {let tmp = new DayInfo('', 0, 0);tmp.date = resultSet.getString(resultSet.getColumnIndex('date'));tmp.targetTaskNum = resultSet.getDouble(resultSet.getColumnIndex('targetTaskNum'));tmp.finTaskNum = resultSet.getDouble(resultSet.getColumnIndex('finTaskNum'));result[i] = tmp;resultSet.goToNextRow();}callback(result);}});
}

全局信息数据表

创建全局信息数据表的SQL语句如下:

CREATE TABLE IF NOT EXISTS globalInfo(id INTEGER PRIMARY KEY, firstDate TEXT NOT NULL, lastDate TEXT NOT NULL, checkInDays INTEGER, achievements TEXT NOT NULL
)

全局信息数据表同样需要提供插入数据、更新数据和查找数据的接口,写法与本节前两条中相应接口类似,不再赘述。

数据库初始化

应用首次打开时,数据库中没有数据,要做数据库的初始化,写入一组空数据。另外,如果用户连续几天没有打开APP,再次打开时需要将数据回写至数据库。因此需要实现一个数据库接口,在应用打开时调用,进行上述操作。代码如下:

// DatabaseModel.ets
query(date: string, callback: Function) {let result: TaskInfo[] = [];let self = this;GlobalInfoApi.query((globalResult: GlobalInfo) => {if (!globalResult.firstDate) { // 如果找不到全局信息,则写入let globalInfo: GlobalInfo = new GlobalInfo(date, date, 0, '');GlobalInfoApi.insertData(globalInfo, (isDone: number) => {if (isDone) {Logger.info('AppStart', 'Insert globalInfo success: ' + JSON.stringify(globalInfo));}});self.insertGlobalTask();let dayInfo: DayInfo = new DayInfo(date, 0, 0);DayInfoApi.insertData(dayInfo, (isDone: number) => {if (isDone) {Logger.info('AppStart', 'Insert dayInfo success: ' + JSON.stringify(dayInfo));}})self.insertTask(date);callback(result, dayInfo);} else { // 如果找到全局信息,则查询当天的任务信息let newGlobalInfo = globalResult;let preDate = globalResult.lastDate;newGlobalInfo.lastDate = date;GlobalInfoApi.updateData(newGlobalInfo, (isDone: number) => {if (isDone) {Logger.info('AppStart', 'update globalInfo success: ' + JSON.stringify(newGlobalInfo));}});self.queryPreInfo(date, preDate, result, callback);}});
}

编写通用工具类

本节将介绍日志打印、时间换算等通用工具类的编写和使用,工具类可以简化应用代码编写和业务流程处理。

日志类

日志类Logger旨在提供一个全局的日志打印、日志管理的地方,既可以规范整个应用的日志打印,也方便日后对日志工具类进行修改,而不需要去改动代码中每一个调用日志的地方,如切换具体的日志实现类(比如不使用Console而是HiLog),将日志记录到本地文件等。

Logger对外的日志API全部使用静态方法,方便调用者使用,目前分verbose,debug,info,warn,error五个级别。

使用方法如下:

  1. import Logger日志类:
import { Logger } from '../utils/log/Logger';

2.调用对应级别的静态方法:

Logger.debug('MyAbilityStage', 'onCreate');

3.Logger类中包括debug、info、warn、error,具体内容如下:

// Logger.ets  
import hilog from '@ohos.hilog';const LOGGER_PREFIX: string = 'Healthy_life';class Logger {private domain: number;private prefix: string;...constructor(prefix: string = '', domain: number = 0xFF00) {this.prefix = prefix;this.domain = domain;}debug(...args: string[]): void {hilog.debug(this.domain, this.prefix, this.format, args);}info(...args: string[]): void {hilog.info(this.domain, this.prefix, this.format, args);}warn(...args: string[]): void {hilog.warn(this.domain, this.prefix, this.format, args);}error(...args: string[]): void {hilog.error(this.domain, this.prefix, this.format, args);}
}export default new Logger(LOGGER_PREFIX, 0xFF02);

时间工具

为全局提供时间工具,避免重复定义。

  1. 常用时间相关常量:
  // Utils.etsconst CHINESE_OF_WEEK: string[] = ['一', '二', '三', '四', '五', '六', '日'];const YEAR: string = '年';const MONTH: string = '月';const DAY: string = '日';const WEEK: string = '星期';DAYS_OF_WEEK: number = 7;const SUNDAY_FIRST_SHIFT: number = 6;
  1. 时间函数示例(由时间常量衍生出星期一到星期日和数字 1-7 的字典映射):
// Utils.ets
export const oneWeekDictFunc = () => {const oneWeekDict: Array<string> = [];for (let index = 0;index < CHINESE_OF_WEEK.length; index++) {oneWeekDict[index] = `${WEEK}${CHINESE_OF_WEEK[index]}`;}return oneWeekDict;
}

单位转换工具

把比例等分浮点数转换为百分比字符串。

例如成就页面,每一行平均分布三个徽章,可以先定义一个浮点数代表等分比例,再转换为百分比字符串。

// Utils.ets
export function ratio2percent(ratio: number): string {return `${ratio * 100}%`;
}

使用方法如下:

  1. import 工具方法:
import { ratio2percent } from '../common/utils/Utils'

2.引用工具方法 ( 例如成就页面,每个徽章占据屏幕宽度的三分之一 ) :

// BadgeCardComponent.ets
Column({space: commonConst.DEFAULT_18}) { ...  // 省略徽章卡片的 UI 布局细节
}
.width(ratio2percent(Const.ACHIEVE_SPLIT_RATIO)) // achieveConst.ACHIEVE_SPLIT_RATIO = 1 / 3

事件分发类

事件分发类提供应用全局的事件注册,分发,接受,可以实现组件之间的解耦。

事件分发类全局共享一个实例, 将事件处理统一管理(HealthDataSrcMgr是单例):

获取事件分发实例

// HomeComponent.ets
@Provide broadCast: BroadCast = HealthDataSrcMgr.getInstance().getBroadCast();// HealthDataSrcMgr.ets
public getBroadCast(): BroadCast {return this.broadCast;
}

事件注册:

// CustomDialogView.ets
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();});
}// BroadCast.ets
public on(event: string, callback: Function) {Logger.info(FILE_TAG, 'register broadcast with type '+ event);switch (event) {case BroadCastType.SHOW_ACHIEVEMENT_DIALOG:this.callBackArray.showAchievementDialog = callback;break;case BroadCastType.SHOW_TASK_DETAIL_DIALOG:this.callBackArray.showTaskDetailDialog = callback;break;case BroadCastType.SHOW_TARGET_SETTING_DIALOG:this.callBackArray.showTargetSettingDialog = callback;break;case BroadCastType.SHOW_REMIND_TIME_DIALOG:this.callBackArray.showRemindTimeDialog = callback;break;case BroadCastType.SHOW_FREQUENCY_DIALOG:this.callBackArray.showFrequencyDialog = callback;break;default:break;}
}

取消事件注册:

// TaskDetailComponent.ets
aboutToAppear() {this.broadCast.off(BroadCastType.SHOW_TARGET_SETTING_DIALOG, () => {});this.broadCast.off(BroadCastType.SHOW_REMIND_TIME_DIALOG, () => {});this.broadCast.off(BroadCastType.SHOW_FREQUENCY_DIALOG, () => {});
}// BroadCast.ets
public off(event: string, callback: Function) {if (event === null) {Logger.info(FILE_TAG, 'cancel all broadcast');this.callBackArray = callBackArrayTemp;}Logger.info(FILE_TAG, 'cancel broadcast with type '+ event);const cbs = this.callBackArray;if (!cbs) {return;}if (callback === null) {switch (event) {case BroadCastType.SHOW_ACHIEVEMENT_DIALOG:this.callBackArray.showAchievementDialog = () => {};break;case BroadCastType.SHOW_TASK_DETAIL_DIALOG:this.callBackArray.showTaskDetailDialog = () => {}; break;case BroadCastType.SHOW_TARGET_SETTING_DIALOG:this.callBackArray.showTargetSettingDialog = () => {};break;case BroadCastType.SHOW_REMIND_TIME_DIALOG: this.callBackArray.showRemindTimeDialog = () => {};break;case BroadCastType.SHOW_FREQUENCY_DIALOG:this.callBackArray.showFrequencyDialog = () => {};break;default:break;}}
}

发送事件:

// 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);editTask.targetValue = item?.targetValue;editTask.isAlarm = item.isAlarm;editTask.startTime = item.startTime;editTask.frequency = item.frequency;editTask.isOpen = item.isOpen;router.pushUrl({ url: 'pages/TaskEditPage', params: { params: JSON.stringify(editTask) } });}
}

总结

通过本次Codelab的学习,您应该已经掌握了页面跳转、自定义弹窗等UI方法,并学会了操作关系型数据库读写数据。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/784063.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL中如何进行多表查询

目录 一、子查询 1.什么是子查询 2.注意事项 二、联结查询 1.什么是联结 2.内部联结&#xff08;等值联结&#xff09; ①WHERE语句 ②ON语句 3.自联结 4.自然联结 5.外部联结 三、组合查询 1.什么是组合查询 2.UNION规则 *本节涉及概念来源于图灵程序设计丛书&a…

【前端面试3+1】07vue2和vue3的区别、vue3响应原理及为什么使用proxy、vue的生命周期中在什么时期给接口发请求、【找出数组最大公约数】

一、vue2和vue3的区别 1.性能优化&#xff1a; Vue 3在性能方面有很大的提升&#xff0c;主要是通过虚拟DOM的优化和响应式系统的改进实现的。 虚拟 DOM 重构&#xff1a;Vue 3 中对虚拟 DOM 进行了重构&#xff0c;使得更新算法更加高效&#xff0c;减少了更新时的开销&#x…

14 - grace数据处理 - 泄露误差改正 - 空域滤波法(Mascon法)

@[TOC](grace数据处理 - 泄露误差改正 - 空域滤波法(Mascon法)) 空域法的基本思想是假设地面某区域的质量变化是由一系列位置已知、质量未知的质量块(小范围区域)引起的,那么将GRACE反演的结果归算到n个质量块上的过程就是泄露信号恢复的过程。个人理解是这样的:假定已知研…

gtsam::Pose3的compose()函数作用

#include <gtsam/geometry/Pose3.h> #include <iostream> int main(int argc, char** argv) {// B 的旋转量为绕 x 轴旋转 180 度gtsam::Pose3 B gtsam::Pose3(gtsam::Rot3(0, 1, 0, 0), gtsam::Point3(1, 2, 0));// A 的旋转量为绕 z 轴旋转 180 度gtsam::Pose3 A…

Linux零基础入门之华为欧拉系统安装

一、名词解释 Linux&#xff1f; Linux是一个开源的免费的操作系统&#xff0c;功能与windows一样。具有处理器管理&#xff0c;存储管理&#xff0c;设备管理&#xff0c;文件管理&#xff0c;作业管理等功能。 可以俗称为Linux操作系统&#xff0c;组织或个人&#xff0c;…

【FIFO】Standard / FWFT FIFO设计实现(一)——同步时钟

标准FIFO 本文使用位扩展的方式实现标准FIFO&#xff0c;原理可参考【AXIS】AXI-Stream FIFO设计实现&#xff08;一&#xff09;——基本模式&#xff0c;核心代码如下&#xff1a; logic [FIFO_DEPTH_WIDTH : 0] rd_ptr_r d0, wr_ptr_r d0;always_ff (posedge clk) beginif…

软考108-上午题-【结构化开发】-杂题+小结

一、杂题 真题1&#xff1a; 真题2&#xff1a; 真题3&#xff1a; 真题4&#xff1a; 数据流图到软件体系结构的映射 根据数据流的特点&#xff0c;可以将数据流图分为&#xff1a; 变换型数据流图事务型数据流图 其对应的映射分别为&#xff1a; 变换分析事物分析 一个…

网安学习笔记-day11,FTP服务器

FTP服务器 FTP介绍 FTP(File Transfer Protocol)文件传输协议 端口号&#xff1a;TCP 20/21 工作方式&#xff1a; 主动模式被动模式 服务器部署 准备阶段 配置IP windowsXP 192.168.1.21&#xff08;也可DHCP自动获取&#xff09; Windows2003 192.168.1.1 安装万维网…

ssm停车场管理系统

点赞收藏关注 → 私信领取本源代码、数据库 摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于停车场管理系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了停…

[leetcode]28. 找出字符串中第一个匹配项的下标

前言&#xff1a;力扣刷题 问题&#xff1a; 给你两个字符串 haystack 和 needle &#xff0c;请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标&#xff08;下标从 0 开始&#xff09;。如果 needle 不是 haystack 的一部分&#xff0c;则返回 -1 。 示例&…

【Linux】进程管理:进程及概念精讲

前言&#xff1a;本节内容包含进程管理操作的各种基础概念精讲&#xff0c;同时部分板块包含Linux操作系统与一般操作系统的概念对比。不仅包含“书面概念”&#xff0c;还包含详细操作以及通俗讲解。 目录 一、进程概念引入 二、进程的描述与组织&#xff1a;进程控制块&…

xftp突然无法连接虚拟机

问题描述 使用xftp连接虚拟机的时候一直显示 连接xxx.xxx.xx.xx失败 问题原因查找 首先打开本地cmd命令提示符 ping 你的虚拟机ip地址 我的是 ping 192.168.xx.xx 显示请求超时 解决方案&#xff1a; 点击打开更改适配器选项 右键vmnet 8——属性 如图前四个选项必选 单…

《操作系统导论》第16章读书笔记:分段

《操作系统导论》第16章读书笔记&#xff1a;分段 —— 杭州 2024-03-31 夜 文章目录 《操作系统导论》第16章读书笔记&#xff1a;分段0.前言1.分段&#xff1a;泛化的基址/界限2.我们引用哪个段&#xff1f;3.栈怎么办4.支持共享5.细粒度与粗粒度的分段、操作系统支持6.小结7…

Unix中的进程和线程-1

目录 1.如何创建一个进程 2.如何终止进程 2.2遗言函数 3.进程资源的回收 4.孤儿进程和僵尸进程 孤儿进程 (Orphan Process)&#xff1a; 僵尸进程 (Zombie Process)&#xff1a; 代码示例&#xff1a; 5. 进程映像的更新 在Linux中&#xff0c;进程和线程是操作系统进行工作调…

CAS 的 ABA 问题

一、什么是 ABA 问题 ABA 的问题: 假设存在两个线程 t1 和 t2. 有⼀个共享变量 num, 初始值为 A. 接下来, 线程 t1 想使⽤ CAS 把 num 值改成 Z, 那么就需要 先读取 num 的值, 记录到 oldNum 变量中. 使⽤ CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z. 但是, 在…

CaT论文翻译

CaT: Balanced Continual Graph Learning with Graph Condensation CaT&#xff1a;通过图压缩实现平衡的连续图学习 Abstract 持续图学习(CGL)的目的是通过以流方式输入图数据来持续更新图模型。由于模型在使用新数据进行训练时很容易忘记以前学到的知识&#xff0c;因此灾…

基于SSM的宠物医院信息管理系统

项目简介 主要功能包括首页、个人中心、用户管理、医学知识管理、科室信息管理、医生信息管理、订单信息管理等。 管理员模块:管理员登录进入宠物医院信息管理系统可以查看个人中心、用户管理、医生管理、医学知识管理、科室信息管理、医生信息管理、预约挂号管理、医嘱信息管理…

AtCoder Beginner Contest 342 A - D

A - Yay! 大意 给定字符串&#xff0c;其中有且仅有一个字符与其他不同&#xff0c;输出这个字符的下标&#xff08;从1开始&#xff09;。 思路 桶排序统计次数即可。 代码 #include<iostream> #include<vector> using namespace std; int main(){string s;…

【前端面试3+1】06继承方式及优缺点、缓存策略、url输入到渲染全过程、【二叉树中序遍历】

一、继承有哪些方式&#xff1f;以及优缺点 继承的方式包括原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承和组合式继承。 1.原型链继承&#xff1a; 实现方式&#xff1a;将子类的原型指向父类的实例来实现继承。优点&#xff1a;简单易懂&#xff0c;代码量少。…

如何制作伸缩侧边栏?

目录 一、html-body 二、CSS 三、JS 四、完整代码 五、效果展示 一、html-body 侧边栏的伸缩需要用户触发事件&#xff0c;这里使用button为例&#xff0c;用户点击按钮实现侧边栏的打开和关闭。 <body><!-- 按钮&#xff0c;可以用文字、图片等作为事件源&am…