HarmonyOS知识库——笔记编辑器的双模式与标签管理
知识库 App 的 NoteEditor 承担了新建和编辑两种完全不同的职责——新建时所有字段为空编辑时需要预填已有数据。这种一个页面两种模式的设计在工具类 App 里很常见但要做到两种模式切换流畅、数据不串需要在状态初始化和保存逻辑上下功夫。完整效果一、八个 State 的职责划分Statenote:Note|undefinedundefined// 原始笔记数据编辑模式StateisNew:booleantrue// 新建/编辑模式标记Statetitle:string// 标题输入Statecontent:string// 内容输入StateselectedNb:number1// 选中的笔记本 idStatefav:booleanfalse// 收藏状态StatetagInput:string// 标签输入框内容Statetags:string[][]// 已添加的标签列表八个 State 可以分为三组分组变量作用模式控制note, isNew区分新建和编辑内容输入title, content, selectedNb, tags笔记的四个核心字段交互状态fav, tagInput收藏切换和标签输入note存原始数据、title/content存编辑副本——为什么要分开因为编辑过程中用户可能修改了但没保存。如果直接修改note.title用户取消编辑后原始数据也被改了。用独立的title/content存编辑副本只有点保存才写回note。踩坑记录最初直接修改this.note.title v用户改了标题但点返回没保存首页列表里标题却变了——因为note是对象引用直接修改会影响原始数据。改成独立的title/content变量后解决。二、aboutToAppear 的模式判断aboutToAppear():void{constprouter.getParams()asRecordstring,Objectif(pp[noteId]){this.notegetNoteById(p[noteId]asnumber)if(this.note){this.isNewfalsethis.titlethis.note.titlethis.contentthis.note.contentthis.selectedNbthis.note.notebookIdthis.favthis.note.favoritedconstt:string[][]for(leti:number0;ithis.note.tags.length;i){t.push(this.note.tags[i])}this.tagst}}}有没有noteId参数决定了是新建还是编辑。从首页点新建时不传 noteId从笔记列表点某篇笔记进来时传 noteId。编辑模式需要复制 tags 数组。this.note.tags是原始数据的引用如果直接赋值this.tags this.note.tags用户在编辑器里添加/删除标签会直接影响原始数据。用 for 循环创建副本保证原始数据安全。为什么 tags 要复制而 title/content 不需要因为 tags 是数组——数组的引用复制会让两个变量指向同一内存修改任何一个都会影响另一个。title/content 是基本类型string赋值时直接复制值不存在引用问题。两种模式的初始化对比字段新建模式编辑模式isNewtruefalsetitle‘’空note.title预填content‘’空note.content预填selectedNb1默认第一个笔记本note.notebookId原笔记本favfalsenote.favorited原状态tags[]空数组note.tags 的副本导航栏标题“新建笔记”“编辑笔记”收藏/删除按钮不显示显示三、笔记本选择器Chip 样式的实现Row(){ForEach(NOTEBOOKS,(nb:Notebook){Text(nb.icon nb.name).fontSize(12).fontWeight(this.selectedNbnb.id?FontWeight.Bold:FontWeight.Regular).fontColor(this.selectedNbnb.id?#FFFFFF:T2).padding({left:12,right:12,top:6,bottom:6}).borderRadius(14).backgroundColor(this.selectedNbnb.id?nb.color:#F5F5FA).margin({right:6}).onClick((){this.selectedNbnb.id})})}选中和未选中的双重样式切换状态背景色文字颜色字重选中笔记本对应颜色如 #FF6B6B白色Bold未选中#F5F5FA浅灰T2灰色Regular选中状态用笔记本自己的颜色——让用户一眼知道这篇笔记属于哪个笔记本。4 个笔记本各有不同颜色红/紫/绿/橙选中时背景变成对应颜色视觉上非常直观。margin({ right: 6 })保证最后一个笔记本右侧也有间距。如果用Row({ space: 6 })最后一个元素右侧没有间距。手动加 margin 更可控。踩坑记录最初笔记本选择器没有fontWeight区分选中和未选中只有颜色差异。在小屏手机上颜色差异不明显用户反馈不知道选了哪个。加了 Bold 字重后视觉区分更清晰。四、标题和内容的输入设计标题单行 TextInputTextInput({placeholder:笔记标题,text:this.title}).fontSize(20).fontWeight(FontWeight.Bold).backgroundColor(transparent).width(100%).padding({left:16,right:16}).onChange((v:string){this.titlev})标题用 TextInput单行输入——因为标题通常只有一行。fontSize 20 Bold 让标题在视觉上最突出。backgroundColor(transparent)让输入框融入白色背景用户感知不到这是一个输入框只看到文字。内容全高 TextAreaTextArea({placeholder:开始记录你的想法...,text:this.content}).fontSize(15).backgroundColor(transparent).width(100%).padding({left:16,right:16}).layoutWeight(1).onChange((v:string){this.contentv})内容用 TextArea多行输入——因为笔记内容可能很长。layoutWeight(1)让 TextArea 占据标题和标签之间的所有剩余空间用户有最大的输入区域。TextArea 和 TextInput 的选择标准组件行数适用场景TextInput单行标题、搜索框、标签输入TextArea多行笔记内容、长文本描述分隔线用 Divider 而不是 Border。Divider().strokeWidth(0.5).color(#F0EEF4)在标题和内容之间画一条细线。比用 border-bottom 更简洁——不需要计算 padding 和宽度。五、标签管理添加与删除标签展示区Row({space:6}){ForEach(this.tags,(t:string,idx:number){Row(){Text(#t).fontSize(11).fontColor(A)Text( ×).fontSize(11).fontColor(T3)}.padding({left:8,right:8,top:3,bottom:3}).backgroundColor(A12).borderRadius(10).onClick((){this.removeTag(idx)})})}标签用#前缀——像社交媒体的 hashtag。紫色文字 浅紫背景和主色保持一致。点击整个标签行触发删除——不需要单独的删除按钮。idx是 ForEach 的第二个参数——当前项的索引。removeTag(idx)按索引删除不需要查找标签内容。标签输入区Row(){TextInput({placeholder:添加标签...,text:this.tagInput}).layoutWeight(1).height(32).fontSize(13).backgroundColor(#F5F5FA).borderRadius(16).padding({left:12}).onChange((v:string){this.tagInputv}).onSubmit((){this.addTag()})Text(添加).fontSize(12).fontWeight(FontWeight.Bold).fontColor(A).margin({left:8}).onClick((){this.addTag()})}两种添加方式点添加按钮或者按键盘确认键。onSubmit和onClick都调addTag()用户习惯哪种操作都能添加。踩坑记录最初只写了onClick没写onSubmit用户在输入标签后习惯按回车确认但按回车没反应。加了onSubmit后体验好很多。addTag 的防御性检查privateaddTag():void{if(this.tagInput.trim().length0)returnconstt:string[][]for(leti:number0;ithis.tags.length;i){t.push(this.tags[i])}t.push(this.tagInput.trim())this.tagstthis.tagInput}三个关键步骤空值检查—trim().length 0防止添加空白标签复制数组— 创建新数组t避免直接修改this.tags引用清空输入—this.tagInput 让输入框恢复空白为什么 addTag 和 removeTag 都要复制数组因为 State 要求替换引用才能触发刷新。直接this.tags.push(...)修改原数组不触发刷新。removeTag 的实现privateremoveTag(idx:number):void{constt:string[][]for(leti:number0;ithis.tags.length;i){if(i!idx)t.push(this.tags[i])}this.tagst}按索引过滤——遍历 tags跳过指定索引创建新数组。和菜谱 App 的 removeFromShop 逻辑完全一致。六、保存逻辑新建与编辑的分支处理privatesave():void{if(this.title.trim().length0)returnif(this.isNew){addNote(this.title.trim(),this.content,this.selectedNb,this.tags)}elseif(this.note){this.note.titlethis.title.trim()this.note.contentthis.contentthis.note.notebookIdthis.selectedNbthis.note.tagsthis.tagsthis.note.updatedAt2026-07-15}router.back()}新建和编辑走不同的保存路径模式保存方式数据变化新建addNote()创建新对象插入 _notes 数组新增一条笔记noteCount1编辑直接修改this.note的属性更新已有笔记updatedAt 改为当天新建用 addNote 函数数据层操作编辑直接修改对象引用操作。为什么编辑不用函数因为编辑是修改已有对象的属性——this.note是 _notes 数组里某个元素的引用修改它就等于修改了 _notes 里的数据。this.title.trim().length 0检查——标题不能为空。如果用户没输入标题就点保存save 直接 return不执行任何操作。内容可以为空用户可能只想记一个标题但标题不能为空。踩坑记录最初没检查标题为空用户保存了一篇标题为空的笔记首页列表里显示一个空标题卡片。加了检查后空标题保存时静默失败——没有错误提示但数据不会保存。更好的做法是弹一个 Toast 提示标题不能为空。七、删除与收藏的交互// 导航栏编辑模式显示收藏和删除按钮if(!this.isNew){Text(this.fav?❤️:).fontSize(20).onClick((){this.toggleFav()}).margin({right:8})Text().fontSize(18).onClick((){if(this.note){deleteNote(this.note.id);router.back()}}).margin({right:8})}// 收藏切换privatetoggleFav():void{if(this.note){this.favtoggleFav(this.note.id)}}新建模式不显示收藏和删除按钮——因为还没保存没有笔记可以收藏或删除。if (!this.isNew)控制按钮的显隐。删除后直接router.back()。删除完不需要等用户确认——因为这是一个轻量级的知识库 App不是重要数据管理工具。真实 App 应该加确认弹窗。收藏切换的返回值设计。toggleFav返回 boolean直接赋值this.fav toggleFav(...)。和菜谱 App 的收藏切换逻辑一致。八、整体嵌套结构Column 页面容器 ├→ Row (导航栏) 返回 标题 操作按钮 └→ Column (白色内容区) 白色背景包裹所有输入区 ├→ Row (笔记本选择器) 横向排列 4 个 Chip ├→ TextInput (标题) 单行输入20px 粗体 ├→ Divider 分隔线 ├→ TextArea (内容) 多行输入layoutWeight(1) 全高 └→ Column (标签区) 白色背景 顶部边框 ├→ Row (已有标签) 标签 Chip点击删除 └→ Row (标签输入) TextInput 添加按钮标签区为什么在底部而不是标题旁边因为标签数量不确定——0 个标签时不占空间5 个标签时占较多空间。放在底部避免了标签区的高度变化影响标题和内容的布局稳定性。标签区的白色背景 顶部边框。backgroundColor(#FFFFFF).border({ width: { top: 0.5 }, color: #F0EEF4 })让标签区和上方的 TextArea 视觉上分开——TextArea 在白色背景上标签区也在白色背景上但标签区多了一条顶部细线作为分隔。九、数据流向图用户输入 title/content/tags ↓ State 变量接收 ↓ 点击保存 ↓ ┌──────┴──────┐ 新建模式 编辑模式 addNote() this.note.xxx xxx (插入新对象) (修改引用属性) ↓ ↓ _notes 数组变化 ↓ router.back() ↓ 首页 onPageShow → refresh() ↓ 列表更新显示新笔记新建和编辑的数据流差异新建走 addNote → 修改 _notes → 返回首页 → refresh → 列表更新。编辑直接修改 note 引用 → 返回首页 → 列表显示修改后的内容。两种方式最终都能让首页看到最新数据。