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

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 展示分类

        在编写分类信息时,需要完成以下几步操作:

  1. 定义全局状态容器用于存储分类信息
  2. 定义分类信息中数据类型
  3. 定义默认分类样式
  4. 定义选中后的分类样式
  5. 添加点击事件,切换当前选中项索引,来达到切换高亮效果。
  6. 使用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)来完成数据的存储、查询、修改、删除等功能。


http://www.kler.cn/a/541049.html

相关文章:

  • TCN时间卷积神经网络多变量多步光伏功率预测(Matlab)
  • 认识网络安全
  • [前端]CRX持久化
  • 【前端开发】HTML+CSS+JavaScript前端三剑客的基础知识体系了解
  • qml RoundButton详解
  • Kickstart自动化安装过程中自动选择较小的磁盘安装操作系统
  • 如何利用 AWS 预测分析功能做精准财务规划?
  • C++ STL Map 学习学案(提高版)
  • java-LinkedList源码详解
  • 本地部署DeepSeek-R1(Mac版)
  • keil5自学笔记1(基础设置)
  • RapidrepairDaoImpl
  • 利用ES6 Set去重
  • 标题:深入探索 gRPC:后端开发中高效通信的利器
  • Python语言的数据可视化
  • 2025年一段代码使用python完成过年放烟花写祝福
  • 【多模态大模型】系列2:如何用多GPU训练一个非常大的模型(数据/模型/流水线/张量并行、MoE、混合精度训练、压缩、激活重新计算)
  • elementplus 使用日期时间选择器,设置可选范围为前后大于2年且只能选择历史时间不能大于当前时间点
  • DeepSeek大模型的发展的十问十答
  • 蓄电池放电技术革新:引领能源存储新时代
  • Day60_补20250208_图论part5_并查集理论基础|寻找存在的路径
  • C++ 中的 cJSON 解析库:用法、实现及递归解析算法与内存高效管理
  • 基于fpga的数字频率计(论文+源码)
  • 鱼塘钓鱼(多路归并,贪心)
  • PDF密码忘了?三步找回超简单
  • 详解享元模式