请添加图片描述
请添加图片描述

一、引言:管理后台的布局挑战与鸿蒙方案

1.1 从移动端到管理后台

在移动应用开发领域,“仪表盘”(Dashboard)是一个经久不衰的界面模式。无论是电商平台的数据监控、企业应用的运营看板,还是 IoT 设备的管理后台,仪表盘始终是信息密度最高、布局要求最严格的界面类型之一。

一个典型的仪表盘界面通常包含以下要素:

  • KPI 指标卡片:展示核心业务数据(用户数、收入、转化率等)
  • 数据可视化区域:折线图、柱状图、饼图等图表占位
  • 最近活动/订单列表:表格形式展示最新动态
  • 顶部导航/标题栏:应用标识和用户操作入口

在传统的移动端开发中,实现这样的布局通常需要多层嵌套的 LinearLayout(Android)、UIStackView(iOS)或 Flexbox(React Native),开发者需要手动计算每个卡片的宽度、间距和换行逻辑。而在鸿蒙 ArkUI 中,Grid 组件 以声明式的方式彻底解决了这个问题。

1.2 HarmonyOS NEXT 6.1.1 与 ArkUI 布局能力

HarmonyOS NEXT 6.1.1(API 24)是鸿蒙生态的重要里程碑。这一版本完全移除了 Android AOSP 代码,基于纯鸿蒙内核运行。对于开发者而言,这意味着应用必须以 ArkTS 语言和 ArkUI 框架进行原生开发。

ArkUI 提供了完整的声明式 UI 能力,其核心布局组件包括:

组件 用途 适用场景
Column 纵向弹性布局 页面整体架构、纵向列表
Row 横向弹性布局 标题栏、输入栏、行内对齐
Grid 二维网格布局 仪表盘卡片网格、图片画廊、日历
Flex 弹性盒布局 自适应换行、等比分布
Stack 层叠布局 浮动按钮、遮罩层、背景叠加

在这些组件中,Grid 是最适合仪表盘场景的布局方案——它天然支持行列定义,配合 columnsTemplaterowsTemplate 属性,开发者可以用寥寥几行代码创建出规整的卡片网格。

1.3 本文的实践内容

本文将以一个完整的 Dashboard 仪表盘页面 为例,深度剖析以下技术:

  • Grid 组件:columnsTemplate、rowsTemplate、GridItem 完整用法
  • 卡片设计:渐变背景(linearGradient)、阴影(shadow)、圆角
  • 状态管理:@State 在静态展示页中的最佳实践
  • 组件化:@Builder 拆分模块化 UI
  • 暗色主题:深色仪表盘的配色方案与视觉设计
  • Scroll + layoutWeight:可滚动内容区域实现

项目基于 HarmonyOS NEXT 6.1.1(API 24),编译链为 Hvigor 6.1.1,所有代码均已在 DevEco Studio 中构建通过。


二、仪表盘的整体布局架构

2.1 七层布局结构

Dashboard 页面的布局采用经典的"固定头部 + 可滚动内容"模式,从上到下分为七个层次:

Column (全屏, 深空蓝黑 #0F1923 底色)
├── Row                        ① 顶部导航栏 (固定高度 56vp)
│   ├── Row                    Logo + "Dashboard" 文字
│   └── Row                    通知图标 + 用户头像
│
├── Scroll (layoutWeight:1)    ② 可滚动主体 (弹性占满)
│   └── Column (padding:16)
│       │
│       ├── Column             ③ 欢迎横幅 (阴影卡片)
│       │   ├── Row            标题 + 日期 + "新建"按钮
│       │   └── Row            三个统计标签 (本月订单/新增用户/好评率)
│       │
│       ├── Text              ④ 区域标题 "📈 核心指标"
│       ├── Grid (2列 × 2行)   ⑤ ★★★ KPI 卡片网格 ★★★
│       │   ├── GridItem       [蓝] 总用户数 12,846 ↑+12.5%
│       │   ├── GridItem       [绿] 总收入 ¥86,942 ↑+8.3%
│       │   ├── GridItem       [橙] 活跃用户 6,231 ↓-2.1%
│       │   └── GridItem       [紫] 转化率 24.8% ↑+3.7%
│       │
│       ├── Text              ⑥ 区域标题 "📊 数据分析"
│       ├── Grid (2列 × 1行)   ⑦ ★★★ 图表卡片网格 ★★★
│       │   ├── GridItem       销售趋势 → 柱状模拟图
│       │   └── GridItem       渠道分布 → 柱状模拟图
│       │
│       ├── Text              ⑧ 区域标题 "📋 最近订单"
│       └── Column             ⑨ 订单列表卡片
│           ├── Row            表头行
│           ├── Row × 4        订单数据行 (带彩色状态标签)
│           └── Divider × 3    行间分隔线
│
└── Blank().height(24)         ⑩ 底部留白

这个架构的核心设计理念是 “固定 + 弹性 + 固定”

  • 顶部标题栏固定 56vp
  • 中间内容区域通过 Scroll + layoutWeight(1) 弹性占满
  • 底部通过空白留白而非固定栏,保持内容完整性

2.2 配色方案设计

Dashboard 采用了深色主题配色方案,这是目前管理后台的主流趋势:

背景底色:     #0F1923 (深空蓝黑)
卡片底色:     #1E2D3D (深蓝灰)
导航栏底色:   #1A2A3A (略浅于卡片)
文字主色:     #FFFFFF (白色)
文字次要色:   #B0C4DE / #8899AA (浅灰蓝)
强调色蓝:     #4A90D9
强调色绿:     #27AE60
强调色橙:     #E67E22
强调色紫:     #8E44AD
上涨色:       #2ECC71 (绿色)
下跌色:       #E74C3C (红色)

四张 KPI 卡片采用不同的渐变色,既区分类别又增加视觉层次感。这种配色方案与 AirBnb、Stripe 等现代 SaaS 产品的设计语言一致。


三、核心布局技术一:Grid 组件深度解析

3.1 Grid 的基本概念

Grid 是 ArkUI 中用于创建二维网格布局的核心组件。与 Row/Column 的线性排列不同,Grid 允许组件在水平和垂直两个方向上同时排列。

Grid 的核心概念包括:

  • 列模板(columnsTemplate):定义网格的列数和每列宽度
  • 行模板(rowsTemplate):定义网格的行数和每行高度
  • 网格项(GridItem):网格中的每个单元格,作为 Grid 的直接子组件
  • 间距(columnsGap / rowsGap):列与列、行与行之间的间隔

3.2 fr 单位详解

fr(fraction unit,分数单位)是 Grid 和 Flex 布局中分配剩余空间的专用单位。理解 fr 是掌握 Grid 布局的关键。

fr 的计算规则

columnsTemplate 设置为 '1fr 1fr' 时:

  1. 先计算所有非 fr 单位的列的总宽度(本例中没有非 fr 列)
  2. 将 Grid 总宽度减去非 fr 列宽度,得到剩余空间
  3. 将剩余空间除以 fr 总数(1+1=2),每份 fr 获得剩余空间的 1/2

因此 '1fr 1fr' 意味着两列各占 50% 宽度。

如果设置为 '1fr 2fr 1fr',则三列的比例为 1:2:1,即 25%、50%、25%。

fr 与百分比的区别

特性 fr 百分比
计算基准 剩余空间 父容器总宽度
受其他列影响 是(fr 总量变化影响分配) 否(各自独立)
与非 fr 列兼容 是(先分配非 fr 列) 需手动计算剩余
嵌套场景 自动适应 需逐层计算

在实际开发中,fr 比百分比更灵活,特别是在与固定宽度的列配合使用时。

3.3 KPI 网格的 Grid 配置

KPI 指标卡片区域的 Grid 配置如下:

Grid() {
  ForEach(this.kpiCards, (card: KpiCardData) => {
    GridItem() {
      // 卡片内容
    }
    .width('100%')
    .height(140)
    .borderRadius(16)
    .linearGradient({ /* ... */ })
    .shadow({ /* ... */ })
  })
}
.columnsTemplate('1fr 1fr')    // ★★★ 2列等宽 ★★★
.rowsTemplate('1fr 1fr')       // ★★★ 2行等高 ★★★
.columnsGap(12)                // 列间距 12vp
.rowsGap(12)                   // 行间距 12vp
.width('100%')
.height(140 * 2 + 12)          // 总高度 = 行高×2 + 行间距

这里有几个关键点需要特别注意:

rowsTemplate 的使用:KPI 网格固定为 2 行,因此设置 rowsTemplate('1fr 1fr') 明确指定行高比例。两个 1fr 意味着两行高度相等,都是父容器分配高度的一半。Grid 的父容器高度通过 .height(140 * 2 + 12) 手动设定,其中 140 是每行高度,12 是行间距。

GridItem 的 height 与 Grid 的 rowsTemplate 的关系:GridItem 上设置的 height(140) 实际上是 GridItem 自身的最小高度请求,而 Grid 的 rowsTemplate('1fr 1fr') 会等分 Grid 的总高度给两行。当 Grid 高度被设置为 140*2+12=292vp 时,两行各获得 140vp,与 GridItem 请求的高度一致。

3.4 图表网格的 Grid 配置

图表区域只有一行两列:

Grid() {
  ForEach(this.chartCards, (chart: ChartCardData) => {
    GridItem() {
      // 图表卡片内容
    }
    .width('100%')
    .height(180)
    .backgroundColor('#1E2D3D')
    .borderRadius(16)
    .shadow({ /* ... */ })
  })
}
.columnsTemplate('1fr 1fr')    // ★★★ 2列等宽 ★★★
.rowsTemplate('1fr')           // ★★★ 1行 ★★★
.columnsGap(12)
.width('100%')
.height(180)                   // 单行高度 180vp

这里的 rowsTemplate('1fr') 表示只有一行,占据 Grid 的全部高度。Grid 的总高度通过 .height(180) 设置。

3.5 Grid 与 Scroll 的配合

仪表盘整体包裹在 Scroll 中实现垂直滚动。Grid 本身不滚动,而是作为 Scroll 的子内容。

Scroll (layoutWeight:1)
  └── Column
      ├── 欢迎横幅 ← 静态卡片
      ├── Grid     ← 静态2×2网格
      ├── Grid     ← 静态2×1网格
      └── Column   ← 订单列表

Grid 的 scrollBarState(BarState.Off) 确保 Grid 内部不产生滚动条。所有滚动行为由外层的 Scroll 统一管理。

为什么 Grid 内部没有滚动? 因为 Grid 的 rowsTemplate 指定了固定的行数(KPI 网格 2 行,图表网格 1 行),且 Grid 的高度被固定为恰好容纳这些行。Grid 的内容不会超过其容器高度,因此不需要内部滚动。


四、核心布局技术二:卡片视觉设计

4.1 线性渐变(linearGradient)

ArkUI 的 linearGradient 属性可以为组件创建渐变背景。KPI 卡片使用从上到下的线性渐变:

.linearGradient({
  direction: GradientDirection.Bottom,  // 渐变方向:从上到下
  colors: [
    [card.gradientStart, 0],            // 起始颜色,位置 0%
    [card.gradientEnd, 1]               // 结束颜色,位置 100%
  ]
})

GradientDirection 枚举

说明
Left 从右到左渐变
Right 从左到右渐变
Top 从下到上渐变
Bottom 从上到下渐变
LeftTop 从右下到左上渐变
LeftBottom 从右上到左下渐变
RightTop 从左下到右上渐变
RightBottom 从左上到右下渐变

颜色的 colors 数组接受 [颜色值, 位置] 的元组,位置取值范围 0~1,表示颜色在渐变路径上的百分比位置。

四张卡片使用了不同的渐变组合:

卡片 起始色 结束色 视觉感受
总用户数 #4A90D9 #357ABD 冷静专业的蓝色
总收入 #27AE60 #1E8449 增长财富的绿色
活跃用户 #E67E22 #CA6F1E 活力充沛的橙色
转化率 #8E44AD #6C3483 高品质的紫色

4.2 阴影(shadow)

shadow 属性为卡片增加立体层次感:

.shadow({
  radius: 8,           // 模糊半径 (vp)
  color: '#33000000',  // 阴影颜色 (含透明度)
  offsetX: 0,          // 水平偏移
  offsetY: 4           // 垂直偏移(向下投射)
})

阴影参数的视觉影响

  • radius(模糊半径):值越大,阴影边缘越柔和、扩散范围越大。radius: 8 产生适中的柔和阴影,适合卡片类组件。更大的值如 16 会更接近"光晕"效果。
  • color(阴影颜色):使用包含透明度(Alpha)的十六进制颜色值。#33000000 表示完全黑色但透明度为 20%(0x33/0xFF ≈ 0.2),叠加在深色背景上不会显得突兀。浅色主题下可以使用 #1A000000 降低强度。
  • offsetX / offsetY(偏移量):模拟光源方向。offsetY: 4 表示阴影向下偏移 4vp,模拟光源从上方照射的效果,这是最自然的阴影方向。

欢迎横幅使用了更大的阴影半径:

.shadow({
  radius: 12,
  color: '#33000000',
  offsetX: 0,
  offsetY: 4
})

更大的 radius(12 vs 8)让横幅的阴影更柔和、扩散更广,从视觉上突出其"最上层"的层次感。

4.3 圆角(borderRadius)

卡片圆角统一使用 borderRadius: 16,营造现代、友好的视觉风格。订单列表表头使用了不对称圆角:

// 表头:仅上方圆角
.borderRadius({
  topLeft: 12, topRight: 12
})

// 表体下方无圆角,因为卡片整体已经有了 borderRadius: 16
// 表头和表体组合成一个完整的圆角卡片

这种"表头圆角 + 表体无下圆角 + 外层卡片圆角"的技巧,在保持整体圆角卡片外观的同时,让表头和表体之间的分隔线更自然。

4.4 Emoji 图标方案

Dashboard 中的图标全部使用 Emoji 字符,而不是图片资源或字体图标:

// 顶部导航 Emoji
Text('🔔').fontSize(20)  // 通知铃铛

// 统计标签 Emoji
this.buildStatChip('📦', '本月订单', '326', '#4A90D9')
this.buildStatChip('👤', '新增用户', '1,247', '#27AE60')
this.buildStatChip('⭐', '好评率', '98.6%', '#E67E22')

// KPI 卡片 Emoji
icon: '👥',   // 总用户数
icon: '💰',   // 总收入
icon: '📊',   // 活跃用户
icon: '🎯',   // 转化率

Emoji 作为图标的优势

  • 无需额外资源文件,降低项目体积
  • 跨平台一致性高(Emoji 由系统渲染)
  • 开发效率高,改图标只需改一个字符
  • 支持 fontSize 控制大小,使用灵活

劣势:在低版本设备上 Emoji 渲染可能不一致,且颜色不可控。正式生产环境建议替换为矢量图标或图片资源。


五、状态管理与数据类型设计

5.1 接口定义

Dashboard 定义了三种数据结构,分别对应不同的卡片类型:

/** KPI 指标卡片数据 */
interface KpiCardData {
  title: string;         // 指标名称(如 "总用户数")
  value: string;         // 数值(如 "12,846")
  change: string;        // 变化率(如 "+12.5%")
  trend: 'up' | 'down';  // 趋势方向,字符串字面量联合类型
  icon: string;          // 图标 Emoji
  gradientStart: string; // 渐变起始色
  gradientEnd: string;   // 渐变结束色
}

/** 图表卡片数据 */
interface ChartCardData {
  title: string;       // 图表标题
  type: string;        // 图表类型描述(如 "折线图")
  subTitle: string;    // 副标题/数据概览
  period: string;      // 时间周期(如 "近30天")
  color: string;       // 主题色
}

/** 订单数据 */
interface OrderData {
  id: string;          // 订单号
  customer: string;    // 客户名
  product: string;     // 商品
  amount: string;      // 金额
  status: string;      // 状态文本(如 "已完成")
  statusColor: string; // 状态颜色(如 "#27AE60")
}

设计要点

  1. trend: 'up' | 'down' 使用 TypeScript 字符串字面量联合类型,比 boolean 的可读性更好。如果使用 isUp: boolean,在分支判断时需要额外记住 true 代表上升、false 代表下降。而 trend === 'up' 一目了然。

  2. statusColor 放在数据而非 UI 中:订单状态的颜色是业务数据的一部分,不是 UI 表现。将来如果从后端获取订单数据,状态颜色可以由后端控制(例如不同状态的订单用不同颜色高亮),因此将其作为数据字段而非 UI 硬编码。

  3. 所有数据字段都是字符串valueamount 虽然是数值含义,但定义为字符串以保留格式化信息(如 “12,846” 包含千分位逗号,“¥86,942” 包含货币符号)。格式化在数据层完成,UI 层直接展示。

5.2 @State 的使用

Dashboard 中的 @State 变量主要用于静态数据的展示:

@Entry
@Component
struct DashboardDemo {
  @State kpiCards: KpiCardData[] = [ /* 4 张卡片数据 */ ];
  @State chartCards: ChartCardData[] = [ /* 2 张图表数据 */ ];
  @State recentOrders: OrderData[] = [ /* 4 条订单数据 */ ];
  @State currentDate: string = '';
  
  aboutToAppear(): void {
    // 初始化日期
    const now: Date = new Date();
    // ... 格式化日期
    this.currentDate = `${year}${month}${day}日 星期${weekday}`;
  }
}

关于 @State 在展示页场景下的思考

虽然 Dashboard 当前是纯静态展示页面(没有用户交互改变数据),但使用 @State 仍然有重要意义:

  1. 为后续动态化预留接口:当仪表盘接入真实数据源时,只需修改 @State 变量,UI 会自动更新,无需修改 build() 方法。
  2. 生命周期钩子支持aboutToAppear() 在组件初始化时自动执行,适合做数据初始化。
  3. 保持与动态页面的一致性:项目中其他页面(聊天界面、日历)都使用 @State,统一风格降低认知负担。

aboutToAppear() 生命周期

这是 ArkUI 组件的生命周期之一,在组件即将挂载到视图树时调用。执行时机在 build() 方法之前,适合做数据准备。与之对应的还有 aboutToDisappear(),在组件即将销毁时调用,适合做资源清理。

5.3 日期格式化实现

currentDateaboutToAppear() 中初始化:

aboutToAppear(): void {
  const now: Date = new Date();
  const year: number = now.getFullYear();
  const month: number = now.getMonth() + 1;  // getMonth() 返回 0-11
  const day: number = now.getDate();
  const weekdays: string[] = ['日', '一', '二', '三', '四', '五', '六'];
  const weekday: string = weekdays[now.getDay()];  // getDay() 返回 0-6
  this.currentDate = `${year}${month}${day}日 星期${weekday}`;
}

这里用到的 JavaScript Date API 技巧:

  • getMonth() 返回 0~11,因此需要 + 1 才能得到实际的月份
  • getDay() 返回 0(周日)~6(周六),刚好对应 weekdays 数组的索引
  • 模板字符串 ${} 是 ES6 语法,ArkTS 完全支持

六、组件化构建 @Builder 详解

6.1 @Builder 的拆分原则

Dashboard 页面将 UI 拆分为 7 个 @Builder 方法:

@Builder 方法 用途 参数
buildHeader() 顶部导航栏
buildWelcomeBanner() 欢迎横幅卡片
buildStatChip(icon, label, value, color) 概要统计小标签 4个参数
buildSectionTitle(title, action) 区域标题生成器 2个参数
buildKpiGrid() KPI 指标卡片网格
buildChartGrid() 图表卡片网格
buildOrderList() 订单列表

拆分的依据

  1. 功能独立性:每个 @Builder 对应一个独立的 UI 区块,有明确的视觉边界
  2. 参数化buildStatChipbuildSectionTitle 接受参数,可以在不同位置复用
  3. 逻辑复杂度buildKpiGridbuildChartGrid 内部包含 Grid + ForEach 循环,不适合直接放在主 build() 中
  4. 可读性:主 build() 方法只有 30 行左右,充当"目录"角色

6.2 @Builder 带参数的定义与调用

buildStatChip 是一个带参数的 @Builder:

@Builder
buildStatChip(icon: string, label: string, value: string, color: string): void {
  Row() {
    Text(icon)
      .fontSize(16)
      .margin({ right: 8 })

    Column() {
      Text(value)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')

      Text(label)
        .fontSize(11)
        .fontColor('#8899AA')
    }
    .alignItems(HorizontalAlign.Start)
  }
  .layoutWeight(1)               // ★★★ 三等分宽度 ★★★
  .padding({ top: 8, bottom: 8 })
  .justifyContent(FlexAlign.Center)
}

在欢迎横幅中调用三次:

Row() {
  this.buildStatChip('📦', '本月订单', '326', '#4A90D9')
  this.buildStatChip('👤', '新增用户', '1,247', '#27AE60')
  this.buildStatChip('⭐', '好评率', '98.6%', '#E67E22')
}
.width('100%')
.margin({ top: 16 })

layoutWeight(1) 的妙用:三个 buildStatChip 都设置了 layoutWeight(1),外层的 Row 按照 1:1:1 的比例等分宽度。如果将来要增加第四个统计标签,只需添加一行调用,不用修改任何布局代码。

6.3 区域标题生成器

buildSectionTitle 是一个简单的标题 + 操作按钮生成器:

@Builder
buildSectionTitle(title: string, action: string): void {
  Row() {
    Text(title)
      .fontSize(17)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')

    Blank()     // ★★★ 弹性占位,把操作按钮推到右侧 ★★★

    Text(action)
      .fontSize(13)
      .fontColor('#4A90D9')
  }
  .width('100%')
  .margin({ top: 16, bottom: 12 })
}

在页面中被调用三次:

this.buildSectionTitle('📈 核心指标', '导出报告')
this.buildSectionTitle('📊 数据分析', '全部报表')
this.buildSectionTitle('📋 最近订单', '查看全部')

这种模式的好处是统一性——所有区域标题的外观和间距完全一致,修改时只需改一处。


七、图表卡片中的模拟可视化

7.1 用 Column 模拟柱状图

Dashboard 的图表卡片区域使用 Column 组件模拟了简单的柱状图:

// ★★★ 图表占位区域:用简化图形模拟趋势线 ★★★
Column() {
  Row() {
    // 用不同高度的柱状条模拟图表
    ForEach([20, 35, 25, 45, 30, 50], (height: number) => {
      Column() {
        // 透明占位
      }
      .width(12)
      .height(height)                  // 每个柱子的高度不同
      .backgroundColor(chart.color)    // 使用卡片的主题色
      .borderRadius(6)                 // 圆角柱状
      .margin({ left: 3, right: 3 })
      .opacity(0.6)                    // 半透明,更柔和
      .alignSelf(ItemAlign.End)        // ★★★ 底部对齐 ★★★
    })
  }
  .width('100%')
  .height(60)
  .alignItems(VerticalAlign.Bottom)    // ★★★ Row 底部对齐 ★★★
  .justifyContent(FlexAlign.SpaceEvenly) // ★★★ 均匀分布 ★★★
  .margin({ top: 12 })
}

模拟柱状图的关键技术

  1. alignSelf(ItemAlign.End):每个柱子在 Row 中底部对齐。柱子本身是 Column() 容器(内部透明占位),其高度通过 .height(height) 设置。底部对齐后,柱子的顶部高度各不相同,形成柱状图效果。

  2. alignItems(VerticalAlign.Bottom):Row 内的所有柱状条在垂直方向底部对齐,与 alignSelf(ItemAlign.End) 配合确保一致。

  3. justifyContent(FlexAlign.SpaceEvenly):柱状条在水平方向上均匀分布,包括与容器边缘的间距也相等,形成规整的视觉排列。

  4. opacity(0.6):半透明效果让柱状条看起来更柔和,不会与卡片内容争夺视觉焦点。

  5. 高度数组 [20, 35, 25, 45, 30, 50]:模拟了一个上升趋势的数据序列,视觉上呈现从左到右的爬升感。

7.2 生产环境中的图表方案

本文中的柱状图模拟仅用于演示 Grid + 卡片的布局能力。在生产环境中,实现真正的图表可视化有以下方案:

  • 使用 Canvas 组件:ArkUI 提供了 Canvas 组件支持 2D 自定义绘制,可以实现任意类型的图表
  • 集成第三方图表库:通过 ohpm 安装图表库(如 echarts 的鸿蒙适配版)
  • 服务端渲染图表图片:由后端生成图表图片,前端用 Image 组件加载

八、订单列表中的表格布局技巧

8.1 layoutWeight 实现列宽比例

订单列表的每一行使用 layoutWeight 控制各列的宽度比例:

Row() {
  Text('订单号').layoutWeight(1.5)  // 占 1.5 份
  Text('客户').layoutWeight(1)     // 占 1 份
  Text('商品').layoutWeight(1.5)   // 占 1.5 份
  Text('金额').layoutWeight(0.8)   // 占 0.8 份
  Text('状态').layoutWeight(0.8)   // 占 0.8 份
}

各列宽度比例:1.5 : 1.0 : 1.5 : 0.8 : 0.8 = 订单号 > 商品 > 客户 > 金额 = 状态

这种比例设计的考虑:

  • “订单号” 和 “商品” 通常较长,分配更多宽度
  • “金额” 和 “状态” 内容较短,分配较少宽度
  • “客户” 名称长短不一,取中间值

8.2 彩色状态标签

订单状态使用带背景色的 Text 组件模拟标签:

Text(order.status)
  .fontSize(12)
  .fontColor('#FFFFFF')
  .backgroundColor(order.statusColor)  // 数据驱动颜色
  .borderRadius(10)                     // 胶囊圆角
  .padding({ left: 8, right: 8, top: 2, bottom: 2 })
  .layoutWeight(0.8)
  .textAlign(TextAlign.Center)

三种状态对应三种颜色:

状态 颜色 十六进制
已完成 绿色 #27AE60
处理中 橙色 #E67E22
待发货 蓝色 #4A90D9

颜色值存储在数据中而非硬编码在 UI 中,意味着未来从后端获取数据时,后端可以灵活控制每个订单的状态颜色。

8.3 条件分隔线

订单行之间的分隔线使用条件渲染,最后一行不显示:

if (order.id !== this.recentOrders[this.recentOrders.length - 1].id) {
  Divider().height(1).color('#2A3A4A').width('100%').margin({ left: 16 })
}

判断逻辑:当前订单的 ID 不等于数组中最后一个订单的 ID 时渲染分隔线。这确保了最后一条订单下方不会有多余的分隔线,视觉上更干净。


九、项目工程配置详解

9.1 build-profile.json5 构建设置

项目根目录的 build-profile.json5 定义了工程级的构建配置:

{
  "app": {
    "signingConfigs": [],
    "products": [
      {
        "name": "default",
        "targetSdkVersion": "6.1.1(24)",
        "compatibleSdkVersion": "6.1.1(24)",
        "runtimeOS": "HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        }
      }
    ],
    "buildModeSet": [
      { "name": "debug" },
      { "name": "release" }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        { "name": "default", "applyToProducts": ["default"] }
      ]
    }
  ]
}

关键配置项说明

  • targetSdkVersion: "6.1.1(24)"compatibleSdkVersion: "6.1.1(24)":目标 SDK 和兼容 SDK 均为 6.1.1,API 等级 24。这是 HarmonyOS NEXT 的关键版本标识。
  • runtimeOS: "HarmonyOS":明确指定运行操作系统为鸿蒙。
  • strictMode:开启严格模式,caseSensitiveCheck 确保文件名大小写匹配,useNormalizedOHMUrl 确保 OHM(OpenHarmony Module)URL 规范化。

9.2 main_pages.json 页面路由

entry/src/main/resources/base/profile/main_pages.json 配置了应用的页面路由:

{
  "src": [
    "pages/DashboardDemo"
  ]
}

每个 src 条目对应 pages/ 目录下的一个 .ets 文件,路径不需要 .ets 后缀。EntryAbility 通过 loadContent('pages/DashboardDemo') 加载页面。

9.3 EntryAbility 启动流程

EntryAbility 是应用启动的入口:

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/DashboardDemo', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'EnglishApp',
          'Failed to load content: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'EnglishApp', 'Succeeded in loading content.');
    });
  }
}

windowStage.loadContent 接受两个参数:

  1. 页面路径:字符串,对应 main_pages.json 中的注册路径
  2. 回调函数(err: BusinessError) => void,加载完成后调用。err.code 为 0 表示成功,非零为错误码

hilog 日志格式说明

hilog.error(domain, tag, format, ...args);
  • domain:十六进制整数,开发者自定义标识领域
  • tag:字符串标签,用于过滤日志
  • format:格式化字符串,%{public}s 表示公开的字符串参数(隐私模式下 %{private}s 表示敏感信息会被脱敏)

十、性能优化与最佳实践

10.1 Grid 的渲染性能

Grid 组件在渲染大量 GridItem 时具有较好的性能表现,因为 Grid 会在内部进行布局优化。但在 Dashboard 场景中,KPI 网格只有 4 个 GridItem,图表网格只有 2 个,属于极轻量级使用,不需要特殊的性能优化。

需要关注的性能场景(当 GridItem 数量 > 50 时):

  1. 避免复杂的 GridItem 嵌套:每层嵌套都会增加布局计算时间
  2. 使用 LazyForEach 替代 ForEach:大数据列表推荐使用懒加载
  3. 避免在 GridItem 中使用频繁变化的动画

10.2 @Builder 的复用与复用限制

@Builder 方法可以在同一个组件内复用,但不支持跨组件复用。如果需要跨组件复用 UI 逻辑,有以下方案:

  1. 全局 @Builder 函数(API 24 支持):
// 全局 Builder 函数
@Builder function GlobalStatChip(icon: string, value: string, label: string) {
  Row() {
    Text(icon)
    Column() {
      Text(value)
      Text(label)
    }
  }
}

// 在任意组件中直接使用函数名调用
@Component
struct SomeComponent {
  build() {
    Column() {
      GlobalStatChip('📦', '326', '本月订单')
    }
  }
}
  1. 自定义组件(推荐方案):

将频繁复用的 UI 片段抽取为独立的 @Component struct:

@Component
struct StatChip {
  private icon: string = ''
  private value: string = ''
  private label: string = ''

  build() {
    Row() {
      Text(this.icon).fontSize(16).margin({ right: 8 })
      Column() {
        Text(this.value).fontSize(16)
          .fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        Text(this.label).fontSize(11).fontColor('#8899AA')
      }
      .alignItems(HorizontalAlign.Start)
    }
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
  }
}

// 使用
StatChip({ icon: '📦', value: '326', label: '本月订单' })

自定义组件相比于 @Builder 的优势在于:独立生命周期、独立的 @State 状态、跨组件复用。

10.3 深色主题的色彩系统设计

Dashboard 的深色主题基于以下原则设计:

原则一:层次分明

底层背景:     #0F1923  (最暗)
卡片表面:     #1E2D3D  (次暗)  
导航栏:       #1A2A3A  (介于之间)
文字主色:     #FFFFFF  (最亮)
文字次要:     #B0C4DE / #8899AA (半亮)

通过 5 个灰度层级建立清晰的视觉层次,用户的视线会自然地被亮色内容吸引。

原则二:色彩克制

深色主题下,大面积使用高饱和颜色会产生视觉疲劳。Dashboard 只在以下三个位置使用颜色:

  1. KPI 卡片的渐变背景(4 种颜色,各占 25% 卡片面积)
  2. 强调按钮/链接(蓝色 #4A90D9)
  3. 涨跌/状态指示(绿色/红色/橙色/蓝色小面积标签)

原则三:透明度辅助

阴影颜色使用 #33000000(20% 透明度黑色),柱状图使用 opacity(0.6),在深色背景上产生微妙的层次变化而不增加新的色相。

10.4 常见陷阱与解决方案

陷阱一:Grid 内容不显示

Grid 必须同时满足以下条件才能显示内容:

  • columnsTemplate 格式正确且有效
  • Grid 本身有有效的宽度(width('100%') 或固定宽度)
  • Grid 或 GridItem 有有效的高度

解决方案

// ✅ 正确的 Grid 配置
Grid() {
  GridItem() { /* ... */ }
}
.columnsTemplate('1fr 1fr')   // 列模板有效
.width('100%')                 // Grid 有宽度
.height(140)                   // Grid 有高度

陷阱二:GridItem 内容被裁剪

当 GridItem 的内容超出 GridItem 的大小时,内容会被裁剪。确保 GridItem 的行高足够容纳内容。

解决方案:使用 constraintSize 限制内容的最大尺寸,或增加 rowsTemplate 中的行高比例。

陷阱三:@State 数组更新不触发 UI

// ❌ 不触发 UI 更新
this.kpiCards[0].value = '15,000'

// ✅ 触发 UI 更新
this.kpiCards = [...this.kpiCards]  // 创建新数组引用
// 然后修改 kpiCards[0].value

对于嵌套对象的修改,需要使用展开运算符创建新的对象层级。


十一、总结与展望

11.1 技术要点回顾

本文通过一个完整的 Dashboard 仪表盘页面,系统性地讲解了 HarmonyOS NEXT ArkUI 中的以下核心技术:

布局方面

  • Grid 组件实现 2 列等宽卡片网格
  • columnsTemplate 和 rowsTemplate 的 fr 单位机制
  • Scroll + layoutWeight 实现可滚动内容区域
  • Row + Blank 实现两端对齐

视觉设计方面

  • linearGradient 实现卡片渐变背景
  • shadow 属性为卡片增加立体感
  • borderRadius 圆角设计
  • 深色主题的配色体系

组件化方面

  • @Builder 装饰器拆分 UI 模块
  • 带参数的 @Builder 实现复用
  • build() 作为 UI 目录

数据管理方面

  • interface 定义数据结构
  • @State 管理响应式数据
  • aboutToAppear 生命周期初始化

11.2 从 Dashboard 到生产环境

本文的 Dashboard 示例展示了核心布局能力,但从示例到生产环境还需补充以下能力:

  1. 数据源接入:使用 @State + 异步请求(http 模块或 @ohos.net.http)从后端 API 获取真实数据
  2. 下拉刷新:使用 Refresh 组件包裹内容区,实现下拉刷新
  3. 图表可视化:集成 Canvas 绘制或第三方图表库
  4. 交互反馈:点击卡片跳转详情页、手势滑动删除订单等
  5. 响应式适配:根据屏幕宽度动态调整 Grid 列数(平板端可改为 3 列或 4 列)

11.3 ArkUI 布局的未来

随着 HarmonyOS NEXT 的持续迭代,ArkUI 的布局能力在不断增强。从 API 24 开始,以下特性值得关注:

  • 自适应布局 API@ohos.adaptive 模块提供屏幕断点适配能力
  • 自定义布局:通过 Layout 接口实现完全自定义的布局算法
  • 布局动画:GridItem 的增删自动伴随动画过渡
  • 跨端布局一致性:同一套 ArkUI 代码在手机、平板、折叠屏、车机上保持一致的布局表现

对于正在进入鸿蒙生态的开发者而言,现在深入学习 ArkUI 的布局系统,将是在鸿蒙原生应用开发领域建立技术优势的最佳时机。


附录:完整代码与构建指南

附录 A:DashboardDemo.ets 结构总览

文件: entry/src/main/ets/pages/DashboardDemo.ets
语言: ArkTS (HarmonyOS NEXT 6.1.1 / API 24)
行数: 686 行

├── 接口定义 (3 个 interface)
│   ├── KpiCardData      — KPI 指标卡片数据
│   ├── ChartCardData    — 图表卡片数据
│   └── OrderData        — 订单数据
│
├── struct DashboardDemo (@Entry @Component)
│   ├── @State 变量 (4 个)
│   │   ├── kpiCards       — KPI 卡片数据数组
│   │   ├── chartCards     — 图表卡片数据数组
│   │   ├── recentOrders   — 订单数据数组
│   │   └── currentDate    — 当前日期字符串
│   │
│   ├── 生命周期
│   │   └── aboutToAppear() — 日期初始化
│   │
│   ├── build() — 主构建方法
│   │   ├── buildHeader()
│   │   ├── Scroll()
│   │   │   └── Column()
│   │   │       ├── buildWelcomeBanner()
│   │   │       ├── buildKpiGrid()      ★ Grid KPI 卡片
│   │   │       ├── buildChartGrid()     ★ Grid 图表卡片
│   │   │       └── buildOrderList()
│   │   └── Blank()
│   │
│   ├── @Builder buildHeader()
│   ├── @Builder buildWelcomeBanner()
│   ├── @Builder buildStatChip(icon, label, value, color)
│   ├── @Builder buildSectionTitle(title, action)
│   ├── @Builder buildKpiGrid()       ★★★ 核心 Grid
│   ├── @Builder buildChartGrid()     ★★★ 核心 Grid
│   └── @Builder buildOrderList()

附录 B:构建与运行命令

# 工程根目录执行

# PreBuild 快速验证
hvigorw PreBuildApp --no-daemon

# Debug 构建
hvigorw assembleApp --mode debug --no-daemon

# Release 构建(需配置签名)
hvigorw assembleApp --mode release --no-daemon

附录 C:Grid 组件 API 速查

属性 类型 说明 示例
columnsTemplate string 列模板,fr 单位 '1fr 1fr 1fr' 三列等宽
rowsTemplate string 行模板,fr 单位 '1fr 1fr' 两行等高
columnsGap number 列间距 (vp) 12
rowsGap number 行间距 (vp) 12
scrollBarState BarState 滚动条状态 BarState.Off
editMode boolean 编辑模式 false
layoutWeight number 弹性权重(在 Row/Column 中) 1

附录 D:卡片视觉属性速查

属性 用途 KPI 卡片 图表卡片 订单卡片
backgroundColor 背景色 linearGradient 渐变 #1E2D3D #1E2D3D
borderRadius 圆角 16 16 16
padding 内边距 18 18 0(由子项决定)
shadow 阴影 radius:8 radius:8 radius:8
.height() 高度 140 180 自动撑开

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐