鸿蒙原生应用实战(二):首页与包裹列表开发——List组件、ForEach渲染与状态管理

鸿蒙原生应用实战(二):首页与包裹列表开发------List组件、ForEach渲染与状态管理

本文是系列第二篇,深入讲解快递追踪 App 首页的开发全过程,包括 List 列表渲染、@State 状态管理、条件渲染空状态、圆形状态标签以及页面路由导航等核心内容。


一、首页需求分析

首页是 App 的门面,我们的快递追踪 App 首页需要实现:

  1. 标题栏:显示"我的包裹"标题 + 右上角快捷操作图标(搜索、统计、公司管理)
  2. 包裹列表:展示所有包裹,每个卡片显示快递公司、单号、状态标签、备注、更新时间
  3. 状态区分:三种状态使用不同颜色标签------运输中(橙色)、已签收(绿色)、异常(红色)
  4. 空状态:无包裹时展示友好提示
  5. 点击跳转:点击卡片进入物流详情页
  6. 底部按钮:添加包裹按钮

二、数据结构设计

2.1 定义数据模型

在 ArkTS 中,我们使用 interface 定义数据结构:

typescript 复制代码
// Index.ets --- 数据接口定义
interface PackageItem {
  id: number;
  trackingNo: string;       // 快递单号
  company: string;           // 快递公司
  status: string;            // 状态码: transit | delivered | exception
  statusText: string;        // 状态文本: 运输中 | 已签收 | 异常
  note: string;              // 备注
  updateTime: string;        // 更新时间
  events: TrackEvent[];      // 物流事件列表
}

interface TrackEvent {
  time: string;
  desc: string;
  location: string;
}

2.2 为什么要把 status 和 statusText 分开?

status 是状态码(枚举值),用于逻辑判断和条件渲染;statusText 是展示文本。这样做的好处:

typescript 复制代码
// 通过 status 状态码决定颜色和文本,而不是写死
backgroundColor(
  item.status === 'transit' ? $r('app.color.status_transit') :
  item.status === 'delivered' ? $r('app.color.status_delivered') :
  $r('app.color.status_exception')
)

// 如果要国际化,只需替换状态文本,状态码不变

三、首页布局结构

3.1 整体框架

typescript 复制代码
@Entry
@Component
struct Index {
  @State packages: PackageItem[] = [ /* 模拟数据 */ ];

  build() {
    Column() {
      // 1. 标题栏 Row
      // 2. 包裹列表 List (或空状态)
      // 3. 添加按钮 Button
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.background'))
  }
}

三层布局:Column 作为垂直容器,从上到下排列标题栏、列表(可滚动)、底部按钮。

3.2 标题栏设计

typescript 复制代码
Row() {
  Text($r('app.string.title_home'))
    .fontSize($r('app.float.page_title_font_size'))
    .fontWeight(FontWeight.Bold)
    .fontColor($r('app.color.text_primary'))
  Blank()  // 弹性空间,将右侧图标推到右边
  Row() {
    Text('🔍').onClick(() => { /* 跳转搜索页 */ })
    Text('📊').onClick(() => { /* 跳转统计页 */ })
    Text('🏢').onClick(() => { /* 跳转公司管理页 */ })
  }
}
.width('100%')
.padding($r('app.float.padding_medium'))
.justifyContent(FlexAlign.SpaceBetween)

使用 Emoji 作为图标是一种快速原型的方式,生产环境建议替换为 SVG 图标组件。

3.3 空状态处理

typescript 复制代码
if (this.packages.length === 0) {
  Column() {
    Text('📦').fontSize(60)
    Text($r('app.string.no_packages'))
      .fontSize($r('app.float.body_font_size'))
      .fontColor($r('app.color.text_hint'))
      .margin({ top: 16 })
  }
  .width('100%')
  .height('80%')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
} else {
  // 列表渲染...
}

设计要点

  • 空状态居中显示,不干扰其他布局
  • 使用大号 Emoji + 提示文案,视觉友好
  • height('80%') 占满空间但给底部按钮留位置

四、List 列表组件详解

4.1 基础结构

typescript 复制代码
List() {
  ForEach(this.packages, (item: PackageItem) => {
    ListItem() {
      // 卡片内容
    }
    .onClick(() => { /* 跳转详情 */ })
  }, (item: PackageItem) => item.id.toString())
}
.layoutWeight(1)  // 列表占据剩余空间

4.2 ForEach 的第三个参数:keyGenerator

ForEach(arr, itemGenerator, keyGenerator) 的第三个参数非常关键:

typescript 复制代码
ForEach(this.packages, 
  (item: PackageItem) => { /* 构建 UI */ },
  (item: PackageItem) => item.id.toString()  // ← key 生成器
)
  • 提供稳定的 key,ArkTS 才能高效 diff 更新列表
  • key 必须唯一且稳定,不推荐使用索引
  • 没有 key 或 key 不稳定会导致列表闪烁或性能问题

4.3 卡片布局设计

每个包裹卡片是一个 Column 嵌套 Row

typescript 复制代码
ListItem() {
  Column() {
    // 第一行:公司名 + 状态标签
    Row() {
      Column() {
        Text(item.company)                      // 公司名
        Text(item.trackingNo)                   // 单号
      }
      .alignItems(HorizontalAlign.Start)

      Blank()

      // 状态标签 --- 圆角矩形背景
      Text(item.statusText)
        .fontSize($r('app.float.badge_font_size'))
        .fontColor(Color.White)
        .backgroundColor(
          item.status === 'transit' ? $r('app.color.status_transit') :
          item.status === 'delivered' ? $r('app.color.status_delivered') :
          $r('app.color.status_exception')
        )
        .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .borderRadius(12)
    }
    .width('100%')

    // 条件渲染:备注
    if (item.note.length > 0) {
      Text(item.note)
    }

    // 更新时间
    Text('更新: ' + item.updateTime)
  }
  .width('100%')
  .padding($r('app.float.padding_medium'))
  .backgroundColor($r('app.color.card_bg'))
  .borderRadius($r('app.float.card_corner_radius'))
}

4.4 状态标签颜色映射

三种状态三种颜色,代码复用:

状态码 状态文本 颜色 色值
transit 运输中 橙色 #FFFF8C00
delivered 已签收 绿色 #FF4CAF50
exception 异常 红色 #FFF44336

通过三元运算符链实现条件颜色,代码简洁但要注意可读性。


五、@State 状态管理详解

5.1 @State 的作用

@State 是 ArkTS 中最核心的装饰器,标记的变量变化时会自动触发 UI 重渲染

typescript 复制代码
@Component
struct Index {
  @State packages: PackageItem[] = [ /* ... */ ];
  // 当 packages 的内容变化时,UI 自动更新
}

注意@State 监听的是引用变化,对于数组:

  • this.packages = newArray → 触发更新(赋值新数组)
  • this.packages.push(newItem) → 触发更新(数组变异方法)
  • this.packages[0].note = 'xxx'不触发更新(直接修改数组元素属性)

5.2 Array 操作的响应性

操作方式 是否触发 UI 更新 说明
this.packages = [] ✅ 触发 赋值新数组
this.packages.push(x) ✅ 触发 数组变异方法
this.packages.splice(i,1) ✅ 触发 数组变异方法
this.packages[i] = x ❌ 不触发 直接索引赋值
this.packages[i].note = x ❌ 不触发 修改嵌套属性

解决方案:修改嵌套属性时使用对象展开创建新对象:

typescript 复制代码
// 正确方式:创建新对象替换
let newItem = { ...this.packages[i], note: '新备注' };
let newArr = [...this.packages];
newArr[i] = newItem;
this.packages = newArr;

六、页面路由跳转

6.1 定义 RouteOpt 接口

typescript 复制代码
interface RouteOpt {
  url: string;
  params?: Object;
}

6.2 无参数跳转

typescript 复制代码
// 底部按钮 --- 跳转添加页
Button($r('app.string.btn_save'))
  .onClick(() => {
    let opt: RouteOpt = { url: 'pages/AddPackagePage' };
    router.pushUrl(opt);
  })

6.3 带参数跳转

typescript 复制代码
// 点击卡片 --- 跳转详情页,携带包裹数据
ListItem() {
  // ...
}
.onClick(() => {
  let params = { packageData: item };  // 参数
  let opt: RouteOpt = { 
    url: 'pages/TrackDetailPage', 
    params: params 
  };
  router.pushUrl(opt);
})

6.4 接收参数(在详情页)

typescript 复制代码
// TrackDetailPage.ets
aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['packageData']) {
    this.packageData = params['packageData'] as PackageItem;
  }
}

重要安全措施

  • 使用 as Record<string, Object> 断言
  • params 判空
  • params['packageData'] 判空

这样即使在跳转时忘记传参,页面也不会崩溃,只会显示默认空数据。


七、实战技巧与避坑

7.1 响应式布局设计

使用 layoutWeight(1) 让列表占满剩余空间,避免底部按钮被挤出屏幕:

typescript 复制代码
List() { /* ... */ }
  .width('100%')
  .layoutWeight(1)  // ← 关键:占据 Column 中的剩余空间

// 底部按钮
Button('添加包裹')
  .margin({ bottom: 16 })  // ← 底部安全间距

7.2 Card 设计规范

  • 背景色 :白色 #FFFFFF(card_bg)
  • 圆角:12vp(card_corner_radius)
  • 内边距:16vp(padding_medium)
  • 列表项间距:通过 ListItem 的 padding 控制(上下 6vp)
  • 阴影:鸿蒙的 Card 组件自带阴影效果

7.3 内联函数优化

图标点击的跳转逻辑可以内联写入,减少函数定义:

typescript 复制代码
// 不推荐(额外函数)
searchClick() { router.pushUrl({ url: 'pages/SearchPage' }); }
Text('🔍').onClick(() => this.searchClick())

// 推荐(直接内联)
Text('🔍').onClick(() => {
  let opt: RouteOpt = { url: 'pages/SearchPage' };
  router.pushUrl(opt);
})

7.4 $r() 资源引用的优势

使用 $r('app.float.xxx') 而非硬编码尺寸:

typescript 复制代码
// ✅ 使用资源引用,统一管理
.fontSize($r('app.float.body_font_size'))
.padding($r('app.float.padding_medium'))

// ❌ 硬编码,不利于维护
.fontSize(16)
.padding(16)

这样当需要调整全局字体或间距时,只需修改 float.json 一处即可。


八、完整首页代码结构

typescript 复制代码
// pages/Index.ets --- 完整结构
import router from '@ohos.router';

interface RouteOpt { url: string; params?: Object; }
interface PackageItem { /* ... */ }
interface TrackEvent { /* ... */ }

@Entry
@Component
struct Index {
  @State packages: PackageItem[] = [ /* 3条模拟数据 */ ];

  build() {
    Column() {
      // 1. 标题栏 Row
      // 2. 空状态 / 包裹列表 List
      // 3. 添加按钮 Button
    }
    .width('100%').height('100%')
    .backgroundColor($r('app.color.background'))
  }
}

九、小结

本篇我们完成了首页的全部开发,核心要点:

  1. List + ForEach:列表渲染的标准范式,keyGenerator 的重要性
  2. @State 状态管理:响应式数据驱动 UI,数组操作的响应性限制
  3. 条件渲染:空状态 vs 列表的切换
  4. 路由跳转router.pushUrl 带参数跳转与接收
  5. 三种状态标签:通过状态码动态渲染颜色
  6. 资源引用$r() 统一管理字体、颜色、尺寸

下一篇将进入 表单交互与搜索筛选,涵盖添加包裹表单验证、搜索页面多条件筛选、快递公司管理等实战功能。


系列索引

  • 第一篇:项目初始化与工程架构
  • 第二篇:首页与列表开发实战(本文)
  • 第三篇:表单交互与搜索筛选
  • 第四篇:物流时间线与历史记录
  • 第五篇:数据统计与个人中心
相关推荐
风华圆舞1 小时前
鸿蒙 MICROPHONE 权限在 Flutter 项目里怎么处理
flutter·华为·harmonyos
xcLeigh1 小时前
鸿蒙平台 NixNote2 富文本笔记应用适配实战:从 Linux 到 鸿蒙PC 的 Electron 迁移
linux·笔记·harmonyos·富文本·nixnote2·evernote
伶俜661 小时前
鸿蒙原生应用实战(一):从零开发一个短视频编辑器 App
编辑器·音视频·harmonyos
伶俜662 小时前
鸿蒙原生应用实战(十)ArkUI 涂鸦画板:Canvas 绘图 + 颜色选择 + 笔画管理 + 导出
华为·harmonyos
祭曦念2 小时前
【共创季稿事节】鸿蒙MediaQueryListener布局实战
华为·harmonyos·媒体
不羁的木木2 小时前
《HarmonyOS 6.1 新能力实战之智感握姿》第五篇:综合实战——打造自适应阅读器
华为·harmonyos
金启攻2 小时前
鸿蒙原生应用开发实战(三):数据管理与多页面交互——渔获记录、装备管理与个人中心
harmonyos
伶俜662 小时前
鸿蒙原生应用实战(九)ArkUI 天气预报 App:HTTP 请求 + 定位 + 动效
http·华为·harmonyos