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

Angular 6中基于Material Design的嵌套表格实现财务支出报表问题

我完全理解你在Angular 6里用Material Table实现多层嵌套财务支出报表时遇到的行错位问题——这种层级深的嵌套数据和Material Table的扁平化数据源天然有点冲突,处理不好就容易列不对齐。下面是一套能完美匹配你需求的解决方案,一步一步来:

1. 先把嵌套数据转成带层级标识的扁平结构

Material Table更适合处理扁平化的数据集,所以我们先写一个递归函数,把你的嵌套JSON转成每行都包含层级信息、标签和数值字段的数组,这样后续绑定就不会错位了:

// 组件内定义递归扁平化函数
flattenExpenditureData(sections: any[]): any[] {
  const flatData: any[] = [];

  const traverse = (item: any, level: number, parentKey: string = '') => {
    // 基础字段:层级、父节点标识,以及所有数值列(无数据则设为null)
    const baseFields = {
      level,
      parentKey,
      actuals: item.actuals || null,
      budgetEstimate: item.budgetEstimate || null,
      revisedEstimate: item.revisedEstimate || null,
      budgetEstimateCurrentYear: item.budgetEstimateCurrentYear || null,
      isExpanded: true // 预留折叠功能的字段
    };

    // 根据节点类型生成标签和类型标识
    if (item.sections) {
      flatData.push({
        ...baseFields,
        type: 'section',
        label: item.sections
      });
      item.headDetail?.forEach(head => traverse(head, level + 1, item.sections));
    } else if (item.majorHeadNumber) {
      flatData.push({
        ...baseFields,
        type: 'major',
        label: `${item.majorHeadNumber} - ${item.majorHeadDescription}`
      });
      item.subMajorHeadDetail?.forEach(subMajor => traverse(subMajor, level + 1, item.majorHeadNumber));
    } else if (item.subMajorHeadNumber) {
      flatData.push({
        ...baseFields,
        type: 'subMajor',
        label: `${item.subMajorHeadNumber} - ${item.subMajorHeadDescription || 'N/A'}`
      });
      item.minorHeadDetail?.forEach(minor => traverse(minor, level + 1, item.subMajorHeadNumber));
    } else if (item.minorHeadNumber) {
      flatData.push({
        ...baseFields,
        type: 'minor',
        label: `${item.minorHeadNumber} - ${item.minorHeadDescription}`
      });
      item.subMinorHeadDetail?.forEach(subMinor => traverse(subMinor, level + 1, item.minorHeadNumber));
    } else if (item.subMinorHeadNumber) {
      flatData.push({
        ...baseFields,
        type: 'subMinor',
        label: `${item.subMinorHeadNumber} - ${item.subMinorHeadDescription}`
      });
      item.detailedHeadDetail?.forEach(detailed => traverse(detailed, level + 1, item.subMinorHeadNumber));
    } else if (item.detailedHeadNumber) {
      flatData.push({
        ...baseFields,
        type: 'detailed',
        label: `${item.detailedHeadNumber} - ${item.detailedHeadDescription}`
      });
      item.objectHeadDetails?.forEach(object => traverse(object, level + 1, item.detailedHeadNumber));
    } else if (item.objectHeadNumber) {
      flatData.push({
        ...baseFields,
        type: 'object',
        label: `${item.objectHeadNumber} - ${item.objectHeadDescription}`
      });
    }
  };

  sections.forEach(section => traverse(section, 0));
  return flatData;
}

// 组件初始化时处理数据
flatExpenditureData: any[] = [];
expenditureDataSource = new MatTableDataSource(this.flatExpenditureData);
displayedColumns = ['item', 'actuals', 'budgetEstimate', 'revisedEstimate', 'budgetEstimateCurrentYear'];

ngOnInit() {
  this.flatExpenditureData = this.flattenExpenditureData(this.detailOfExpenditureSection);
  this.expenditureDataSource.data = this.flatExpenditureData;
}

2. 自定义Material Table模板,实现层级缩进和列对齐

通过层级值设置左边距来区分不同层级的项目,同时给数值列设置右对齐,确保视觉上的整齐:

<table mat-table [dataSource]="expenditureDataSource" class="mat-elevation-z8">
  <!-- 支出项目列 -->
  <ng-container matColumnDef="item">
    <th mat-header-cell *matHeaderCellDef> Head Details </th>
    <td mat-cell *matCellDef="let element" [style.padding-left.px]="element.level * 24">
      <!-- 折叠按钮(仅非顶级节点显示) -->
      <button 
        *ngIf="element.level > 0 && hasChildren(element)" 
        mat-icon-button 
        (click)="toggleExpand(element)"
        class="expand-btn"
      >
        <mat-icon>{{ element.isExpanded ? 'expand_more' : 'chevron_right' }}</mat-icon>
      </button>
      <span [class]="getItemClass(element.type)">{{ element.label }}</span>
    </td>
  </ng-container>

  <!-- 数值列定义 -->
  <ng-container matColumnDef="actuals">
    <th mat-header-cell *matHeaderCellDef> Actuals </th>
    <td mat-cell *matCellDef="let element" class="numeric-cell">
      {{ element.actuals || '-' }}
    </td>
  </ng-container>

  <ng-container matColumnDef="budgetEstimate">
    <th mat-header-cell *matHeaderCellDef> Budget Estimate </th>
    <td mat-cell *matCellDef="let element" class="numeric-cell">
      {{ element.budgetEstimate || '-' }}
    </td>
  </ng-container>

  <ng-container matColumnDef="revisedEstimate">
    <th mat-header-cell *matHeaderCellDef> Revised Estimate </th>
    <td mat-cell *matCellDef="let element" class="numeric-cell">
      {{ element.revisedEstimate || '-' }}
    </td>
  </ng-container>

  <ng-container matColumnDef="budgetEstimateCurrentYear">
    <th mat-header-cell *matHeaderCellDef> Budget Estimate (Current Year) </th>
    <td mat-cell *matCellDef="let element" class="numeric-cell">
      {{ element.budgetEstimateCurrentYear || '-' }}
    </td>
  </ng-container>

  <!-- 表头和行渲染 -->
  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns;" [class.hidden-row]="!row.isExpanded"></tr>
</table>

3. 补充辅助函数和样式

添加折叠功能的辅助方法,以及区分层级的样式:

// 判断当前节点是否有子节点
hasChildren(element: any): boolean {
  return this.flatExpenditureData.some(item => 
    item.parentKey === element.label.split(' - ')[0] && item.level === element.level + 1
  );
}

// 切换折叠状态
toggleExpand(element: any) {
  element.isExpanded = !element.isExpanded;
  // 递归切换所有子节点的展开状态
  const toggleChildren = (parentKey: string, level: number) => {
    const children = this.flatExpenditureData.filter(item => 
      item.parentKey === parentKey && item.level === level
    );
    children.forEach(child => {
      child.isExpanded = element.isExpanded;
      toggleChildren(child.label.split(' - ')[0], level + 1);
    });
  };
  toggleChildren(element.label.split(' - ')[0], element.level + 1);
  // 更新数据源
  this.expenditureDataSource.data = [...this.flatExpenditureData];
}

// 返回层级对应的样式类
getItemClass(type: string): string {
  switch(type) {
    case 'section': return 'section-header';
    case 'major': return 'major-head';
    case 'subMajor': return 'submajor-head';
    default: return 'regular-item';
  }
}

对应的CSS样式:

.section-header {
  font-weight: bold;
  font-size: 1.1rem;
  color: #1976d2;
}

.major-head {
  font-weight: 600;
  color: #2d3748;
}

.submajor-head {
  font-weight: 500;
}

.numeric-cell {
  text-align: right;
  padding-right: 16px;
}

.expand-btn {
  padding: 4px;
  margin-right: 4px;
}

.hidden-row {
  display: none;
}

核心解决思路

之前的行错位问题本质是嵌套数据中父节点没有数值字段,导致Material Table渲染时部分列无内容,破坏了表格的网格对齐。通过扁平化处理后,每一行都包含所有列的字段(无数据则用占位符),再结合层级缩进和样式区分,就能完美实现你想要的嵌套报表效果,同时还支持折叠展开的交互优化。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 08:14:47