鸿蒙原生应用实战(三):笔记详情与编辑页面的路由与CRUD 鸿蒙原生应用实战三笔记详情与编辑页面的路由与CRUD系列目录第一篇项目搭建与页面架构设计第二篇首页开发与全局数据流设计第三篇笔记详情与编辑页面的路由与CRUD ← 当前第四篇分类浏览与个人中心的多维数据展示第五篇构建调试、异常处理与HAP发布一、前言上一篇我们完成了首页开发实现了笔记列表展示、搜索筛选和数据流设计。本篇将开发两个核心交互页面——笔记详情页NotePage和编辑页EditPage涵盖页面间路由参数传递笔记 CRUD增删改查编辑态/新建态的双模式切换删除确认弹窗bindContentCoverAPI 23 下 router 的正确使用方法二、鸿蒙路由机制详解2.1 router 的正确导入在 API 23 中路由模块必须从ohos.router导入importrouterfromohos.router;⚠️不要从kit.AbilityKit导入——API 23 版本中该路径不导出 router。2.2 页面跳转与传参// 跳转并传参router.pushUrl({url:pages/NotePage,params:{noteId:note.id}});// 无参数跳转router.pushUrl({url:pages/EditPage});2.3 接收参数含空值保护接收参数时使用router.getParams()必须处理 null 情况aboutToAppear():void{// 关键类型声明为 | null加 if 保护letparams:Recordstring,Object|nullrouter.getParams()asRecordstring,Object|null;if(params){letnoteId:number|undefinedparams[noteId]asnumber|undefined;// ... 处理逻辑}}这是最容易出错的点如果不加| null判断当从无参数跳转进入页面时router.getParams()返回 null访问params[noteId]会直接崩溃。2.4 页面返回router.back();// 返回上一页关于弃用警告在 API 23 SDK 中pushUrl、getParams、back都会显示 deprecation warning但功能正常可用。这些 API 要到更高版本才移除目前无需处理。三、笔记详情页 (NotePage)3.1 页面结构Column ├── 顶部导航栏 (Row) │ ├── 返回按钮 (带点击) │ ├── 标题 笔记详情 │ └── 编辑 文字按钮 ├── Scroll │ └── Column (内容区) │ ├── 分类标签 (带颜色边框) │ ├── 标题 (大字体粗体) │ ├── 日期 │ ├── Divider 分隔线 │ └── 正文 (lineHeight 26) └── 底部删除按钮 (Row → Button) └── bindContentCover 删除确认弹窗3.2 加载笔记数据通过路由参数noteId从全局 AppStorage 中查找对应笔记Statenote:Note{id:0,title:,content:,category:,date:};aboutToAppear():void{letparams:Recordstring,Objectrouter.getParams()asRecordstring,Object;letnoteId:numberparams[noteId]asnumber;this.loadNote(noteId);}loadNote(noteId:number):void{letstored:string|undefinedAppStorage.getstring(notes);if(stored){letallNotes:Note[]JSON.parse(stored)asNote[];letfound:Note|undefinedallNotes.find((n:Note)n.idnoteId);if(found){this.notefound;}}}这里find方法返回Note | undefined如果数据被删除或不存在页面会显示空内容。实际生产环境可以加上错误提示。3.3 分类颜色标签每个分类有不同的颜色标识getCategoryColor(category:string):ResourceColor{letcolorMap:Recordstring,ResourceColor{工作:#007AFF,// 蓝色学习:#34C759,// 绿色生活:#FF9500,// 橙色灵感:#AF52DE// 紫色};returncolorMap[category]||#999999;}应用在 UI 上Text(this.note.category).fontColor(this.getCategoryColor(this.note.category)).border({width:1,color:this.getCategoryColor(this.note.category)}).borderRadius(6).alignSelf(ItemAlign.Start)3.4 删除确认弹窗使用bindContentCover实现底部弹出确认对话框StateshowDeleteDialog:booleanfalse;// 在 Column 上绑定.bindContentCover($$this.showDeleteDialog,this.DeleteDialogBuilder())// Builder 定义弹窗内容BuilderDeleteDialogBuilder(){Column(){Text(确认删除).fontSize($r(app.float.subtitle_font_size)).fontWeight(FontWeight.Bold).margin({bottom:12})Text(确定要删除这条笔记吗).fontColor($r(app.color.text_secondary)).margin({bottom:24})Row(){Button(取消).onClick((){this.showDeleteDialogfalse;})Blank().width(12)Button(确定).backgroundColor($r(app.color.delete_red)).onClick((){this.showDeleteDialogfalse;this.deleteNote();})}.width(100%)}.padding(24).backgroundColor($r(app.color.card_bg)).borderRadius(16).width(80%)}⚠️ 注意$$this.showDeleteDialog的双向绑定语法——$$前缀实现状态变量和弹窗显示状态的同步。四、编辑页面 (EditPage)4.1 双模式设计编辑页面同时处理新建笔记和编辑已有笔记两种场景场景路由参数页面标题保存行为新建无或 noteId0“新建笔记”生成新 id插入列表头部编辑noteId目标ID“编辑笔记”覆盖原数据StateisEditing:booleanfalse;StateeditNoteId:number0;aboutToAppear():void{letparams:Recordstring,Object|nullrouter.getParams()asRecordstring,Object|null;if(params){letnoteId:number|undefinedparams[noteId]asnumber|undefined;if(noteId!undefinednoteId0){this.isEditingtrue;this.editNoteIdnoteId;// 从 AppStorage 加载已有数据letstored:string|undefinedAppStorage.getstring(notes);if(stored){letallNotes:Note[]JSON.parse(stored)asNote[];letfound:Note|undefinedallNotes.find((n:Note)n.idnoteId);if(found){this.titlefound.title;this.contentfound.content;this.selectedCategoryfound.category;}}}}}4.2 页面结构Column ├── 顶部导航栏 │ ├── 取消 文字按钮 → router.back() │ ├── 新建笔记 或 编辑笔记 标题 │ └── 保存 文字按钮 → saveNote() ├── 标题输入框 (TextInput) ├── 分类选择器 (Row) │ ├── 分类 标签 │ └── [工作] [学习] [生活] [灵感] 按钮组 ├── Divider └── 正文输入 (TextArea) ← layoutWeight(1) 撑满剩余空间4.3 分类选择器实现分类采用按钮组样式单选的交互模式privatecategoryOptions:CategoryOption[][{label:工作,value:工作},{label:学习,value:学习},{label:生活,value:生活},{label:灵感,value:灵感}];Row(){Text(分类).fontColor($r(app.color.text_secondary))Blank()ForEach(this.categoryOptions,(option:CategoryOption){Text(option.label).fontColor(this.selectedCategoryoption.value?Color.White:$r(app.color.text_secondary)).backgroundColor(this.selectedCategoryoption.value?$r(app.color.primary):$r(app.color.card_bg)).borderRadius(14).onClick((){this.selectedCategoryoption.value;})},(option:CategoryOption)option.value)}4.4 保存逻辑 (CRUD)saveNote():void{// 标题为空时不保存if(this.title.trim().length0){return;}letstored:string|undefinedAppStorage.getstring(notes);letallNotes:Note[]stored?JSON.parse(stored)asNote[]:[];// 生成当前日期字符串letnow:DatenewDate();letdateStr:stringnow.getFullYear()-String(now.getMonth()1).padStart(2,0)-String(now.getDate()).padStart(2,0);if(this.isEditing){// UPDATE: 查找并替换letindex:numberallNotes.findIndex((n:Note)n.idthis.editNoteId);if(index!-1){allNotes[index]{id:this.editNoteId,title:this.title.trim(),content:this.content.trim(),category:this.selectedCategory,date:allNotes[index].date// 保留原日期};}}else{// CREATE: 生成新ID插入列表头部letmaxId:number0;for(letnoteofallNotes){if(note.idmaxId){maxIdnote.id;}}letnewNote:Note{id:maxId1,title:this.title.trim(),content:this.content.trim(),category:this.selectedCategory,date:dateStr};allNotes[newNote,...allNotes];// 新笔记在顶部}// 持久化到 AppStorageAppStorage.setOrCreatestring(notes,JSON.stringify(allNotes));router.back();// 返回上一页}4.5 删除逻辑deleteNote():void{letstored:string|undefinedAppStorage.getstring(notes);if(stored){letallNotes:Note[]JSON.parse(stored)asNote[];// DELETE: 过滤掉目标IDallNotesallNotes.filter((n:Note)n.id!this.note.id);AppStorage.setOrCreatestring(notes,JSON.stringify(allNotes));}router.back();// 返回上一页}五、ArkTS 对象字面量陷阱这是本项目遇到的一个典型编译错误Object literal must correspond to some explicitly declared class or interface (arkts-no-untyped-obj-literals)以下写法不允许// ❌ 编译错误Builder 参数类型不能是对象字面量BuilderStatBadge(params:{label:string;value:string;color:string;}){}// ❌ 编译错误调用 Builder 时不能直接传对象字面量this.StatBadge({label:工作,value:3,color:#007AFF});正确写法// ✅ 方案1定义接口interfaceStatBadgeParams{label:string;value:string;color:string;}BuilderStatBadge(params:StatBadgeParams){}// ✅ 方案2使用独立参数BuilderStatBadge(label:string,value:string,color:string){}this.StatBadge(工作,3,#007AFF);// 直接传值六、本篇总结本篇我们完成了✅ 鸿蒙路由机制pushUrl传参、getParams接收含空值保护、back返回✅ 笔记详情页数据加载、分类颜色标签、删除确认弹窗✅ 编辑页新建/编辑双模式、分类选择器、标题正文输入✅ 完整 CRUD创建id递增头部插入、读取、更新、删除✅ Builder 参数类型的 ArkTS 严格模式避坑下一篇将开发分类浏览页和个人中心页展示更丰富的数据可视化内容。