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

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

前言

上一篇我们完成了项目的初始化搭建,本篇正式开始编码!我们将构建"钓点日记"App的首页,包含:

  • 天气卡片(展示当日天气)
  • 附近钓点列表(含评分、距离、鱼种标签)
  • 底部导航栏(4个Tab)
  • 空状态处理
  • 页面路由跳转

通过本篇文章,你将掌握 ArkTS组件化开发 的核心技巧,学会 ForEach列表渲染条件渲染状态管理 等关键能力。


一、ArkTS 基础回顾

在动手之前,先回顾一下ArkTS的几个核心概念:

1.1 @Component + @Entry

typescript 复制代码
@Entry        // 标记为页面入口
@Component    // 标记为可复用组件
struct Index {  // 使用struct定义组件
  build() {     // build方法描述UI结构
    Column() {
      Text('Hello World')
    }
  }
}

1.2 @State 状态管理

@State 装饰的变量会触发UI重新渲染:

typescript 复制代码
@Component
struct Counter {
  @State count: number = 0;

  build() {
    Column() {
      Text('计数: ' + this.count)
      Button('点击+1').onClick(() => { this.count++ })
    }
  }
}

1.3 条件渲染

typescript 复制代码
if (this.spots.length === 0) {
  // 空状态展示
} else {
  // 列表展示
}

1.4 循环渲染 ForEach

typescript 复制代码
ForEach(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 组合实现垂直和水平布局:

typescript 复制代码
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 关键字。由于项目的严格模式要求,对象字面量必须有显式类型声明:

typescript 复制代码
interface FishingSpot {
  id: number;
  name: string;
  location: string;
  waterDepth: string;
  fishTypes: string[];
  rating: number;
  distance: string;
}

interface RouteOpt {
  url: string;
  params?: Object;
}

数据通过 @State 管理,初始化时赋值:

typescript 复制代码
@State spots: FishingSpot[] = [
  {
    id: 1,
    name: '月亮湾水库',
    location: '城西区月亮湾',
    waterDepth: '3-5m',
    fishTypes: ['鲫鱼', '鲤鱼', '草鱼'],
    rating: 4,
    distance: '12km'
  },
  // ... 更多钓点
];

@State weather: string = '晴 18°C 微风';

注意 :数组字面量必须能被推断类型。如果直接写 [{...}, {...}] 会触发 arkts-no-noninferrable-arr-literals 规则,所以需要显式类型注解 FishingSpot[]


四、组件化构建详情

4.1 天气卡片

天气卡片使用水平布局,左侧显示天气图标,右侧显示文字:

typescript 复制代码
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 钓点卡片

钓点卡片展示多种信息:名称、位置、距离、水深、评分和鱼种标签:

typescript 复制代码
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(star <= spot.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 空状态处理

当钓点列表为空时,显示友好的空状态:

typescript 复制代码
if (this.spots.length === 0) {
  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.CenterHorizontalAlign.Center 实现水平和垂直居中。


五、底部导航栏实现

底部导航是App中最重要的交互组件之一。我们使用4个 Column 平分宽度:

typescript 复制代码
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组件实现更流畅的切换体验。


六、路由跳转与传参

点击钓点卡片时,需要跳转到详情页并传递钓点数据:

typescript 复制代码
.onClick(() => {
  let p: SpotParams = { spotData: spot };
  let opt: RouteOpt = { url: 'pages/SpotDetailPage', params: p };
  router.pushUrl(opt);
})

对应的接口定义:

typescript 复制代码
interface SpotParams {
  spotData: FishingSpot;
}

interface RouteOpt {
  url: string;
  params?: Object;
}

在详情页接收参数:

typescript 复制代码
aboutToAppear(): void {
  const params = router.getParams() as SpotDetailParams;
  if (params && params.spotData) {
    this.spot = params.spotData;
  }
}

aboutToAppear 是组件的生命周期回调,在组件即将显示时触发,比 build() 执行更早,适合做数据初始化。


七、Scroll 滚动容器

当内容超过屏幕高度时,需要使用 Scroll 组件包裹:

typescript 复制代码
Scroll() {
  Column() {
    // 天气卡片
    // 钓点列表
  }
  .width('90%')  // 两侧留白
}
.width('100%')
.layoutWeight(1)  // 填充剩余空间

注意

  • Scroll 只能有一个子组件
  • 内部用 Column 包含所有内容
  • Scroll 默认垂直滚动,无需显式指定方向

八、完整首页代码解读

最终首页 Index.ets 的核心结构如下:

typescript 复制代码
import router from '@ohos.router';

interface FishingSpot { /* ... */ }
interface RouteOpt { /* ... */ }
interface SpotParams { /* ... */ }

@Entry
@Component
struct Index {
  @State spots: FishingSpot[] = [ /* 数据 */ ];
  @State weather: 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)要求对象字面量必须有显式类型。解决方式:

typescript 复制代码
// ❌ 错误
let spot = { name: '月亮湾', rating: 4 };

// ✅ 正确
let spot: FishingSpot = { id: 1, name: '月亮湾', ... };

9.2 ForEach 的 key 生成

ForEach 的第三个参数是键值生成函数,用于高效更新:

typescript 复制代码
ForEach(arr,
  (item) => { /* 渲染 */ },
  (item) => item.id.toString()  // 唯一键
)

如果数据顺序不变但内容变化,使用索引作为key即可:

typescript 复制代码
(item, index) => index.toString()

9.3 资源引用路径

$r() 的路径格式:

  • $r('app.string.xxx') --- 应用级别字符串
  • $r('app.color.xxx') --- 颜色资源
  • $r('app.float.xxx') --- 尺寸资源
  • $r('media.xxx') --- 图片资源

十、效果展示

完成首页开发后,你应该能看到:

  1. ✅ 顶部标题栏显示"附近钓点"
  2. ✅ 天气卡片展示今日天气
  3. ✅ 3个钓点卡片,包含名称、位置、水深、评分和鱼种标签
  4. ✅ 底部导航四个Tab,点击可跳转
  5. ✅ 列表可滚动

总结

本篇我们完成了:

  • ✅ ArkTS组件化开发的核心概念
  • ✅ 首页布局的Column+Row层级设计
  • ✅ 天气卡片和钓点卡片的组件构建
  • ✅ 星级评分和标签徽章的自定义实现
  • ✅ 底部导航栏与路由跳转
  • ✅ 空状态和Scroll滚动处理

下一篇我们将继续开发 渔获记录、装备管理和个人中心 三个页面,深入讲解列表渲染、组件复用和状态管理的进阶技巧!


项目源码 :基于 HarmonyOS API 23 + Stage模型 + ArkTS

系列目录

  • 第一篇:项目初始化与环境配置
  • 第二篇:首页与钓点列表开发(本篇)
  • 第三篇:数据管理与多页面交互
  • 第四篇:复杂页面与交互体验
  • 第五篇:地图可视化与性能优化
相关推荐
浮芷.2 小时前
鸿蒙 6.1 新特性-60fps流畅人物跳跃功能算法深度解析-鸿蒙PC端正弦值计算法
算法·华为·harmonyos·鸿蒙·鸿蒙系统
金启攻2 小时前
【鸿蒙原生应用开发实战】第五篇:收藏管理与个人中心 — 收尾两个关键页面的完整实现
华为·harmonyos
烛衔溟2 小时前
HarmonyOS 页面生命周期与组件生命周期
华为·harmonyos
金启攻2 小时前
【鸿蒙原生应用开发实战】第三篇:列表页与标签筛选功能 — 打造高效的天体列表
华为·harmonyos
烛衔溟2 小时前
HarmonyOS 工程目录、配置文件与 Stage 模型核心
华为·harmonyos
祭曦念3 小时前
【共创季稿事节】鸿蒙原生 ArkTS 布局:NavRouter + NavDestination 导航布局实战
ubuntu·华为·harmonyos
木咺吟12 小时前
鸿蒙原生应用实战(一):从零搭建快递追踪App——项目初始化与工程架构详解
华为·harmonyos
坚果派·白晓明15 小时前
【鸿蒙PC】SDL3 移植:AtomCode Skills 4 步速通多媒体库适配
c++·华为·ai编程·harmonyos·atomcode·c/c++三方库
风满城3316 小时前
鸿蒙原生应用实战(三):设置与统计页面开发 — 数据驱动的功能模块
harmonyos