NotesApp - 笔记应用教程
项目介绍
项目背景
笔记应用是现代移动设备上最实用的工具类应用之一。它展示了如何处理复杂的数据管理、多页面切换、表单输入等高级功能。通过构建一个笔记应用,你将深入理解 HarmonyOS NEXT 中的高级组件使用、数据管理、页面导航等核心概念。
应用场景
笔记应用在日常生活中有着广泛的应用场景:
- 学习笔记:记录课堂笔记、学习心得
- 工作记录:会议记录、项目文档
- 生活日记:日常感悟、旅行日记
- 灵感收集:创意想法、灵感闪现
- 知识管理:整理知识体系、构建个人知识库
功能特性
本笔记应用实现了以下功能:
- 新建笔记:创建新的笔记,支持标题和内容
- 编辑笔记:修改现有笔记的内容
- 删除笔记:删除不需要的笔记
- 分类管理:按分类(工作/生活/学习/其他)管理笔记
- 搜索功能:通过关键词搜索笔记
- 列表展示:以卡片形式展示笔记列表
最终效果
应用包含两个主要页面:
- 列表页面:展示所有笔记,支持搜索和筛选
- 编辑页面:创建和编辑笔记,支持分类选择
技术栈
- 开发框架:HarmonyOS NEXT API 23
- 编程语言:ArkTS
- UI 框架:ArkUI 声明式 UI
- 页面切换:条件渲染
- 数据管理:@State 状态管理
- 表单组件:TextInput、TextArea


开发环境准备
1. 创建项目
创建一个新的 HarmonyOS NEXT 项目:
方式一:使用 DevEco Studio 创建
- 打开 DevEco Studio
- 选择 "Create HarmonyOS Project"
- 选择 "Empty Ability" 模板
- 设置项目名称为 "NotesApp"
- 选择 API 版本为 23
方式二:复制模板项目
- 复制
project-template目录 - 重命名为 "NotesApp"
- 修改配置文件
2. 项目结构
NotesApp/
├── AppScope/ # 应用全局配置
│ ├── app.json5 # 应用级配置
│ └── resources/ # 应用级资源
├── entry/ # 主模块
│ └── src/main/
│ ├── ets/ # ArkTS 源代码
│ │ ├── entryability/ # UIAbility 入口
│ │ └── pages/ # 页面组件
│ └── resources/ # 资源文件
├── build-profile.json5 # 构建配置
└── oh-package.json5 # 依赖配置
知识点讲解
1. TextArea 组件 - 多行文本输入详解
TextArea 组件用于创建多行文本输入框,适合输入较长的内容,如笔记正文。
基本用法:
typescript
TextArea({ placeholder: '请输入内容', text: this.content })
.width('100%')
.height(200)
.fontSize(16)
.onChange((value: string) => {
this.content = value;
})
核心属性:
typescript
TextArea({
placeholder: '占位符文本', // 输入框为空时显示的提示文本
text: this.content // 绑定的文本变量
})
.width('100%') // 宽度
.height(200) // 高度
.fontSize(16) // 字体大小
.fontColor('#333333') // 字体颜色
.placeholderColor('#999999') // 占位符颜色
.backgroundColor('#FFFFFF') // 背景颜色
.borderRadius(8) // 圆角
.padding(16) // 内边距
.maxLength(1000) // 最大输入长度
.onChange((value: string) => {
// 输入内容变化时触发
this.content = value;
})
.onSubmit(() => {
// 按下回车键时触发
console.info('提交');
})
.onFocus(() => {
// 获得焦点时触发
console.info('获得焦点');
})
.onBlur(() => {
// 失去焦点时触发
console.info('失去焦点');
})
与 TextInput 的区别:
- TextInput:单行输入,适合简短内容
- TextArea:多行输入,适合长文本
在笔记应用中的应用:
typescript
TextArea({ placeholder: '开始写作...', text: this.content })
.width('100%')
.layoutWeight(1) // 占据剩余空间
.fontSize(16)
.backgroundColor('#FFFBEB') // 淡黄色背景,模拟纸张
.borderRadius(16)
.margin(16)
.padding(16)
.onChange((value: string) => {
this.content = value;
})
2. @Builder 装饰器 - 自定义构建函数详解
@Builder 装饰器用于定义可复用的 UI 构建函数,类似于 React 中的 render 函数。
基本语法:
typescript
@Builder 函数名() {
// UI 组件
}
使用场景:
typescript
@Component
struct MyComponent {
build() {
Column() {
this.Header() // 调用 Builder 函数
this.Content()
this.Footer()
}
}
@Builder Header() {
Row() {
Text('标题')
}
}
@Builder Content() {
Column() {
Text('内容')
}
}
@Builder Footer() {
Row() {
Text('底部')
}
}
}
带参数的 Builder:
typescript
@Builder ButtonComponent(text: string, color: string) {
Button(text)
.backgroundColor(color)
.onClick(() => {
console.info(`点击了 ${text}`);
})
}
// 使用
this.ButtonComponent('确定', '#6366F1')
this.ButtonComponent('取消', '#EF4444')
在笔记应用中的应用:
typescript
@Entry
@Component
struct Index {
@State showEditor: boolean = false;
build() {
Column() {
if (this.showEditor) {
this.NoteEditor() // 显示编辑器
} else {
this.NoteList() // 显示列表
}
}
}
@Builder NoteList() {
// 列表页面内容
Column() {
// 头部
// 搜索栏
// 笔记列表
}
}
@Builder NoteEditor() {
// 编辑器页面内容
Column() {
// 头部
// 标题输入
// 内容输入
}
}
}
3. 条件渲染 - 页面切换详解
使用 if/else 实现页面切换,这是单页面应用常用的导航方式。
基本语法:
typescript
Column() {
if (condition) {
// 条件为真时显示
Page1()
} else {
// 条件为假时显示
Page2()
}
}
在笔记应用中的应用:
typescript
@Entry
@Component
struct Index {
@State showEditor: boolean = false;
@State editingNote: Note | null = null;
build() {
Column() {
if (this.showEditor) {
// 显示编辑器页面
this.NoteEditor()
} else {
// 显示列表页面
this.NoteList()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F8FAFC')
}
// 切换到编辑器
openEditor(note?: Note): void {
if (note) {
this.editingNote = note;
this.title = note.title;
this.content = note.content;
} else {
this.editingNote = null;
this.title = '';
this.content = '';
}
this.showEditor = true;
}
// 返回列表
goBack(): void {
this.showEditor = false;
this.editingNote = null;
}
}
页面切换动画:
typescript
Column() {
if (this.showEditor) {
this.NoteEditor()
.transition(TransitionType.Slide) // 滑动动画
} else {
this.NoteList()
}
}
4. 数组排序 - sort详解
使用 sort 方法对数组进行排序,可以按照自定义规则排序。
基本语法:
typescript
array.sort((a, b) => {
// 返回负数:a 排在 b 前面
// 返回正数:b 排在 a 前面
// 返回 0:保持原顺序
})
排序示例:
typescript
// 数字升序
[3, 1, 4, 1, 5].sort((a, b) => a - b)
// 结果: [1, 1, 3, 4, 5]
// 数字降序
[3, 1, 4, 1, 5].sort((a, b) => b - a)
// 结果: [5, 4, 3, 1, 1]
// 字符串排序
['banana', 'apple', 'cherry'].sort()
// 结果: ['apple', 'banana', 'cherry']
在笔记应用中的应用:
typescript
// 按置顶状态排序(置顶的排在前面)
return filtered.sort((a, b) => {
if (a.pinned && !b.pinned) return -1; // a置顶,b不置顶,a排前面
if (!a.pinned && b.pinned) return 1; // a不置顶,b置顶,b排前面
return 0; // 保持原顺序
});
// 按日期排序(最新的在前面)
return filtered.sort((a, b) => {
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
// 按标题排序
return filtered.sort((a, b) => {
return a.title.localeCompare(b.title);
});
5. 字符串方法详解
toLowerCase - 转换为小写:
typescript
const str = 'Hello World';
const lower = str.toLowerCase();
// 结果: 'hello world'
includes - 检查是否包含:
typescript
const str = 'Hello World';
const hasHello = str.includes('Hello'); // true
const hasHi = str.includes('Hi'); // false
trim - 去除首尾空格:
typescript
const str = ' Hello World ';
const trimmed = str.trim();
// 结果: 'Hello World'
在搜索功能中的应用:
typescript
// 搜索过滤
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase(); // 转换为小写
filtered = filtered.filter(n =>
n.title.toLowerCase().includes(query) || // 标题包含搜索词
n.content.toLowerCase().includes(query) // 内容包含搜索词
);
}
6. 数组方法详解
filter - 过滤数组:
typescript
// 过滤出符合条件的元素
const completed = todos.filter(todo => todo.completed);
const active = todos.filter(todo => !todo.completed);
findIndex - 查找索引:
typescript
// 查找元素的索引
const index = todos.findIndex(todo => todo.id === targetId);
if (index >= 0) {
// 找到元素
}
find - 查找元素:
typescript
// 查找元素
const todo = todos.find(todo => todo.id === targetId);
map - 映射数组:
typescript
// 提取所有标题
const titles = notes.map(note => note.title);
在笔记删除中的应用:
typescript
// 删除笔记
deleteNote(noteId: number): void {
this.notes = this.notes.filter(n => n.id !== noteId);
}
// 编辑笔记
updateNote(noteId: number, title: string, content: string): void {
const index = this.notes.findIndex(n => n.id === noteId);
if (index >= 0) {
this.notes[index].title = title;
this.notes[index].content = content;
this.notes[index].date = new Date().toLocaleDateString();
}
}
7. 非空断言 - !详解
当确定一个值不为 null 或 undefined 时,可以使用非空断言操作符 !。
使用场景:
typescript
// 可能为 null 的变量
let editingNote: Note | null = null;
// 在使用时确定不为 null
const index = notes.findIndex(n => n.id === editingNote!.id);
// editingNote! 表示确定 editingNote 不为 null
注意事项:
- 只在确定不为 null 时使用
- 错误使用可能导致运行时错误
- 可以使用可选链
?.替代
替代方案:
typescript
// 使用可选链
const index = notes.findIndex(n => n.id === editingNote?.id);
// 使用条件判断
if (editingNote) {
const index = notes.findIndex(n => n.id === editingNote.id);
}
8. Scroll 横向滚动详解
创建横向滚动的标签栏,适合展示多个筛选选项。
基本用法:
typescript
Scroll() {
Row() {
Text('标签1')
.margin({ right: 8 })
Text('标签2')
.margin({ right: 8 })
Text('标签3')
}
}
.scrollable(ScrollDirection.Horizontal) // 设置为横向滚动
.width('100%')
在笔记应用中的应用:
typescript
Scroll() {
Row() {
Text('全部')
.fontSize(14)
.fontWeight(this.selectedCategory === 0 ? FontWeight.Medium : FontWeight.Regular)
.fontColor(this.selectedCategory === 0 ? '#8B5CF6' : '#64748B')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor(this.selectedCategory === 0 ? '#EDE9FE' : '#FFFFFF')
.borderRadius(999)
.margin({ right: 8 })
.onClick(() => { this.selectedCategory = 0 })
Text('工作')
.fontSize(14)
.fontWeight(this.selectedCategory === 1 ? FontWeight.Medium : FontWeight.Regular)
.fontColor(this.selectedCategory === 1 ? '#8B5CF6' : '#64748B')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor(this.selectedCategory === 1 ? '#EDE9FE' : '#FFFFFF')
.borderRadius(999)
.margin({ right: 8 })
.onClick(() => { this.selectedCategory = 1 })
// ... 更多标签
}
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
.margin({ bottom: 16 })
9. 分类颜色映射详解
使用方法返回不同的颜色值,实现分类的颜色区分。
基本实现:
typescript
getCategoryColor(category: number): string {
switch (category) {
case 1: return '#EF4444'; // 红色 - 工作
case 2: return '#F59E0B'; // 黄色 - 生活
case 3: return '#10B981'; // 绿色 - 学习
case 4: return '#3B82F6'; // 蓝色 - 其他
default: return '#94A3B8'; // 灰色 - 默认
}
}
在笔记卡片中的应用:
typescript
Row() {
// 分类指示器
Column()
.width(4)
.height(40)
.backgroundColor(this.getCategoryColor(note.category))
.borderRadius(2)
// 笔记内容
Column() {
// ...
}
}
10. 多条件过滤详解
实现多条件组合过滤,支持分类筛选和搜索。
完整实现:
typescript
getFilteredNotes(): Note[] {
let filtered = this.notes;
// 1. 按分类过滤
if (this.selectedCategory > 0) {
filtered = filtered.filter(n => n.category === this.selectedCategory);
}
// 2. 按搜索词过滤
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase();
filtered = filtered.filter(n =>
n.title.toLowerCase().includes(query) ||
n.content.toLowerCase().includes(query)
);
}
// 3. 排序(可选)
filtered = filtered.sort((a, b) => {
// 按日期降序
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
return filtered;
}
过滤流程:
- 获取所有笔记
- 按分类筛选
- 按搜索词筛选
- 排序
- 返回结果
完整代码解析
数据结构定义
typescript
// 定义笔记数据结构
interface Note {
id: number; // 唯一标识
title: string; // 标题
content: string; // 内容
date: string; // 日期
category: number; // 分类:1-工作,2-生活,3-学习,4-其他
}
页面组件定义
typescript
@Entry
@Component
struct Index {
@State notes: Note[] = []; // 笔记数组
@State nextId: number = 1; // 下一个ID
@State showEditor: boolean = false; // 是否显示编辑器
@State editingNote: Note | null = null; // 正在编辑的笔记
@State title: string = ''; // 编辑中的标题
@State content: string = ''; // 编辑中的内容
@State searchQuery: string = ''; // 搜索词
@State selectedCategory: number = 0; // 选中的分类
build() {
Column() {
if (this.showEditor) {
this.NoteEditor() // 编辑器页面
} else {
this.NoteList() // 列表页面
}
}
.width('100%')
.height('100%')
.backgroundColor('#F8FAFC')
}
}
列表页面 - NoteList
typescript
@Builder NoteList() {
Column() {
// 头部区域
Row() {
Column() {
Text('我的笔记')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#1E293B')
Text(`${this.notes.length} 条笔记`)
.fontSize(12)
.fontColor('#64748B')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Blank()
Button('+ 新建')
.height(44)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.backgroundColor('#8B5CF6')
.fontColor('#FFFFFF')
.borderRadius(12)
.onClick(() => {
this.showEditor = true;
this.editingNote = null;
this.title = '';
this.content = '';
this.selectedCategory = 0;
})
}
.width('100%')
.padding({ bottom: 16 })
// 搜索栏
TextInput({ placeholder: '搜索笔记...', text: this.searchQuery })
.width('100%')
.height(44)
.fontSize(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ bottom: 16 })
.onChange((value: string) => {
this.searchQuery = value;
})
// 分类筛选标签
Scroll() {
Row() {
this.CategoryChip('全部', 0)
this.CategoryChip('工作', 1)
this.CategoryChip('生活', 2)
this.CategoryChip('学习', 3)
this.CategoryChip('其他', 4)
}
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
.margin({ bottom: 16 })
// 笔记列表
if (this.getFilteredNotes().length === 0) {
// 空状态
Column() {
Text('📝')
.fontSize(48)
.margin({ bottom: 16 })
Text(this.searchQuery ? '没有找到匹配的笔记' : '暂无笔记')
.fontSize(16)
.fontColor('#94A3B8')
if (!this.searchQuery) {
Text('点击右上角按钮创建第一条笔记')
.fontSize(12)
.fontColor('#94A3B8')
.margin({ top: 8 })
}
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else {
// 笔记列表
List() {
ForEach(this.getFilteredNotes(), (note: Note) => {
ListItem() {
this.NoteCard(note)
}
.margin({ bottom: 8 })
}, (note: Note) => note.id.toString())
}
.layoutWeight(1)
.width('100%')
}
}
.padding(16)
}
分类标签组件
typescript
@Builder CategoryChip(label: string, category: number) {
Text(label)
.fontSize(14)
.fontWeight(this.selectedCategory === category ? FontWeight.Medium : FontWeight.Regular)
.fontColor(this.selectedCategory === category ? '#8B5CF6' : '#64748B')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor(this.selectedCategory === category ? '#EDE9FE' : '#FFFFFF')
.borderRadius(999)
.margin({ right: 8 })
.onClick(() => {
this.selectedCategory = category;
})
}
笔记卡片组件
typescript
@Builder NoteCard(note: Note) {
Row() {
// 分类指示器
Column()
.width(4)
.height(40)
.backgroundColor(this.getCategoryColor(note.category))
.borderRadius(2)
// 笔记内容
Column() {
Text(note.title)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#1E293B')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(note.content)
.fontSize(14)
.fontColor('#64748B')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 4 })
Row() {
Text(note.date)
.fontSize(12)
.fontColor('#94A3B8')
Blank()
Button('编辑')
.height(28)
.fontSize(12)
.backgroundColor('#EDE9FE')
.fontColor('#8B5CF6')
.onClick(() => {
this.editingNote = note;
this.title = note.title;
this.content = note.content;
this.selectedCategory = note.category;
this.showEditor = true;
})
Button('删除')
.height(28)
.fontSize(12)
.backgroundColor('#FEE2E2')
.fontColor('#EF4444')
.margin({ left: 8 })
.onClick(() => {
this.notes = this.notes.filter(n => n.id !== note.id);
})
}
.width('100%')
.margin({ top: 8 })
}
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
编辑器页面 - NoteEditor
typescript
@Builder NoteEditor() {
Column() {
// 编辑器头部
Row() {
Button('← 返回')
.height(44)
.fontSize(14)
.backgroundColor('#F3F4F6')
.fontColor('#64748B')
.onClick(() => {
this.showEditor = false;
})
Text(this.editingNote ? '编辑笔记' : '新建笔记')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1E293B')
.margin({ left: 16 })
Blank()
Button('保存')
.height(44)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.backgroundColor('#8B5CF6')
.fontColor('#FFFFFF')
.borderRadius(12)
.onClick(() => {
this.saveNote();
})
}
.width('100%')
.padding(16)
// 分类选择
Scroll() {
Row() {
this.CategoryChip('工作', 1)
this.CategoryChip('生活', 2)
this.CategoryChip('学习', 3)
this.CategoryChip('其他', 4)
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
.margin({ bottom: 16 })
// 标题输入
TextInput({ placeholder: '输入标题...', text: this.title })
.width('100%')
.height(56)
.fontSize(20)
.fontWeight(FontWeight.Medium)
.backgroundColor('#FFFFFF')
.margin({ left: 16, right: 16 })
.onChange((value: string) => {
this.title = value;
})
// 内容输入
TextArea({ placeholder: '开始写作...', text: this.content })
.width('100%')
.layoutWeight(1)
.fontSize(16)
.backgroundColor('#FFFBEB')
.borderRadius(16)
.margin(16)
.padding(16)
.onChange((value: string) => {
this.content = value;
})
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
核心方法
typescript
// 保存笔记
saveNote(): void {
if (this.title.trim() && this.content.trim()) {
if (this.editingNote) {
// 编辑现有笔记
const index = this.notes.findIndex(n => n.id === this.editingNote!.id);
if (index >= 0) {
this.notes[index].title = this.title;
this.notes[index].content = this.content;
this.notes[index].date = new Date().toLocaleDateString();
this.notes[index].category = this.selectedCategory;
}
} else {
// 创建新笔记
this.notes.push({
id: this.nextId++,
title: this.title,
content: this.content,
date: new Date().toLocaleDateString(),
category: this.selectedCategory || 4 // 默认为"其他"
});
}
this.showEditor = false; // 返回列表页面
}
}
// 获取筛选后的笔记
getFilteredNotes(): Note[] {
let filtered = this.notes;
// 按分类过滤
if (this.selectedCategory > 0) {
filtered = filtered.filter(n => n.category === this.selectedCategory);
}
// 按搜索词过滤
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase();
filtered = filtered.filter(n =>
n.title.toLowerCase().includes(query) ||
n.content.toLowerCase().includes(query)
);
}
return filtered;
}
// 获取分类颜色
getCategoryColor(category: number): string {
switch (category) {
case 1: return '#EF4444'; // 红色 - 工作
case 2: return '#F59E0B'; // 黄色 - 生活
case 3: return '#10B981'; // 绿色 - 学习
case 4: return '#3B82F6'; // 蓝色 - 其他
default: return '#94A3B8'; // 灰色 - 默认
}
}
常见问题与解决方案
问题1:搜索性能问题
问题描述:当笔记数量很多时,搜索可能卡顿。
解决方案:
typescript
// 使用防抖优化搜索
private searchTimer: number = 0;
onSearchChange(value: string): void {
clearTimeout(this.searchTimer);
this.searchTimer = setTimeout(() => {
this.searchQuery = value;
}, 300);
}
问题2:数据丢失
问题描述:应用关闭后数据丢失。
解决方案:使用 Preferences 或数据库存储数据。
问题3:长文本显示
问题描述:长文本可能超出显示区域。
解决方案:
typescript
Text(note.content)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
扩展学习
1. 添加更多功能
- 富文本编辑
- 图片插入
- 语音笔记
- 笔记加密
2. 优化用户体验
- 手势操作
- 动画效果
- 主题切换
- 字体大小调整
3. 数据管理
- 数据导出
- 云同步
- 回收站
- 版本历史
总结
通过本教程,你学习了:
- TextArea - 多行文本输入组件
- @Builder - 自定义构建函数
- 条件渲染 - 页面切换
- 数组排序 - sort 方法
- 字符串方法 - toLowerCase、includes、trim
- 数组方法 - filter、findIndex、find、map
- 非空断言 - ! 操作符
- 横向滚动 - ScrollDirection.Horizontal
- 多条件过滤 - 组合过滤逻辑
- 分类颜色映射 - switch/case 返回值
这些知识点构成了 HarmonyOS NEXT 中复杂应用开发的基础,掌握它们后,你将能够构建更完整的笔记、日记、文档管理等应用。