鸿蒙原生应用开发实战(二):ArkTS组件化构建首页——钓点列表与底部导航

鸿蒙原生应用开发实战(二):ArkTS组件化构建首页——钓点列表与底部导航 鸿蒙原生应用开发实战二ArkTS组件化构建首页——钓点列表与底部导航前言上一篇我们完成了项目的初始化搭建本篇正式开始编码我们将构建钓点日记App的首页包含天气卡片展示当日天气附近钓点列表含评分、距离、鱼种标签底部导航栏4个Tab空状态处理页面路由跳转通过本篇文章你将掌握ArkTS组件化开发的核心技巧学会ForEach列表渲染、条件渲染、状态管理等关键能力。一、ArkTS 基础回顾在动手之前先回顾一下ArkTS的几个核心概念1.1 Component EntryEntry// 标记为页面入口Component// 标记为可复用组件struct Index{// 使用struct定义组件build(){// build方法描述UI结构Column(){Text(Hello World)}}}1.2 State 状态管理State装饰的变量会触发UI重新渲染Componentstruct Counter{Statecount:number0;build(){Column(){Text(计数: this.count)Button(点击1).onClick((){this.count})}}}1.3 条件渲染if(this.spots.length0){// 空状态展示}else{// 列表展示}1.4 循环渲染 ForEachForEach(this.spots,(spot:FishingSpot){// 渲染每个钓点卡片},(spot:FishingSpot)spot.id.toString())第三个参数是键值生成函数用于优化列表更新性能。二、首页布局设计2.1 页面结构分析首页的UI结构分为三层┌─────────────────────────────┐ │ 标题栏 │ Row Text ├─────────────────────────────┤ │ ┌───────────────────────┐ │ │ │ ️ 今日天气 晴 18°C │ │ 天气卡片 │ └───────────────────────┘ │ │ │ │ 附近钓点 │ 标题 │ │ │ ┌───────────────────────┐ │ │ │ 月亮湾水库 12km │ │ │ │ 城西区月亮湾 │ │ 钓点卡片1 │ │ 水深: 3-5m ★★★★☆ │ │ │ │ [鲫鱼] [鲤鱼] [草鱼] │ │ │ └───────────────────────┘ │ │ ┌───────────────────────┐ │ │ │ 清溪河下游 8km │ │ │ │ ... │ │ 钓点卡片2 │ └───────────────────────┘ │ │ │ ├─────────────────────────────┤ │ 附近 │ 记录 │ 装备 │ 我的 │ 底部导航 └─────────────────────────────┘2.2 核心布局代码使用Column Row组合实现垂直和水平布局build(){Column(){// 1. 标题栏Row(){...}// 2. 可滚动内容区Scroll(){Column(){// 天气卡片Row(){...}// 钓点列表Text(附近钓点)if(...){// 空状态}else{ForEach(...){/* 钓点卡片 */}}}}.layoutWeight(1)// 占据剩余空间// 3. 底部导航Row(){...}.height(60)}.width(100%).height(100%)}关键布局技巧layoutWeight(1)让中间区域自适应填充底部导航固定height(60)外层width(100%).height(100%)撑满全屏三、数据模型定义在ArkTS中接口定义使用interface关键字。由于项目的严格模式要求对象字面量必须有显式类型声明interfaceFishingSpot{id:number;name:string;location:string;waterDepth:string;fishTypes:string[];rating:number;distance:string;}interfaceRouteOpt{url:string;params?:Object;}数据通过State管理初始化时赋值Statespots:FishingSpot[][{id:1,name:月亮湾水库,location:城西区月亮湾,waterDepth:3-5m,fishTypes:[鲫鱼,鲤鱼,草鱼],rating:4,distance:12km},// ... 更多钓点];Stateweather:string晴 18°C 微风;注意数组字面量必须能被推断类型。如果直接写[{...}, {...}]会触发arkts-no-noninferrable-arr-literals规则所以需要显式类型注解FishingSpot[]。四、组件化构建详情4.1 天气卡片天气卡片使用水平布局左侧显示天气图标右侧显示文字Row(){Text(️).fontSize(32)Column(){Text($r(app.string.weather_today)).fontSize($r(app.float.small_font_size)).fontColor($r(app.color.text_hint))Text(this.weather).fontSize($r(app.float.body_font_size)).fontWeight(FontWeight.Medium).margin({top:2})}.margin({left:12})}.width(100%).padding(16).backgroundColor($r(app.color.card_bg)).borderRadius($r(app.float.card_corner_radius))知识点$r()引用资源实现主题统一.borderRadius()圆角处理视觉更柔和.fontWeight(FontWeight.Medium)文字粗细控制4.2 钓点卡片钓点卡片展示多种信息名称、位置、距离、水深、评分和鱼种标签ForEach(this.spots,(spot:FishingSpot){Column(){// 第一行名称 距离徽章Row(){Column(){Text(spot.name).fontSize(18).fontWeight(FontWeight.Medium)Text(spot.location).fontSize($r(app.float.small_font_size)).fontColor($r(app.color.text_hint)).margin({top:4})}Blank()// 自动撑开Text(spot.distance).fontSize($r(app.float.small_font_size)).fontColor($r(app.color.text_secondary)).backgroundColor($r(app.color.background)).padding({left:8,right:8,top:2,bottom:2}).borderRadius(10)}// 第二行水深 评分Row(){Text(水深: spot.waterDepth)Blank()Row(){ForEach([1,2,3,4,5],(star){Text(starspot.rating?★:☆).fontSize(16).fontColor($r(app.color.rating_star))})}}.margin({top:8})// 第三行鱼种标签Row(){ForEach(spot.fishTypes,(fish){Text(fish).fontSize($r(app.float.badge_font_size)).fontColor($r(app.color.primary)).backgroundColor(#FFE3F2FD).padding({left:8,right:8,top:2,bottom:2}).borderRadius(10).margin({right:6})})}.margin({top:8})}.padding(16).backgroundColor($r(app.color.card_bg)).borderRadius($r(app.float.card_corner_radius)).margin({top:8}).onClick((){// 点击跳转到详情页})},(spot:FishingSpot)spot.id.toString())设计亮点Blank()组件自动填充剩余空间实现两端对齐距离徽章使用背景色圆角模拟Tag效果星级评分三元运算符★/☆结合条件判断鱼种标签蓝色文字浅蓝背景视觉区分4.3 空状态处理当钓点列表为空时显示友好的空状态if(this.spots.length0){Column(){Text().fontSize(60)Text($r(app.string.no_spots)).fontSize($r(app.float.body_font_size)).fontColor($r(app.color.text_hint)).margin({top:16})}.width(100%).height(200).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}使用FlexAlign.Center和HorizontalAlign.Center实现水平和垂直居中。五、底部导航栏实现底部导航是App中最重要的交互组件之一。我们使用4个Column平分宽度Row(){// Tab 1: 附近当前选中Column(){Text().fontSize(22)Text(附近).fontSize(11).fontColor($r(app.color.primary))}.layoutWeight(1).alignItems(HorizontalAlign.Center)// Tab 2: 记录Column(){Text().fontSize(22)Text(记录).fontSize(11).fontColor($r(app.color.text_hint))}.layoutWeight(1).alignItems(HorizontalAlign.Center).onClick((){router.pushUrl({url:pages/CatchRecordPage})})// Tab 3: 装备Column(){...}.onClick((){router.pushUrl({url:pages/GearPage})})// Tab 4: 我的Column(){...}.onClick((){router.pushUrl({url:pages/ProfilePage})})}.width(100%).height(60).backgroundColor($r(app.color.card_bg))设计要点layoutWeight(1)实现四个Tab均分宽度当前Tab的图标/文字使用主题色primary其他使用灰色点击事件通过router.pushUrl跳转到对应页面思考这里使用router.pushUrl而不是自定义Tab切换是因为我们设计的是多页面架构。后续可以优化为使用Tabs组件实现更流畅的切换体验。六、路由跳转与传参点击钓点卡片时需要跳转到详情页并传递钓点数据.onClick((){letp:SpotParams{spotData:spot};letopt:RouteOpt{url:pages/SpotDetailPage,params:p};router.pushUrl(opt);})对应的接口定义interfaceSpotParams{spotData:FishingSpot;}interfaceRouteOpt{url:string;params?:Object;}在详情页接收参数aboutToAppear():void{constparamsrouter.getParams()asSpotDetailParams;if(paramsparams.spotData){this.spotparams.spotData;}}aboutToAppear是组件的生命周期回调在组件即将显示时触发比build()执行更早适合做数据初始化。七、Scroll 滚动容器当内容超过屏幕高度时需要使用Scroll组件包裹Scroll(){Column(){// 天气卡片// 钓点列表}.width(90%)// 两侧留白}.width(100%).layoutWeight(1)// 填充剩余空间注意Scroll只能有一个子组件内部用Column包含所有内容Scroll默认垂直滚动无需显式指定方向八、完整首页代码解读最终首页Index.ets的核心结构如下importrouterfromohos.router;interfaceFishingSpot{/* ... */}interfaceRouteOpt{/* ... */}interfaceSpotParams{/* ... */}EntryComponentstruct Index{Statespots:FishingSpot[][/* 数据 */];Stateweather:string晴 18°C 微风;build(){Column(){// 标题栏Row(){/* ... */}Scroll(){Column(){// 天气卡片// 附近钓点标题// 空状态 或 钓点列表}}.layoutWeight(1)// 底部导航Row(){/* 4个Tab */}.height(60)}.backgroundColor($r(app.color.background))}}九、常见问题与避坑9.1 严格模式下的对象字面量鸿蒙的ArkTS严格模式arkts-no-untyped-obj-literals要求对象字面量必须有显式类型。解决方式// ❌ 错误letspot{name:月亮湾,rating:4};// ✅ 正确letspot:FishingSpot{id:1,name:月亮湾,...};9.2 ForEach 的 key 生成ForEach的第三个参数是键值生成函数用于高效更新ForEach(arr,(item){/* 渲染 */},(item)item.id.toString()// 唯一键)如果数据顺序不变但内容变化使用索引作为key即可(item,index)index.toString()9.3 资源引用路径$r()的路径格式$r(app.string.xxx)— 应用级别字符串$r(app.color.xxx)— 颜色资源$r(app.float.xxx)— 尺寸资源$r(media.xxx)— 图片资源十、效果展示完成首页开发后你应该能看到✅ 顶部标题栏显示附近钓点✅ 天气卡片展示今日天气✅ 3个钓点卡片包含名称、位置、水深、评分和鱼种标签✅ 底部导航四个Tab点击可跳转✅ 列表可滚动总结本篇我们完成了✅ ArkTS组件化开发的核心概念✅ 首页布局的ColumnRow层级设计✅ 天气卡片和钓点卡片的组件构建✅ 星级评分和标签徽章的自定义实现✅ 底部导航栏与路由跳转✅ 空状态和Scroll滚动处理下一篇我们将继续开发渔获记录、装备管理和个人中心三个页面深入讲解列表渲染、组件复用和状态管理的进阶技巧项目源码基于 HarmonyOS API 23 Stage模型 ArkTS系列目录第一篇项目初始化与环境配置第二篇首页与钓点列表开发本篇第三篇数据管理与多页面交互第四篇复杂页面与交互体验第五篇地图可视化与性能优化