自动化工具者快速学习鸿蒙开发第三部份 组件部份

鸿蒙物联网应用开发快速入门

适用场景:课程讲义 / CSDN 博客 / 新手教学  元歌 出品,必是精品
技术栈:HarmonyOS + ArkTS(声明式 UI)
目标:从零开始掌握鸿蒙物联网 APP 的开发核心技能


目录

  1. 开发环境准备

  2. 第一个鸿蒙页面:Hello IoT

  3. Flex 弹性布局:构建工业监控面板

  4. Grid 网格布局:多设备数据卡片

  5. 组件封装:可复用的仪表盘组件

  6. 页面路由与传参

  7. 数据驱动:网络请求与生命周期

  8. 全局状态管理:@AppStorage

  9. 实战:工业传感器数据展示页面

  10. 课程总结


一、开发环境准备

1.1 这是什么

鸿蒙物联网应用开发基于 HarmonyOS NEXT 系统,使用 ArkTS 语言(TypeScript 的超集)和声明式 UI 开发范式。开发者需要安装官方 IDE —— DevEco Studio

1.2 怎么实现

  1. 下载并安装 DevEco Studio(官网:https://developer.huawei.com/consumer/cn/deveco-studio/)

  2. 配置 HarmonyOS SDK 和模拟器

  3. 创建一个新项目,选择 Empty Ability 模板

  4. 项目结构核心目录说明:

entry/src/main/ets/
├── entryability/          # 入口 Ability(应用生命周期)
├── pages/                 # 页面目录
│   └── Index.ets          # 主页面
└── components/            # 自定义组件(自己创建)
resources/
└── base/element/          # 颜色、字体、尺寸等样式资源

1.3 技术细节

  • ArkTS 是鸿蒙官方推荐的开发语言,它在 TypeScript 基础上增加了严格的类型约束和运行时检查,提升应用性能。

  • 声明式 UI:开发者只需描述"界面应该长什么样",系统会自动处理渲染和更新,类似 Flutter/React。

  • 每个 .ets 文件都是一个独立的模块,通过 @Entry@Component 装饰器定义页面和组件。

小贴士:如果你已有 TypeScript / Vue3 / Flutter 基础,上手 ArkTS 会非常轻松。


二、第一个鸿蒙页面:Hello IoT

2.1 这是什么

本示例展示鸿蒙应用最基础的页面结构,包括文本显示、图片加载和按钮点击。这是每一个鸿蒙开发者写的第一段代码,类似于 "Hello World"。

2.2 怎么实现

entry/src/main/ets/pages/Index.ets 中编写以下代码:

// entry/src/main/ets/pages/Index.ets

@Entry              // 标记这是一个页面入口
@Component           // 标记这是一个 UI 组件
struct Index {
  // 页面状态变量:按钮点击次数
  @State clickCount: number = 0

  build() {
    // Column:垂直方向排列子组件
    Column({ space: 20 }) {
      // 标题文本
      Text('鸿蒙物联网应用')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1a1a1a')

      // 副标题
      Text('Hello HarmonyOS IoT')
        .fontSize(16)
        .fontColor('#666666')

      // 显示点击次数
      Text(`按钮被点击了 ${this.clickCount} 次`)
        .fontSize(20)
        .fontColor('#007DFF')

      // 按钮
      Button('点击我')
        .width(200)
        .height(50)
        .fontSize(18)
        .backgroundColor('#007DFF')
        .onClick(() => {
          // 点击时状态变量 +1,UI 会自动重新渲染
          this.clickCount++
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)  // 垂直居中
    .alignItems(HorizontalAlign.Center) // 水平居中
    .backgroundColor('#F1F3F5')
  }
}

2.3 技术细节

关键概念

说明

@Entry

标记该组件为页面入口,每个页面必须有且仅有一个

@Component

标记该结构体为UI 组件,鸿蒙中所有可见界面元素都是组件

@State

状态装饰器。被 @State 修饰的变量发生变化时,系统会自动重新渲染依赖该变量的 UI 部分

build()

组件的 UI 构建函数,所有界面描述都写在这里面

Column

垂直布局容器,space 属性设置子组件之间的间距

FlexAlign.Center

在主轴(垂直方向)上居中对齐

HorizontalAlign.Center

在交叉轴(水平方向)上居中对齐

核心原理:声明式 UI 的核心是数据驱动界面。你只需修改数据(如 clickCount),界面会自动更新,无需手动操作 DOM。


三、Flex 弹性布局:构建工业监控面板

3.1 这是什么

在物联网场景中,经常需要在同一行展示多个传感器数据(如温度、湿度、压力)。Flex 布局可以灵活地控制子组件在主轴和交叉轴上的排列方式,非常适合构建这种横向排列的监控面板。

3.2 怎么实现

// entry/src/main/ets/pages/MonitorPanel.ets

@Entry
@Component
struct MonitorPanel {
  // 模拟传感器数据
  @State temperature: number = 26.5
  @State humidity: number = 62
  @State pressure: number = 101.3

  build() {
    Column({ space: 16 }) {
      // 页面标题
      Text('工业传感器监控面板')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 20 })

      // ========== 第一行:三个传感器卡片 ==========
      Row({ space: 12 }) {
        // 温度卡片
        SensorCard({
          title: '温度',
          value: this.temperature,
          unit: '°C',
          icon: '🌡️',
          color: '#FF6B6B'
        })

        // 湿度卡片
        SensorCard({
          title: '湿度',
          value: this.humidity,
          unit: '%',
          icon: '💧',
          color: '#4ECDC4'
        })

        // 气压卡片
        SensorCard({
          title: '气压',
          value: this.pressure,
          unit: 'kPa',
          icon: '📊',
          color: '#45B7D1'
        })
      }
      .width('100%')
      .padding(16)
      .justifyContent(FlexAlign.SpaceEvenly)  // 子组件均匀分布

      // ========== 模拟数据刷新按钮 ==========
      Button('刷新传感器数据')
        .width(200)
        .height(48)
        .backgroundColor('#007DFF')
        .margin({ top: 30 })
        .onClick(() => {
          // 模拟从硬件读取新数据
          this.temperature = 20 + Math.random() * 15
          this.humidity = 40 + Math.random() * 40
          this.pressure = 100 + Math.random() * 5
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

// ========== 自定义传感器卡片组件 ==========
@Component
struct SensorCard {
  // 接收父组件传入的参数
  @Prop title: string
  @Prop value: number
  @Prop unit: string
  @Prop icon: string
  @Prop color: string

  build() {
    Column({ space: 8 }) {
      Text(this.icon)
        .fontSize(36)

      Text(this.title)
        .fontSize(14)
        .fontColor('#666666')

      Text(`${this.value.toFixed(1)} ${this.unit}`)
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.color)
    }
    .width('30%')
    .padding(16)
    .borderRadius(12)
    .backgroundColor(Color.White)
    .shadow({
      radius: 8,
      color: 'rgba(0, 0, 0, 0.08)',
      offsetY: 4
    })
  }
}

3.3 技术细节

关键概念

说明

Row

水平布局容器,子组件从左到右排列

FlexAlign.SpaceEvenly

子组件在主轴上均匀分布,两端和中间间距相等

FlexAlign.SpaceBetween

首尾贴边,中间均匀分布

FlexAlign.Start / Center / End

子组件靠起点 / 居中 / 靠终点排列

@Prop

父组件向子组件单向传递数据。子组件可以读取但不能修改(修改会报错)

.toFixed(1)

JavaScript 方法,保留 1 位小数

.shadow()

给组件添加阴影,提升视觉层次感

工业场景提示:实际项目中,这些传感器数据通常通过串口、蓝牙或 WiFi 从 STM32 / PLC 等硬件设备实时读取,而不是用随机数模拟。


四、Grid 网格布局:多设备数据卡片

4.1 这是什么

当需要展示大量同类数据时(如一个工厂有 20 台设备,每台设备显示温度、状态、运行时间),使用 Grid 网格布局可以自动换行排列,比手动写多个 Row 更加简洁优雅。

4.2 怎么实现

// entry/src/main/ets/pages/DeviceGrid.ets

// 定义设备数据模型(接口)
interface DeviceInfo {
  id: number
  name: string
  temperature: number
  status: 'running' | 'stopped' | 'alarm'  // 联合类型
  runtime: number  // 运行小时数
}

@Entry
@Component
struct DeviceGrid {
  // 设备列表数据
  @State deviceList: DeviceInfo[] = [
    { id: 1, name: '1号机床', temperature: 45.2, status: 'running', runtime: 128 },
    { id: 2, name: '2号机床', temperature: 38.5, status: 'running', runtime: 96 },
    { id: 3, name: '3号机床', temperature: 72.0, status: 'alarm', runtime: 200 },
    { id: 4, name: '4号机床', temperature: 25.0, status: 'stopped', runtime: 0 },
    { id: 5, name: '5号机床', temperature: 41.3, status: 'running', runtime: 156 },
    { id: 6, name: '6号机床', temperature: 39.8, status: 'running', runtime: 88 },
  ]

  // 根据状态返回对应的颜色
  getStatusColor(status: string): string {
    switch (status) {
      case 'running': return '#52C41A'  // 绿色 - 运行中
      case 'stopped': return '#999999'  // 灰色 - 已停止
      case 'alarm':   return '#FF4D4F'  // 红色 - 报警
      default:        return '#999999'
    }
  }

  // 根据状态返回中文描述
  getStatusText(status: string): string {
    switch (status) {
      case 'running': return '运行中'
      case 'stopped': return '已停止'
      case 'alarm':   return '报警'
      default:        return '未知'
    }
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('车间设备监控')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)

        Text(`共 ${this.deviceList.length} 台设备`)
          .fontSize(14)
          .fontColor('#999999')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .padding(16)

      // Grid 网格布局:2 列,自适应
      Grid() {
        // ForEach 循环渲染每个设备卡片
        ForEach(this.deviceList, (device: DeviceInfo) => {
          GridItem() {
            DeviceCard({
              name: device.name,
              temperature: device.temperature,
              statusColor: this.getStatusColor(device.status),
              statusText: this.getStatusText(device.status),
              runtime: device.runtime
            })
          }
        })
      }
      .columnsTemplate('1fr 1fr')     // 两列,等宽
      .columnsGap(12)                // 列间距
      .rowsGap(12)                   // 行间距
      .padding(16)
      .layoutDirection(GridDirection.Row)  // 按行填充

    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

// ========== 设备卡片组件 ==========
@Component
struct DeviceCard {
  @Prop name: string
  @Prop temperature: number
  @Prop statusColor: string
  @Prop statusText: string
  @Prop runtime: number

  build() {
    Column({ space: 10 }) {
      // 设备名称 + 状态点
      Row() {
        Text(this.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)

        Row({ space: 6 }) {
          // 状态指示圆点
          Circle({ width: 10, height: 10 })
            .fill(this.statusColor)

          Text(this.statusText)
            .fontSize(12)
            .fontColor(this.statusColor)
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      // 温度
      Row({ space: 4 }) {
        Text('温度:')
          .fontSize(13)
          .fontColor('#666666')
        Text(`${this.temperature.toFixed(1)} °C`)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
      }
      .width('100%')

      // 运行时长
      Row({ space: 4 }) {
        Text('运行:')
          .fontSize(13)
          .fontColor('#666666')
        Text(`${this.runtime} 小时`)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
      }
      .width('100%')
    }
    .width('100%')
    .padding(14)
    .borderRadius(10)
    .backgroundColor(Color.White)
    .shadow({ radius: 6, color: 'rgba(0,0,0,0.06)', offsetY: 3 })
  }
}

4.3 技术细节

关键概念

说明

Grid()

网格布局容器,可以设置行列模板

columnsTemplate('1fr 1fr')

定义 2 列,每列占 1fr(等比例分配剩余空间)

columnsTemplate('1fr 2fr 1fr')

三列,比例为 1:2:1

ForEach

循环遍历数组,为每个元素渲染 UI。必须提供唯一 key,这里默认用数组索引

GridItem()

Grid 中的每个单元格包裹器

interface

ArkTS 中定义数据结构的接口,鸿蒙强烈推荐使用强类型

联合类型 'running' | 'stopped' | 'alarm'

限制变量只能取这三个值之一,提升代码健壮性

布局技巧1fr 是 CSS Grid 的单位,表示"一份剩余空间"。鸿蒙 Grid 布局借鉴了 Web CSS Grid 的设计,有前端基础的同学会倍感亲切。


五、组件封装:可复用的仪表盘组件

5.1 这是什么

在工业物联网 APP 中,**仪表盘(Gauge)**是一种常见的数据可视化方式,用于直观展示某个数值在量程中的位置。本示例封装一个可复用的圆形仪表盘组件,支持传入量程、数值、颜色等参数。

5.2 怎么实现

// entry/src/main/ets/components/GaugeComponent.ets

@Component
export struct GaugeComponent {
  // ===== 可配置属性(父组件传入)=====
  @Prop value: number        // 当前数值
  @Prop min: number = 0      // 最小值(默认0)
  @Prop max: number = 100    // 最大值(默认100)
  @Prop title: string = ''   // 仪表盘标题
  @Prop unit: string = ''    // 单位
  @Prop primaryColor: string = '#007DFF'  // 主色调

  // 计算当前值在量程中的百分比(0~1)
  getPercentage(): number {
    let p = (this.value - this.min) / (this.max - this.min)
    if (p < 0) p = 0
    if (p > 1) p = 1
    return p
  }

  // 根据百分比决定颜色(绿->黄->红)
  getColorByValue(): string {
    const p = this.getPercentage()
    if (p < 0.5) return '#52C41A'   // 安全 - 绿色
    if (p < 0.8) return '#FAAD14'   // 警告 - 黄色
    return '#FF4D4F'                // 危险 - 红色
  }

  build() {
    Column({ space: 8 }) {
      // 标题
      if (this.title !== '') {
        Text(this.title)
          .fontSize(14)
          .fontColor('#666666')
      }

      // 仪表盘圆形(用 Stack 叠加实现)
      Stack() {
        // 底层:灰色背景圆环
        Circle({ width: 140, height: 140 })
          .fill('transparent')
          .stroke('#E8E8E8')
          .strokeWidth(12)

        // 上层:进度圆环(通过设置 dashArray 实现弧形)
        Circle({ width: 140, height: 140 })
          .fill('transparent')
          .stroke(this.getColorByValue())
          .strokeWidth(12)
          .strokeLineCap(LineCapStyle.Round)
          // dashArray: [实线长度, 虚线间隔]
          // 周长 ≈ 440,这里用 0~75% 的圆弧表示量程
          .strokeDashArray([
            440 * this.getPercentage() * 0.75,
            440
          ])
          .rotate({ angle: 135 })  // 旋转到从底部开始

        // 中心数值
        Column({ space: 2 }) {
          Text(`${this.value.toFixed(1)}`)
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1a1a1a')

          if (this.unit !== '') {
            Text(this.unit)
              .fontSize(12)
              .fontColor('#999999')
          }
        }
      }
      .width(160)
      .height(160)

      // 量程提示
      Row() {
        Text(`${this.min}`)
          .fontSize(11)
          .fontColor('#BBBBBB')

        Text(`${this.max}`)
          .fontSize(11)
          .fontColor('#BBBBBB')
      }
      .width(120)
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetY: 4 })
  }
}

在页面中使用封装好的仪表盘组件:

// entry/src/main/ets/pages/GaugeDemo.ets

import { GaugeComponent } from '../components/GaugeComponent'

@Entry
@Component
struct GaugeDemo {
  @State motorSpeed: number = 1450    // 电机转速 (rpm)
  @State voltage: number = 24.5       // 电压 (V)
  @State current: number = 8.2        // 电流 (A)

  build() {
    Column({ space: 20 }) {
      Text('工业仪表盘监控')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 20 })

      // 三个仪表盘横向排列
      Row({ space: 16 }) {
        // 电机转速仪表盘:量程 0~2000 rpm
        GaugeComponent({
          title: '电机转速',
          value: this.motorSpeed,
          min: 0,
          max: 2000,
          unit: 'rpm'
        })

        // 电压仪表盘:量程 0~30V
        GaugeComponent({
          title: '电压',
          value: this.voltage,
          min: 0,
          max: 30,
          unit: 'V'
        })

        // 电流仪表盘:量程 0~20A
        GaugeComponent({
          title: '电流',
          value: this.current,
          min: 0,
          max: 20,
          unit: 'A'
        })
      }
      .padding(16)

      // 模拟数据变化
      Button('模拟数据波动')
        .width(200)
        .height(48)
        .backgroundColor('#007DFF')
        .onClick(() => {
          this.motorSpeed = 1000 + Math.random() * 1000
          this.voltage = 20 + Math.random() * 10
          this.current = Math.random() * 15
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

5.3 技术细节

关键概念

说明

export struct

使用 export 导出组件,其他文件才能通过 import 引用

Stack()

层叠布局,子组件按顺序叠加显示,后写的在上面

Circle

圆形组件,可设置填充色、描边、描边宽度等

strokeDashArray

描边虚线模式,[实线长度, 间隙长度],是实现弧形进度的核心技巧

LineCapStyle.Round

描边端点样式为圆角,让进度条末端更美观

.rotate({ angle: 135 })

将圆环旋转 135 度,让开口朝下(类似汽车仪表盘)

组件封装原则:把通用的 UI 逻辑封装成独立组件,通过 @Prop 接收外部参数,提高代码复用率,减少重复代码。


六、页面路由与传参

6.1 这是什么

一个完整的物联网 APP 通常包含多个页面:设备列表页 → 设备详情页 → 历史数据页 → 设置页。页面路由负责管理页面之间的跳转和数据传递。鸿蒙提供了 router 模块来实现这一功能。

6.2 怎么实现

// entry/src/main/ets/pages/DeviceList.ets

import router from '@ohos.router'

interface DeviceItem {
  id: number
  name: string
  type: string
}

@Entry
@Component
struct DeviceList {
  @State devices: DeviceItem[] = [
    { id: 1, name: '1号温湿度传感器', type: 'TH_SENSOR' },
    { id: 2, name: '2号压力传感器', type: 'PRESSURE_SENSOR' },
    { id: 3, name: '3号电流传感器', type: 'CURRENT_SENSOR' },
  ]

  // 跳转到设备详情页
  goToDetail(device: DeviceItem) {
    router.pushUrl({
      url: 'pages/DeviceDetail',   // 目标页面路径(不带 .ets 后缀)
      params: {                    // 传递给目标页面的参数
        deviceId: device.id,
        deviceName: device.name,
        deviceType: device.type
      }
    })
  }

  build() {
    Column() {
      Text('设备列表')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin(16)

      List({ space: 12 }) {
        ForEach(this.devices, (device: DeviceItem) => {
          ListItem() {
            Row() {
              Column({ space: 4 }) {
                Text(device.name)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)

                Text(`类型: ${device.type}`)
                  .fontSize(12)
                  .fontColor('#999999')
              }
              .alignItems(HorizontalAlign.Start)

              Text('详情 >')
                .fontSize(14)
                .fontColor('#007DFF')
            }
            .width('100%')
            .padding(16)
            .backgroundColor(Color.White)
            .borderRadius(10)
            .justifyContent(FlexAlign.SpaceBetween)
          }
          .onClick(() => this.goToDetail(device))
        })
      }
      .padding(16)
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}
// entry/src/main/ets/pages/DeviceDetail.ets

import router from '@ohos.router'

@Entry
@Component
struct DeviceDetail {
  // 从路由参数中获取设备信息
  @State deviceId: number = 0
  @State deviceName: string = ''
  @State deviceType: string = ''

  // 页面即将出现时,解析路由参数
  aboutToAppear() {
    // 获取路由传递的参数
    const params = router.getParams() as Record<string, object>
    if (params) {
      this.deviceId = params['deviceId'] as number
      this.deviceName = params['deviceName'] as string
      this.deviceType = params['deviceType'] as string
    }
  }

  // 返回上一页
  goBack() {
    router.back()
  }

  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Button('< 返回')
          .fontSize(14)
          .fontColor('#007DFF')
          .backgroundColor('transparent')
          .onClick(() => this.goBack())

        Text('设备详情')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)

        Blank()  // 占位,让标题居中
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .backgroundColor(Color.White)

      // 设备信息卡片
      Column({ space: 12 }) {
        InfoRow({ label: '设备编号', value: `${this.deviceId}` })
        InfoRow({ label: '设备名称', value: this.deviceName })
        InfoRow({ label: '设备类型', value: this.deviceType })
        InfoRow({ label: '连接状态', value: '在线 ✅' })
        InfoRow({ label: '最后上报', value: '2025-07-31 14:32:18' })
      }
      .width('100%')
      .padding(16)
      .margin(16)
      .backgroundColor(Color.White)
      .borderRadius(12)

    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

// 信息行组件
@Component
struct InfoRow {
  @Prop label: string
  @Prop value: string

  build() {
    Row() {
      Text(this.label)
        .fontSize(14)
        .fontColor('#666666')
        .width(100)

      Text(this.value)
        .fontSize(14)
        .fontColor('#1a1a1a')
        .layoutWeight(1)
    }
    .width('100%')
    .height(40)
  }
}

同时需要在 main_pages.json 中注册新页面:

// entry/src/main/resources/base/profile/main_pages.json
{
  "src": [
    "pages/Index",
    "pages/MonitorPanel",
    "pages/DeviceGrid",
    "pages/GaugeDemo",
    "pages/DeviceList",
    "pages/DeviceDetail"
  ]
}

6.3 技术细节

API

说明

router.pushUrl({ url, params })

跳转到新页面,当前页面保留在页面栈中

router.replaceUrl({ url, params })

替换当前页面,不保留在栈中(适用于登录页跳转首页)

router.back()

返回上一页

router.getParams()

在目标页面获取传入的参数

aboutToAppear()

页面即将显示时的生命周期回调,适合在这里初始化数据、解析参数

Blank()

空白占位组件,自动填充剩余空间,常用于让标题居中

页面栈管理:鸿蒙使用页面栈管理导航历史。pushUrl 会把新页面压入栈,back 会弹出栈顶页面。栈深度不宜过深,避免内存占用过高。


七、数据驱动:网络请求与生命周期

7.1 这是什么

物联网 APP 的核心价值在于连接硬件、展示数据。本节演示如何使用鸿蒙的 http 模块从后端接口获取传感器数据,并在合适的生命周期中管理数据的初始化和资源的释放。

7.2 怎么实现

// entry/src/main/ets/utils/HttpUtil.ets

import http from '@ohos.net.http'

// 封装 HTTP 请求工具类
export class HttpUtil {
  private static baseUrl: string = 'https://your-iot-api.example.com'

  // GET 请求封装
  static async get<T>(path: string): Promise<T> {
    return new Promise((resolve, reject) => {
      const httpRequest = http.createHttp()

      httpRequest.request(
        `${this.baseUrl}${path}`,
        {
          method: http.RequestMethod.GET,
          header: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer YOUR_TOKEN_HERE'
          },
          connectTimeout: 10000,   // 连接超时 10 秒
          readTimeout: 10000       // 读取超时 10 秒
        },
        (err, data) => {
          if (!err && data.responseCode === 200) {
            // 解析 JSON 响应
            const result = JSON.parse(data.result.toString()) as T
            resolve(result)
          } else {
            reject(err || new Error(`HTTP ${data?.responseCode}`))
          }
          // 销毁请求对象,释放资源
          httpRequest.destroy()
        }
      )
    })
  }

  // POST 请求封装
  static async post<T>(path: string, body: object): Promise<T> {
    return new Promise((resolve, reject) => {
      const httpRequest = http.createHttp()

      httpRequest.request(
        `${this.baseUrl}${path}`,
        {
          method: http.RequestMethod.POST,
          header: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer YOUR_TOKEN_HERE'
          },
          extraData: JSON.stringify(body),
          connectTimeout: 10000,
          readTimeout: 10000
        },
        (err, data) => {
          if (!err && data.responseCode === 200) {
            const result = JSON.parse(data.result.toString()) as T
            resolve(result)
          } else {
            reject(err || new Error(`HTTP ${data?.responseCode}`))
          }
          httpRequest.destroy()
        }
      )
    })
  }
}
// entry/src/main/ets/pages/NetworkDemo.ets

import { HttpUtil } from '../utils/HttpUtil'

// 定义后端返回的数据结构
interface SensorData {
  deviceId: number
  temperature: number
  humidity: number
  pressure: number
  timestamp: string
}

interface ApiResponse<T> {
  code: number
  message: string
  data: T
}

@Entry
@Component
struct NetworkDemo {
  @State isLoading: boolean = false       // 加载状态
  @State errorMessage: string = ''        // 错误信息
  @State sensorData: SensorData | null = null  // 传感器数据

  // 页面即将显示时,自动获取数据
  aboutToAppear() {
    this.fetchSensorData()
  }

  // 页面隐藏时,可以在这里停止定时器
  onPageHide() {
    console.info('页面隐藏,可以在这里停止轮询定时器')
  }

  // 获取传感器数据
  async fetchSensorData() {
    this.isLoading = true
    this.errorMessage = ''

    try {
      // 调用封装好的 HTTP 工具
      const response = await HttpUtil.get<ApiResponse<SensorData>>('/api/sensor/latest')

      if (response.code === 200) {
        this.sensorData = response.data
      } else {
        this.errorMessage = response.message || '获取数据失败'
      }
    } catch (error) {
      this.errorMessage = `请求错误: ${JSON.stringify(error)}`
    } finally {
      this.isLoading = false
    }
  }

  build() {
    Column({ space: 16 }) {
      Text('网络数据获取示例')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40 })

      // 加载中提示
      if (this.isLoading) {
        LoadingProgress()
          .width(50)
          .height(50)
          .color('#007DFF')

        Text('正在获取传感器数据...')
          .fontSize(14)
          .fontColor('#999999')
      }

      // 错误提示
      if (this.errorMessage !== '') {
        Text(this.errorMessage)
          .fontSize(14)
          .fontColor('#FF4D4F')
          .padding(16)
          .backgroundColor('#FFF1F0')
          .borderRadius(8)
      }

      // 数据展示
      if (this.sensorData !== null) {
        Column({ space: 12 }) {
          DataCard({
            label: '温度',
            value: `${this.sensorData.temperature} °C`,
            color: '#FF6B6B'
          })
          DataCard({
            label: '湿度',
            value: `${this.sensorData.humidity} %`,
            color: '#4ECDC4'
          })
          DataCard({
            label: '气压',
            value: `${this.sensorData.pressure} kPa`,
            color: '#45B7D1'
          })
          DataCard({
            label: '更新时间',
            value: this.sensorData.timestamp,
            color: '#666666'
          })
        }
        .width('100%')
        .padding(16)
      }

      // 刷新按钮
      Button('刷新数据')
        .width(200)
        .height(48)
        .backgroundColor('#007DFF')
        .enabled(!this.isLoading)  // 加载中禁用按钮
        .onClick(() => {
          this.fetchSensorData()
        })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#F1F3F5')
  }
}

// 数据卡片组件
@Component
struct DataCard {
  @Prop label: string
  @Prop value: string
  @Prop color: string

  build() {
    Row() {
      Text(this.label)
        .fontSize(14)
        .fontColor('#666666')
        .width(80)

      Text(this.value)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.color)
        .layoutWeight(1)
    }
    .width('100%')
    .height(50)
    .padding({ left: 16, right: 16 })
    .backgroundColor(Color.White)
    .borderRadius(8)
  }
}

7.3 技术细节

关键概念

说明

http.createHttp()

创建 HTTP 请求对象,每个请求应独立创建,用完调用 destroy() 释放

RequestMethod.GET / POST

HTTP 请求方法枚举

Promise + async/await

异步编程模式,让网络请求代码看起来像同步写法,更易读

aboutToAppear()

页面即将出现时调用,适合在这里发起初始化请求

onPageHide()

页面隐藏时调用,适合在这里清理定时器、停止轮询、释放资源

LoadingProgress()

鸿蒙内置的加载动画组件

.enabled()

控制组件是否可交互,false 时按钮变灰且不可点击

工业场景最佳实践:物联网数据通常需要定时轮询(如每 5 秒刷新一次)。可以使用 setIntervalaboutToAppear() 中启动定时器,在 onPageHide() 中用 clearInterval 停止,避免后台持续消耗资源。


八、全局状态管理:@AppStorage

8.1 这是什么

在多页面的物联网 APP 中,有些数据需要在整个应用范围内共享,例如:

  • 用户的登录信息(用户名、Token)

  • 全局配置(服务器地址、主题色)

  • 设备连接状态

鸿蒙提供了 @AppStorage 装饰器,用于实现应用级的全局状态管理,类似 Vuex / Redux 的轻量版。

8.2 怎么实现

// entry/src/main/ets/utils/AppStore.ets

// 定义全局存储的 Key 常量,避免拼写错误
export class StoreKeys {
  static readonly USER_NAME: string = 'user_name'
  static readonly USER_TOKEN: string = 'user_token'
  static readonly SERVER_URL: string = 'server_url'
  static readonly DEVICE_LIST: string = 'device_list'
}

// 初始化全局存储(在 Ability 入口中调用)
export function initAppStorage(): void {
  AppStorage.setOrCreate(StoreKeys.USER_NAME, '')
  AppStorage.setOrCreate(StoreKeys.USER_TOKEN, '')
  AppStorage.setOrCreate(StoreKeys.SERVER_URL, 'https://default-server.com')
  AppStorage.setOrCreate(StoreKeys.DEVICE_LIST, [])
}
// entry/src/main/ets/pages/LoginPage.ets

import { StoreKeys } from '../utils/AppStore'

@Entry
@Component
struct LoginPage {
  @State username: string = ''
  @State password: string = ''

  // 登录逻辑
  async doLogin() {
    // 模拟登录请求...
    const mockToken = 'token_' + Date.now()

    // 将用户信息存入全局存储
    AppStorage.set(StoreKeys.USER_NAME, this.username)
    AppStorage.set(StoreKeys.USER_TOKEN, mockToken)

    console.info(`登录成功,用户名: ${this.username}`)

    // 跳转到首页
    // router.replaceUrl({ url: 'pages/Index' })
  }

  build() {
    Column({ space: 20 }) {
      Text('物联网平台登录')
        .fontSize(26)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 80, bottom: 40 })

      // 用户名输入
      TextInput({ placeholder: '请输入用户名' })
        .width(300)
        .height(48)
        .backgroundColor(Color.White)
        .onChange((value: string) => {
          this.username = value
        })

      // 密码输入
      TextInput({ placeholder: '请输入密码' })
        .width(300)
        .height(48)
        .type(InputType.Password)
        .backgroundColor(Color.White)
        .onChange((value: string) => {
          this.password = value
        })

      Button('登 录')
        .width(300)
        .height(50)
        .backgroundColor('#007DFF')
        .onClick(() => this.doLogin())
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}
// entry/src/main/ets/pages/ProfilePage.ets

import { StoreKeys } from '../utils/AppStore'

@Entry
@Component
struct ProfilePage {
  // 使用 @StorageProp 将变量绑定到全局存储
  // 当全局存储中的值变化时,UI 会自动更新
  @StorageProp(StoreKeys.USER_NAME) userName: string = ''
  @StorageProp(StoreKeys.USER_TOKEN) token: string = ''

  // 退出登录
  logout() {
    // 清空全局存储
    AppStorage.set(StoreKeys.USER_NAME, '')
    AppStorage.set(StoreKeys.USER_TOKEN, '')

    // 跳转到登录页
    // router.replaceUrl({ url: 'pages/LoginPage' })
  }

  build() {
    Column({ space: 20 }) {
      Text('个人中心')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40 })

      // 用户信息卡片
      Column({ space: 12 }) {
        // 头像占位
        Circle({ width: 80, height: 80 })
          .fill('#007DFF')

        Text(this.userName || '未登录')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)

        Text(`Token: ${this.token ? this.token.substring(0, 20) + '...' : '无'}`)
          .fontSize(12)
          .fontColor('#999999')
      }
      .padding(24)
      .backgroundColor(Color.White)
      .borderRadius(16)

      Button('退出登录')
        .width(200)
        .height(48)
        .backgroundColor('#FF4D4F')
        .onClick(() => this.logout())
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#F1F3F5')
  }
}

8.3 技术细节

关键概念

说明

AppStorage

应用级别的全局键值存储,在整个应用生命周期内持久存在

AppStorage.setOrCreate(key, value)

设置键值,如果不存在则创建

AppStorage.set(key, value)

设置键值,必须已存在否则会报错

AppStorage.get(key)

获取键对应的值

@StorageProp(key)

将组件状态变量绑定到全局存储,实现双向同步

@StorageLink(key)

类似 @StorageProp,但支持双向修改(会同步回全局存储)

注意@StorageProp单向绑定(全局 → 组件),适合读取全局状态。如果需要在组件内修改并同步回全局,使用 @StorageLink


九、实战:工业传感器数据展示页面

9.1 这是什么

本节将前面所学的知识点整合起来,实现一个完整的工业传感器数据展示页面,包含:

  • 顶部标题栏

  • 实时数据仪表盘

  • 设备状态列表

  • 底部刷新按钮

  • 加载状态和错误处理

9.2 怎么实现

// entry/src/main/ets/pages/SensorDashboard.ets

import { HttpUtil } from '../utils/HttpUtil'

// ==================== 数据模型 ====================
interface RealtimeData {
  temperature: number   // 温度 (°C)
  humidity: number      // 湿度 (%)
  pressure: number      // 气压 (kPa)
  motorSpeed: number    // 电机转速 (rpm)
  voltage: number       // 电压 (V)
  current: number       // 电流 (A)
  timestamp: string
}

interface DeviceStatus {
  id: number
  name: string
  isOnline: boolean
  alarmCount: number
}

// ==================== 主页面 ====================
@Entry
@Component
struct SensorDashboard {
  // ---- 状态变量 ----
  @State isLoading: boolean = true
  @State errorMsg: string = ''
  @State realtimeData: RealtimeData = {
    temperature: 0,
    humidity: 0,
    pressure: 0,
    motorSpeed: 0,
    voltage: 0,
    current: 0,
    timestamp: '--'
  }
  @State devices: DeviceStatus[] = [
    { id: 1, name: '1号温湿度传感器', isOnline: true, alarmCount: 0 },
    { id: 2, name: '2号压力变送器', isOnline: true, alarmCount: 1 },
    { id: 3, name: '3号电机控制器', isOnline: false, alarmCount: 0 },
    { id: 4, name: '4号电流传感器', isOnline: true, alarmCount: 0 },
  ]

  // ---- 生命周期 ----
  aboutToAppear() {
    this.loadData()
  }

  // ---- 加载数据 ----
  async loadData() {
    this.isLoading = true
    this.errorMsg = ''

    try {
      // 模拟 API 调用(实际项目中替换为真实接口)
      // const res = await HttpUtil.get<ApiResponse<RealtimeData>>('/api/realtime')
      // this.realtimeData = res.data

      // 模拟数据(用于演示)
      await this.mockDelay(800)
      this.realtimeData = {
        temperature: 32.5 + Math.random() * 5,
        humidity: 55 + Math.random() * 20,
        pressure: 101.3 + Math.random() * 2,
        motorSpeed: 1400 + Math.random() * 200,
        voltage: 23.5 + Math.random() * 3,
        current: 5 + Math.random() * 8,
        timestamp: new Date().toLocaleString()
      }
    } catch (e) {
      this.errorMsg = '数据加载失败,请检查网络连接'
    } finally {
      this.isLoading = false
    }
  }

  // 模拟延迟
  mockDelay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms))
  }

  build() {
    Column() {
      // ========== 顶部标题栏 ==========
      Row() {
        Text('🏭 工业传感器监控中心')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)

        Text(this.isLoading ? '同步中...' : '已同步')
          .fontSize(12)
          .fontColor('rgba(255,255,255,0.7)')
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .backgroundColor('#1a1a2e')
      .justifyContent(FlexAlign.SpaceBetween)

      // ========== 可滚动内容区 ==========
      Scroll() {
        Column({ space: 16 }) {

          // ---- 实时数据卡片区 ----
          Text('实时数据')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .textAlign(TextAlign.Start)

          // 第一行:温度、湿度、气压
          Row({ space: 10 }) {
            MiniCard({
              title: '温度',
              value: `${this.realtimeData.temperature.toFixed(1)}`,
              unit: '°C',
              color: '#FF6B6B'
            })
            MiniCard({
              title: '湿度',
              value: `${this.realtimeData.humidity.toFixed(1)}`,
              unit: '%',
              color: '#4ECDC4'
            })
            MiniCard({
              title: '气压',
              value: `${this.realtimeData.pressure.toFixed(1)}`,
              unit: 'kPa',
              color: '#45B7D1'
            })
          }
          .width('100%')

          // 第二行:转速、电压、电流
          Row({ space: 10 }) {
            MiniCard({
              title: '电机转速',
              value: `${Math.round(this.realtimeData.motorSpeed)}`,
              unit: 'rpm',
              color: '#A78BFA'
            })
            MiniCard({
              title: '电压',
              value: `${this.realtimeData.voltage.toFixed(1)}`,
              unit: 'V',
              color: '#FBBF24'
            })
            MiniCard({
              title: '电流',
              value: `${this.realtimeData.current.toFixed(1)}`,
              unit: 'A',
              color: '#34D399'
            })
          }
          .width('100%')

          // 更新时间
          Text(`更新时间: ${this.realtimeData.timestamp}`)
            .fontSize(11)
            .fontColor('#999999')
            .width('100%')
            .textAlign(TextAlign.End)

          // ---- 设备状态区 ----
          Text('设备状态')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .textAlign(TextAlign.Start)
            .margin({ top: 8 })

          Column({ space: 8 }) {
            ForEach(this.devices, (device: DeviceStatus) => {
              DeviceStatusItem({
                name: device.name,
                isOnline: device.isOnline,
                alarmCount: device.alarmCount
              })
            })
          }
          .width('100%')

        }
        .width('100%')
        .padding(16)
      }
      .width('100%')
      .layoutWeight(1)
      .backgroundColor('#F1F3F5')

      // ========== 底部操作栏 ==========
      Row({ space: 12 }) {
        Button('🔄 刷新数据')
          .layoutWeight(1)
          .height(48)
          .backgroundColor('#007DFF')
          .enabled(!this.isLoading)
          .onClick(() => this.loadData())

        Button('⚙️ 设置')
          .width(100)
          .height(48)
          .backgroundColor('#6B7280')
          .onClick(() => {
            // router.pushUrl({ url: 'pages/Settings' })
          })
      }
      .width('100%')
      .height(72)
      .padding({ left: 16, right: 16 })
      .backgroundColor(Color.White)
      .shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetY: -2 })
    }
    .width('100%')
    .height('100%')
  }
}

// ==================== 小型数据卡片组件 ====================
@Component
struct MiniCard {
  @Prop title: string
  @Prop value: string
  @Prop unit: string
  @Prop color: string

  build() {
    Column({ space: 4 }) {
      Text(this.title)
        .fontSize(11)
        .fontColor('#888888')

      Row({ space: 2 }) {
        Text(this.value)
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.color)

        Text(this.unit)
          .fontSize(11)
          .fontColor('#AAAAAA')
      }
    }
    .layoutWeight(1)
    .height(80)
    .padding(8)
    .backgroundColor(Color.White)
    .borderRadius(10)
    .justifyContent(FlexAlign.Center)
    .shadow({ radius: 4, color: 'rgba(0,0,0,0.06)', offsetY: 2 })
  }
}

// ==================== 设备状态项组件 ====================
@Component
struct DeviceStatusItem {
  @Prop name: string
  @Prop isOnline: boolean
  @Prop alarmCount: number

  build() {
    Row() {
      Row({ space: 8 }) {
        // 在线状态指示点
        Circle({ width: 8, height: 8 })
          .fill(this.isOnline ? '#52C41A' : '#999999')

        Text(this.name)
          .fontSize(14)
          .fontColor('#333333')
      }

      Row({ space: 8 }) {
        // 报警数量徽章
        if (this.alarmCount > 0) {
          Text(`${this.alarmCount}`)
            .fontSize(11)
            .fontColor(Color.White)
            .width(20)
            .height(20)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FF4D4F')
            .borderRadius(10)
        }

        Text(this.isOnline ? '在线' : '离线')
          .fontSize(12)
          .fontColor(this.isOnline ? '#52C41A' : '#999999')
      }
    }
    .width('100%')
    .height(48)
    .padding({ left: 12, right: 12 })
    .backgroundColor(Color.White)
    .borderRadius(8)
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

9.3 技术细节

关键概念

说明

Scroll()

滚动容器,当内容超出屏幕高度时可上下滑动查看

layoutWeight(1)

在 Row/Column 中设置权重,按比例分配剩余空间

TextAlign.Start / End

文本左对齐 / 右对齐(支持 RTL 语言自动适配)

new Date().toLocaleString()

获取本地格式化的日期时间字符串

Math.round()

四舍五入取整,适用于转速等不需要小数的场景


十、课程总结

10.1 核心知识点回顾

模块

关键技能

应用场景

声明式 UI 基础

@Entry@Component@Statebuild()

所有页面的基础结构

Flex 弹性布局

RowColumnFlexAlign

传感器卡片横向/纵向排列

Grid 网格布局

GridcolumnsTemplateForEach

大量同类设备的网格展示

组件封装

@Propexport struct、自定义组件

仪表盘、卡片等复用 UI

页面路由

router.pushUrlrouter.back、传参

多页面跳转导航

生命周期

aboutToAppearonPageHide

数据初始化、资源释放

网络请求

http 模块、Promise、async/await

从后端/硬件获取实时数据

全局状态

@StoragePropAppStorage

登录信息、全局配置共享

10.2 鸿蒙物联网开发核心流程

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  硬件设备    │────▶│  通信网关    │────▶│  后端 API   │────▶│  鸿蒙 APP   │
│ (STM32/PLC) │     │ (WiFi/蓝牙) │     │ (数据解析)  │     │ (ArkTS UI) │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
      │                                                        │
      │              数据采集 → 传输 → 存储 → 展示                │
      │                                                        ▼
      └───────────────────────────────────────────────────  工业监控大屏
                                                            实时数据面板
                                                            报警通知

10.3 学习路线建议

  1. 第一阶段(基础):掌握 ArkTS 语法 → 熟悉声明式 UI → 学会基础布局

  2. 第二阶段(进阶):组件封装 → 页面路由 → 网络请求 → 生命周期管理

  3. 第三阶段(实战):对接真实硬件 → 完成综合项目 → 性能优化

10.4 下一步学习方向

  • PLC 智能工业控制:学习西门子/三菱 PLC 编程,实现鸿蒙 APP 与产线设备的联动控制

  • 物联网云平台:接入 AGC 云控制台,实现设备远程管理和数据上云

  • 机器视觉:集成 OpenCV / YOLO,实现工业零件缺陷检测


📌 文档信息

  • 作者:课程讲师: 稷下元歌  QQ309647724 

  • 技术栈:HarmonyOS NEXT + ArkTS

  • 适用版本:API 12+

  • 转载请注明出处


本文档专为课程教学设计,代码可直接复制到 DevEco Studio 中使用。如有疑问,欢迎在qq309647724 加好友   区交流讨论!

posted @ 2026-07-31 16:36  鬼门元歌  阅读(3)  评论(0)    收藏  举报