You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中创建支持单元格合并的数据表(适配API数据)

Flutter DataTable 单元格合并实现

需求

需要实现指定样式的DataTable:核心要求为**前两列(Zone、Point)和最后两列(Accessories、Remarks)**的单元格,根据中间警员数据的行数进行跨行合并。

当前已通过API获取数据并实现基础DataTable,但缺少单元格合并功能,现有代码如下:

DataTable(
      columns: const [
        DataColumn(label: Text("Zone")),
        DataColumn(label: Text("Point")),
        DataColumn(label: Text("Designation")),
        DataColumn(label: Text("Buckle Number")),
        DataColumn(label: Text("Police Name")),
        DataColumn(label: Text("District")),
        DataColumn(label: Text("Police Station Name")),
        DataColumn(label: Text("Age")),
        DataColumn(label: Text("Gender")),
        DataColumn(label: Text("Number")),
        DataColumn(label: Text("Accessories")),
        DataColumn(label: Text("Remarks"))
      ],
      rows: controller.eventAssignmentModel.value?.pointAssignments![index]
              .assignedPoliceList!
              .asMap()
              .map((index4, police) => MapEntry(
                  index4,
                  DataRow(cells: [
                    DataCell(Text(controller.eventAssignmentModel.value!.pointAssignments![index].zoneName!)),
                    DataCell(Text(controller.eventAssignmentModel.value!.pointAssignments![index].pointName!)),
                    DataCell(Text(police.designation ?? '')),
                    DataCell(Text(police.buckleNumber ?? '')),
                    DataCell(Text(police.policeName ?? '')),
                    DataCell(Text(police.district ?? '')),
                    DataCell(Text(police.policeStationName ?? '')),
                    DataCell(Text(police.age ?? '')),
                    DataCell(Text(police.gender ?? '')),
                    DataCell(Text(police.number ?? '')),
                    DataCell(Text(controller.eventAssignmentModel.value!.pointAssignments![index].pointAccessories!)),
                    DataCell(Text(controller.eventAssignmentModel.value!.pointAssignments![index].pointRemarks!)),
                  ])))
              .values
              .toList() ??
          const [],
    ),

实现方案

Flutter原生DataTable不支持单元格合并,推荐以下两种实现方式:

方案一:使用第三方包(推荐)

采用data_table_2包,它支持单元格跨行合并,且自带自适应、滚动等功能。

  1. 添加依赖到pubspec.yaml:
dependencies:
  data_table_2: ^2.5.0
  1. 修改代码实现合并:
import 'package:data_table_2/data_table_2.dart';

// 构建带合并的表格
DataTable2(
  columnSpacing: 12,
  horizontalMargin: 12,
  minWidth: 600,
  columns: const [
    DataColumn2(label: Text("Zone"), size: ColumnSize.S),
    DataColumn2(label: Text("Point"), size: ColumnSize.S),
    DataColumn2(label: Text("Designation"), size: ColumnSize.M),
    DataColumn2(label: Text("Buckle Number"), size: ColumnSize.M),
    DataColumn2(label: Text("Police Name"), size: ColumnSize.M),
    DataColumn2(label: Text("District"), size: ColumnSize.M),
    DataColumn2(label: Text("Police Station Name"), size: ColumnSize.M),
    DataColumn2(label: Text("Age"), size: ColumnSize.S),
    DataColumn2(label: Text("Gender"), size: ColumnSize.S),
    DataColumn2(label: Text("Number"), size: ColumnSize.M),
    DataColumn2(label: Text("Accessories"), size: ColumnSize.L),
    DataColumn2(label: Text("Remarks"), size: ColumnSize.L),
  ],
  rows: _buildMergedRows(controller, index),
)

// 生成带合并单元格的行列表
List<DataRow> _buildMergedRows(YourController controller, int index) {
  final point = controller.eventAssignmentModel.value?.pointAssignments![index];
  if (point == null || point.assignedPoliceList == null) return [];
  
  final policeList = point.assignedPoliceList!;
  final rowCount = policeList.length;
  final rows = <DataRow>[];

  for (int i = 0; i < rowCount; i++) {
    final police = policeList[i];
    rows.add(DataRow(
      cells: [
        // Zone列:跨行合并,仅第一行显示内容
        DataCell(
          Text(point.zoneName!),
          placeholder: i != 0,
          rowSpan: rowCount,
        ),
        // Point列:同理合并
        DataCell(
          Text(point.pointName!),
          placeholder: i != 0,
          rowSpan: rowCount,
        ),
        // 中间列正常渲染警员数据
        DataCell(Text(police.designation ?? '')),
        DataCell(Text(police.buckleNumber ?? '')),
        DataCell(Text(police.policeName ?? '')),
        DataCell(Text(police.district ?? '')),
        DataCell(Text(police.policeStationName ?? '')),
        DataCell(Text(police.age ?? '')),
        DataCell(Text(police.gender ?? '')),
        DataCell(Text(police.number ?? '')),
        // Accessories列:跨行合并
        DataCell(
          Text(point.pointAccessories!),
          placeholder: i != 0,
          rowSpan: rowCount,
        ),
        // Remarks列:跨行合并
        DataCell(
          Text(point.pointRemarks!),
          placeholder: i != 0,
          rowSpan: rowCount,
        ),
      ],
    ));
  }
  return rows;
}

方案二:原生Table组件实现

如果不想依赖第三方包,可使用Flutter原生Table组件手动实现合并:

Table(
  border: TableBorder.all(color: Colors.grey),
  children: _buildCustomTableRows(controller, index),
)

List<TableRow> _buildCustomTableRows(YourController controller, int index) {
  final point = controller.eventAssignmentModel.value?.pointAssignments![index];
  if (point == null || point.assignedPoliceList == null) return [];
  
  final policeList = point.assignedPoliceList!;
  final rowCount = policeList.length;
  final rows = <TableRow>[];

  for (int i = 0; i < rowCount; i++) {
    final police = policeList[i];
    rows.add(TableRow(
      children: [
        // Zone列合并:仅第一行渲染内容,其余行留空
        i == 0
            ? TableCell(
                verticalAlignment: TableCellVerticalAlignment.fill,
                rowSpan: rowCount,
                child: Container(
                  alignment: Alignment.center,
                  padding: const EdgeInsets.all(8),
                  child: Text(point.zoneName!),
                ),
              )
            : const TableCell(child: SizedBox()),
        // Point列合并
        i == 0
            ? TableCell(
                verticalAlignment: TableCellVerticalAlignment.fill,
                rowSpan: rowCount,
                child: Container(
                  alignment: Alignment.center,
                  padding: const EdgeInsets.all(8),
                  child: Text(point.pointName!),
                ),
              )
            : const TableCell(child: SizedBox()),
        // 中间警员信息列
        TableCell(
          child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(8),
            child: Text(police.designation ?? ''),
          ),
        ),
        TableCell(
          child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(8),
            child: Text(police.buckleNumber ?? ''),
          ),
        ),
        TableCell(
          child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(8),
            child: Text(police.policeName ?? ''),
          ),
        ),
        TableCell(
          child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(8),
            child: Text(police.district ?? ''),
          ),
        ),
        TableCell(
          child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(8),
            child: Text(police.policeStationName ?? ''),
          ),
        ),
        TableCell(
          child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(8),
            child: Text(police.age ?? ''),
          ),
        ),
        TableCell(
          child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(8),
            child: Text(police.gender ?? ''),
          ),
        ),
        TableCell(
          child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(8),
            child: Text(police.number ?? ''),
          ),
        ),
        // Accessories列合并
        i == 0
            ? TableCell(
                verticalAlignment: TableCellVerticalAlignment.fill,
                rowSpan: rowCount,
                child: Container(
                  alignment: Alignment.center,
                  padding: const EdgeInsets.all(8),
                  child: Text(point.pointAccessories!),
                ),
              )
            : const TableCell(child: SizedBox()),
        // Remarks列合并
        i == 0
            ? TableCell(
                verticalAlignment: TableCellVerticalAlignment.fill,
                rowSpan: rowCount,
                child: Container(
                  alignment: Alignment.center,
                  padding: const EdgeInsets.all(8),
                  child: Text(point.pointRemarks!),
                ),
              )
            : const TableCell(child: SizedBox()),
      ],
    ));
  }
  return rows;
}

注意事项

  • 第三方包方案开发效率更高,适合复杂表格场景;
  • 原生方案需自行处理滚动、响应式等细节,但无额外依赖;
  • 核心逻辑:同一个point下的所有警员行,合并列的第一个单元格设置rowSpan为警员总数,其余行对应位置留空。

内容的提问来源于stack exchange,提问作者Parthish Joshi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 05:45:00