鸿蒙 ArkTS 实战:表格数据 Grid 学生成绩表(示例 93)

引言
在数据展示类应用中,表格(Table/Grid)是最经典的数据呈现方式。从 Excel 的电子表格到数据库管理系统的查询结果,从学生成绩单到财务报表,表格以其行列表头的结构化布局,让用户能够快速浏览、对比和查找数据。HarmonyOS NEXT 的 ArkUI 框架提供了 Grid 组件,通过 columnsTemplate 和 rowsTemplate 属性定义网格的行列模板,配合 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 中放 Text、Row 等其他组件,编译会报错。
// 正确写法
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 内部放一个 Row,Row 内部再用 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 调用:
- 第一层
this.rows.slice():创建原数组的浅拷贝,避免直接修改原数组(sort会原地修改)。 - 第二层
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 > 0 时 b 排前面,即高分在前。
3. 源码逐段解析
现在开始按源码顺序逐段解读 index93.ets,从导入声明到 build 方法,完整展示表格数据的实现细节。
3.1 导入声明与组件声明
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct Index93 {
导入 router 和 promptAction,声明页面入口组件 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 方法计算所有学生分数的平均值:
- 用
for循环遍历每一行,this.rows[i][3]取第 4 列(分数列),Number()转为数值后累加到sum。 sum / this.rows.length计算平均值。Math.round(sum / this.rows.length * 10) / 10实现保留一位小数——先乘以 10,四舍五入取整,再除以 10。例如87.4 * 10 = 874,Math.round(874) = 874,874 / 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 方法实现按分数降序排序:
this.rows.slice()创建原数组的浅拷贝。sorted.sort(...)按分数降序排列——比较函数取b[3](后者的分数)减去a[3](前者的分数),结果为正时后者排前。this.rows = sorted.slice()将排序后的新数组赋值给@State变量,触发 UI 刷新。- 弹出 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 内部是一个 Row,Row 内用 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 页面加载流程
- 页面创建时,
@State rows初始化为 5 名学生的成绩数据。 build方法渲染表头行、数据区 Grid、统计卡片和排序按钮。average()方法在渲染时被调用,计算并显示平均分。scoreColor()方法对每个分数单元格调用,返回对应的颜色。
4.2 排序流程
用户点击「排序」按钮:
sortByScore()方法被调用。this.rows.slice()创建原数组的浅拷贝sorted。sorted.sort(...)按分数降序排列。排序后顺序为:赵六(96) → 张三(92) → 王五(88) → 李四(85) → 孙七(79)。this.rows = sorted.slice()将新数组赋值给@State变量。- ArkUI 检测到
@State变量变化,重新渲染Grid中的ForEach。 - 表格行重新排列,斑马纹背景也随行索引变化而更新。
- 平均分文本不变(平均值与排序无关),但会重新计算。
- 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 运行步骤
- 使用 DevEco Studio 打开项目。
- 运行项目到模拟器或真机。
- 在首页找到「表格数据」示例入口,点击进入。
- 查看表格的表头、数据行、斑马纹、分数颜色。
- 查看底部统计栏的学生人数和平均分。
- 点击「排序」按钮,观察表格行重新排列。
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内放Row、Text等组件。 - 检查
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'。 - 检查每个
Text的width是否设为'25%'(4 列等分)。 - 确认
Grid的width为'100%',占满屏幕宽度。
9. 技术总结
示例 93 的表格数据展示了 Grid 组件在数据表格场景下的完整应用。通过这个示例,我们可以总结出表格实现的几个关键范式:
- Grid 模板定义:用
columnsTemplate和rowsTemplate的1fr序列定义等分网格,简单直观。 - GridItem 强制包裹:
Grid的子组件必须是GridItem,否则编译报错。GridItem内部可以放任意组件。 - ForEach 嵌套:外层遍历行数据生成
GridItem,内层遍历列数据生成单元格Text,实现二维表格渲染。 - 数组整体替换:排序时必须用
slice()创建新数组再赋值给@State变量,直接sort无法触发 UI 刷新。 - 条件样式绑定:用三元表达式根据列索引和单元格值动态计算颜色和字重,实现分数颜色分级和高分加粗。
掌握了这些范式后,就可以灵活地将 Grid 组件应用到各种数据展示场景中。表格作为数据可视化的基础形式,是每个鸿蒙开发者必须掌握的 UI 模式。
更多推荐



所有评论(0)