【鸿蒙原生应用开发实战】第三篇列表页与标签筛选功能 — 打造高效的天体列表前言列表页是移动App中最常见也最重要的页面类型之一。在宇宙探索App中CelestialPage天体列表页承担着展示天体和分类筛选的核心职责。本篇我们将深入实现5个分类标签的切换筛选天体卡片组件的完整设计收藏交互的即时反馈列表渲染性能优化要点一、页面功能总览CelestialPage实现了三个核心功能标签导航— 顶部5个标签全部/行星/恒星/星系/星云点击切换筛选列表展示— 按标签筛选结果展示天体卡片收藏交互— 每个卡片可以直接收藏/取消收藏页面结构┌──────────────────────────────┐ │ ← 天体列表 │ ← 顶部导航栏 ├──────────────────────────────┤ │ [全部] [行星] [恒星] [星系] [星云]│ ← 标签栏 ├──────────────────────────────┤ │ ██ 太阳 │ ← 天体卡片 │ Sun · 恒星 │ (颜色标识条 信息 收藏) │ 太阳是太阳系的中心... │ │ ☆ │ ├──────────────────────────────┤ │ ██ 地球 │ │ Earth · 行星 │ │ 地球是太阳系中唯一... │ │ ★ │ ← 已收藏状态 ├──────────────────────────────┤ │ ... │ └──────────────────────────────┘二、完整代码实现2.1 接口定义importrouterfromohos.router;import{CelestialData,CELESTIAL_LIST,FavoriteManager}from../model/CelestialData;interfaceTabItem{label:string;type:string;}TabItem接口label是显示文本type是筛选类型值。2.2 天体卡片组件 — CelestialCardComponentstruct CelestialCard{item:CelestialData{id:0,name:,englishName:,type:,description:,mass:,diameter:,distance:,temperature:,fact:,color:#FFFFFF,isFavorite:false};StateisFav:booleanfalse;aboutToAppear():void{this.isFavFavoriteManager.isFavorite(this.item.id);}toggleFav():void{constnewStateFavoriteManager.toggle(this.item.id);this.isFavnewState;this.item.isFavoritenewState;}build(){Row(){// 颜色标识条 — 不同天体不同颜色Column().width(6).height(100%).backgroundColor(this.item.color).borderRadius(3);// 信息区域Column(){Row(){Text(this.item.name).fontSize($r(app.float.app_body_size)).fontColor($r(app.color.app_color_white)).fontWeight(FontWeight.Bold);Text(this.item.englishName).fontSize($r(app.float.app_caption_size)).fontColor($r(app.color.app_color_text_secondary)).margin({left:8});}.alignItems(VerticalAlign.Bottom);Text(this.item.type).fontSize($r(app.float.app_caption_size)).fontColor(this.item.color).margin({top:4});Text(this.item.description.length50?this.item.description.substring(0,50)...:this.item.description).fontSize($r(app.float.app_caption_size)).fontColor($r(app.color.app_color_text_secondary)).maxLines(2).lineHeight(18).margin({top:6});}.alignItems(HorizontalAlign.Start).layoutWeight(1).padding({left:12,right:8,top:12,bottom:12});// 收藏按钮Text(this.isFav?★:☆).fontSize(24).fontColor(this.isFav?$r(app.color.app_color_favorite):$r(app.color.app_color_unfavorite)).onClick((event:ClickEvent){this.toggleFav();}).padding({right:12});}.width(100%).height(110).backgroundColor($r(app.color.app_color_card)).borderRadius($r(app.float.app_card_radius)).margin({bottom:12}).onClick((){router.pushUrl({url:pages/DetailPage,params:{id:this.item.id}});});}}2.3 卡片设计要点1. 颜色标识条左侧6px宽的竖条使用this.item.color颜色。每个天体有专属色太阳 →#FF6B35橙色地球 →#4B7B8A蓝绿色火星 →#C1440E红色黑洞 →#2D2D3D深灰视觉上让卡片更生动同时帮助用户快速识别天体类型。2. 收藏按钮的点击事件隔离Text(this.isFav?★:☆).onClick((event:ClickEvent){this.toggleFav();// 只触发收藏切换})收藏按钮的点击事件和卡片的点击事件是独立的点击收藏按钮 → 只切换收藏状态点击卡片其他区域 → 跳转到详情页这是通过event事件冒泡机制实现的——收藏按钮消费了点击事件不会冒泡到卡片容器。3. 描述文本截断Text(this.item.description.length50?this.item.description.substring(0,50)...:this.item.description).maxLines(2).lineHeight(18)双重截断保障代码层面截取前50字符样式层面限制最多2行确保UI整齐。2.4 主页面 — CelestialPageEntryComponentstruct CelestialPage{Statetabs:TabItem[][{label:全部,type:全部},{label:行星,type:行星},{label:恒星,type:恒星},{label:星系,type:星系},{label:星云,type:星云}];StateactiveTab:string全部;StatefilteredList:CelestialData[]CELESTIAL_LIST;privatefilterType:string;aboutToAppear():void{// 从路由参数获取筛选类型从首页分类入口跳转过来时constparamsrouter.getParams()asRecordstring,Object;if(paramsparams[filterType]!undefined){this.filterTypeString(params[filterType]);this.activeTabthis.filterType;}this.applyFilter();}onPageShow():void{this.applyFilter();// 返回此页面时重新应用筛选刷新收藏状态}selectTab(type:string):void{this.activeTabtype;this.applyFilter();}applyFilter():void{if(this.activeTab全部){this.filteredListCELESTIAL_LIST;}else{constarr:CelestialData[][];for(leti0;iCELESTIAL_LIST.length;i){if(CELESTIAL_LIST[i].typethis.activeTab){arr.push(CELESTIAL_LIST[i]);}}this.filteredListarr;}}build(){Column(){// 顶部导航栏 Row(){Text(←).fontSize(24).fontColor($r(app.color.app_color_white)).onClick((){router.back();});Text(天体列表).fontSize($r(app.float.app_subtitle_size)).fontColor($r(app.color.app_color_white)).fontWeight(FontWeight.Bold).margin({left:12});}.width(100%).padding({left:16,top:12,bottom:12});// 标签栏 Row(){ForEach(this.tabs,(tab:TabItem){Text(tab.label).fontSize($r(app.float.app_small_size)).fontColor(this.activeTabtab.type?$r(app.color.app_color_accent):$r(app.color.app_color_tab_inactive)).fontWeight(this.activeTabtab.type?FontWeight.Bold:FontWeight.Normal).padding({left:12,right:12,top:6,bottom:6}).backgroundColor(this.activeTabtab.type?rgba(255, 215, 0, 0.15):transparent).borderRadius(16).onClick((){this.selectTab(tab.type);});})}.width(100%).padding({left:16,bottom:12});// 列表区域 Scroll(){Column(){ForEach(this.filteredList,(item:CelestialData){CelestialCard({item:item})});}.width(100%).padding({left:16,right:16});}.layoutWeight(1);}.width(100%).height(100%).backgroundColor($r(app.color.app_color_background));}}三、标签筛选机制详解3.1 状态变量设计StateactiveTab:string全部;// 当前激活的标签StatefilteredList:CelestialData[]CELESTIAL_LIST;// 筛选后的列表activeTab— 控制哪个标签高亮同时作为筛选依据filteredList— 筛选后的数据驱动列表渲染3.2 筛选核心逻辑applyFilter():void{if(this.activeTab全部){this.filteredListCELESTIAL_LIST;// 显示全部}else{// 手动遍历筛选避免使用 filter 等ES6方法constarr:CelestialData[][];for(leti0;iCELESTIAL_LIST.length;i){if(CELESTIAL_LIST[i].typethis.activeTab){arr.push(CELESTIAL_LIST[i]);}}this.filteredListarr;}}为什么要用 for 循环而不是 filter 方法ArkTS 严格模式对 ES6 的数组高阶方法支持有限。在 API 23 下filter、map等方法的类型推断可能会出问题。使用传统的for循环是更稳妥的方案。3.3 标签激活状态样式.fontColor(this.activeTabtab.type?$r(app.color.app_color_accent):// 激活金色$r(app.color.app_color_tab_inactive))// 未激活灰色.backgroundColor(this.activeTabtab.type?rgba(255, 215, 0, 0.15):transparent)// 激活半透明金色背景.borderRadius(16)// 椭圆胶囊效果视觉反馈三要素属性激活态未激活态文字颜色金色#FFD700灰色#555555字体粗细BoldNormal背景色金色半透明透明3.4 路由入口支持页面支持两种进入方式方式一从首页分类入口进入用户点击行星分类卡片 → 自动筛选行星标签// Index.ets 中CategoryCard.onClick((){router.pushUrl({url:pages/CelestialPage,params:{filterType:行星}});});// CelestialPage 中接收aboutToAppear():void{constparamsrouter.getParams()asRecordstring,Object;if(paramsparams[filterType]!undefined){this.filterTypeString(params[filterType]);this.activeTabthis.filterType;// 直接激活对应标签}this.applyFilter();}方式二直接从天体列表中进入通过底栏或收藏页去探索按钮进入 → 默认显示全部四、onPageShow 的重要作用onPageShow():void{this.applyFilter();}为什么onPageShow中也要调用applyFilter()场景重现用户进入列表页收藏了地球☆ → ★点击地球卡片跳转到详情页在详情页取消收藏地球★ → ☆点击返回回到列表页此时列表页需要通过onPageShow重新加载数据刷新收藏状态如果只用aboutToAppear步骤4返回后列表不会刷新收藏状态还是旧的造成数据显示不一致。五、收藏交互的即时反馈CelestialCard组件内部维护了自己的收藏状态Componentstruct CelestialCard{StateisFav:booleanfalse;aboutToAppear():void{this.isFavFavoriteManager.isFavorite(this.item.id);}toggleFav():void{constnewStateFavoriteManager.toggle(this.item.id);this.isFavnewState;// State 变化 → UI 自动刷新this.item.isFavoritenewState;// 同步到数据对象}}用户点击收藏按钮的完整链路用户点击 ★/☆ ↓ toggleFav() 被调用 ↓ FavoriteManager.toggle(id) → 数据层修改 ↓ this.isFav newState → State 变量变化 ↓ 框架检测到 State 变化 → 重新渲染 build() ↓ this.isFav ? ★ : ☆ → 图标变化 this.isFav ? 红色(#FF6B6B) : 灰色(#666666) → 颜色变化整个过程完全由数据驱动无需手动操作DOM六、ForEach 渲染要点6.1 基本用法ForEach(this.filteredList,// 数据源(item:CelestialData){// UI 生成函数CelestialCard({item:item})})6.2 性能优化建议虽然当前列表只有10个条目但了解优化方法有助于未来处理大数据量// 方式一带 key推荐性能更优ForEach(this.filteredList,(item:CelestialData)CelestialCard({item:item}),(item:CelestialData)item.id.toString()// 唯一 key)// 方式二不带 keyForEach(this.filteredList,(item:CelestialData)CelestialCard({item:item}))当列表项顺序可能变化或数据量较大时提供 key 可以让框架最小化DOM操作。6.3 类型注解ForEach的回调函数参数必须显式标注类型// ✅ 正确ForEach(this.filteredList,(item:CelestialData){...})// ❌ 错误 - 缺少类型注解ForEach(this.filteredList,(item){...})七、完整页面效果当用户从首页点击行星分类CelestialPage启动接收filterType 行星标签栏高亮行星标签金色applyFilter()筛选出6个行星水星、金星、地球、火星、木星、土星列表中展示6个天体卡片每个左侧有不同颜色的标识条用户可以点击☆收藏任意天体点击卡片跳转到详情页八、本篇总结本篇我们完成了CelestialPage的完整开发核心收获✅标签筛选机制—activeTabapplyFilter()实现分类切换✅CelestialCard 组件— 颜色标识条、信息展示、收藏按钮的完整设计✅点击事件隔离— 收藏按钮和卡片点击各自独立✅页面生命周期—aboutToAppearvsonPageShow的区别与使用场景✅路由参数传递— 从首页分类入口携带筛选参数跳转✅数据驱动UI—State isFav变化自动刷新收藏图标下篇预告我们将开发最丰富的页面 —DetailPage天体详情页包含动态数据切换、四个信息维度展示、趣味知识模块以及收藏按钮的完整交互。本篇涉及的文件entry/src/main/ets/pages/CelestialPage.ets— 列表页与卡片组件entry/src/main/ets/model/CelestialData.ets— 数据源
【鸿蒙原生应用开发实战】第三篇:列表页与标签筛选功能 — 打造高效的天体列表
发布时间:2026/6/14 21:11:46
【鸿蒙原生应用开发实战】第三篇列表页与标签筛选功能 — 打造高效的天体列表前言列表页是移动App中最常见也最重要的页面类型之一。在宇宙探索App中CelestialPage天体列表页承担着展示天体和分类筛选的核心职责。本篇我们将深入实现5个分类标签的切换筛选天体卡片组件的完整设计收藏交互的即时反馈列表渲染性能优化要点一、页面功能总览CelestialPage实现了三个核心功能标签导航— 顶部5个标签全部/行星/恒星/星系/星云点击切换筛选列表展示— 按标签筛选结果展示天体卡片收藏交互— 每个卡片可以直接收藏/取消收藏页面结构┌──────────────────────────────┐ │ ← 天体列表 │ ← 顶部导航栏 ├──────────────────────────────┤ │ [全部] [行星] [恒星] [星系] [星云]│ ← 标签栏 ├──────────────────────────────┤ │ ██ 太阳 │ ← 天体卡片 │ Sun · 恒星 │ (颜色标识条 信息 收藏) │ 太阳是太阳系的中心... │ │ ☆ │ ├──────────────────────────────┤ │ ██ 地球 │ │ Earth · 行星 │ │ 地球是太阳系中唯一... │ │ ★ │ ← 已收藏状态 ├──────────────────────────────┤ │ ... │ └──────────────────────────────┘二、完整代码实现2.1 接口定义importrouterfromohos.router;import{CelestialData,CELESTIAL_LIST,FavoriteManager}from../model/CelestialData;interfaceTabItem{label:string;type:string;}TabItem接口label是显示文本type是筛选类型值。2.2 天体卡片组件 — CelestialCardComponentstruct CelestialCard{item:CelestialData{id:0,name:,englishName:,type:,description:,mass:,diameter:,distance:,temperature:,fact:,color:#FFFFFF,isFavorite:false};StateisFav:booleanfalse;aboutToAppear():void{this.isFavFavoriteManager.isFavorite(this.item.id);}toggleFav():void{constnewStateFavoriteManager.toggle(this.item.id);this.isFavnewState;this.item.isFavoritenewState;}build(){Row(){// 颜色标识条 — 不同天体不同颜色Column().width(6).height(100%).backgroundColor(this.item.color).borderRadius(3);// 信息区域Column(){Row(){Text(this.item.name).fontSize($r(app.float.app_body_size)).fontColor($r(app.color.app_color_white)).fontWeight(FontWeight.Bold);Text(this.item.englishName).fontSize($r(app.float.app_caption_size)).fontColor($r(app.color.app_color_text_secondary)).margin({left:8});}.alignItems(VerticalAlign.Bottom);Text(this.item.type).fontSize($r(app.float.app_caption_size)).fontColor(this.item.color).margin({top:4});Text(this.item.description.length50?this.item.description.substring(0,50)...:this.item.description).fontSize($r(app.float.app_caption_size)).fontColor($r(app.color.app_color_text_secondary)).maxLines(2).lineHeight(18).margin({top:6});}.alignItems(HorizontalAlign.Start).layoutWeight(1).padding({left:12,right:8,top:12,bottom:12});// 收藏按钮Text(this.isFav?★:☆).fontSize(24).fontColor(this.isFav?$r(app.color.app_color_favorite):$r(app.color.app_color_unfavorite)).onClick((event:ClickEvent){this.toggleFav();}).padding({right:12});}.width(100%).height(110).backgroundColor($r(app.color.app_color_card)).borderRadius($r(app.float.app_card_radius)).margin({bottom:12}).onClick((){router.pushUrl({url:pages/DetailPage,params:{id:this.item.id}});});}}2.3 卡片设计要点1. 颜色标识条左侧6px宽的竖条使用this.item.color颜色。每个天体有专属色太阳 →#FF6B35橙色地球 →#4B7B8A蓝绿色火星 →#C1440E红色黑洞 →#2D2D3D深灰视觉上让卡片更生动同时帮助用户快速识别天体类型。2. 收藏按钮的点击事件隔离Text(this.isFav?★:☆).onClick((event:ClickEvent){this.toggleFav();// 只触发收藏切换})收藏按钮的点击事件和卡片的点击事件是独立的点击收藏按钮 → 只切换收藏状态点击卡片其他区域 → 跳转到详情页这是通过event事件冒泡机制实现的——收藏按钮消费了点击事件不会冒泡到卡片容器。3. 描述文本截断Text(this.item.description.length50?this.item.description.substring(0,50)...:this.item.description).maxLines(2).lineHeight(18)双重截断保障代码层面截取前50字符样式层面限制最多2行确保UI整齐。2.4 主页面 — CelestialPageEntryComponentstruct CelestialPage{Statetabs:TabItem[][{label:全部,type:全部},{label:行星,type:行星},{label:恒星,type:恒星},{label:星系,type:星系},{label:星云,type:星云}];StateactiveTab:string全部;StatefilteredList:CelestialData[]CELESTIAL_LIST;privatefilterType:string;aboutToAppear():void{// 从路由参数获取筛选类型从首页分类入口跳转过来时constparamsrouter.getParams()asRecordstring,Object;if(paramsparams[filterType]!undefined){this.filterTypeString(params[filterType]);this.activeTabthis.filterType;}this.applyFilter();}onPageShow():void{this.applyFilter();// 返回此页面时重新应用筛选刷新收藏状态}selectTab(type:string):void{this.activeTabtype;this.applyFilter();}applyFilter():void{if(this.activeTab全部){this.filteredListCELESTIAL_LIST;}else{constarr:CelestialData[][];for(leti0;iCELESTIAL_LIST.length;i){if(CELESTIAL_LIST[i].typethis.activeTab){arr.push(CELESTIAL_LIST[i]);}}this.filteredListarr;}}build(){Column(){// 顶部导航栏 Row(){Text(←).fontSize(24).fontColor($r(app.color.app_color_white)).onClick((){router.back();});Text(天体列表).fontSize($r(app.float.app_subtitle_size)).fontColor($r(app.color.app_color_white)).fontWeight(FontWeight.Bold).margin({left:12});}.width(100%).padding({left:16,top:12,bottom:12});// 标签栏 Row(){ForEach(this.tabs,(tab:TabItem){Text(tab.label).fontSize($r(app.float.app_small_size)).fontColor(this.activeTabtab.type?$r(app.color.app_color_accent):$r(app.color.app_color_tab_inactive)).fontWeight(this.activeTabtab.type?FontWeight.Bold:FontWeight.Normal).padding({left:12,right:12,top:6,bottom:6}).backgroundColor(this.activeTabtab.type?rgba(255, 215, 0, 0.15):transparent).borderRadius(16).onClick((){this.selectTab(tab.type);});})}.width(100%).padding({left:16,bottom:12});// 列表区域 Scroll(){Column(){ForEach(this.filteredList,(item:CelestialData){CelestialCard({item:item})});}.width(100%).padding({left:16,right:16});}.layoutWeight(1);}.width(100%).height(100%).backgroundColor($r(app.color.app_color_background));}}三、标签筛选机制详解3.1 状态变量设计StateactiveTab:string全部;// 当前激活的标签StatefilteredList:CelestialData[]CELESTIAL_LIST;// 筛选后的列表activeTab— 控制哪个标签高亮同时作为筛选依据filteredList— 筛选后的数据驱动列表渲染3.2 筛选核心逻辑applyFilter():void{if(this.activeTab全部){this.filteredListCELESTIAL_LIST;// 显示全部}else{// 手动遍历筛选避免使用 filter 等ES6方法constarr:CelestialData[][];for(leti0;iCELESTIAL_LIST.length;i){if(CELESTIAL_LIST[i].typethis.activeTab){arr.push(CELESTIAL_LIST[i]);}}this.filteredListarr;}}为什么要用 for 循环而不是 filter 方法ArkTS 严格模式对 ES6 的数组高阶方法支持有限。在 API 23 下filter、map等方法的类型推断可能会出问题。使用传统的for循环是更稳妥的方案。3.3 标签激活状态样式.fontColor(this.activeTabtab.type?$r(app.color.app_color_accent):// 激活金色$r(app.color.app_color_tab_inactive))// 未激活灰色.backgroundColor(this.activeTabtab.type?rgba(255, 215, 0, 0.15):transparent)// 激活半透明金色背景.borderRadius(16)// 椭圆胶囊效果视觉反馈三要素属性激活态未激活态文字颜色金色#FFD700灰色#555555字体粗细BoldNormal背景色金色半透明透明3.4 路由入口支持页面支持两种进入方式方式一从首页分类入口进入用户点击行星分类卡片 → 自动筛选行星标签// Index.ets 中CategoryCard.onClick((){router.pushUrl({url:pages/CelestialPage,params:{filterType:行星}});});// CelestialPage 中接收aboutToAppear():void{constparamsrouter.getParams()asRecordstring,Object;if(paramsparams[filterType]!undefined){this.filterTypeString(params[filterType]);this.activeTabthis.filterType;// 直接激活对应标签}this.applyFilter();}方式二直接从天体列表中进入通过底栏或收藏页去探索按钮进入 → 默认显示全部四、onPageShow 的重要作用onPageShow():void{this.applyFilter();}为什么onPageShow中也要调用applyFilter()场景重现用户进入列表页收藏了地球☆ → ★点击地球卡片跳转到详情页在详情页取消收藏地球★ → ☆点击返回回到列表页此时列表页需要通过onPageShow重新加载数据刷新收藏状态如果只用aboutToAppear步骤4返回后列表不会刷新收藏状态还是旧的造成数据显示不一致。五、收藏交互的即时反馈CelestialCard组件内部维护了自己的收藏状态Componentstruct CelestialCard{StateisFav:booleanfalse;aboutToAppear():void{this.isFavFavoriteManager.isFavorite(this.item.id);}toggleFav():void{constnewStateFavoriteManager.toggle(this.item.id);this.isFavnewState;// State 变化 → UI 自动刷新this.item.isFavoritenewState;// 同步到数据对象}}用户点击收藏按钮的完整链路用户点击 ★/☆ ↓ toggleFav() 被调用 ↓ FavoriteManager.toggle(id) → 数据层修改 ↓ this.isFav newState → State 变量变化 ↓ 框架检测到 State 变化 → 重新渲染 build() ↓ this.isFav ? ★ : ☆ → 图标变化 this.isFav ? 红色(#FF6B6B) : 灰色(#666666) → 颜色变化整个过程完全由数据驱动无需手动操作DOM六、ForEach 渲染要点6.1 基本用法ForEach(this.filteredList,// 数据源(item:CelestialData){// UI 生成函数CelestialCard({item:item})})6.2 性能优化建议虽然当前列表只有10个条目但了解优化方法有助于未来处理大数据量// 方式一带 key推荐性能更优ForEach(this.filteredList,(item:CelestialData)CelestialCard({item:item}),(item:CelestialData)item.id.toString()// 唯一 key)// 方式二不带 keyForEach(this.filteredList,(item:CelestialData)CelestialCard({item:item}))当列表项顺序可能变化或数据量较大时提供 key 可以让框架最小化DOM操作。6.3 类型注解ForEach的回调函数参数必须显式标注类型// ✅ 正确ForEach(this.filteredList,(item:CelestialData){...})// ❌ 错误 - 缺少类型注解ForEach(this.filteredList,(item){...})七、完整页面效果当用户从首页点击行星分类CelestialPage启动接收filterType 行星标签栏高亮行星标签金色applyFilter()筛选出6个行星水星、金星、地球、火星、木星、土星列表中展示6个天体卡片每个左侧有不同颜色的标识条用户可以点击☆收藏任意天体点击卡片跳转到详情页八、本篇总结本篇我们完成了CelestialPage的完整开发核心收获✅标签筛选机制—activeTabapplyFilter()实现分类切换✅CelestialCard 组件— 颜色标识条、信息展示、收藏按钮的完整设计✅点击事件隔离— 收藏按钮和卡片点击各自独立✅页面生命周期—aboutToAppearvsonPageShow的区别与使用场景✅路由参数传递— 从首页分类入口携带筛选参数跳转✅数据驱动UI—State isFav变化自动刷新收藏图标下篇预告我们将开发最丰富的页面 —DetailPage天体详情页包含动态数据切换、四个信息维度展示、趣味知识模块以及收藏按钮的完整交互。本篇涉及的文件entry/src/main/ets/pages/CelestialPage.ets— 列表页与卡片组件entry/src/main/ets/model/CelestialData.ets— 数据源