在移动端开发里,“表格”一直不是一个特别轻松的 UI 形态。
尤其是在鸿蒙 ArkUI 中,当我们遇到“列很多、部分列要固定、其余列可以横向滑动、表头还要支持排序”这样的需求时,通常不会直接使用一个现成的 Table 组件,而是采用布局组合的方式自己搭建。
这篇文章就结合我在项目里的一个实际页面,记录一下鸿蒙中表格的实现思路和落地方式。
这次要实现的是一个“业绩排行表”,一共 8 列:
其中有两个核心要求:
- 3. 右侧这些列的表头支持点击排序,可以切换升序和降序

一、为什么不用传统 Table 组件
在 ArkUI 里,并没有一个特别适合这种“固定列 + 横向滚动 + 排序交互”的高阶表格组件。
如果只是简单的两三列数据展示,用 List、Grid 或普通 Column + Row 就够了。
但一旦进入这种“移动端复杂表格”的场景,最稳妥的方案其实是:
也就是说,这不是“调用一个组件解决问题”,而是“通过布局拼装出表格”。
二、页面需求拆解
这次页面里的业绩排行区域,原本是卡片式列表,后来改成了更适合数据对比的表格形式。
整个结构拆开后,可以理解为下面三层:
第一层:数据层
接口返回的数据模型是 RankEntity。
里面包含这次表格需要的核心字段:
exportclassRankEntity {
Dept1Name: string;
Dept2Name: string;
Dept3Name: string;
Dept4Name: string;
Dept5Name: string;
IsDelStr: string;
RowNum: number;
NAME: string;
DepartName: string;
EmpNo: number;
CustomEmpNo: string;
DCODE: string;
IsMyPrivilege: number;
SellPerformance: number;
SellRealityPerformance: number;
RentPerformance: number;
RentRealityPerformance: number;
RentSum: number;
SellSum: number;
SignSum: number;
AllPerformance: number;
RealityPerformance: number;
DepLevel: number;
AvgPeopleNum: number;
AvgPerformance: number;
AvgSign: number;
IsDel: number;
PositionName: string;
EntryTime: string;
}
第二层:状态层
在页面里增加排序字段和排序方向,用于控制表格当前的展示状态。
例如:
@StaterankSortField: string = "AllPerformance"
@StaterankSortAscending: boolean = false
这里表示:
第三层:视图层
真正的表格主体在 rankBuild 中实现。
/**
* 构建部门排行组件
*
*/
@Builder
rankBuild() {
Column() {
if (this.getSortedPerformanceRankList().length == 0) {
Column() {
Image($r("app.media.icon_empty_content")).width(200)
Text("暂无数据")
}.width("100%")
.padding({ bottom: 100 })
.alignItems(HorizontalAlign.Center)
} else {
Row() {
Column() {
Row() {
this.rankHeaderCell("排名", this.rankNoColumnWidth)
this.rankHeaderCell(this.isSubset == 1 ? "姓名" : "部门", this.rankNameColumnWidth)
}
ForEach(this.getSortedPerformanceRankList(), (item: RankEntity, index: number) => {
Row() {
this.rankBodyCell((index + 1).toString(), this.rankNoColumnWidth)
this.rankNameCell(this.getRankName(item), this.rankNameColumnWidth)
}
})
}
.width(this.rankNoColumnWidth + this.rankNameColumnWidth)
.backgroundColor(Color.White)
Scroll() {
Column() {
Row() {
this.rankSortHeaderCell("总业绩(元)", "AllPerformance")
this.rankSortHeaderCell("买卖业绩(元)", "SellPerformance")
this.rankSortHeaderCell("租赁业绩(元)", "RentPerformance")
this.rankSortHeaderCell("总单数", "SignSum")
this.rankSortHeaderCell("买卖单数", "SellSum")
this.rankSortHeaderCell("租赁单数", "RentSum")
}
ForEach(this.getSortedPerformanceRankList(), (item: RankEntity) => {
Row() {
this.rankBodyCell(this.formatRankNumber(item.AllPerformance), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.SellPerformance), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.RentPerformance), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.SignSum), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.SellSum), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.RentSum), this.rankMetricColumnWidth)
}
})
}
}
.layoutWeight(1)
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.backgroundColor(Color.White)
}
.width("100%")
.borderRadius(8)
.clip(true)
}
}
.alignItems(HorizontalAlign.Start)
.padding({ left: 12, right: 12 })
}
三、表格结构怎么搭
1. 左侧固定列
固定列本质上就是一个普通的 Column,里面按“表头 + 多行数据”的方式渲染:
Column() {
Row() {
this.rankHeaderCell("排名", this.rankNoColumnWidth)
this.rankHeaderCell(this.isSubset == 1 ? "姓名" : "部门", this.rankNameColumnWidth)
}
ForEach(rankList, (item: RankEntity, index: number) => {
Row() {
this.rankBodyCell((index + 1).toString(), this.rankNoColumnWidth)
this.rankNameCell(this.getRankName(item), this.rankNameColumnWidth)
}
})
}
这里的关键点是:
这样左边看起来才像是和右边属于同一张表。
2. 右侧可横向滑动列
右侧 6 列放在一个 Scroll 中,并设置为横向滚动:
Scroll() {
Column() {
Row() {
this.rankSortHeaderCell("总业绩(元)", "AllPerformance")
this.rankSortHeaderCell("买卖业绩(元)", "SellPerformance")
this.rankSortHeaderCell("租赁业绩(元)", "RentPerformance")
this.rankSortHeaderCell("总单数", "SignSum")
this.rankSortHeaderCell("买卖单数", "SellSum")
this.rankSortHeaderCell("租赁单数", "RentSum")
}
ForEach(rankList, (item: RankEntity) => {
Row() {
this.rankBodyCell(this.formatRankNumber(item.AllPerformance), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.SellPerformance), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.RentPerformance), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.SignSum), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.SellSum), this.rankMetricColumnWidth)
this.rankBodyCell(this.formatRankNumber(item.RentSum), this.rankMetricColumnWidth)
}
})
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
这里有几个实现重点:
- •
scrollable(ScrollDirection.Horizontal) 表示允许横向滑动 - •
scrollBar(BarState.Off) 可以隐藏滚动条,让界面更干净
这种方式比强行压缩所有列更合理,也更符合用户操作习惯。
四、表头排序怎么做
表格要支持排序,最重要的是把“排序规则”从 UI 里拆出来,变成独立状态。
我在页面里用了两个状态值:
@StaterankSortField: string = "AllPerformance"
@StaterankSortAscending: boolean = false
然后点击表头时调用一个切换方法 toggleRankSort:
privatetoggleRankSort(field: string) {
if (this.rankSortField == field) {
this.rankSortAscending = !this.rankSortAscending
return
}
this.rankSortField = field
this.rankSortAscending = false
}
这段逻辑的意思很直接:
- • 如果点击的是当前已经选中的列,就切换升序/降序
- • 如果点击的是新列,就把排序字段切到新列,并默认按降序排列
这样交互上就比较符合用户预期。
五、排序结果如何生成
真正的数据排序逻辑在 getSortedPerformanceRankList 中完成:
privategetSortedPerformanceRankList(): RankEntity[] {
let list = [...this.performanceRankList]
list.sort((left: RankEntity, right: RankEntity) => {
let leftValue = this.getRankSortValue(left, this.rankSortField)
let rightValue = this.getRankSortValue(right, this.rankSortField)
if (leftValue == rightValue) {
returnthis.rankSortAscending ? left.RowNum - right.RowNum : right.RowNum - left.RowNum
}
returnthis.rankSortAscending ? leftValue - rightValue : rightValue - leftValue
})
return list
}
这里做了两件事:
第一,把原始列表复制一份
这样不会直接修改接口原始数据,避免副作用。
第二,根据当前排序字段动态取值
字段映射逻辑在 getRankSortValue 中:
privategetRankSortValue(item: RankEntity, field: string): number {
switch (field) {
case"SellPerformance":
return item.SellPerformance ?? 0
case"RentPerformance":
return item.RentPerformance ?? 0
case"SignSum":
return item.SignSum ?? 0
case"SellSum":
return item.SellSum ?? 0
case"RentSum":
return item.RentSum ?? 0
default:
return item.AllPerformance ?? 0
}
}
这样做的好处是:
- • 后续如果还要增加列,比如“人均业绩”“人均单量”,扩展会比较容易
六、固定列和滚动列为什么能对齐
这是很多人第一次做这种表格最容易出错的地方。
固定列和滚动列看起来像一张完整表格,但实际上它们是左右两个独立布局。
之所以能看起来“像一体的”,靠的是以下几个细节:
在当前页面里,我单独定义了这些尺寸常量:
privatereadonlyrankNoColumnWidth: number = 52
privatereadonlyrankNameColumnWidth: number = 116
privatereadonlyrankMetricColumnWidth: number = 116
privatereadonlyrankHeaderHeight: number = 44
privatereadonlyrankRowHeight: number = 76
这类尺寸最好不要散落在代码里,而是集中声明。
因为一旦视觉要调整,比如“姓名列再宽一点”“行高改成 68”,你只改常量就行。
七、这个页面里的实践结论
对于移动端 ArkUI 页面来说,表格的核心不是“找一个 Table 组件”,而是“把它拆解成布局问题”。
具体来说就是:
- • 可滑动列,用
ScrollDirection.Horizontal - • 通过 builder 抽离表头和单元格,减少重复代码
这套方式不算“最省代码”,但在实际项目里是非常稳的。
尤其是像业绩排行、统计报表、明细对账这种页面,基本都能沿着这个思路往下扩展。
结语
鸿蒙 ArkUI 在做移动端业务页面时,很多复杂组件本质上都不是“直接拿来就用”,而是靠布局能力和状态管理能力一点点拼出来的。
只要把问题拆成“固定列、滚动列、排序状态、数据映射”这几个部分,复杂的表格需求其实也能稳定落地。
而且一旦第一版搭起来,后面无论是换字段、加列、改排序,成本都会低很多。