HarmonyOS旅行探索——首页交互功能与路由跳转实现

📅 2026/7/12 13:25:03
HarmonyOS旅行探索——首页交互功能与路由跳转实现
旅行探索首页的 UI 布局确定之后接下来要解决的是交互问题——用户点击城市卡片怎么跳到详情页点击底部导航栏怎么切换页面点击工具按钮怎么打开对应功能。这些交互的核心是router.pushUrl路由跳转和onClick事件绑定实现起来不复杂但有几个容易踩坑的地方。完整效果交互点梳理首页上可点击的元素一共有三类城市卡片点击后跳转到城市详情页需要传递城市 ID工具按钮四个快捷入口分别跳转到行程规划、美食指南、汇率换算、旅行短语底部导航栏五个 Tab非当前页跳转当前页无操作三类交互的实现方式都是onClickrouter.pushUrl但各自的参数传递和边界处理有所不同。城市卡片的参数化跳转城市网格的每张卡片都绑定了点击事件.onClick((){router.pushUrl({url:pages/CityDetail,params:{cityId:city.id}asRecordstring,Object})})这里有两个细节值得说一下。参数类型断言{ cityId: city.id } as Recordstring, Object里的as Recordstring, Object是必须的。router.pushUrl的params参数类型是Recordstring, Object而直接写{ cityId: city.id }会被 ArkTS 推断为更具体的类型比如{ cityId: number }类型检查会报错。加as Recordstring, Object显式声明类型编译才能通过。这个写法在 ArkTS 路由里非常常见几乎每次传参都会用到。如果忘了加IDE 会报类似类型不兼容的错误新手容易卡在这里。只传 ID不传对象city对象本身包含了名称、国家、描述、景点、美食等大量数据。如果把整个对象传过去代码会变成params: { city: city } as Recordstring, Object但这样做的问题是——ArkTS 的路由参数只支持基本类型string、number、boolean和它们的数组不支持自定义对象。传对象会导致运行时数据丢失或序列化错误。所以正确的做法是只传 ID详情页根据 ID 从数据源重新查询。这是一种参数最小化原则——路由参数越简单出问题的概率越低。详情页的参数接收CityDetail 页面在aboutToAppear里接收参数aboutToAppear():void{constprouter.getParams()asRecordstring,Objectif(pp[cityId]){constid:numberp[cityId]asnumberfor(leti:number0;iCITIES.length;i){if(CITIES[i].idid){this.cityCITIES[i];break}}}}router.getParams()返回的是object | undefined需要先断言为Recordstring, Object才能用键名访问。然后逐个字段取出并做类型断言——p[cityId] as number。这个查找逻辑用的是for循环而不是find()原因和之前一样——ArkTS 里数组方法的类型推断不够精确find()返回的类型有时会被推断为T | undefined后续使用时 IDE 会报可能为空的警告。手动循环 break虽然啰嗦但类型安全。找到匹配的城市后赋值给State cityUI 自动更新。如果参数里没有cityId或者找不到对应城市city保持undefined页面会显示空白——这是一个需要处理的边界情况。工具按钮的直接跳转四个工具按钮的跳转逻辑比城市卡片简单因为不需要传参数Row({space:10}){this.ToolBtn(,行程规划,#5DADE2,(){router.pushUrl({url:pages/TripPlanner})})this.ToolBtn(,美食指南,#E8734A,(){router.pushUrl({url:pages/CuisinePage})})this.ToolBtn(,汇率换算,#00B894,(){router.pushUrl({url:pages/CurrencyPage})})this.ToolBtn(️,旅行短语,#8A5DFA,(){router.pushUrl({url:pages/PhraseBook})})}每个 ToolBtn 接收一个action回调点击时执行对应的路由跳转。这种Builder 只负责 UI回调函数负责逻辑的模式让 Builder 保持了通用性——如果以后要加第五个工具按钮只需要在Row里多加一行this.ToolBtn(...)就行不需要改 Builder 的代码。ToolBtn Builder 的回调参数BuilderToolBtn(icon:string,label:string,color:string,action:()void){Column(){Text(icon).fontSize(28).margin({bottom:6})Text(label).fontSize(11).fontColor(T1)}.layoutWeight(1).padding({top:14,bottom:14}).backgroundColor(color10).borderRadius(14).onClick(action)}action参数的类型是() void——一个不接收参数、没有返回值的函数。.onClick(action)把这个回调绑定到点击事件上。Builder 本身不知道点击后会发生什么它只负责用户点了之后调用这个函数。这种设计让 ToolBtn 可以复用于任何需要图标 文字 点击的场景。运动记录的快捷按钮、社区的入口按钮只要换一下图标、文字和回调就行。底部导航栏的条件路由底部导航栏的跳转逻辑和工具按钮类似但多了一个条件判断——当前 Tab 不跳转BuilderNavBtn(idx:number,label:string,active:boolean){Column({space:2}){Text(label探索?:(label行程?:(label美食?:(label工具?:)))).fontSize(20)Text(label).fontSize(9).fontColor(active?A:#C0BFC6)}.onClick((){if(idx1)router.pushUrl({url:pages/TripPlanner})elseif(idx2)router.pushUrl({url:pages/CuisinePage})elseif(idx3)router.pushUrl({url:pages/CurrencyPage})elseif(idx4)router.pushUrl({url:pages/ProfilePage})})}注意idx 0探索 Tab的情况——onClick 里没有对应的if分支。当用户点击探索时什么都不会发生。这是合理的用户已经在首页了再点探索没有意义。但如果要做得更完善应该在idx 0时让页面滚动到顶部if(idx0){// 滚动到顶部的逻辑}目前首页的 Scroll 没有绑定ScrollController所以暂时无法用代码控制滚动位置。如果以后需要这个功能可以给 Scroll 绑一个 controller然后在 onClick 里调用controller.scrollToTop()。图标的三元表达式嵌套Text(label探索?:(label行程?:(label美食?:(label工具?:))))这段代码用嵌套的三元表达式根据 label 选择对应的 emoji 图标。写法虽然紧凑但可读性不太好——嵌套了四层读起来需要从外往里逐层解析。更清晰的做法是用映射表privategetIcon(label:string):string{consticons:Recordstring,string{探索:,行程:,美食:,工具:,我的:}returnicons[label]||}但在当前规模下只有 5 个 Tab嵌套三元表达式也能接受。代码量少的时候简洁比可读性更重要代码量多了再重构也不迟。颜色透明度的计算ToolBtn 和 NavBtn 的背景色都用了一个 10的后缀.backgroundColor(color10)这里的10是十六进制的透明度值。在 ARGB 颜色模型里#FF6B3510表示#FF6B35这个颜色加上0x10十进制 16的透明度。0x10 / 0xFF 6.3%也就是说背景色是主题色的 6% 不透明度——几乎看不见但能让按钮区域和周围有一点微妙的区分。这个透明度计算方式在整个项目里统一使用。如果以后要调整按钮的背景深浅只需要改这个后缀值就行——20是 12%30是 18%以此类推。路由跳转的错误处理目前所有的router.pushUrl调用都没有错误处理。如果目标页面不存在比如module.json5里忘了注册运行时会崩溃而不是优雅降级。一个更健壮的做法是加上回调router.pushUrl({url:pages/CityDetail,params:{cityId:city.id}asRecordstring,Object},(err){if(err){console.error(路由跳转失败: JSON.stringify(err))}})pushUrl的第二个参数是一个回调函数接收错误信息。在原型阶段可以不加这个回调方便快速调试但上线前应该补上。同样的问题也存在于城市详情页的参数接收——如果cityId无效或CITIES数组为空this.city会保持undefined页面会显示空白。应该加一个兜底逻辑aboutToAppear():void{constprouter.getParams()asRecordstring,Objectif(pp[cityId]){constid:numberp[cityId]asnumberfor(leti:number0;iCITIES.length;i){if(CITIES[i].idid){this.cityCITIES[i];break}}}if(!this.city){// 兜底显示默认城市或返回上一页router.back()}}页面跳转的数据流整个首页的跳转数据流可以用一张图概括首页 (Index) ├── 点击城市卡片 → CityDetail (params: cityId) ├── 点击行程规划 → TripPlanner ├── 点击美食指南 → CuisinePage ├── 点击汇率换算 → CurrencyPage ├── 点击旅行短语 → PhraseBook └── 点击底部我的 → ProfilePage六个跳转目标只有一个需要传参CityDetail其他五个都是直接跳转。这种大多数简单、少数复杂的分布是典型的 App 首页模式——首页是分发中心把用户引导到各个功能页面不需要在首页做太多复杂交互。数据流向是单向的首页只负责把用户送出去不负责把数据收回来。如果以后需要从详情页返回首页时刷新数据比如用户在详情页收藏了一个城市返回首页后要更新收藏状态需要用router.back的result参数或者全局状态管理来实现。但在当前版本里这个需求还不存在。交互设计的一致性三类交互虽然场景不同但在实现上保持了一致都用onClick绑定事件都用router.pushUrl做页面跳转跳转参数都用as Recordstring, Object类型断言Builder 都只负责 UI业务逻辑通过回调传入这种一致性不是偶然的——它来自于对 ArkTS 路由机制的理解和对项目代码风格的统一。当所有跳转都用同一种写法时排查问题变得简单路由跳不通先检查module.json5有没有注册页面参数没收到先检查as Recordstring, Object有没有加。这些约定比技术更重要。