当前位置:首页>鸿蒙APP>鸿蒙 ArkUI 中如何实现可横向滚动的表格

鸿蒙 ArkUI 中如何实现可横向滚动的表格

  • 2026-10-20 05:24:50
鸿蒙 ArkUI 中如何实现可横向滚动的表格

在移动端开发里,“表格”一直不是一个特别轻松的 UI 形态。
尤其是在鸿蒙 ArkUI 中,当我们遇到“列很多、部分列要固定、其余列可以横向滑动、表头还要支持排序”这样的需求时,通常不会直接使用一个现成的 Table 组件,而是采用布局组合的方式自己搭建。

这篇文章就结合我在项目里的一个实际页面,记录一下鸿蒙中表格的实现思路和落地方式。

这次要实现的是一个“业绩排行表”,一共 8 列:

  • • 排名
  • • 姓名
  • • 总业绩(元)
  • • 买卖业绩(元)
  • • 租赁业绩(元)
  • • 总单数
  • • 买卖单数
  • • 租赁单数

其中有两个核心要求:

  1. 1. 排名 和 姓名 要固定在左侧
  2. 2. 其他列可以横向滑动
  3. 3. 右侧这些列的表头支持点击排序,可以切换升序和降序

一、为什么不用传统 Table 组件

在 ArkUI 里,并没有一个特别适合这种“固定列 + 横向滚动 + 排序交互”的高阶表格组件。
如果只是简单的两三列数据展示,用 List、Grid 或普通 Column + Row 就够了。
但一旦进入这种“移动端复杂表格”的场景,最稳妥的方案其实是:

  • • 用 Row 把整个表格拆成左右两部分
  • • 左侧放固定列
  • • 右侧放横向 Scroll
  • • 每一行自己用 Row 对齐单元格
  • • 排序交给 @State 自己维护

也就是说,这不是“调用一个组件解决问题”,而是“通过布局拼装出表格”。

二、页面需求拆解

这次页面里的业绩排行区域,原本是卡片式列表,后来改成了更适合数据对比的表格形式。
整个结构拆开后,可以理解为下面三层:

第一层:数据层
接口返回的数据模型是 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)

这里有几个实现重点:

  • • 右侧列全部放进 Scroll
  • • 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
  }
}

这样做的好处是:

  • • 视图层只关心“当前按哪列排”
  • • 数据层只负责“根据字段取值”
  • • 后续如果还要增加列,比如“人均业绩”“人均单量”,扩展会比较容易

六、固定列和滚动列为什么能对齐

这是很多人第一次做这种表格最容易出错的地方。

固定列和滚动列看起来像一张完整表格,但实际上它们是左右两个独立布局。
之所以能看起来“像一体的”,靠的是以下几个细节:

  • • 表头高度统一
  • • 每一行高度统一
  • • 每个单元格都加统一边框
  • • 左右区域数据顺序完全一致
  • • 左右区域使用相同的数据源 rankList

在当前页面里,我单独定义了这些尺寸常量:

privatereadonlyrankNoColumnWidth: number = 52
privatereadonlyrankNameColumnWidth: number = 116
privatereadonlyrankMetricColumnWidth: number = 116
privatereadonlyrankHeaderHeight: number = 44
privatereadonlyrankRowHeight: number = 76

这类尺寸最好不要散落在代码里,而是集中声明。
因为一旦视觉要调整,比如“姓名列再宽一点”“行高改成 68”,你只改常量就行。

七、这个页面里的实践结论

对于移动端 ArkUI 页面来说,表格的核心不是“找一个 Table 组件”,而是“把它拆解成布局问题”。

具体来说就是:

  • • 固定列,用普通 Column + Row
  • • 可滑动列,用 ScrollDirection.Horizontal
  • • 排序,用 @State + sort()
  • • 单元格统一尺寸,保证视觉对齐
  • • 通过 builder 抽离表头和单元格,减少重复代码

这套方式不算“最省代码”,但在实际项目里是非常稳的。
尤其是像业绩排行、统计报表、明细对账这种页面,基本都能沿着这个思路往下扩展。

结语

鸿蒙 ArkUI 在做移动端业务页面时,很多复杂组件本质上都不是“直接拿来就用”,而是靠布局能力和状态管理能力一点点拼出来的。

只要把问题拆成“固定列、滚动列、排序状态、数据映射”这几个部分,复杂的表格需求其实也能稳定落地。
而且一旦第一版搭起来,后面无论是换字段、加列、改排序,成本都会低很多。

最新文章

随机文章