鸿蒙原生应用实战(二):游戏库列表与筛选排序 — 卡片式UI设计 鸿蒙原生应用实战二游戏库列表与筛选排序 — 卡片式UI设计一、前言上一篇我们完成了项目搭建和首页开发。本篇聚焦GameListPage游戏库页面的开发这是 App 的核心浏览页面。我们将实现多标签状态筛选全部/在玩/通关/想玩循环排序切换最近/名称/评分/时长游戏卡片 UI 设计动态进度条接收首页路由参数二、页面功能概览GameListPage 的功能架构如下┌─────────────────────────────────┐ │ ← 返回 游戏库 搜索 │ ← Header ├─────────────────────────────────┤ │ [全部] [在玩] [通关] [想玩] │ ← 筛选标签横向滚动 ├─────────────────────────────────┤ │ 排序: 最近 ▼ 共5款 │ ← 排序行 ├─────────────────────────────────┤ │ ┌───────────────────────────┐ │ │ │ ┌────────────────────┐ │ │ │ │ │ 艾尔登法环 49 │ │ │ │ │ │ PC · 动作RPG │ │ │ │ │ │ [通关] 186h │ │ │ │ │ │ ████████░░ 100% │ │ │ │ │ └────────────────────┘ │ │ ← 游戏卡片列表 │ └───────────────────────────┘ │ │ ┌───────────────────────────┐ │ │ │ 塞尔达传说... 48 │ │ │ │ Switch · 动作冒险 │ │ │ │ [在玩] 72h │ │ │ │ █████░░░░░ 55% │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘三、数据模型为了更好地区分我们在列表页定义独立的GameItem接口interfaceGameItem{id:number;title:string;platform:string;genre:string;status:string;// 通关 / 在玩 / 想玩rating:number;// 媒体评分0-49hours:number;// 游玩时长progress:number;// 进度百分比0-100coverColor:string;// 封面颜色代码}四、核心实现4.1 组件结构与状态定义EntryComponentstruct GameListPage{Stategames:GameItem[][];Statefilter:string全部;// 当前筛选状态Statefilters:string[][全部,在玩,通关,想玩];StatesortBy:string最近;// 当前排序方式aboutToAppear():void{// 接收首页传来的筛选参数constparamsrouter.getParams()asRecordstring,Object;if(paramsparams[filter]!undefined){this.filterparams[filter]asstring;}this.games[/* 游戏数据表 */];}}4.2 接收路由参数 ⭐这是连接首页筛选标签和列表页的关键aboutToAppear():void{constparams:Recordstring,Objectrouter.getParams()asRecordstring,Object;if(paramsparams[filter]!undefined){this.filterparams[filter]asstring;}this.games[/* ... */];}首页跳转代码来自 Index.ets.onClick((){router.pushUrl({url:pages/GameListPage,params:{filter:通关}});})4.3 筛选与排序逻辑筛选函数getFilteredGames():GameItem[]{if(this.filter全部){returnthis.games;}returnthis.games.filter((g:GameItem)g.statusthis.filter);}排序循环切换我们实现了一个循环切换的逻辑每次点击切换到下一种排序方式BuilderbuildSortRow(){Row(){Text(排序:).fontSize(12).fontColor(#999999)Row(){Text(this.sortBy).fontSize(12).fontColor(#FF6B35)Text( ▼).fontSize(10).fontColor(#FF6B35)}.padding({left:8,right:10,top:3,bottom:3}).backgroundColor(#FFF0E8).borderRadius(10).margin({left:6}).onClick((){constsorts:string[][最近,名称,评分,时长];constidx:numbersorts.indexOf(this.sortBy);this.sortBysorts[(idx1)%sorts.length];// TODO: 实际项目中在此处调用排序函数})Blank()Text(共${this.getFilteredGames().length}款).fontSize(12).fontColor(#999999)}.width(100%).padding({left:16,right:16,top:8})}设计思路采用索引取模(idx 1) % length的方式实现循环切换比 if-else 链更简洁优雅。4.4 筛选标签 — 带状态的高亮BuilderbuildFilters(){Scroll(){Row(){ForEach(this.filters,(f:string){Text(f).fontSize(13).fontColor(this.filterf?#FFFFFF:#666666).padding({left:16,right:16,top:6,bottom:6}).backgroundColor(this.filterf?#FF6B35:#F0F0F0).borderRadius(16).margin({right:8}).onClick((){this.filterf;})},(f:string)f)}.padding({left:16})}.scrollable(ScrollDirection.Horizontal).height(40)}交互细节选中标签橙色背景 白色文字#FF6B35/#FFFFFF未选中标签浅灰背景 深灰文字#F0F0F0/#666666点击即切换this.filterArkTS 自动触发getFilteredGames()重渲染4.5 游戏卡片 UI — 装饰器模式这是整个页面最核心的 UI 组件。我们使用Builder将卡片封装为一个可复用的构建函数BuilderbuildGameCard(game:GameItem){Row(){// 左侧封面色块 emojiStack(){Column().width(60).height(80).borderRadius(8).backgroundColor(game.coverColor).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)Text().fontSize(26)}.width(60).height(80)// 右侧文字信息Column(){// 第一行标题 评分Row(){Text(game.title).fontSize(15).fontWeight(FontWeight.Medium).fontColor(#1A1A2E).layoutWeight(1)if(game.rating0){Text(game.rating.toString()).fontSize(12).fontWeight(FontWeight.Bold).fontColor(game.rating48?#E74C3C:#F39C12)}}.width(100%)// 第二行平台 类型Text(${game.platform}·${game.genre}).fontSize(12).fontColor(#999999).margin({top:4})// 第三行状态标签 时长Row(){Text(game.status).fontSize(11).fontColor(Color.White).padding({left:6,right:6,top:2,bottom:2}).backgroundColor(game.status通关?#2ECC71:game.status在玩?#3498DB:#9B59B6).borderRadius(6)if(game.hours0){Text(${game.hours}h).fontSize(11).fontColor(#BBBBBB).margin({left:8})}}.margin({top:4})// 进度条仅进行中且未完成时显示if(game.progress0game.progress100){Progress({value:game.progress,total:100,style:ProgressStyle.Linear}).width(80%).height(4).value(game.progress).color(#FF6B35).backgroundColor(#F0F0F0).borderRadius(2).margin({top:4})}}.alignItems(HorizontalAlign.Start).margin({left:10}).layoutWeight(1)}.width(100%).padding(12).backgroundColor(#FFFFFF).borderRadius(10).margin({top:8,left:16,right:16}).onClick((){router.pushUrl({url:pages/GameDetailPage,params:{gameId:game.id}})})}4.6 卡片设计详解(1) 封面色块每个游戏分配一个独特的十六进制颜色代码作为封面的替代方案游戏色值寓意艾尔登法环#FFD700黄金树的金色塞尔达传说#2ECC71海拉鲁的翠绿博德之门3#E74C3C夺心魔的深红赛博朋克2077#3498DB夜之城的霓虹蓝这种方案避免了实际截图的资源占用同时通过颜色传递游戏氛围。(2) 评分颜色分级game.rating48?#E74C3C:#F39C12评分 ≥ 48满分 50红色高亮表示顶级神作评分 48橙色表示优质作品(3) 状态标签颜色编码通关 →绿色(#2ECC71) 已完成 在玩 →蓝色(#3498DB) 进行中 想玩 →紫色(#9B59B6) 待开始(4) 条件渲染进度条仅当0 progress 100时显示进度条。通关游戏100%不显示进度条避免视觉冗余if(game.progress0game.progress100){Progress({value:game.progress,total:100,style:ProgressStyle.Linear}).width(80%).height(4).color(#FF6B35).backgroundColor(#F0F0F0)}ArkTS 的Progress组件支持线性样式自带动画效果。4.7 组装页面build():void{Column(){this.buildHeader()// 固定头部this.buildFilters()// 可滚动筛选标签this.buildSortRow()// 排序行Scroll(){Column(){ForEach(this.getFilteredGames(),(game:GameItem){this.buildGameCard(game)},(game:GameItem)game.id.toString()this.filter)}.width(100%).padding({bottom:30})}.scrollable(ScrollDirection.Vertical).layoutWeight(1).width(100%)}.width(100%).height(100%).backgroundColor(#F5F5F5)}关于 key 的思考在 ForEach 的 key 生成器中我们使用了game.id.toString() this.filter。这样做的好处是筛选切换时key 变化会触发列表完全重建确保筛选后的数据正确渲染避免仅用game.id时ArkTS 的 diff 机制保留已删除 DOM 节点的问题五、进阶话题ForEach 的复用策略ArkTS 的ForEach基于 key 进行 diff 更新。理解 key 策略对性能至关重要Key 策略行为适用场景唯一且稳定 (如id)尽量复用已有组件只更新数据数据不增减的静态列表包含筛选条件 (如idfilter)筛选变化时重建全部筛选条件变化需要重新布局索引 (index)紧耦合于位置慎用不推荐用于可排序列表在我们的场景中筛选切换需要卡片布局完全刷新因此使用id filter作为复合 key。六、ArkTS 严格模式避坑6.1 对象字面量类型声明// ❌ 错误arkts-no-untyped-obj-literalsRow(){Text(通关).onClick((){})}// ✅ 正确将对象字面量提取为类型变量interfaceFilterItem{label:string;key:string;}constfilterItems:FilterItem[][{label:全部,key:all}];6.2 数组字面量类型推断// ❌ 错误arkts-no-noninferrable-arr-literalsconstsorts[最近,名称,评分,时长];// ✅ 正确显式声明constsorts:string[][最近,名称,评分,时长];6.3 Filter 回调类型标注// ✅ 必须显式声明参数类型this.games.filter((g:GameItem)g.statusthis.filter)七、Header 的设计BuilderbuildHeader(){Row(){Text(←).fontSize(20).fontColor(#333333).onClick((){router.back();})Blank()Text( 游戏库).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#1A1A2E)Blank()Text().fontSize(18)}.width(100%).padding({left:16,right:16,top:12,bottom:12}).backgroundColor(#FFFFFF)}左侧返回按钮调用router.back()回到首页右侧搜索图标为后续功能预留。八、小结本篇我们完成了✅ 游戏库列表页完整开发✅ 多标签筛选 循环排序切换✅ 卡片式 UI 设计封面色块、评分、进度条✅ 路由参数接收与 ForEach 复用策略✅ ArkTS 严格模式常见问题解决下一篇将深入游戏详情页实现全屏 Header、状态切换、星级评分、成就系统和用户评测等丰富交互功能。系列目录第一篇项目搭建与首页开发第二篇游戏库列表与筛选排序本文第三篇游戏详情页与交互功能第四篇愿望单与个人统计第五篇路由导航与工程优化