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

Angular删除数据后维持当前Tab选中状态的技术咨询

问题

我使用mat-tab-group实现了多Tab切换功能,在第二个Tab中删除数据后,页面重新加载并自动切回第一个Tab。我需要实现删除操作后仍停留在当前Tab的效果。请问仅修改deleteViolation函数即可实现该需求,还是需要其他额外操作?

相关代码如下:

Tab组HTML代码

<div class="card-header" *ngIf="!loading">
<mat-tab-group (selectedTabChange)="changeActiveTab($event)">
    <mat-tab *ngFor="let tab of topTabs" [label]="tab">
    </mat-tab>
</mat-tab-group>
</div>

删除操作菜单HTML代码

<mat-menu #rowActions>
                    <button mat-menu-item (click)="navigate(['/app/violations/detail/' + violation.id])">View</button>
                    <button *ngIf="hasWriteAccess" mat-menu-item (click)="deleteViolation(violation)">Delete</button>
                </mat-menu>

TypeScript代码

export class UnitViolationListComponent implements OnInit, AfterViewInit {
  @Input() unitId: number = null;
  @Input() unit: Unit;

  searchValue: string = '';

  // Tabs
  activeTab: string = 'All Outstanding';
  topTabs: string[] = [
    'All Outstanding',
    'Completed',
  ];

  downloadingPdf: boolean = false;

  tags: any[] = [];
  unitTags: any[] = [];
  unitOrgTags: Tag[];

  completeViolations: ViolationStatement[] = [];
  notCompleteViolations: ViolationStatement[] = [];
  violations: ViolationStatement[] = [];

  tableDataSource: MatTableDataSource<ViolationStatement> = new MatTableDataSource<ViolationStatement>();
  displayedColumns: string[] = [
    'unit',
    'title',
    'createdAt',
    'resolutionTime',
    'completedTime',
    'actions',
  ];
  pageSizeOptions: number[] = [25, 50, 100, 200];
  orgViolationStatuses: ViolationStatus[] = [];
  @ViewChild(MatTable) table: MatTable<any>;
  @ViewChild(MatPaginator) matpaginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;

  // Component State
  uploading: boolean = false;
  loading: boolean = true;

  hasWriteAccess: boolean = false;

  _jwt: JwtLegFiClaims;

  constructor(
    private _dialog: MatDialog,
    private _fb: FormBuilder,
    private _growler: GrowlerService,
    private _router: Router,
    private _scrollService: ScrollService,
    private _violationsService: ViolationsService,
    private _csvExportService: CsvExportService,
  ) {}

  async ngOnInit() {
    this._scrollService.scrollToTop();
    this._jwt = LegFiJwtService.read();

    this.hasWriteAccess = LegFiJwtService.doesUserHaveModulePermission(
      'violation',
      true,
    );

    if (this.unitId) {
      this.displayedColumns = this.displayedColumns.filter(c => c !== 'unit');
    }

    if (this._jwt !== null) {
      if (this._jwt.superUser || this._jwt.admin) {
        this.hasWriteAccess = true;
      }
    }

    await this.getOrgViolationStatuses();
    this.getUnitViolations();
  }

  ngAfterViewInit() {
    this.tableDataSource.sort = this.sort;
    this.tableDataSource.paginator = this.matpaginator;

    const originalFilterFunction = this.tableDataSource.filterPredicate;
    this.tableDataSource.filterPredicate = (data: ViolationStatement) => {
      if (originalFilterFunction(data.unit as any, this.searchValue)) {
        return true;
      }
      return originalFilterFunction(data, this.searchValue);
    };
  }

  /** Get the available statuses for violations for this org */
  async getOrgViolationStatuses() {
    await this._violationsService
      .getViolationStatusesPromise()
      .then(
        async (statuses: ViolationStatus[]) => {
          this.orgViolationStatuses = statuses;
          if (this.orgViolationStatuses.length) {
            this.displayedColumns.unshift('status');
            this.topTabs = [
              'All Outstanding',
              ...this.orgViolationStatuses.map(s => s.title),
              'Completed',
            ];
          }
        },
        (err: any) => {
          console.error('cant get template: ', err);
        },
      );
  }

  parseTableDataByStatus() {
    if (this.activeTab === 'Completed') {
      this.tableDataSource.data = this.completeViolations;
    } else if (this.activeTab === 'All Outstanding') {
      this.tableDataSource.data = this.notCompleteViolations;
    } else if (this.orgViolationStatuses.length) {
      this.tableDataSource.data = this.notCompleteViolations.filter(s => {
        return s.status === this.activeTab;
      });
    }
  }

  getUnitViolations() {
    this.loading = true;

    this._violationsService
      .getUnitViolations(null, this.unitId)
      .pipe(untilDestroyed(this))
      .subscribe(async (violations: ViolationStatement[]) => {
        this.completeViolations = violations.filter(v => v.completedTime);
        this.notCompleteViolations = violations.filter(v => !v.completedTime);

        this.parseTableDataByStatus();
        this.updateFilter();
        this.loading = false;
      }, () => {
        this.loading = false;
        this._growler.error('Error', 'There was an error loading violations for this unit.');
      });
  }

  /**
   * Trigger a re-filter when any of the things we filter by change
   */
  updateFilter() {
    this.tableDataSource.filter = this.searchValue;
    if (this.tags.length > 0) {
      this.tableDataSource.filter += '//TAGS//';
    }
    if (this.unitTags.length > 0) {
      this.tableDataSource.filter += '//UNITTAGS//';
    }
  }

  changeActiveTab(event: MatTabChangeEvent) {
    this.activeTab = event.tab.textLabel;

    if (this.activeTab === 'Completed') {
      this.displayedColumns = [
        'unit',
        'title',
        'createdAt',
        'resolutionTime',
        'completedTime',
        'actions',
      ];
    } else {
      this.displayedColumns = [
        'unit',
        'title',
        'createdAt',
        'resolutionTime',
        'actions',
      ];
    }

    if (this.unitId) {
      this.displayedColumns = this.displayedColumns.filter(c => c !== 'unit');
    }

    if (this.orgViolationStatuses.length) {
      this.displayedColumns.unshift('status');
    }

    this.parseTableDataByStatus();
    this.updateFilter();
  }

  /**
   * Navigate to Request Detail Page
   * @param {any[]} routerLink
   */
  navigate(routerLink: any[]) {
    if (this._jwt !== null) {
      this._router.navigate(routerLink);
    }
  }

  deleteViolation(violation: ViolationStatement) {
    const baseDialog = this._dialog.open(ConfirmDeleteModalComponent, {
      width: MatDialogSizes.XS,
      data: 'violation',
    });

    baseDialog.afterClosed().subscribe((confirmation: boolean) => {
      if (confirmation) {
        this._violationsService
          .deleteViolation([violation.id])
          .subscribe(() => {
            this.getUnitViolations();
          });
      }
    });
  }

  exportCsv() {
    const c = this.tableDataSource.filteredData.map((v: ViolationStatement) => {
      return new ViolationExportListItem(v);
    });

    const options = {
      headers: [
        'status',
        'unit',
        'title',
        'message',
        'created',
        'resolveBy',
        'completed',
        'address',
        'city',
        'state',
        'zip',
        'comments',
      ],
      showLabels: true,
    };

    this._csvExportService.generateCsv(c, 'violation-export', options);
  }

  exportPdf() {
    this.downloadingPdf = true;
    this._violationsService.getUnitViolationListPdf(this.unitId, this.activeTab)
      .pipe(untilDestroyed(this))
      .subscribe(
        response => {
          this._csvExportService.downloadFile(response, (this.unitId
            ? this.unitId + '-'
            : '') + this.activeTab + '-violations.pdf', 'application/pdf');
          this.downloadingPdf = false;
        },
        () => {
          this.downloadingPdf = false;
        },
      );
  }

  /**
   * Handle Toggle of Modals
   * @param {boolean} state
   * @param {string} modal
   */
  toggleModal(state: boolean, modal: string) {
    this[modal] = state;
  }
}
解决方案

仅修改deleteViolation函数无法解决问题,需要额外调整HTML和组件逻辑。核心原因是当前代码中mat-tab-group通过*ngIf="!loading"控制显示:删除数据后调用getUnitViolations()时,loading会被设为true,导致Tab组被销毁;加载完成后loading设为false,Tab组重新渲染,但此时未绑定选中状态,默认会切回第一个Tab。

需要做以下两步修改:

1. 给mat-tab-group绑定选中索引

修改Tab组的HTML代码,添加[selectedIndex]绑定,根据当前activeTab计算对应的索引:

<div class="card-header" *ngIf="!loading">
<mat-tab-group (selectedTabChange)="changeActiveTab($event)" [selectedIndex]="getActiveTabIndex()">
    <mat-tab *ngFor="let tab of topTabs" [label]="tab">
    </mat-tab>
</mat-tab-group>
</div>

然后在组件中添加getActiveTabIndex()方法:

getActiveTabIndex(): number {
  return this.topTabs.findIndex(tab => tab === this.activeTab);
}

2. 确保activeTab在Tab列表更新后依然有效(可选,增强健壮性)

由于getOrgViolationStatuses()会动态修改topTabs列表,若删除操作后当前activeTab不在新的topTabs中(比如自定义状态被删除的极端情况),需要设置回退值。在getUnitViolations()的订阅回调中添加检查:

getUnitViolations() {
 this.loading = true;

 this._violationsService
        .getUnitViolations(null, this.unitId)
        .pipe(untilDestroyed(this))
        .subscribe(async (violations: ViolationStatement[]) => 
   {
            this.completeViolations = violations.filter(v => v.completedTime);
            this.notCompleteViolations = violations.filter(v => !v.completedTime);

            // 检查activeTab是否存在于当前topTabs中,不存在则回退到默认值
            if (!this.topTabs.includes(this.activeTab)) {
                this.activeTab = 'All Outstanding';
            }

            this.parseTableDataByStatus();
            this.updateFilter();
            this.loading = false;
        }, () => {
            this.loading = false;
            this._growler.error('Error', 'There was an error loading violations for this unit.');
        });
}

可选:优化deleteViolation函数的用户体验

目前deleteViolation函数逻辑正常,但可以添加成功/失败提示,提升用户体验:

deleteViolation(violation: ViolationStatement) {
    const baseDialog = this._dialog.open(ConfirmDeleteModalComponent, {
        width: MatDialogSizes.XS,
        data: 'violation',
    });

    baseDialog.afterClosed().subscribe((confirmation: boolean) => {
        if (confirmation) {
            this._violationsService
                    .deleteViolation([violation.id])
                    .subscribe(() => {
                        this._growler.success('Success', 'Violation deleted successfully');
                        this.getUnitViolations();
                    }, () => {
                        this._growler.error('Error', 'Failed to delete violation');
                    });
        }
    });
 }
总结

核心问题是Tab组在加载时被销毁重建,导致选中状态丢失。通过绑定selectedIndex到当前activeTab的索引,就能确保重建后选中正确的Tab。仅修改deleteViolation无法解决这个问题,必须配合HTML和组件逻辑的调整。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 14:38:10