HarmonyOS开发 - 记事本实例一(界面搭建)
开发一个记事本App的主要功能点包括以下几点:
创建笔记:用户可以在应用中创建新的笔记,包括输入笔记标题、内容,以及记录创建时间和更新时间等。
编辑笔记:用户可以对已创建的笔记进行修改。
删除笔记:用户可以删除不需要的笔记。
分类管理:笔记可以按照类别管理,自定义类别等。
查询功能:支持按标题或内容进行查询。
选择数据库:
这里使用关系型数据库(Relational Database,RDB),它是一种基于关系模型来管理数据的数据库。关系型数据库基于SQLite组件提供了一套完整的对本地数据库进行管理的机制,对外提供了一系列的增、删、改、查等接口,也可以直接运行用户输入的SQL语句来满足复杂的场景需要。支持通过ResultSet.getSendableRow方法获取Sendable数据,进行跨线程传递。
为保证插入并读取数据成功,建议一条数据不要超过2M。超出该大小,插入成功,读取失败。
注意:大数据量场景下查询数据可能会导致耗时长甚至应用卡死,建议如下:
- 单次查询数据量不超过5000条。
- 在TaskPool中查询。
- 拼接SQL语句尽量简洁。
- 合理地分批次查询。
一、创建项目
首先打开DevEco Studio,点击File -> New -> Create Project创建项目。如下图:
创建一个空的Ability界面,如下图:
点击”下一步“,修改项目名称,如下图:
点击”完成“即可,如下图,项目就已创建好了。
页面效果如下图:
二、创建顶部
创建头部文件,可以直接创建ArkTS File文件,也可以通过Visual / Component通过低代码形式创建。
这里直接创建ArkTS File文件,并定义struct结构,代码如下:
@Component
export default struct Header {
build() {
}
}
头部包含部分有搜索框、标题、分类项等,根据下图完成代码的编写。在点击“搜索“功能时,展示出搜索框等内容。
在ets/components目录下,创建Header.ets文件。代码如下:
@Component
export default struct Header {
@State isSearch: boolean = false // 是否为搜索状态
build() {
Row(){
Column(){
// 非搜索状态下显示内容
if(!this.isSearch) {
Row(){
Text('笔记').fontSize(20).fontWeight(FontWeight.Bold)
Blank()
Row(){
Button(){
Image($rawfile('search.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
Text('搜索').fontSize(15)
}
.onClick(() => {
this.isSearch = !this.isSearch
})
}.width('100%')
}
// 搜索状态下显示内容
else {
Row(){
Image($rawfile('search.png')).width(24).margin({right: 10})
TextInput({placeholder: '请输入搜索内容'}).width(230).height(36)
Blank()
Button('取消').fontSize(15).fontColor(Color.Orange)
.width(80)
.height(36)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.isSearch = !this.isSearch
})
}.width('100%').justifyContent(FlexAlign.Center)
}
}.width('100%')
}
.width('100%')
.padding({top: '10vp', bottom: '10vp', left: '20vp', right: '20vp'})
.shadow({radius: 10, color: 'rgba(0, 0, 0, .05)', offsetX: 0, offsetY: 1})
}
}
三、分类信息
分类信息这里也是放在头部组件中,不过它在呈现搜索框等内容时,需要隐藏起来,所以分类信息则放在 this.isSearch为false区域即可。
3.1 展示分类
在编写分类信息时,需要完成以下几步操作:
- 定义全局状态容器用于存储分类信息
- 定义分类信息中数据类型
- 定义默认分类样式
- 定义选中后的分类样式
- 添加点击事件,切换当前选中项索引,来达到切换高亮效果。
- 使用Scroll组件解决横向超出自动添加滚动条,可滑动查看等。
代码如下:
// 定义:分类信息的类型
class ClassifyInfo {
id: number = 0
name: string = ''
}
// 定义分类默认样式
@Extend(Button) function classifyNormalStyle(){
.fontSize(12)
.fontColor(Color.Black)
.padding({left: 15, right: 15})
.height(26).backgroundColor(Color.White)
}
// 定义分类项选中后的样式
@Extend(Button) function classifyActiveStyle(){
.fontColor(Color.White).backgroundColor(Color.Grey)
}
@Component
export default struct Header {
@State isSearch: boolean = false // 是否为搜索状态
// 分类信息
@State classifyList: Array<ClassifyInfo> = [
{id: 1, name: '全部'},
{id: 2, name: '速记'},
{id: 3, name: '默认'}
]
@State classifyActive: number = 0 // 分类选中项索引,默认为0
build() {
Row(){
Column(){
// 非搜索状态下显示内容
if(!this.isSearch) {
Row(){
Text('笔记').fontSize(20).fontWeight(FontWeight.Bold)
Blank()
Row(){
Button(){
Image($rawfile('search.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
Text('搜索').fontSize(15)
}
.onClick(() => {
this.isSearch = !this.isSearch
})
}.width('100%')
// 显示当前笔记数量
Row(){
Text('15篇笔记').fontSize(12).fontColor(Color.Gray)
}.width('100%')
// 分类信息
Scroll(){
Row({space: 5}){
ForEach(this.classifyList, (item: ClassifyInfo, index) => {
if(index === this.classifyActive) {
Button(item.name).classifyNormalStyle().classifyActiveStyle().onClick(() => {
this.classifyActive = index
})
} else {
Button(item.name).classifyNormalStyle().onClick(() => {
this.classifyActive = index
})
}
})
// 添加分类按钮
Button(){
Image($rawfile('add.png')).width(20).height(20)
}.backgroundColor(Color.Transparent).margin({left: 10})
}.padding({top: 10, bottom: 0}).justifyContent(FlexAlign.SpaceBetween)
}.width('100%').height(40).scrollable(ScrollDirection.Horizontal)
}
// 搜索状态下显示内容
else {
Row(){
Image($rawfile('search.png')).width(24).margin({right: 10})
TextInput({placeholder: '请输入搜索内容'}).width(230).height(36)
Blank()
Button('取消').fontSize(15).fontColor(Color.Orange)
.width(80)
.height(36)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.isSearch = !this.isSearch
})
}.width('100%').justifyContent(FlexAlign.Center)
}
}.width('100%')
}
.width('100%')
.padding({top: '10vp', bottom: '10vp', left: '20vp', right: '20vp'})
}
}
页面效果图如下:
3.2 创建分类
通过自定义弹框,完成分类信息的添加功能; 首先打开API参考文档,找到UI界面的自定义弹窗(CustomDialog),根据其提供的代码示例,完成文本输入弹框。
如下图,接下来我们使用自定义弹窗功能,完成笔记分类添加功能界面。
首先,在components目录中,创建classifyAddDialog.ets文件,用于自定义分类信息添加弹框信息。
目录结构如下图:
classifyAddDialog.ets代码如下:
@CustomDialog
export struct ClassifyAddDialog {
@Link textValue: string
@Link inputValue: string
controller: CustomDialogController
cancel: () => void = () => {}
confirm: () => void = () => {}
build() {
Column() {
Text('笔记分类').fontSize(20).margin({ top: 10, bottom: 10 })
TextInput({ placeholder: '请输入分类名称', text: this.textValue })
.height(40)
.width('90%')
.margin({bottom: 15})
.onChange((value: string) => {
this.textValue = value
})
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Button('取消')
.onClick(() => {
this.controller.close()
this.cancel()
}).backgroundColor(0xffffff).fontColor(Color.Black)
Button('保存')
.onClick(() => {
this.inputValue = this.textValue
this.controller.close()
this.confirm()
}).backgroundColor(0xffffff).fontColor(Color.Red)
}.margin({ bottom: 10 })
}
}
}
头部文件中引入自定义弹框,来实现分类信息添加弹框的调用功能。代码如下:
import { ClassifyAddDialog } from './classifyAddDialog'
// 定义:分类信息的类型
class ClassifyInfo {
id: number = 0
name: string = ''
}
// 定义分类默认样式
@Extend(Button) function classifyNormalStyle(){
.fontSize(12)
.fontColor(Color.Black)
.padding({left: 15, right: 15})
.height(26).backgroundColor(Color.White)
}
// 定义分类项选中后的样式
@Extend(Button) function classifyActiveStyle(){
.fontColor(Color.White).backgroundColor(Color.Grey)
}
@Component
export default struct Header {
@State isSearch: boolean = false // 是否为搜索状态
// 分类信息
@State classifyList: Array<ClassifyInfo> = [
{id: 1, name: '全部'},
{id: 2, name: '速记'},
{id: 3, name: '默认'}
]
@State classifyActive: number = 0 // 分类选中项索引,默认为0
@State textValue: string = '' // 文本信息
@State inputValue: string = '' // 输入信息
dialogController: CustomDialogController = new CustomDialogController({
builder: ClassifyAddDialog({
cancel: this.onCancel,
confirm: this.onAccept,
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Default,
// offset: {dx: 0, dy: -20},
gridCount: 4,
customStyle: false
})
// 取消事件回调函数
onCancel() {
this.textValue = ''
console.info('Callback when the cancel button is clicked', this.textValue, this.inputValue)
}
// 确认完成回调函数,追加分类信息到classifyList容器中
onAccept() {
this.textValue = ''
this.classifyList.push({
id: this.classifyList.length + 1,
name: this.inputValue
})
console.info('Callback when the accept button is clicked', this.textValue, this.inputValue)
}
existApp() {
console.info('Click the callback in the blank area')
}
build() {
Row(){
Column(){
// 非搜索状态下显示内容
if(!this.isSearch) {
Row(){
Text('笔记').fontSize(20).fontWeight(FontWeight.Bold)
Blank()
Row(){
Button(){
Image($rawfile('search.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
Text('搜索').fontSize(15)
}
.onClick(() => {
this.isSearch = !this.isSearch
})
}.width('100%')
// 显示当前笔记数量
Row(){
Text('15篇笔记').fontSize(12).fontColor(Color.Gray)
}.width('100%')
// 分类信息
Scroll(){
Row({space: 5}){
ForEach(this.classifyList, (item: ClassifyInfo, index) => {
if(index === this.classifyActive) {
Button(item.name).classifyNormalStyle().classifyActiveStyle().onClick(() => {
this.classifyActive = index
})
} else {
Button(item.name).classifyNormalStyle().onClick(() => {
this.classifyActive = index
})
}
})
// 添加分类按钮
Button(){
Image($rawfile('add.png')).width(20).height(20)
}.backgroundColor(Color.Transparent).margin({left: 10}).onClick(() => {
this.dialogController.open()
})
}.padding({top: 10, bottom: 0}).justifyContent(FlexAlign.SpaceBetween)
}.width('100%').height(40).scrollable(ScrollDirection.Horizontal)
}
// 搜索状态下显示内容
else {
Row(){
Image($rawfile('search.png')).width(24).margin({right: 10})
TextInput({placeholder: '请输入搜索内容'}).type(InputType.Normal).width(230).height(36)
Blank()
Button('取消').fontSize(15).fontColor(Color.Orange)
.width(80)
.height(36)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.isSearch = !this.isSearch
})
}.width('100%').justifyContent(FlexAlign.Center)
}
}.width('100%')
}
.width('100%')
.padding({top: '10vp', bottom: '10vp', left: '20vp', right: '20vp'})
}
}
如下图,分类信息弹框则被成功调出来了。
这里,我们使用模拟器来作演示,因为模拟器中可以调用出中文输入法,便于我们更好输入相关内容。现在我们输入一个“私密”分类,来看下结果会怎样,如下图:
点击“保存”后,会发现并未像预期一样,“私密”分类并没有成功追加到classifyList中,而是控制台报错或闪退。如下图:
出现以上错误,原因是绑定在confirm和cancel上的回调函数this.onAccept和this.onCancel的this指针已改变,所以在两个回调函数中使用this调取classifyList为undefined;解决此问题,也很简单,在回调函数后面追加bind(this)即可。如下图:
通过bind(this)将this指向重新指回本域对象上,这样在回调函数中则可以正常调用本对象内的状态属性了。此时,重新添加“私密”分类,则可以成功添加到分类信息列表中。如下图:
四、笔记信息列表
紧接着,我们需要完成笔记的列表信息界面,如下图:
4.1 基础部分
完成信息列表前,我们先来了解以下几个知识点:
1、Foreach循环遍历数组,根据数组内容渲染页面组件,示例如下:
ForEach(
arr: Array, //要遍历的数据数组
(item: any, index?: number) => { // 页面组件生成函数
Row(){
Image(item.Image)
Column(){
Text(item.name)
Text(item.price)
}
},
keyGenerator?: (item:any, index?: number): string => {
// 键的生成函数,为数组每一项生成一个唯一标识,组件是否重新渲染的判断标准
}
}
)
2、列表(List)容器,当内容过多并超过屏幕可见区域时,会自动提供滚动条功能。示例如下:
List({space: 20}){
ForEach([1, 3, 5, 7, 9], item => {
ListItem(){
Text("listItem" + item) // 列表项内容,只包含一个根组件
}
})
}.width(‘100%’)
3、自定义构建函数(@Builder可以定义在struct内部,也可以定义全局),示例如下:
// 全局定义
@Builder function cardItem(item: Items){
Row({space: 5}){
Image(item.image).width(80)
Column(){
Text(item.name)
}.alignItems(HorizontalAlign.Start)
}.padding(15).width('100%').borderRadius(10).alignItems(VerticalAlign.Top)
4.2 信息列表
上述知识点功能都了解后,则可以开始编写信息列表了。打开src/main/ets/pages/index.ets文件,先通过模拟JSON数据完成界面渲染。代码如下:
import Header from '../components/Header'
import { NotesInfo } from '../types/types'
@Entry
@Component
struct Index {
@State notes: Array<NotesInfo> = [
{
id: 1,
title: '明媚的星期天的早晨',
content: 'test',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
},
{
id: 2,
title: '成功的喜悦',
content: '',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
},
{
id: 3,
title: '冲动是魔鬼',
content: '',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
},
{
id: 4,
title: '意外惊喜',
content: '',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
}
]
// 自定义面板panel item
@Builder listItem(item: NotesInfo){
Column(){
Row(){
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.padding({ right: 10 }) // 右侧内填充10,与日期相隔10个间距
Text(item.create_time.toDateString())
.fontSize(12)
.fontColor(Color.Gray)
}.width('100%')
.justifyContent(FlexAlign.SpaceBetween) // 两边对齐
.alignItems(VerticalAlign.Top) // 内容顶部对齐
// 判断描述内容是否存在
if (item.content) {
Text(item.content.substring(0, 50) + (item.content.length>50?'...':''))
.fontSize(12)
.fontColor(Color.Gray)
.width('100%')
.align(Alignment.Start)
.margin({top: 10})
}
}.padding(15)
.backgroundColor(Color.White)
.borderRadius(10)
.shadow({color: '#00000050', radius: 10})
}
build() {
Row() {
Column() {
Header() // 顶部组件
Divider() //分割线
// List容器
List(){
// 循环输出笔记列表内容
ForEach(this.notes, (item: NotesInfo) => {
ListItem(){
this.listItem(item) // 渲染面板内容
}.border({color: Color.Gray, style: BorderStyle.Dashed})
.padding({ top: 10, bottom: 10 })
})
}.width('100%')
.layoutWeight(6)
.padding({ left: 10, right: 10, top: 10, bottom: 10 })
.backgroundColor('#f1f1f1')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.height('100%').alignItems(VerticalAlign.Top)
}
}
打开 src/main/ets/types/types.ets ,定义 NotesInfo 类,代码如下:
// 定义:分类信息的类型
export class ClassifyInfo {
id: number = 0
name: string = ''
}
// 定义笔记的类型
export class NotesInfo {
id: number = 0
title: string = '' // 笔记名称
content: string = '' // 笔记内容
classify_id: number = 0 //对应分类ID
create_time: Date = new Date() // 创建笔记时间
update_time: Date = new Date() // 修改笔记时间
}
页面效果如下图:
4.3 格式化日期
如上图,日期格式可能不是我们想要的,需要根据自己的格式进行重组,并且能在不同场景下输出不同的日期组合。我们在src/main/ets目录下创建一个utils工具包,定义utils.ets文件,来定义格式化日期的函数。代码如下:
/**
* 补缺
* @param {*} _val
*/
const fillZero = (_val: number) => {
return _val < 10 ? '0' + _val : _val;
}
/**
* 日期转换功能
*/
interface DateStrType {
YYYY: number;
MM: string | number;
DD: string | number;
hh: string | number;
ii: string | number;
ss: string | number;
}
/**
* 日期格式化
*/
export const formatDate = (date: Date, _format?: string) => {
let format = 'undefined'===typeof _format||!_format?'YYYY-MM-DD hh:ii:ss':_format;
const _values: DateStrType = {
YYYY: date.getFullYear(),
MM: fillZero(date.getMonth()+1),
DD: fillZero(date.getDate()),
hh: fillZero(date.getHours()),
ii: fillZero(date.getMinutes()),
ss: fillZero(date.getSeconds())
};
Object.keys(_values).reduce((allStr: string, key: string) => {
switch (key) {
case 'YYYY': format = format.replace(key, _values.YYYY.toString()); break;
case 'MM': format = format.replace(key, _values.MM.toString()); break;
case 'DD': format = format.replace(key, _values.DD.toString()); break;
case 'hh': format = format.replace(key, _values.hh.toString()); break;
case 'ii': format = format.replace(key, _values.ii.toString()); break;
case 'ss': format = format.replace(key, _values.ss.toString()); break;
}
return format;
}, format)
return format;
}
此时,将函数formatDate引入到index.ets页面,替换日期输出内容。代码如下:
import Header from '../components/Header'
import { NotesInfo } from '../types/types'
import { formatDate } from '../utils/utils'
@Entry
@Component
struct Index {
@State notes: Array<NotesInfo> = [
{
id: 1,
title: '明媚的星期天的早晨',
content: 'test',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
},
{
id: 2,
title: '成功的喜悦',
content: '',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
},
{
id: 3,
title: '冲动是魔鬼',
content: '',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
},
{
id: 4,
title: '意外惊喜',
content: '',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
}
]
// 自定义面板panel item
@Builder listItem(item: NotesInfo){
Column(){
Row(){
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.padding({ right: 10 }) // 右侧内填充10,与日期相隔10个间距
// 使用 formatDate 格式化日期
Text(formatDate(item.create_time, 'YYYY/MM/DD'))
.fontSize(12)
.fontColor(Color.Gray)
}.width('100%')
.justifyContent(FlexAlign.SpaceBetween) // 两边对齐
.alignItems(VerticalAlign.Top) // 内容顶部对齐
// 判断描述内容是否存在
if (item.content) {
Text(item.content.substring(0, 50) + (item.content.length>50?'...':''))
.fontSize(12)
.fontColor(Color.Gray)
.width('100%')
.align(Alignment.Start)
.margin({top: 10})
}
}.padding(15)
.backgroundColor(Color.White)
.borderRadius(10)
.shadow({color: '#00000050', radius: 10})
}
build() {
// 略...
}
}
页面效果如下图:
五、新增笔记页面
新增笔记的具体步骤有如下几个步骤:
- 在App主界面,点击添加按钮进入编辑页面添加新笔记,编写完成后点击保存。
- 在App主界面,点击列表中信息进入编辑页面修改笔记标题或内容等,修改完成后点击保存。
- 当通过“新增”入口进入,编辑页面应显示为标题输入框和内容输入框;当是列表信息点击进入显示标题和内容部分,当点击右上角编辑按钮时,再进入编辑状态。
5.1 创建页面
新建页面,可以通过以下步骤完成创建,在pages目录上右击 -> 选择“新建” -> 选择“Page” -> 选择 Empty Page,如下图:
创建CreateNote.ets页面,用于添加新笔记内容界面。如下图:
打开新建的CreateNote.ets页面,代码如下:
@Entry
@Component
struct CreateNote {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('CreateNoteHelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}
.height('100%')
.width('100%')
}
}
当然,上面自动生成的内容并不是我们所需要的,将其不需要的部分删除,添加标题和内容输入框,以下返回按钮。代码如下:
@Entry
@Component
struct CreateNote {
@State title: string = ''
@State content: string = ''
build() {
RelativeContainer() {
Row({space: 20}){
Column(){
Row(){
Image($rawfile('back.png')).width(24).height(24)
}.width('100%').justifyContent(FlexAlign.SpaceBetween).margin({bottom: 15})
// 标题
TextInput({placeholder: '请输入标题', text: this.title})
// 分割线
Divider().margin({top: 15, bottom: 15})
// 内容输入框,(layoutWeight 比重为1,表示剩余空间分配给 内容输入框)
TextArea({placeholder: '请输入内容', text: this.content}).layoutWeight(1)
}.height('100%')
}.width('100%').height('100%')
.padding(15)
}
.height('100%')
.width('100%')
}
}
效果如下图:
5.2 路由跳转
当添加新笔记页面完成后,在主界面中点击列表信息 和 新增按钮时,可以跳转到编辑界面。打开src/main/ets/pages/index/ets, 代码如下:
import Header from '../components/Header'
import { NotesInfo } from '../types/types'
import { formatDate } from '../utils/utils'
import router from '@ohos.router'
@Entry
@Component
struct Index {
@State notes: Array<NotesInfo> = [
{
id: 1,
title: '明媚的星期天的早晨',
content: 'test',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
},
{
id: 2,
title: '成功的喜悦',
content: '',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
},
{
id: 3,
title: '冲动是魔鬼',
content: '',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
},
{
id: 4,
title: '意外惊喜',
content: '',
classify_id: 1,
create_time: new Date(),
update_time: new Date()
}
]
// 自定义面板panel item
@Builder listItem(item: NotesInfo){
Column(){
Row(){
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.padding({ right: 10 }) // 右侧内填充10,与日期相隔10个间距
Text(formatDate(item.create_time, 'YYYY/MM/DD'))
.fontSize(12)
.fontColor(Color.Gray)
}.width('100%')
.justifyContent(FlexAlign.SpaceBetween) // 两边对齐
.alignItems(VerticalAlign.Top) // 内容顶部对齐
// 判断描述内容是否存在
if (item.content) {
Text(item.content.substring(0, 50) + (item.content.length>50?'...':''))
.fontSize(12)
.fontColor(Color.Gray)
.width('100%')
.align(Alignment.Start)
.margin({top: 10})
}
}.padding(15)
.backgroundColor(Color.White)
.borderRadius(10)
.shadow({color: '#00000050', radius: 10})
// 列表信息 点击跳转到编辑界面
.onClick(() => {
router.pushUrl({url: 'pages/CreateNote', params: item})
})
}
build() {
Row() {
Column() {
Header() // 顶部组件
Divider() //分割线
// List容器
List(){
// 循环输出笔记列表内容
ForEach(this.notes, (item: NotesInfo) => {
ListItem(){
this.listItem(item) // 渲染面板内容
}.border({color: Color.Gray, style: BorderStyle.Dashed})
.padding({ top: 10, bottom: 10 })
})
}.width('100%')
.layoutWeight(6)
.padding({ left: 10, right: 10, top: 10, bottom: 10 })
.backgroundColor('#f1f1f1')
// 添加按钮
Button(){
Image($rawfile('add.png'))
.width(40)
}.position({ right: 10, bottom: 10 })
.backgroundColor(Color.Transparent)
// 新增按钮 点击跳转到编辑界面
.onClick(() => {
router.pushUrl({url: 'pages/CreateNote'})
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.height('100%').alignItems(VerticalAlign.Top)
}
}
当上述代码完成后,则可以点击列表信息或添加按钮 进入编辑界面了。如下图:
5.3 信息传递
当点击“添加"按钮时,直接进入编辑界面添加即可;但是点击列表中信息时,则需要将对应参数传递到编辑界面,先以预览形式展示,当点击编辑按钮时调整为编辑状态。 在"5.2 路径跳转”中,列表信息点击跳转时,已将参数通过 params带入到编辑界面,需要通过router.getParams()获取传递的参数,并作出对应数据处理即可。
CreateNote.ets文件的代码如下:
import router from '@ohos.router'
import { NotesInfo } from '../types/types'
@Entry
@Component
struct CreateNote {
@State title: string = '' // 标题
@State content: string = '' // 内容
@State isShowEditButton: boolean = false // 是否显示编辑按钮
@State isEditor: boolean = true // 是否为编辑模式
// 获取详情数据ID
aboutToAppear(): void {
const params = router.getParams() as NotesInfo
if (!('undefined' === typeof params || 'undefined' === typeof params.id)) {
this.title = params.title // 赋值标题
this.content = params.content // 赋值内容
this.isShowEditButton = true // 显示编辑按钮
this.isEditor = false
}
console.log('params', JSON.stringify(params))
}
build() {
RelativeContainer() {
Row({space: 20}){
Column(){
Row(){
Image($rawfile('back.png')).width(24).height(24)
.onClick(() => {
router.back()
})
// 判断是否需要显示编辑按钮
if (this.isShowEditButton) {
Button(){
// 当isEditor为false时,为预览模式显示编辑按钮图标,当为true时,为编辑模式显示取消编辑图标
Image(!this.isEditor?$rawfile('edit.png'):$rawfile('edit_cancel.png')).width(24).height(24)
}.backgroundColor(Color.Transparent)
.onClick(() => {
this.isEditor = !this.isEditor
})
}
}.width('100%').justifyContent(FlexAlign.SpaceBetween).margin({bottom: 15})
// 预览模式
if (!this.isEditor) {
Text(this.title).align(Alignment.Start)
Divider().margin({top: 15, bottom: 15})
Text(this.content).width('100%')
}
// 编辑模式
else {
// 标题
TextInput({placeholder: '请输入标题', text: this.title})
.onChange((e) => {
// 更新标题部分信息
this.title = e
})
// 分割线
Divider().margin({top: 15, bottom: 15})
// 内容输入框,(layoutWeight 比重为1,表示剩余空间分配给 内容输入框)
TextArea({placeholder: '请输入内容', text: this.content}).layoutWeight(1)
.onChange((e) => {
// 更新内容部分
this.content = e
})
}
}.height('100%')
}.width('100%').height('100%')
.padding(15)
}
.height('100%')
.width('100%')
}
}
预览模式 编辑模式
六、分类管理
由于分类信息的修改、删除等功能还未完成,并且全部集中在主界面上,会使界面操作过于繁琐,并增加功能的实现难度。所以,决定增加分类管理界面,用于增加、修改、删除等功能操作,主界面中“增加分类”功能继续保留。
6.1 管理入口
在主界面中,将分类信息操作按钮集中放在右侧,增加分类管理二级页面入口。打开文件src/main/ets/components/Header.ets,修改分类信息区域功能。代码如下:
import { ClassifyAddDialog } from './classifyAddDialog'
import { ClassifyInfo } from '../types/types'
import router from '@ohos.router'
// 定义分类默认样式
@Extend(Button) function classifyNormalStyle(){
.fontSize(12)
.fontColor(Color.Black)
.padding({left: 15, right: 15})
.height(26).backgroundColor(Color.White)
}
// 定义分类项选中后的样式
@Extend(Button) function classifyActiveStyle(){
.fontColor(Color.White).backgroundColor(Color.Grey)
}
@Component
export default struct Header {
@State isSearch: boolean = false // 是否为搜索状态
// 分类信息
@State classifyList: Array<ClassifyInfo> = [
{id: -1, name: '全部'},
{id: 1, name: '速记'},
{id: 2, name: '默认'},
]
@State classifyActive: number = 0 // 分类选中项索引,默认为0
@State textValue: string = '' // 文本信息
@State inputValue: string = '' // 输入信息
dialogController: CustomDialogController = new CustomDialogController({
builder: ClassifyAddDialog({
cancel: this.onCancel.bind(this),
confirm: this.onAccept.bind(this),
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Default,
// offset: {dx: 0, dy: -20},
gridCount: 4,
customStyle: false
})
// 取消事件回调函数
onCancel() {
this.textValue = ''
console.info('Callback when the cancel button is clicked', this.textValue, this.inputValue)
}
// 确认完成回调函数,追加分类信息到classifyList容器中
async onAccept() {
this.textValue = ''
this.classifyList.push({
id: this.classifyList.length + 1,
name: this.inputValue
})
console.info('Callback when the accept button is clicked', this.textValue, this.inputValue)
}
existApp() {
console.info('Click the callback in the blank area')
}
/**
* 更新分类选中索引
* @param index
*/
updateClassifyActive(index: number){
this.classifyActive = index
}
build() {
Row(){
Column(){
// 非搜索状态下显示内容
if(!this.isSearch) {
Row(){
Text('笔记').fontSize(20).fontWeight(FontWeight.Bold)
Blank()
Row(){
Button(){
Image($rawfile('search.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
Text('搜索').fontSize(15)
}
.onClick(() => {
this.isSearch = !this.isSearch
})
}.width('100%')
// 显示当前笔记数量
Row(){
Text('15篇笔记').fontSize(12).fontColor(Color.Gray)
}.width('100%')
Row(){
// 分类信息
Scroll(){
Row({ space: 5 }){
ForEach(this.classifyList, (item: ClassifyInfo, index) => {
if(index === this.classifyActive) {
Button(item.name).classifyNormalStyle().classifyActiveStyle().onClick(() => {
this.updateClassifyActive(index)
})
} else {
Button(item.name).classifyNormalStyle().onClick(() => {
this.updateClassifyActive(index)
})
}
})
}.padding({top: 10, bottom: 0}).justifyContent(FlexAlign.Start)
}.height(40).scrollable(ScrollDirection.Horizontal).layoutWeight(1)
// 添加分类按钮
Button(){
Image($rawfile('add.png')).width(20).height(20)
}.backgroundColor(Color.Transparent).margin({left: 10}).onClick(() => {
this.dialogController.open()
})
// 管理界面按钮
Button(){
Image($rawfile('manage.png')).width(20).height(20)
}.backgroundColor(Color.Transparent).margin({left: 15}).onClick(() => {
router.pushUrl({
url: 'pages/ClassifyPage'
})
})
}.justifyContent(FlexAlign.Start)
}
// 搜索状态下显示内容
else {
Row(){
Image($rawfile('search.png')).width(24).margin({right: 10})
TextInput({placeholder: '请输入搜索内容'})
.type(InputType.Normal)
// .width(230)
.height(36)
.layoutWeight(1)
Blank()
Button('取消').fontSize(15).fontColor(Color.Orange)
.width(70)
.height(36)
.backgroundColor(Color.Transparent)
.stateEffect(false)
.align(Alignment.End)
.onClick(() => {
this.isSearch = !this.isSearch
})
}.width('100%').justifyContent(FlexAlign.SpaceAround)
}
}.width('100%')
}
.width('100%')
.padding({top: '10vp', bottom: '10vp', left: '20vp', right: '20vp'})
// .shadow({radius: 10, color: 'rgba(0, 0, 0, .05)', offsetX: 0, offsetY: 1})
}
}
界面效果如下图:
6.2 管理界面
在管理界面,完成顶部导航栏功能,左侧为返回按钮,右侧为新增按钮。打开src/main/ets/pages/ClassifyPage.ets文件,编写顶部导航栏。代码如下:
import { ClassifyInfo } from '../types/types'
import { router } from '@kit.ArkUI'
import { ClassifyAddDialog } from '../components/classifyAddDialog'
@Entry
@Component
struct ClassifyPage {
// 分类信息
@State classifyList: Array<ClassifyInfo> = []
@State textValue: string = '' // 文本信息
@State inputValue: string = '' // 输入信息
private selectedTextId: number = -1 // 选中修改项id
dialogController: CustomDialogController = new CustomDialogController({
builder: ClassifyAddDialog({
cancel: this.onCancel.bind(this),
confirm: this.onAccept.bind(this),
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Default,
// offset: {dx: 0, dy: -20},
gridCount: 4,
customStyle: false
})
// 取消事件回调函数
onCancel() {
this.textValue = ''
this.selectedTextId = -1
console.info('Callback when the cancel button is clicked', this.textValue, this.inputValue)
}
// 确认完成回调函数,追加分类信息到classifyList容器中
async onAccept() {
this.textValue = ''
console.info('Callback when the accept button is clicked', this.textValue, this.inputValue)
}
existApp() {
this.selectedTextId = -1
console.info('Click the callback in the blank area')
}
build() {
RelativeContainer() {
Row({space: 20}){
Column(){
Row(){
Image($rawfile('back.png')).width(24).height(24)
.onClick(() => {
router.back()
})
Blank()
Text('分类管理')
Blank()
Button(){
Image($rawfile('add.png')).width(20).height(20)
}.backgroundColor(Color.Transparent).margin({left: 10}).onClick(() => {
this.dialogController.open()
})
}.width('100%')
.justifyContent(FlexAlign.SpaceAround)
.margin({bottom: 15})
.padding({left: 15, right: 15})
// List容器
}.height('100%')
}
// end
}
.height('100%')
.width('100%')
}
}
页面效果如下图:
6.3 添加分类
添加分类信息部分代码,将主界面中拷贝过来直接使用即可。效果如下图:
6.4 分类列表
分类信息列表中,左侧显示分类名称,右侧显示操作功能(编辑、删除)等。列表功能同样使用List()组件完成,打开 ClassifyPage.ets文件,完成列表渲染。代码如下:
import { ClassifyInfo } from '../types/types'
import { router } from '@kit.ArkUI'
import { ClassifyAddDialog } from '../components/classifyAddDialog'
@Entry
@Component
struct ClassifyPage {
// 分类信息
@State classifyList: Array<ClassifyInfo> = [
{id: 1, name: '速记'},
{id: 2, name: '默认'}
]
@State textValue: string = '' // 文本信息
@State inputValue: string = '' // 输入信息
private selectedTextId: number = -1 // 选中修改项id
dialogController: CustomDialogController = new CustomDialogController({
builder: ClassifyAddDialog({
cancel: this.onCancel.bind(this),
confirm: this.onAccept.bind(this),
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Default,
// offset: {dx: 0, dy: -20},
gridCount: 4,
customStyle: false
})
// 取消事件回调函数
onCancel() {
this.textValue = ''
this.selectedTextId = -1
console.info('Callback when the cancel button is clicked', this.textValue, this.inputValue)
}
// 确认完成回调函数,追加分类信息到classifyList容器中
async onAccept() {
this.textValue = ''
console.info('Callback when the accept button is clicked', this.textValue, this.inputValue)
}
existApp() {
this.selectedTextId = -1
console.info('Click the callback in the blank area')
}
build() {
RelativeContainer() {
Row({space: 20}){
Column(){
Row(){
Image($rawfile('back.png')).width(24).height(24)
.onClick(() => {
router.back()
})
Blank()
Text('分类管理')
Blank()
Button(){
Image($rawfile('add.png')).width(20).height(20)
}.backgroundColor(Color.Transparent).margin({left: 10}).onClick(() => {
this.dialogController.open()
})
}.width('100%')
.justifyContent(FlexAlign.SpaceAround)
.margin({bottom: 15})
.padding({left: 15, right: 15})
// List容器
List(){
// 循环输出笔记列表内容
ForEach(this.classifyList, (item: ClassifyInfo) => {
ListItem(){
Row(){
// Text('ID:' + item.id).width(50)
Text(item.name).margin({left: 15})
Blank()
Row(){
// 修改按钮
Button(){
Image($rawfile('edit.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
// 删除按钮
Button(){
Image($rawfile('delete.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
}
}.width('100%')
.padding({ left: 15, right: 15, top: 10, bottom: 10 })
.backgroundColor(Color.White).borderRadius(5)
}.border({color: Color.Gray, style: BorderStyle.Dashed})
.padding({ top: 5, bottom: 5 })
})
}.width('100%')
.layoutWeight(1)
.padding({ left: 10, right: 10, top: 10, bottom: 10 })
.backgroundColor('#f1f1f1')
}.height('100%')
}
// end
}
.height('100%')
.width('100%')
}
}
界面效果如下图:
6.5 编辑功能
在编辑按钮上添加点击事件,记录修改分类信息的id,以及将分类名称赋值给textValue变量。代码如下:
import { ClassifyInfo } from '../types/types'
import { router } from '@kit.ArkUI'
import { ClassifyAddDialog } from '../components/classifyAddDialog'
@Entry
@Component
struct ClassifyPage {
// 分类信息
@State classifyList: Array<ClassifyInfo> = [
{id: 1, name: '速记'},
{id: 2, name: '默认'}
]
@State textValue: string = '' // 文本信息
@State inputValue: string = '' // 输入信息
private selectedTextId: number = -1 // 选中修改项id
// 略...
build() {
RelativeContainer() {
Row({space: 20}){
Column(){
// 略...
// List容器
List(){
// 循环输出笔记列表内容
ForEach(this.classifyList, (item: ClassifyInfo) => {
ListItem(){
Row(){
// Text('ID:' + item.id).width(50)
Text(item.name).margin({left: 15})
Blank()
Row(){
// 修改按钮
Button(){
Image($rawfile('edit.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
.onClick(() => {
this.selectedTextId = item.id
this.textValue = item.name
this.dialogController.open()
})
// 删除按钮
Button(){
Image($rawfile('delete.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
}
}.width('100%')
.padding({ left: 15, right: 15, top: 10, bottom: 10 })
.backgroundColor(Color.White).borderRadius(5)
}.border({color: Color.Gray, style: BorderStyle.Dashed})
.padding({ top: 5, bottom: 5 })
})
}.width('100%')
.layoutWeight(1)
.padding({ left: 10, right: 10, top: 10, bottom: 10 })
.backgroundColor('#f1f1f1')
}.height('100%')
}
// end
}
.height('100%')
.width('100%')
}
}
界面效果如下图:
6.6 删除功能
在 HarmonyOS ArkTS 中,AlertDialog 是一个用于显示警告或提示信息的弹框组件,支持多种配置和交互方式。这里,将使用它完成删除操作的提示与执行操作。
AlertDialog基本用法:
AlertDialog.show() 方法用于显示弹框,支持多种参数配置,包括标题、内容、按钮、动画效果等。
AlertDialog常用的参数说明:
- title:弹框标题。
- message:弹框内容。
- buttons:按钮配置,支持多个按钮。
- autoCancel:点击弹框外部是否自动关闭。
- alignment:弹框显示位置(如 DialogAlignment.Center)。
- transition:自定义弹框的显示和消失动画。
- isModal:是否为模态弹框(模态弹框会覆盖整个屏幕,非模态弹框不会)。
- cancel:弹框关闭时的回调函数。
- onWillDismiss:弹框即将关闭时的回调函数,可以控制是否允许关闭。
在删除按钮上添加点击事件,并完成AlertDialog提示功能实现。代码如下:
import { ClassifyInfo } from '../types/types'
import { router } from '@kit.ArkUI'
import { ClassifyAddDialog } from '../components/classifyAddDialog'
@Entry
@Component
struct ClassifyPage {
// 分类信息
@State classifyList: Array<ClassifyInfo> = [
{id: 1, name: '速记'},
{id: 2, name: '默认'}
]
@State textValue: string = '' // 文本信息
@State inputValue: string = '' // 输入信息
private selectedTextId: number = -1 // 选中修改项id
// 略...
build() {
RelativeContainer() {
Row({space: 20}){
Column(){
// 略...
// List容器
List(){
// 循环输出笔记列表内容
ForEach(this.classifyList, (item: ClassifyInfo) => {
ListItem(){
Row(){
// Text('ID:' + item.id).width(50)
Text(item.name).margin({left: 15})
Blank()
Row(){
// 修改按钮
Button(){
Image($rawfile('edit.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
.onClick(() => {
this.selectedTextId = item.id
this.textValue = item.name
this.dialogController.open()
})
// 删除按钮
Button(){
Image($rawfile('delete.png')).width(24)
}.backgroundColor(Color.Transparent).width(36).height(36)
.onClick(() => {
AlertDialog.show({
title: '提示',
message: `是否确认要删除 [${item.name}]?`,
alignment: DialogAlignment.Center,
buttons: [
{
value: '删除',
action: async () => {
console.log('testTag delete', item.id)
}
},
{
value: '取消',
action: () => {
this.dialogController.close()
}
}
]
})
// end
})
}
}.width('100%')
.padding({ left: 15, right: 15, top: 10, bottom: 10 })
.backgroundColor(Color.White).borderRadius(5)
}.border({color: Color.Gray, style: BorderStyle.Dashed})
.padding({ top: 5, bottom: 5 })
})
}.width('100%')
.layoutWeight(1)
.padding({ left: 10, right: 10, top: 10, bottom: 10 })
.backgroundColor('#f1f1f1')
}.height('100%')
}
// end
}
.height('100%')
.width('100%')
}
}
界面效果如下图:
静态界面的实现讲到这里就结束了,后续将通过 关系型数据库(Relational Database,RDB)来完成数据的存储、查询、修改、删除等功能。