
知识库 App 的 NoteList 是一个万能列表页——从不同入口进来时显示不同数据从笔记本卡片进来显示该笔记本的笔记从搜索 tab 进来显示全部笔记从标签进来显示该标签的笔记。同一个页面承载三种数据源靠的是路由参数的灵活判断。完整效果一、三种数据来源的路由参数设计入口参数数据源标题首页笔记本卡片notebookId notebookNamegetNotesByNotebook(id)笔记本名称如工作底部导航搜索notebookId-1, notebookName‘全部笔记’getNotes()“全部笔记”标签页 TagFilterPage无直接调 searchNotessearchNotes(keyword)“搜索结果”三种来源走不同的查询函数aboutToAppear():void{constprouter.getParams()asRecordstring,Objectif(p){if(p[notebookId]){constid:numberp[notebookId]asnumberthis.notesid0?getNotes():getNotesByNotebook(id)this.titlep[notebookName]asstring}}}id 0 ? getNotes() : getNotesByNotebook(id)的特殊值判断。notebookId-1 是一个约定值——表示不按笔记本筛选显示全部笔记。NoteList 收到 -1 后走getNotes()收到其他值走getNotesByNotebook(id)。为什么不传 notebookId 来表示全部笔记因为 router.getParams() 返回的对象可能没有 notebookId 字段需要额外判断if (p[notebookId])。用 -1 这个约定值判断逻辑更简单——只需要比较数字。踩坑记录最初传了notebookId: 0但 NOTEBOOKS 里没有 id0 的笔记本getNotesByNotebook(0)返回空数组页面显示空白。改成 -1 后在 NoteList 里加了id 0判断解决。二、搜索功能的实时过滤StatesearchText:stringprivatedoSearch():void{this.notesthis.searchText.trim().length0?searchNotes(this.searchText):getNotes()}// 搜索框TextInput({placeholder:搜索笔记...,text:this.searchText}).layoutWeight(1).height(36).fontSize(14).backgroundColor(#F5F5FA).borderRadius(18).padding({left:14}).onChange((v:string){this.searchTextv;this.doSearch()})搜索框的 onChange 同时做两件事同步搜索词到 State然后调用 doSearch 重新过滤。用户每打一个字都会触发过滤——实时响应。doSearch 的二分支逻辑搜索词非空时调用searchNotes(keyword)为空时调用getNotes()恢复全部笔记。这个恢复逻辑很重要——用户清空搜索框后应该看到全部笔记而不是空列表。搜索词为空时为什么调 getNotes() 而不是 getNotesByNotebook()因为搜索是全局搜索——不限定笔记本。清空搜索词后恢复全部笔记更符合用户预期。踩坑记录最初搜索词为空时没有恢复逻辑用户搜完后清空搜索框列表还是空的。加了getNotes()恢复后解决。三、getNotesByNotebook 的双重遍历exportfunctiongetNotesByNotebook(nbId:number):Note[]{constr:Note[][]for(leti:number0;i_notes.length;i){if(_notes[i].notebookIdnbId)r.push(_notes[i])}returnr}单层遍历——比 getFavs 的双重遍历简单。因为 Notebook 的关联是单向的——笔记直接存了 notebookId不需要额外匹配。菜谱 App 的收藏需要遍历 RECIPES 和 _favs 两个数组做交叉匹配复杂度更高。返回新数组——r.push(_notes[i])创建了独立数组。调用方修改返回值不会影响 _notes。四、搜索结果的两种空状态NoteList 有两种空状态场景场景原因当前处理笔记本内没有笔记该笔记本是空的显示空白搜索无结果关键词没有匹配显示空白当前两种空状态都没有提示——页面只有导航栏和空白区域。优化方案if(this.notes.length0){Column(){Text(this.searchText.length0?:).fontSize(48).opacity(0.2).margin({bottom:12})Text(this.searchText.length0?没有找到相关笔记:还没有笔记).fontSize(14).fontColor(T2)Text(this.searchText.length0?试试其他关键词:点击 新建一篇).fontSize(12).fontColor(T3).margin({top:4})}.width(100%).layoutWeight(1).justifyContent(FlexAlign.Center)}根据搜索状态显示不同的空状态文案。有搜索词时提示没有找到相关笔记没有搜索词时提示还没有笔记。两种场景的解决方案不同——前者建议换关键词后者建议新建笔记。五、笔记卡片的信息精简Row(){Text(this.nbIcon(n.notebookId)).fontSize(20).width(44).height(44).borderRadius(12).backgroundColor(this.nbColor(n.notebookId)12).textAlign(TextAlign.Center)Column(){Row(){Text(n.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(T1).maxLines(1).layoutWeight(1)if(n.favorited)Text(❤️).fontSize(12)}Text(this.nbName(n.notebookId) · n.createdAt).fontSize(10).fontColor(T3).margin({top:2})}.alignItems(HorizontalAlign.Start).margin({left:10}).layoutWeight(1)SymbolGlyph($r(sys.symbol.chevron_right)).fontSize(14).fontColor([#DDD])}和首页笔记卡片的对比部分首页卡片NoteList 卡片第一行标题 收藏图标标题 收藏图标第二行内容预览前 50 字笔记本名称 日期第三行笔记本标签 日期 标签无右侧箭头无有NoteList 卡片比首页卡片更精简——去掉了内容预览和标签。原因是NoteList 可能显示很多笔记全部 12 篇或搜索结果精简的卡片让用户一屏能看到更多条目。首页是仪表盘需要展示更多信息密度所以加了内容预览和标签。信息密度的选择取决于使用场景仪表盘首页高密度一屏展示尽量多信息列表页NoteList中密度平衡信息量和浏览效率空状态零密度留白引导用户操作六、nbName/nbIcon/nbColor 的重复出现NoteList、FavoritesPage、TagFilterPage 三个页面都有这三个辅助函数privatenbName(id:number):string{...}privatenbIcon(id:number):string{...}privatenbColor(id:number):string{...}三个函数结构完全一样——只是返回的字段不同。这是抽取公共函数的典型场景。两种抽取方案方案一抽取到 NoteService 数据层// NoteService.ets 新增exportfunctiongetNotebookName(id:number):string{...}exportfunctiongetNotebookIcon(id:number):string{...}exportfunctiongetNotebookColor(id:number):string{...}三个页面直接导入调用不需要各自定义。优点是代码零重复缺点是数据层承担了 UI 相关的职责图标和颜色是展示逻辑。方案二保持各自定义三个页面各自写一份。代码有重复但每个页面自包含——不依赖其他页面或额外的函数。在 6 个页面的规模下重复代码的维护成本可以接受。当前选了方案二。原因是知识库 App 只有 3 个页面用到这三个函数重复量不大。如果以后增加到 10 页面都用到再抽取也不迟。七、整体嵌套结构Column 页面容器 ├→ Row (导航栏) 返回 标题 数量 ├→ Row (搜索框) 搜索图标 TextInput └→ Scroll 可滚动内容 └→ Column({ space: 8 }) 垂直排列笔记卡片 ├→ ForEach(this.notes, ...) 笔记卡片列表 └→ Blank(20) 底部留白嵌套深度只有 3 层——比首页少一层。因为没有底部导航栏不需要 Stack 层叠。这是列表页的标准结构导航栏 搜索框 列表三层嵌套足够。搜索框在导航栏和列表之间。这是移动端搜索列表的标准位置——用户先看到标题再看到搜索框最后看到列表。搜索框靠近列表让视觉焦点集中在搜索 → 结果的流程上。八、数据流的完整链路入口一首页笔记本卡片 → router.pushUrl({ notebookId: 1, notebookName: 工作 }) → NoteList.aboutToAppear → getNotesByNotebook(1) → this.notes 3 篇工作笔记 → ForEach 渲染列表 入口二底部导航搜索 → router.pushUrl({ notebookId: -1, notebookName: 全部笔记 }) → NoteList.aboutToAppear → getNotes() (id 0) → this.notes 12 篇全部笔记 → ForEach 渲染列表 用户在搜索框输入HarmonyOS → onChange → doSearch → searchNotes(HarmonyOS) → this.notes 1 篇匹配笔记 → ForEach 重新渲染三种入口最终都走到同一个 ForEach 渲染逻辑。数据来源不同但 UI 结构完全一样。这就是一个页面多种数据源的架构优势——只需要一套卡片组件不同数据源只需切换查询函数。九、和其他列表页的对比维度NoteListFavoritesPageTagFilterPage数据来源三种全部/按笔记本/搜索一种收藏两种标签列表/按标签筛选搜索功能有实时过滤无无空状态无有无标签展示无无有标签网格代码量最多最少中等NoteList 是三个列表页里功能最全的——有搜索、有多种数据源。FavoritesPage 最简单——只显示收藏笔记。TagFilterPage 介于两者之间——有两种状态标签列表/筛选结果。