表格组件(DataGrid):开发高性能、可排序的复杂表格(78)

在鸿蒙(HarmonyOS)PC 端应用开发中,构建高性能、支持排序和筛选的复杂数据表格(DataGrid)是管理后台、数据分析等场景的核心诉求。结合鸿蒙 ArkUI 的组件特性与性能优化策略,以下是实现高级数据表格的核心架构与实战代码:

一、 基础架构:网格布局与列定义

鸿蒙原生推荐使用 Grid 组件结合 GridLayoutOptions 来构建表格的行列结构。通过预定义列宽模板(如 columnsTemplate),可以轻松实现类似 Excel 的固定列宽效果。

核心代码示例:

javascript 复制代码
// 定义表格列模型
interface GridColumn {
  key: string;
  label: string;
  width: number; // 单位 vp
  isNumeric: boolean;
}

// 构建表头
@Builder
TableHeader(columns: GridColumn[]) {
  GridRow() { // 或使用 Grid 配合 columnsTemplate
    ForEach(columns, (col: GridColumn) => {
      GridCol({ span: 1 }) {
        Text(col.label)
          .fontWeight(FontWeight.Medium)
          .padding(12)
      }
    })
  }
}

二、 核心业务:排序与筛选引擎

表格的高级交互依赖于数据层的处理。建议将排序和筛选逻辑从 UI 层剥离,封装为独立的引擎(如 SortEngineFilterEngine),通过状态变量驱动 UI 更新。

核心代码示例:

javascript 复制代码
// 排序方向枚举
enum SortDirection { ascending, descending }

// 排序处理逻辑
private sortData(columnKey: string, direction: SortDirection) {
  this.filteredData.sort((a, b) => {
    let valA = a[columnKey];
    let valB = b[columnKey];
    
    // 数字与字符串的比较逻辑区分
    let comparison = (valA > valB ? 1 : -1);
    return direction === SortDirection.ascending ? comparison : -comparison;
  });
}

// 结合 UI 触发排序
Text('年龄')
  .onClick(() => {
    this.currentSortDir = this.currentSortDir === SortDirection.ascending 
      ? SortDirection.descending : SortDirection.ascending;
    this.sortData('age', this.currentSortDir);
  })
1、 核心数据源引擎(DataTableSource)

为了实现高性能的排序与筛选,我们需要将数据处理逻辑从 UI 层剥离。通过继承或实现类似 DataTableSource 的机制,可以在不修改原始数据源的情况下,动态计算排序和过滤后的结果,并通知 UI 刷新。

javascript 复制代码
// 模拟用户数据模型
class User {
  id: number;
  name: string;
  email: string;
  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }
}

// 数据源处理引擎
class UserDataSource {
  private users: User[];
  private sortColumnIndex: number = 0;
  private isAscending: boolean = true;

  constructor(users: User[]) {
    this.users = users;
  }

  // 获取排序后的数据(避免直接修改原始数组)
  getSortedUsers(): User[] {
    const sorted = [...this.users];
    sorted.sort((a, b) => {
      let comparison = 0;
      if (this.sortColumnIndex === 0) comparison = a.id - b.id;
      else if (this.sortColumnIndex === 1) comparison = a.name.localeCompare(b.name);
      return this.isAscending ? comparison : -comparison;
    });
    return sorted;
  }

  // 更新排序状态
  updateSort(columnIndex: number, ascending: boolean) {
    this.sortColumnIndex = columnIndex;
    this.isAscending = ascending;
  }
}

2、 完整表格组件实战(PaginatedDataTable)

结合鸿蒙 ArkUI 的 Grid 组件,实现包含搜索框、可点击排序表头、分页控件以及拖拽排序的完整交互界面。

javascript 复制代码
@Entry
@Component
struct UserManagementScreen {
  // 1. 状态变量定义
  @State searchText: string = '';
  @State sortColumnIndex: number = 0;
  @State isAscending: boolean = true;
  @State currentPage: number = 0;
  @State rowsPerPage: number = 10;
  @State editMode: boolean = false; // 控制拖拽排序模式

  // 2. 模拟初始化数据与数据源
  private allUsers: User[] = [];
  private dataSource: UserDataSource = new UserDataSource([]);

  aboutToAppear() {
    // 生成100条模拟数据
    for (let i = 0; i < 100; i++) {
      this.allUsers.push(new User(i + 1, `用户${i + 1}`, `user${i + 1}@example.com`));
    }
    this.dataSource = new UserDataSource(this.allUsers);
  }

  // 3. 过滤与分页计算属性
  getFilteredData(): User[] {
    if (!this.searchText) return this.dataSource.getSortedUsers();
    return this.dataSource.getSortedUsers().filter(user => 
      user.name.toLowerCase().includes(this.searchText.toLowerCase()) ||
      user.email.toLowerCase().includes(this.searchText.toLowerCase())
    );
  }

  getPagedData(): User[] {
    const start = this.currentPage * this.rowsPerPage;
    return this.getFilteredData().slice(start, start + this.rowsPerPage);
  }

  // 4. 构建 UI
  build() {
    Column({ space: 16 }) {
      // 【搜索与操作区】
      Row({ space: 16 }) {
        TextInput({ placeholder: '搜索姓名或邮箱...', text: this.searchText })
          .layoutWeight(1)
          .onChange(value => {
            this.searchText = value;
            this.currentPage = 0; // 搜索时重置页码
          })
        
        Button(this.editMode ? '完成排序' : '编辑排序')
          .onClick(() => this.editMode = !this.editMode)
      }.width('100%').padding(16)

      // 【表格区域】
      Column() {
        // 表头(支持点击排序)
        GridRow() {
          GridCol({ span: 1 }) {
            Text(`ID ${this.sortColumnIndex === 0 ? (this.isAscending ? '↑' : '↓') : ''}`)
              .fontWeight(FontWeight.Bold).onClick(() => this.handleSort(0))
          }
          GridCol({ span: 2 }) {
            Text(`姓名 ${this.sortColumnIndex === 1 ? (this.isAscending ? '↑' : '↓') : ''}`)
              .fontWeight(FontWeight.Bold).onClick(() => this.handleSort(1))
          }
          GridCol({ span: 3 }) { Text('邮箱').fontWeight(FontWeight.Bold) }
        }.width('100%').padding(12).backgroundColor('#F5F5F5')

        // 表体(使用 Grid 支持拖拽与懒加载)
        Grid() {
          ForEach(this.getPagedData(), (user: User) => {
            GridItem() {
              GridRow() {
                GridCol({ span: 1 }) { Text(user.id.toString()) }
                GridCol({ span: 2 }) { Text(user.name) }
                GridCol({ span: 3 }) { Text(user.email) }
              }.width('100%').padding(12)
            }
          }, (user: User) => user.id.toString())
        }
        .columnsTemplate('1fr 2fr 3fr') // 定义列宽比例
        .editMode(this.editMode) // 开启编辑模式
        .onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number) => {
          // 拖拽放下时,交换数据源中的位置并触发UI更新
          const sortedData = this.getFilteredData();
          const temp = sortedData[itemIndex];
          sortedData[itemIndex] = sortedData[insertIndex];
          sortedData[insertIndex] = temp;
        })
        .layoutWeight(1)
      }.width('100%').border({ width: 1, color: '#E0E0E0' })

      // 【分页控件】
      Row({ space: 16 }) {
        Text(`共 ${this.getFilteredData().length} 条`)
        Button('上一页').enabled(this.currentPage > 0).onClick(() => this.currentPage--)
        Text(`第 ${this.currentPage + 1} 页`)
        Button('下一页').enabled((this.currentPage + 1) * this.rowsPerPage < this.getFilteredData().length).onClick(() => this.currentPage++)
      }.width('100%').justifyContent(FlexAlign.End).padding(16)
    }
  }

  // 排序处理逻辑
  private handleSort(columnIndex: number) {
    if (this.sortColumnIndex === columnIndex) {
      this.isAscending = !this.isAscending;
    } else {
      this.sortColumnIndex = columnIndex;
      this.isAscending = true;
    }
    this.dataSource.updateSort(this.sortColumnIndex, this.isAscending);
  }
}

三、 性能优化:海量数据渲染与不规则布局

在 PC 端展示成千上万条数据时,性能是最大瓶颈。必须遵循以下鸿蒙性能优化规范:

  1. 强制懒加载(LazyForEach) :严禁使用 ForEach 渲染全量数据。必须配合 LazyForEach 实现按需渲染,并通过 cachedCount 预加载可视区域外的数据,防止快速滚动时出现白屏。
  2. 使用 GridLayoutOptions 替代动态跨列 :如果表格存在合并单元格等不规则布局,切忌在 LazyForEach 内部使用 if/else 结合 columnStart/columnEnd 动态计算跨度。这会导致系统在 scrollToIndex 时触发全量遍历(耗时极高)。应提前将不规则项的索引存入数组,通过 irregularIndexes 预定义规则,将时间复杂度从 O(n) 降至 O(1)。

核心代码示例:

javascript 复制代码
Grid(this.scroller, {
  layoutOptions: {
    regularSize: [1, 1],
    irregularIndexes: this.irregularData // 提前计算好的不规则项索引
  }
}) {
  LazyForEach(this.dataSource, (item: DataItem) => {
    GridItem() {
      TableRowComponent({ data: item })
    }
  }, (item: DataItem) => item.id) // 必须提供稳定的 keyGenerator
}
.cachedCount(5) // 上下各缓存 5 屏数据
.columnsTemplate('80vp 150vp 1fr 120vp') // 固定列宽与自适应列

四、 桌面级交互:PC 端专属特性增强

PC 端用户操作精准,表格应充分利用鼠标与键盘的交互优势:

  1. 多选与框选(multiSelectable) :开启 multiSelectable(true) 属性,允许用户使用鼠标拖拽框选多个行,配合 onSelect 事件实现批量删除、导出等操作。
  2. 拖拽排序(editMode) :通过设置 .editMode(true),允许用户长按并拖拽 GridItem 来调整行顺序或自定义列宽。
  3. 滚动条联动 :结合前文提到的 ScrollBar 组件,为表格配置常驻的自定义滚动条,并预留 Gutter 空间防止表头与内容区错位。
  4. 虚拟滚动与吸顶表头 :对于超长表格,表头必须实现"吸顶"效果。可以将表头与表体分离,表体使用独立的 Scroll 容器,并通过监听 onScrollIndexonScroll 事件,同步表头与表体的水平滚动偏移量。
  5. 避免单帧加载过多数据 :在初始化或切换筛选条件时,如果一次性向 LazyForEach 注入过多数据,会导致首帧渲染耗时飙升。建议将数据合理拆分,每帧仅加载一小部分(如半个月的数据或 50 条记录),保证 UI 响应的流畅度。
  6. 组件复用 :在 LazyForEach 中,务必为 GridItem 内部的复杂单元格(如包含头像、进度条、操作按钮的复合组件)实现 @Reusable 装饰器,以最大化复用组件实例,减少内存分配与 GC 压力。
  7. 状态隔离 :表格的排序状态、筛选条件、分页页码应统一收敛到一个 TableViewModel 中管理。UI 层仅作为该 ViewModel 的只读映射,避免状态更新时的重复计算。