公司动态

HarmonyOS知识库——标签筛选页的双状态切换与Flex布局

📅 2026/7/15 15:58:44
HarmonyOS知识库——标签筛选页的双状态切换与Flex布局
知识库 App 的 TagFilterPage 有两种完全不同的 UI 状态——没有选中标签时显示标签网格选中后切换为笔记列表。这种同一页面两种视图的设计让用户不需要跳转就能在浏览标签和查看笔记之间切换减少了页面跳转的开销。完整效果一、三种入口的不同状态入口selectedTag 初始值页面初始状态首页笔记标签点击有值如HarmonyOS直接显示筛选结果ProfilePage 标签云点击有值如编程直接显示筛选结果直接打开 TagFilterPage空字符串显示标签网格有入口传了标签名——页面跳过标签网格直接显示该标签的笔记。没有传标签名——先展示所有标签供用户选择。两种入口共享同一个页面靠selectedTag是否有值来区分状态。二、aboutToAppear 的初始化逻辑Statetags:TagCount[][]StateselectedTag:stringStatenotes:Note[][]aboutToAppear():void{this.tagsgetAllTags()constprouter.getParams()asRecordstring,Objectif(pp[tag]){this.selectedTagp[tag]asstringthis.filterNotes()}}三个 State 的初始化顺序先加载所有标签getAllTags()再判断有没有传标签名有就设置 selectedTag 并调用 filterNotes。为什么tags无论有没有传标签名都要加载因为标签网格和筛选列表都需要 tags 数据——标签网格直接展示筛选列表的导航栏可能需要显示标签信息。提前加载避免后续使用时数据为空。踩坑记录最初this.tags getAllTags()只在没有传标签名时才执行传了标签名时跳过了。结果筛选列表的导航栏想显示所有标签数量时数据为空。改成无条件加载后解决。三、filterNotes 的双重遍历privatefilterNotes():void{if(this.selectedTag.length0){this.notes[];return}constall:Note[]getNotes()constr:Note[][]for(leti:number0;iall.length;i){for(letj:number0;jall[i].tags.length;j){if(all[i].tags[j]this.selectedTag){r.push(all[i]);break}}}this.notesr}外层遍历笔记内层遍历每篇笔记的标签。找到匹配的标签后立即break跳出内层循环——一篇笔记只需要匹配一次不需要检查所有标签。selectedTag 为空时返回空数组。这个边界检查防止了没有选中标签时显示全部笔记的错误行为。和 getRecipesByCat 的对比两者都是遍历数据 条件过滤的模式。区别是 getRecipesByCat 用单层遍历category 是字符串直接比较filterNotes 用双重遍历tags 是数组需要逐个比较。双重遍历的时间复杂度 O(n×m)n 是笔记数m 是每篇笔记的标签数。四、双状态的条件渲染if(this.selectedTag.length0){// 状态一标签网格Scroll(){Column(){Text(所有标签 (this.tags.length)).fontSize(16).fontWeight(FontWeight.Bold).fontColor(T1).width(100%).padding({left:16}).margin({bottom:8})Flex({wrap:FlexWrap.Wrap}){ForEach(this.tags,(t:TagCount){Column(){Text(#t.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(A).margin({bottom:2})Text(t.count 篇).fontSize(11).fontColor(T3)}.padding(14).backgroundColor(#FFFFFF).borderRadius(14).margin({right:8,bottom:8}).onClick((){this.selectedTagt.name;this.filterNotes()})})}.width(100%).padding({left:16,right:16})}}}else{// 状态二筛选笔记列表Scroll(){Column({space:8}){ForEach(this.notes,(n:Note){Row(){/* 笔记卡片 */}})Blank().height(20)}}}if-else 切换两种完全不同的 UI 结构——不是隐藏/显示而是条件渲染。选中标签前渲染标签网格选中后渲染笔记列表。两种结构差异太大Flex 布局 vs Column 列表无法用隐藏/显示实现。状态切换的触发// 标签网格里点击某个标签.onClick((){this.selectedTagt.name;this.filterNotes()})点击标签时同时做两件事设置 selectedTag 为标签名调用 filterNotes 查询匹配笔记。selectedTag 变化后if-else 判断条件改变UI 自动切换到筛选列表状态。状态重置用户点击返回按钮回到标签网格时需要重置 selectedTag。当前实现是router.back()返回上一页而不是在当前页面重置。这是路由式导航的标准做法——返回上一页比在当前页面重置更符合用户预期。五、Flex 布局的标签网格Flex({wrap:FlexWrap.Wrap}){ForEach(this.tags,(t:TagCount){Column(){Text(#t.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(A).margin({bottom:2})Text(t.count 篇).fontSize(11).fontColor(T3)}.padding(14).backgroundColor(#FFFFFF).borderRadius(14).margin({right:8,bottom:8})})}为什么用 Flex 而不是 RowRow 不支持自动换行——所有标签挤在一行超出屏幕的部分被截断。Flex 配合FlexWrap.Wrap实现自动换行——放不下的标签自动移到下一行。Flex FlexWrap.Wrap 是 ArkTS 里实现流式布局的标准方式。和 CSS 的flex-wrap: wrap效果一样。每个标签是一个 Flex 子项宽度由内容决定放不换行。每个标签的结构Column标签卡片 ├→ Text(# t.name) 标签名紫色粗体 └→ Text(t.count 篇) 笔记数量灰色小字两行文字的标签卡片——比单行文字信息量更大。用户不仅能看到标签名还能看到该标签下有多少篇笔记。数量信息帮助用户判断这个标签值得点进去看吗。margin({ right: 8, bottom: 8 })保证标签之间有间距。右间距让同一行的标签不会紧贴底部间距让换行后的标签不会紧贴上一行。标签网格 vs 首页笔记本卡片维度标签网格Flex笔记本卡片Row布局方式Flex FlexWrap.Wrap 自动换行Row 固定一行子项宽度内容决定layoutWeight(1) 均分子项数量不确定当前 12 个标签固定4 个笔记本滚动方向垂直滚动外层 Scroll水平不动一屏放下Flex 适合数量不确定的场景Row 适合数量固定的场景。标签可能增加到 20 个需要 Flex 自动换行。笔记本固定 4 个Row 一屏能放下。六、导航栏的动态标题Text(this.selectedTag.length0?#this.selectedTag:标签).fontSize(20).fontWeight(FontWeight.Bold).fontColor(T1).margin({left:10}).layoutWeight(1)if(this.selectedTag.length0)Text(this.notes.length篇).fontSize(12).fontColor(T3)选中标签前标题是标签选中后是#标签名。#前缀让用户知道当前在看哪个标签的筛选结果。数量显示也根据状态切换。选中标签后显示3篇没选中时不显示——因为标签网格不需要显示笔记数量。七、标签名的点击跳转链路整个 App 里有三个地方可以触发标签筛选首页笔记卡片 → 点击 #标签 → TagFilterPageselectedTag标签名 → 点击标签网格某标签 → 同页面切换到筛选列表 ProfilePage 标签云 → 点击 #标签 → TagFilterPageselectedTag标签名 → 直接显示筛选列表三种入口最终都汇聚到 TagFilterPage。这是统一入口的设计模式——标签筛选只有一个实现页面不同入口只是传不同的参数。和 NoteList 的万能列表页模式一致。NoteList 承载三种数据来源TagFilterPage 承载两种入口状态。两个页面都是一个页面多种用途的设计。八、整体嵌套结构Column 页面容器 ├→ Row (导航栏) 返回 动态标题 数量 └→ if (selectedTag ) 状态判断 ├→ 状态一标签网格 │ └→ Scroll │ └→ Column │ ├→ Text (标题) │ └→ Flex(FlexWrap.Wrap) ForEach 标签网格 └→ 状态二筛选列表 └→ Scroll └→ Column({ space: 8 }) ├→ ForEach(this.notes, ...) 笔记卡片 └→ Blank(20)两种状态共享同一个导航栏——导航栏在 if-else 外面。无论显示哪种状态导航栏始终存在。只有下方的内容区域根据状态切换。这种共享导航栏、切换内容区的模式避免了导航栏的重复代码。九、getAllTags 的标签统计逻辑exportfunctiongetAllTags():TagCount[]{constresult:TagCount[][]for(leti:number0;i_notes.length;i){for(letj:number0;j_notes[i].tags.length;j){constt:string_notes[i].tags[j]letfound:booleanfalsefor(letk:number0;kresult.length;k){if(result[k].namet){result[k].count;foundtrue;break}}if(!found){result.push({name:t,count:1})}}}returnresult}三重循环的执行流程循环遍历对象作用外层 i_notes12 篇笔记逐篇检查中层 j_notes[i].tags每篇的标签数组逐个标签检查内层 kresult已统计的标签检查是否已存在已存在则 count1不存在则新增的统计模式。这是频率统计的标准实现——遍历数据用结果数组记录每个元素出现的次数。12 篇笔记、每篇 2-3 个标签、约 15 个不重复标签——三重循环的总执行次数约 12×3×15540 次。在这个数据规模下性能完全不是问题。如果标签数量增大到上千可以用 Map 优化到 O(n×m)。十、标签统计的实时性aboutToAppear():void{this.tagsgetAllTags()}onPageShow():void{this.tagsgetAllTags()}每次页面显示都重新调用 getAllTags()。用户在 NoteEditor 新增了笔记带新标签返回 TagFilterPage 后标签网格自动更新——新标签出现旧标签的 count 增加。为什么不用缓存因为标签数据来自 _notes 数组每次 addNote/deleteNote 都会修改 _notes。重新调用 getAllTags() 能保证拿到最新数据。在 12 篇笔记的规模下重新统计的性能开销可以忽略。