在这里插入图片描述

引言

在数据展示类应用中,表格(Table/Grid)是最经典的数据呈现方式。从 Excel 的电子表格到数据库管理系统的查询结果,从学生成绩单到财务报表,表格以其行列表头的结构化布局,让用户能够快速浏览、对比和查找数据。HarmonyOS NEXT 的 ArkUI 框架提供了 Grid 组件,通过 columnsTemplaterowsTemplate 属性定义网格的行列模板,配合 GridItem 子组件和 ForEach 渲染,可以灵活地构建各种表格布局。

示例 93 以「学生成绩表」为业务场景,实现了一个 5 行 4 列的成绩数据表格。表头包含姓名、年龄、城市、分数四列,深色背景白色文字;数据行采用斑马纹设计(奇偶行交替背景色);分数列根据值的高低显示不同颜色——90 分以上绿色、80 分以下红色、其余深色;底部统计栏展示学生人数和平均分;点击「排序」按钮可按分数从高到低重新排列所有行。整个流程涉及 Grid + GridItem 的网格渲染、二维数组 string[][] 的数据管理、ForEach 嵌套渲染行和列、Array.sort 排序算法、以及动态颜色绑定,几乎涵盖了表格数据展示的全部核心要点。

这篇文章会严格按源码顺序,先介绍应用的整体功能与布局结构,再拆解 Grid 组件的核心属性与模板配置,接着逐段解读 .ets 源码中的数据结构、颜色分级、排序逻辑、表格渲染,然后分析斑马纹实现、分数颜色策略、排序算法的稳定性,最后给出运行操作指南、可扩展方向与常见问题调试技巧。读完后,你不仅能看懂这一个成绩表格页面,还能举一反三,把它应用到商品列表、工资明细、库存盘点等任何需要表格展示的场景。

1. 应用概述与功能

「表格数据」是一个面向数据展示场景的工具型页面,交互路径清晰:查看成绩表 → 点击排序 → 查看排序结果与平均分

页面自上而下分为五块区域:顶部返回栏(返回按钮 + 标题「表格数据」);表头行(姓名/年龄/城市/分数四列,蓝色背景白色文字);数据区(Grid 5 行 4 列,斑马纹交替背景,分数列动态颜色);统计卡片(学生人数 + 平均分);排序按钮 + 使用提示文字。

1.1 核心功能清单

  • 5×4 表格渲染:使用 Grid 组件渲染 5 行 4 列的成绩数据,每行包含姓名、年龄、城市、分数。
  • 斑马纹背景:奇数行白色背景、偶数行浅灰色背景,提升数据行的可读性。
  • 分数颜色分级:分数 >90 显示绿色、<80 显示红色、其余深色,让分数高低一目了然。
  • 高分加粗:分数 >90 的数字以粗体显示,进一步突出优秀成绩。
  • 平均分计算:底部统计栏自动计算所有学生的平均分,保留一位小数。
  • 按分数降序排序:点击「排序」按钮,所有行按分数从高到低重新排列,并弹出 Toast 提示。

1.2 技术要点一览

整个示例用到的关键技术对「表格数据展示」类页面很有代表性:Grid 组件的 columnsTemplate/rowsTemplate 模板定义、GridItem + ForEach 的网格渲染、二维数组 string[][] 的数据结构、ForEach 嵌套(外层渲染行、内层渲染列)、条件样式绑定(分数颜色和字重)、Array.slice() + Array.sort() 的排序算法、以及 Math.round 保留小数位。把这些要点串起来,就构成了一条完整的「数据定义 → 网格渲染 → 条件样式 → 排序更新」的数据流。

2. 核心知识点

在逐段读代码之前,先把表格数据页面承载的 ArkTS 核心知识讲清楚。

2.1 Grid 组件与模板定义

Grid 是 ArkUI 提供的网格容器组件,通过行列模板定义网格的布局结构:

Grid() {
  ForEach(this.rows, (row: string[], idx: number) => {
    GridItem() {
      Row() {
        ForEach(row, (cell: string, cIdx: number) => {
          Text(cell) ...
        }, ...)
      }
    }
  }, ...)
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.width('100%')
.height(220)

Grid 的核心属性:

  • columnsTemplate:列模板,'1fr 1fr 1fr 1fr' 表示 4 列等宽,每列占 1 份(fraction)。
  • rowsTemplate:行模板,'1fr 1fr 1fr 1fr 1fr' 表示 5 行等高,每行占 1 份。
  • width/height:网格容器的宽高,决定了每列每份的实际像素宽度。

1fr 是 CSS Grid 中的「分数单位」,表示「占用剩余空间的 1 份」。4 个 1fr 表示 4 列等分容器宽度,5 个 1fr 表示 5 行等分容器高度。

2.2 GridItem 子组件约束

Grid 组件有一个重要约束:只能包含 GridItem 类型的子组件。如果直接在 Grid 中放 TextRow 等其他组件,编译会报错。

// 正确写法
Grid() {
  ForEach(this.rows, (row: string[], idx: number) => {
    GridItem() {      // 必须包裹 GridItem
      Row() {
        ForEach(row, (cell: string) => {
          Text(cell)  // GridItem 内部可以放任意组件
        }, ...)
      }
    }
  }, ...)
}

// 错误写法(编译报错)
Grid() {
  ForEach(this.rows, (row: string[], idx: number) => {
    Row() {           // 不能直接放 Row,必须先包 GridItem
      Text('...')
    }
  }, ...)
}

每个 GridItem 占据网格中的一个单元格。在本示例中,每个 GridItem 内部放一个 RowRow 内部再用 ForEach 渲染 4 个 Text,实现一行 4 列的效果。

2.3 二维数组 string[][] 的数据结构

学生成绩数据使用二维数组 string[][] 存储:

@State rows: string[][] = [
  ['张三', '20', '北京', '92'],
  ['李四', '21', '上海', '85'],
  ['王五', '19', '广州', '88'],
  ['赵六', '22', '深圳', '96'],
  ['孙七', '20', '杭州', '79']
];

每个外层元素是一行数据(string[]),每个内层元素是一个单元格的值(string)。使用 string[][] 而非自定义类的原因是简洁——所有值统一为字符串,方便 Text 组件直接渲染,也方便 Number() 转换进行数值比较。

2.4 ForEach 嵌套渲染

表格需要嵌套两层 ForEach——外层渲染行,内层渲染列:

ForEach(this.rows, (row: string[], idx: number) => {
  GridItem() {
    Row() {
      ForEach(row, (cell: string, cIdx: number) => {
        Text(cell)
          .width('25%')
          .fontColor(cIdx === 3 ? this.scoreColor(cell) : '#333333')
      }, (cell: string, cIdx: number) => cIdx.toString())
    }
  }
}, (row: string[], idx: number) => idx.toString())

外层 ForEach 遍历 this.rows,每行生成一个 GridItem;内层 ForEach 遍历当前行的 4 个单元格,每列生成一个 Text。内层通过 cIdx === 3 判断是否是分数列(第 4 列),如果是则调用 scoreColor 方法获取动态颜色。

2.5 Array.sort 排序与 slice 整体替换

排序功能使用 Array.sort 方法,配合 slice 实现数组整体替换:

private sortByScore(): void {
  const sorted: string[][] = this.rows.slice();
  sorted.sort((a: string[], b: string[]) => {
    return Number(b[3]) - Number(a[3]);
  });
  this.rows = sorted.slice();
}

这里有两层 slice 调用:

  1. 第一层 this.rows.slice():创建原数组的浅拷贝,避免直接修改原数组(sort 会原地修改)。
  2. 第二层 sorted.slice():将排序后的数组再次拷贝,赋值给 this.rows,触发 @State 的 UI 刷新。

在 ArkTS 中,@State 数组需要整体替换才能触发 UI 更新。直接调用 this.rows.sort() 虽然修改了数组内容,但 @State 感知不到变化,UI 不会刷新。所以必须用 slice() 创建新数组再赋值。

排序比较函数 (a, b) => Number(b[3]) - Number(a[3]) 是降序排列——b[3] 是后一个元素的分数,a[3] 是前一个元素的分数,b - a > 0b 排前面,即高分在前。

3. 源码逐段解析

现在开始按源码顺序逐段解读 index93.ets,从导入声明到 build 方法,完整展示表格数据的实现细节。

3.1 导入声明与组件声明

import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct Index93 {

导入 routerpromptAction,声明页面入口组件 Index93

3.2 数据定义

@State rows: string[][] = [
  ['张三', '20', '北京', '92'],
  ['李四', '21', '上海', '85'],
  ['王五', '19', '广州', '88'],
  ['赵六', '22', '深圳', '96'],
  ['孙七', '20', '杭州', '79']
];
private headers: string[] = ['姓名', '年龄', '城市', '分数'];

rows@State 二维数组,包含 5 名学生的成绩数据。headers 是私有数组,定义表头四列的名称。注意所有数据都以字符串形式存储,包括年龄和分数,需要用 Number() 转换进行数值运算。

3.3 average 平均分计算

private average(): number {
  let sum: number = 0;
  for (let i: number = 0; i < this.rows.length; i++) {
    sum += Number(this.rows[i][3]);
  }
  return Math.round(sum / this.rows.length * 10) / 10;
}

average 方法计算所有学生分数的平均值:

  1. for 循环遍历每一行,this.rows[i][3] 取第 4 列(分数列),Number() 转为数值后累加到 sum
  2. sum / this.rows.length 计算平均值。
  3. Math.round(sum / this.rows.length * 10) / 10 实现保留一位小数——先乘以 10,四舍五入取整,再除以 10。例如 87.4 * 10 = 874Math.round(874) = 874874 / 10 = 87.4

3.4 scoreColor 分数颜色分级

private scoreColor(score: string): string {
  const n: number = Number(score);
  if (n > 90) {
    return '#2f9e44';
  }
  if (n < 80) {
    return '#e03131';
  }
  return '#333333';
}

scoreColor 方法根据分数值返回对应的颜色字符串:

  • 分数 > 90:返回绿色 #2f9e44,表示优秀。
  • 分数 < 80:返回红色 #e03131,表示需要努力。
  • 其余(80~90):返回深灰色 #333333,表示正常。

这种颜色分级策略让用户一眼就能识别高分和低分,提升了数据表的可读性。

3.5 sortByScore 排序方法

private sortByScore(): void {
  const sorted: string[][] = this.rows.slice();
  sorted.sort((a: string[], b: string[]) => {
    return Number(b[3]) - Number(a[3]);
  });
  this.rows = sorted.slice();
  promptAction.showToast({ message: '已按分数降序排序' });
}

sortByScore 方法实现按分数降序排序:

  1. this.rows.slice() 创建原数组的浅拷贝。
  2. sorted.sort(...) 按分数降序排列——比较函数取 b[3](后者的分数)减去 a[3](前者的分数),结果为正时后者排前。
  3. this.rows = sorted.slice() 将排序后的新数组赋值给 @State 变量,触发 UI 刷新。
  4. 弹出 Toast 提示。

3.6 build 方法整体结构

build() {
  Column() {
    // 顶部返回栏
    Row() { ... }
    // 表头行
    Row() { ... }
    // 数据区 Grid
    Grid() { ... }
    // 平均分卡片
    Row() { ... }
    // 排序按钮
    Button('排序') { ... }
    // 使用提示
    Text('点击排序按钮,按分数从高到低排列') { ... }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#f2f3f5')
}

3.7 表头行

Row() {
  ForEach(this.headers, (header: string, i: number) => {
    Text(header)
      .width('25%')
      .height(44)
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.White)
      .textAlign(TextAlign.Center)
      .backgroundColor('#1a6cff')
  }, (header: string, i: number) => i.toString())
}
.width('100%')
.height(44)

表头行使用 Row + ForEach 渲染四个表头文字。每个 Text 宽度 25%(4 列等分),高度 44vp,蓝色背景白色文字,居中对齐,加粗显示。表头行的整体高度也是 44vp,与每个单元格一致。

3.8 数据区 Grid

Grid() {
  ForEach(this.rows, (row: string[], idx: number) => {
    GridItem() {
      Row() {
        ForEach(row, (cell: string, cIdx: number) => {
          Text(cell)
            .width('25%')
            .height(44)
            .fontSize(14)
            .fontColor(cIdx === 3 ? this.scoreColor(cell) : '#333333')
            .fontWeight(cIdx === 3 && Number(cell) > 90 ? FontWeight.Bold : FontWeight.Normal)
            .textAlign(TextAlign.Center)
        }, (cell: string, cIdx: number) => cIdx.toString())
      }
      .width('100%')
      .height(44)
      .backgroundColor(idx % 2 === 1 ? '#f2f3f5' : '#ffffff')
    }
  }, (row: string[], idx: number) => idx.toString())
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.width('100%')
.height(220)
.margin({ top: 4 })

数据区是整个页面的核心。Grid 设置 4 列等宽模板('1fr 1fr 1fr 1fr')和 5 行等高模板('1fr 1fr 1fr 1fr 1fr'),容器高度 220vp,每行实际高度为 44vp。

每个 GridItem 内部是一个 RowRow 内用 ForEach 渲染 4 个 Text 单元格。关键样式:

  • 分数列颜色cIdx === 3 时调用 this.scoreColor(cell) 获取动态颜色。
  • 高分加粗cIdx === 3 && Number(cell) > 90 时字重为 Bold。
  • 斑马纹背景idx % 2 === 1 时背景为浅灰 #f2f3f5,否则为白色 #ffffff。即第 2 行和第 4 行为浅灰,第 1/3/5 行为白色。

3.9 统计卡片

Row() {
  Text('学生人数:' + this.rows.length)
    .fontSize(14)
    .fontColor('#333333')
  Blank()
  Text('平均分:' + this.average().toString())
    .fontSize(14)
    .fontWeight(FontWeight.Bold)
    .fontColor('#1a6cff')
}
.width('90%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.backgroundColor('#ffffff')
.borderRadius(12)
.margin({ top: 20 })

统计卡片使用 Row 布局,左侧显示学生人数(this.rows.length),右侧显示平均分(this.average().toString())。Blank() 弹性分隔将两个文本推到两端。平均分用蓝色加粗显示,突出关键信息。

3.10 排序按钮与提示

Button('排序')
  .width(200)
  .height(46)
  .backgroundColor('#1a6cff')
  .fontColor(Color.White)
  .margin({ top: 20 })
  .onClick(() => {
    this.sortByScore();
  })

Text('点击排序按钮,按分数从高到低排列')
  .fontSize(12)
  .fontColor('#aaaaaa')
  .margin({ top: 14 })

排序按钮宽度 200vp、高度 46vp,点击调用 sortByScore() 方法。按钮下方有灰色提示文字,引导用户操作。

4. 交互流程详解

4.1 页面加载流程

  1. 页面创建时,@State rows 初始化为 5 名学生的成绩数据。
  2. build 方法渲染表头行、数据区 Grid、统计卡片和排序按钮。
  3. average() 方法在渲染时被调用,计算并显示平均分。
  4. scoreColor() 方法对每个分数单元格调用,返回对应的颜色。

4.2 排序流程

用户点击「排序」按钮:

  1. sortByScore() 方法被调用。
  2. this.rows.slice() 创建原数组的浅拷贝 sorted
  3. sorted.sort(...) 按分数降序排列。排序后顺序为:赵六(96) → 张三(92) → 王五(88) → 李四(85) → 孙七(79)。
  4. this.rows = sorted.slice() 将新数组赋值给 @State 变量。
  5. ArkUI 检测到 @State 变量变化,重新渲染 Grid 中的 ForEach
  6. 表格行重新排列,斑马纹背景也随行索引变化而更新。
  7. 平均分文本不变(平均值与排序无关),但会重新计算。
  8. Toast 弹出「已按分数降序排序」提示。

4.3 斑马纹更新流程

排序后斑马纹会自动更新,因为背景色由 idx % 2 决定。排序前第 2 行是李四(浅灰),排序后第 2 行变为张三(浅灰)。这种设计确保了排序后仍然保持良好的可读性。

5. UI 样式设计思路

5.1 表头与数据区视觉分离

表头使用蓝色背景(#1a6cff)白色文字,与数据区的白色/浅灰背景形成鲜明对比,用户一眼就能区分表头和数据行。表头加粗显示,进一步强化了视觉层级。

5.2 斑马纹交替背景

斑马纹是表格设计的经典技巧——奇偶行使用不同的背景色,帮助用户在长表格中保持对行的跟踪。示例中偶数行(idx % 2 === 1,即第 2/4 行)使用浅灰色 #f2f3f5,奇数行使用白色 #ffffff。两种颜色都很浅,不会干扰数据阅读。

5.3 分数颜色分级

分数列的颜色分级策略:

  • 绿色 #2f9e44(>90):传达积极的信号,表示成绩优秀。
  • 红色 #e03131(<80):传达警示信号,表示需要关注。
  • 深灰 #333333(80~90):中性色,表示正常水平。

这种颜色策略不仅美观,更重要的是提升了信息传递效率——用户扫一眼就能识别高分和低分学生。

5.4 卡片式统计栏

统计栏采用白色圆角卡片设计,与页面浅灰色背景形成对比。学生人数在左、平均分在右的布局,让两个关键指标一目了然。平均分用蓝色加粗显示,作为页面的核心数据点。

6. 运行与测试

6.1 运行步骤

  1. 使用 DevEco Studio 打开项目。
  2. 运行项目到模拟器或真机。
  3. 在首页找到「表格数据」示例入口,点击进入。
  4. 查看表格的表头、数据行、斑马纹、分数颜色。
  5. 查看底部统计栏的学生人数和平均分。
  6. 点击「排序」按钮,观察表格行重新排列。

6.2 测试场景

测试场景 预期结果
页面加载 5 行数据按原始顺序显示,斑马纹交替,分数列有颜色分级
查看平均分 显示 88.0((92+85+88+96+79)/5 = 88.0)
点击「排序」按钮 行按分数降序排列:赵六(96)、张三(92)、王五(88)、李四(85)、孙七(79)
排序后斑马纹 斑马纹随行索引更新,仍然交替显示
排序后平均分 仍为 88.0(平均值不变)
排序后分数颜色 颜色分级仍然正确(96绿、92绿、88灰、85灰、79红)

7. 可扩展方向

7.1 多列排序

当前只支持按分数排序。可以扩展为点击表头切换排序列——点击「姓名」按字母排序,点击「年龄」按年龄排序,点击「城市」按城市排序。需要在表头添加 onClick 事件,并维护一个排序方向状态(升序/降序)。

7.2 数据编辑

为每个单元格添加点击编辑功能——点击单元格后弹出输入框,修改后更新数据并重新计算平均分。需要将 string[][] 改为可编辑的数据结构,并添加编辑状态的 UI 反馈。

7.3 分页加载

当数据量很大时(如 100+ 行),可以将数据分页展示,每页显示 10 行,底部添加翻页按钮。需要维护当前页码状态,并在 ForEach 中只渲染当前页的数据。

7.4 数据筛选

添加搜索框,输入关键字后只显示匹配的行。需要在 ForEach 中添加过滤逻辑,类似示例 94 的搜索列表功能。

7.5 导出功能

添加「导出」按钮,将表格数据导出为 CSV 文件,方便用户在 Excel 中进一步分析。需要使用 HarmonyOS 的文件系统 API。

8. 常见问题与调试

8.1 Grid 子组件报错

问题:编译报错 “The component ‘Grid’ can only have the child component ‘GridItem’”。

排查

  • 确认 Grid 的所有子组件都包裹在 GridItem 中。不能直接在 Grid 内放 RowText 等组件。
  • 检查 ForEach 的渲染函数是否返回 GridItem

8.2 排序后 UI 不更新

问题:点击「排序」按钮后,数据已排序但表格没有重新渲染。

排查

  • 确认 sortByScore 方法中使用了 this.rows = sorted.slice() 而非 this.rows.sort()。直接 sort 是原地修改,@State 感知不到变化。
  • 在排序后添加 console.log(this.rows[0][3]) 确认数据确实已更新。

8.3 斑马纹不正确

问题:斑马纹的颜色与预期不符,或所有行背景相同。

排查

  • 检查 idx % 2 的判断逻辑。idx 从 0 开始,第 1 行 idx=0(白色),第 2 行 idx=1(浅灰)。
  • 确认 backgroundColor 设置在 GridItem 内部的 Row 上,而不是 Text 上。如果设在 Text 上,只有文字区域有背景色,行间会有空隙。

8.4 平均分小数位不对

问题:平均分显示了过多小数位,如 88.333333。

排查

  • 确认 average() 方法中使用了 Math.round(sum / this.rows.length * 10) / 10 保留一位小数。
  • 如果需要两位小数,改为 Math.round(sum / this.rows.length * 100) / 100

8.5 列宽不均匀

问题:表格列宽不均匀,某些列特别宽或特别窄。

排查

  • 确认 columnsTemplate 使用了等分的 1fr,如 '1fr 1fr 1fr 1fr'
  • 检查每个 Textwidth 是否设为 '25%'(4 列等分)。
  • 确认 Gridwidth'100%',占满屏幕宽度。

9. 技术总结

示例 93 的表格数据展示了 Grid 组件在数据表格场景下的完整应用。通过这个示例,我们可以总结出表格实现的几个关键范式:

  1. Grid 模板定义:用 columnsTemplaterowsTemplate1fr 序列定义等分网格,简单直观。
  2. GridItem 强制包裹Grid 的子组件必须是 GridItem,否则编译报错。GridItem 内部可以放任意组件。
  3. ForEach 嵌套:外层遍历行数据生成 GridItem,内层遍历列数据生成单元格 Text,实现二维表格渲染。
  4. 数组整体替换:排序时必须用 slice() 创建新数组再赋值给 @State 变量,直接 sort 无法触发 UI 刷新。
  5. 条件样式绑定:用三元表达式根据列索引和单元格值动态计算颜色和字重,实现分数颜色分级和高分加粗。

掌握了这些范式后,就可以灵活地将 Grid 组件应用到各种数据展示场景中。表格作为数据可视化的基础形式,是每个鸿蒙开发者必须掌握的 UI 模式。

Logo

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

更多推荐