鸿蒙原生应用从0到1:备忘录模块 —— 多视图切换与搜索实战
鸿蒙原生应用从0到1:备忘录模块 —— 多视图切换与搜索实战
系列第四篇,深入「备忘录」页面开发,重点讲解分类筛选 + 关键词搜索、详情视图、编辑模式、多视图切换等核心功能。
一、功能概览
备忘录是生活助手 App 中功能最丰富的页面,因为它需要在有限空间内承载多种视图状态:列表浏览、详情查看、新增编辑。
┌─────────────────────────────────┐ │ ← 返回 备忘录 共6篇 │ ├─────────────────────────────────┤ │ 🔍 搜索笔记... │ ← 搜索栏 ├─────────────────────────────────┤ │ [全部] [工作] [学习] [生活] ... │ ← 分类标签 ├─────────────────────────────────┤ │ ┌─────────────────────────────┐ │ │ │ 工作 2025-01-15 │ │ │ │ 项目管理要点 │ │ │ │ 1. 明确项目目标和范围... │ │ ← 笔记卡片 │ │ 查看全文 → │ │ │ └─────────────────────────────┘ │ │ ┌─────────────────────────────┐ │ │ │ 学习 2025-01-14 │ │ │ │ ArkTS学习笔记 │ │ │ │ ArkTS是鸿蒙原生开发语言... │ │ │ │ 查看全文 → │ │ │ └─────────────────────────────┘ │ ├─────────────────────────────────┤ │ [+ 写笔记] │ └─────────────────────────────────┘二、数据模型
interfaceNoteItem{id:number;// 唯一标识title:string;// 标题content:string;// 内容(支持多行)category:string;// 分类date:string;// 日期}interfaceNoteCategory{name:string;// 筛选标识label:string;// 显示文字color:string;// 颜色}与待办页面不同,笔记的content是多行文本,内容可能较长,因此在列表显示时需要做截断处理。
三、复杂状态管理
备忘录页面有多种视图状态,需要 5 个状态变量来协调:
@Componentstruct NotePage{@Statenotes:NoteItem[]=[];@StateactiveCategory:string='全部';@StateshowAddDialog:boolean=false;@StateshowDetail:boolean=false;// 详情视图@StatedetailNote:NoteItem|null=null;// 当前查看的笔记@StateeditingNote:NoteItem|null=null;// 编辑中的笔记@StatenewTitle:string='';@StatenewContent:string='';@StatenewCategory:string='生活';@StatesearchText:string='';}3.1 视图状态转移
列表浏览 ──点击卡片──→ 详情查看 ──点击编辑──→ 编辑模式 ↑ │ │ └──── 返回列表 ──────┘ │ └──────────────── 保存/取消 ──────────────────┘ 列表浏览 ──点击 + ──→ 新增模式3.2 核心状态管理原则
showAddDialog控制弹窗显隐:新增和编辑共用同一个弹窗editingNote区分新增/编辑:为null表示新增,有值表示编辑showDetail+detailNote控制详情视图:两者配合使用
四、数据过滤——分类 + 搜索双过滤
这是本节最重要的知识点:
getfilteredNotes():NoteItem[]{letresult:NoteItem[]=this.notes;// 第一层过滤:分类if(this.activeCategory!=='全部'){result=result.filter((item:NoteItem)=>item.category===this.activeCategory);}// 第二层过滤:关键字搜索if(this.searchText.trim()){constkeyword:string=this.searchText.trim().toLowerCase();result=result.filter((item:NoteItem)=>item.title.toLowerCase().includes(keyword)||item.content.toLowerCase().includes(keyword));}returnresult;}设计亮点:
- 链式过滤:先分类后搜索,逻辑清晰
- 大小写无关:
toLowerCase()让搜索对大小写不敏感 - 标题+内容双字段匹配:用户可以在笔记全文搜索
getter写法:使用get filteredNotes()而非方法,让模板中直接使用this.filteredNotes,调用更简洁
五、UI 构建详解
5.1 搜索栏
TextInput({placeholder:'搜索笔记...',text:this.searchText}).width('90%').height(40).borderRadius(20)// 圆角效果.backgroundColor($r('app.color.bg_card')).padding({left:16}).onChange((value:string)=>{this.searchText=value;})细节:
borderRadius(20)让搜索栏呈现胶囊形状onChange回调实时更新searchText,实现即时搜索
5.2 笔记卡片
@BuildernoteCard(note:NoteItem):void{Column(){// 顶部:分类标签 + 日期Row(){Text(note.category).fontSize(10).fontColor(Color.White).padding({left:10,right:10,top:3,bottom:3}).backgroundColor(this.getCategoryColor(note.category)).borderRadius(8)Blank()Text(note.date).fontSize($r('app.float.tiny_font_size')).fontColor($r('app.color.text_secondary'))}.width('100%')// 标题Text(note.title).fontSize($r('app.float.body_font_size')).fontWeight(FontWeight.Medium).fontColor($r('app.color.text_primary')).width('100%').margin({top:8})// 内容预览(截断)Text(this.getPreviewText(note.content)).fontSize($r('app.float.small_font_size')).fontColor($r('app.color.text_secondary')).lineHeight(20).width('100%').margin({top:4})// 查看全文入口Row(){Blank()Text('查看全文 →').fontSize($r('app.float.tiny_font_size')).fontColor($r('app.color.primary'))}.width('100%').margin({top:8})}.width('100%').padding(16).backgroundColor($r('app.color.bg_card')).borderRadius($r('app.float.card_radius')).margin({bottom:10}).shadow({radius:4,color:$r('app.color.shadow'),offsetY:1}).onClick(()=>{this.viewDetail(note);})}5.3 内容截断
getPreviewText(content:string):string{returncontent.length>40?content.substring(0,40)+'...':content;}为什么截断:笔记内容可能很长,在列表卡片中完整显示会导致卡片高度不一致、信息密度降低。截断到 40 个字符并加...是移动端常见做法。
六、多视图切换——详情模式
6.1 视图切换逻辑
列表浏览和详情查看是两种完全不同的视图,我通过条件渲染实现切换:
// build() 中if(this.showDetail&&this.detailNote){// 详情视图this.detailView()}else{// 列表视图(包含搜索、分类、卡片列表)this.listView()}6.2 详情视图
@BuilderdetailView():void{Column(){// 顶部操作栏Row(){Text('← 返回列表').onClick(()=>{this.showDetail=false;this.detailNote=null;})Blank()Text('✏️')// 编辑.onClick(()=>{this.startEdit(this.detailNote!);})Text('🗑️')// 删除.onClick(()=>{this.deleteNote(this.detailNote!.id);})}// 笔记标题Text(this.detailNote.title).fontSize($r('app.float.title_font_size')).fontWeight(FontWeight.Bold)// 元信息Row(){Text(this.detailNote.category).backgroundColor(this.getCategoryColor(this.detailNote.category))Text(this.detailNote.date)}// 正文内容Text(this.detailNote.content).fontSize($r('app.float.body_font_size')).lineHeight(26).width('100%')}.padding(20)}6.3 进入详情
viewDetail(note:NoteItem):void{this.detailNote=note;this.showDetail=true;}注意:detailNote赋值为原始对象的引用。如果在详情页修改笔记内容,会影响原数组中的对象。这里因为实现了编辑功能后同步更新,所以没问题。如果只是只读详情,建议深拷贝。
七、编辑功能
7.1 进入编辑模式
startEdit(note:NoteItem):void{this.editingNote=note;this.newTitle=note.title;this.newContent=note.content;this.newCategory=note.category;this.showAddDialog=true;// 复用新增弹窗this.showDetail=false;// 关闭详情}复用设计:新增和编辑共用同一个弹窗,通过editingNote区分:
editingNote === null→ 新增模式,按钮文字为"创建"editingNote !== null→ 编辑模式,按钮文字为"保存修改"
7.2 保存编辑
updateNote():void{if(!this.editingNote||!this.newTitle.trim()){return;}constindex:number=this.notes.findIndex((item:NoteItem)=>item.id===this.editingNote!.id);if(index!==-1){constupdatedNote:NoteItem={id:this.notes[index].id,// 保持 ID 不变title:this.newTitle.trim(),content:this.newContent.trim(),category:this.newCategory,date:this.notes[index].date// 保留原日期};this.notes[index]=updatedNote;this.notes=this.notes.slice();// 触发 UI 刷新this.detailNote=this.notes[index];// 同步更新详情视图}this.resetForm();this.editingNote=null;this.showAddDialog=false;}7.3 新增与编辑的按钮逻辑
Button(this.editingNote?'保存修改':'创建').onClick(()=>{if(this.editingNote){this.updateNote();}else{this.addNote();}})八、CRUD 完整流程
8.1 Create —— 新增
addNote():void{if(!this.newTitle.trim())return;constnewId:number=this.notes.length>0?this.notes[this.notes.length-1].id+1:1;constdateStr:string=this.getTodayString();constnewNote:NoteItem={id:newId,title:this.newTitle.trim(),content:this.newContent.trim(),category:this.newCategory,date:dateStr};this.notes.push(newNote);this.resetForm();this.showAddDialog=false;}8.2 Delete —— 删除
deleteNote(id:number):void{this.notes=this.notes.filter((item:NoteItem)=>item.id!==id);// 如果当前在详情页且删除的就是正在查看的笔记,关闭详情if(this.detailNote&&this.detailNote.id===id){this.detailNote=null;this.showDetail=false;}}边界处理:删除时检查当前详情视图是否展示的就是被删除的笔记,如果是则关闭详情返回列表。
8.3 重置表单
resetForm():void{this.newTitle='';this.newContent='';this.newCategory='生活';}九、与 ToDo 页面的对比分析
备忘录和待办看起来很相似,但实际差异不小:
| 维度 | 待办页面 | 备忘录页面 |
|---|---|---|
| 数据字段 | title + category + priority | title + content + category |
| 核心操作 | 切换完成状态 | 查看详情 + 编辑内容 |
| 搜索功能 | ❌ 无 | ✅ 双字段搜索 |
| 详情视图 | ❌ 无(列表即全部) | ✅ 独立详情页 |
| 编辑功能 | ❌ 无(只能删除) | ✅ 支持编辑 |
| 内容展示 | 单行标题 | 标题 + 内容预览 |
| 视图复杂度 | 单一列表 | 列表/详情双视图 |
备忘录比待办多了一层详情视图,这是 CRUD 中的 “Read/Update” 更完整的体现。
十、技术难点与解决方案
10.1 难点一:多视图状态协调
问题:新增、编辑、详情、列表四种状态如何切换而不冲突?
方案:使用showAddDialog、showDetail、editingNote三个变量组合控制:
showAddDialog = true, editingNote = null→ 新增弹窗showAddDialog = true, editingNote != null→ 编辑弹窗showDetail = true→ 详情视图- 全部 false/null → 列表视图
10.2 难点二:刷新不丢失编辑状态
问题:ArkTS 的@State只追踪顶层引用变化,修改对象属性不会触发渲染。
方案:使用this.notes = this.notes.slice()或[...this.notes]创建新数组引用。
10.3 难点三:搜索性能
问题:每次输入都实时过滤,大数据量下会不会卡?
方案:当前数据量(6条示例数据)毫无压力。如果未来数据量上万,可以加上防抖(debounce):
// 防抖示例(仅当需要时添加)privatesearchTimer:number=-1;onSearch(value:string):void{clearTimeout(this.searchTimer);this.searchTimer=setTimeout(()=>{this.searchText=value;},300);// 300ms 防抖}十一、本篇总结
核心知识点
| 知识点 | 实战应用 |
|---|---|
| 多视图切换 | 列表/详情/编辑三种视图状态管理 |
| 双过滤系统 | 分类筛选 + 关键词搜索 |
| CRUD 完整流程 | 新增、读取、编辑、删除 |
| 弹窗复用 | 新增和编辑共享同一弹窗 |
| 内容截断 | 列表预览截断 + 详情完整展示 |
| 条件渲染 | if/else控制不同视图 |
完整用户操作路径
- 浏览:进入页面 → 查看所有笔记列表
- 搜索:输入关键词 → 实时过滤笔记
- 筛选:点击分类标签 → 只看某分类
- 查看详情:点击卡片 → 进入详情视图 → 查看完整内容
- 编辑:点击 ✏️ → 弹窗编辑 → 保存修改 → 返回详情
- 删除:点击 🗑️ → 从列表移除 → 自动返回列表
- 新增:点击 “+” → 弹窗输入 → 创建 → 列表新增卡片
十二、下篇预告
最后一篇将开发**「心情日记」页面**,这是最具视觉趣味性的页面,涵盖:
- 日历网格的纯算法实现
- 情绪记录的 Emoji 可视化
- 月度统计与情绪分析
- 个人中心页面开发
敬请期待最终篇!
