如何为可复用Angular Material表格添加服务端分页、筛选与排序?
实现Angular Material可复用表格的服务端分页、筛选与排序
一、前端(Angular)部分修改
1. 更新TableColumn接口
补充排序相关配置,让组件支持列级排序控制:
export interface TableColumn<T> { columnDef: string; header: string; cell: (element: T) => string; // 新增排序属性 sortable?: boolean; // 是否允许该列排序 defaultSortDirection?: 'asc' | 'desc'; // 可选,默认排序方向 }
2. 修改table.component.ts
添加分页、筛选、排序的状态管理,封装数据加载逻辑,并通过输入属性接收业务端的API调用方法:
import { Component, Input, ViewChild, OnInit } from '@angular/core'; import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatSort, Sort } from '@angular/material/sort'; import { Observable } from 'rxjs'; import { TableColumn } from './table-column.interface'; @Component({ selector: 'app-reusable-table', templateUrl: './table.component.html', styleUrls: ['./table.component.css'] }) export class TableComponent<T> implements OnInit { @Input() columns: TableColumn<T>[] = []; @Input() fetchData: (params: any) => Observable<{ data: T[], totalCount: number }>; @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort!: MatSort; displayedColumns: string[] = []; filterValue = ''; totalItems = 0; dataSource = new MatTableDataSource<T>(); ngOnInit(): void { this.displayedColumns = this.columns.map(col => col.columnDef); this.loadData(); } // 核心数据加载方法,聚合所有请求参数 loadData(): void { const params = { pageNumber: this.paginator?.pageIndex + 1 || 1, // 适配后端从1开始的页码规则 pageSize: this.paginator?.pageSize || 10, sortField: this.sort?.active || this.columns.find(col => col.sortable)?.columnDef || 'id', sortDirection: this.sort?.direction || 'asc', filter: this.filterValue }; this.fetchData(params).subscribe(result => { this.dataSource.data = result.data; this.totalItems = result.totalCount; }); } // 分页切换事件 onPageChange(event: PageEvent): void { this.loadData(); } // 排序切换事件,校验列是否允许排序 onSortChange(event: Sort): void { const targetColumn = this.columns.find(col => col.columnDef === event.active); if (!targetColumn?.sortable) { this.sort.active = ''; this.sort.direction = ''; return; } this.loadData(); } // 筛选输入事件,重置分页到第一页 applyFilter(event: Event): void { this.filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase(); if (this.paginator) this.paginator.pageIndex = 0; this.loadData(); } }
3. 修改table.component.html
添加筛选框、分页器,并给可排序列绑定排序表头:
<!-- 筛选区域 --> <div class="table-filter"> <mat-form-field appearance="outline"> <mat-label>筛选</mat-label> <input matInput (keyup)="applyFilter($event)" placeholder="输入关键词"> </mat-form-field> </div> <!-- 表格主体 --> <table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)"> <ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef"> <th mat-header-cell *matHeaderCellDef mat-sort-header="{{column.sortable ? column.columnDef : ''}}"> {{column.header}} </th> <td mat-cell *matCellDef="let element"> {{column.cell(element)}} </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table> <!-- 分页器 --> <mat-paginator [length]="totalItems" [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons (page)="onPageChange($event)" ></mat-paginator>
4. 父组件使用示例
以用户列表为例,传入列配置和自定义API调用:
// user-list.component.ts import { Component } from '@angular/core'; import { UserService } from './user.service'; import { TableColumn } from '../reusable-table/table-column.interface'; import { User } from './user.model'; @Component({ selector: 'app-user-list', templateUrl: './user-list.component.html' }) export class UserListComponent { columns: TableColumn<User>[] = [ { columnDef: 'id', header: 'ID', cell: (user) => user.id.toString(), sortable: true }, { columnDef: 'name', header: '姓名', cell: (user) => user.name, sortable: true }, { columnDef: 'email', header: '邮箱', cell: (user) => user.email } ]; constructor(private userService: UserService) {} // 传递给复用表格的API调用方法 fetchUsers(params: any) { return this.userService.getUsers( params.pageNumber, params.pageSize, params.sortField, params.sortDirection, params.filter ); } }
对应的父组件模板:
<!-- user-list.component.html --> <app-reusable-table [columns]="columns" [fetchData]="fetchUsers.bind(this)" ></app-reusable-table>
二、后端(ASP.NET Core + EF Core)部分修改
1. 创建分页结果模型
统一返回数据和总条数格式:
public class PagedResult<T> { public List<T> Data { get; set; } = new List<T>(); public int TotalCount { get; set; } }
2. 修改API接口实现
接收前端参数,依次执行筛选、排序、分页逻辑:
[HttpGet] public async Task<IActionResult> GetUsers( int pageNumber = 1, int pageSize = 10, string? sortField = "Id", string? sortDirection = "asc", string? filter = null) { var query = _context.Users.AsQueryable(); // 筛选逻辑:多字段模糊匹配 if (!string.IsNullOrEmpty(filter)) { query = query.Where(u => u.Name.Contains(filter) || u.Email.Contains(filter)); } // 排序逻辑:类型安全的字段枚举,避免反射风险 query = sortField?.ToLower() switch { "name" => sortDirection?.ToLower() == "desc" ? query.OrderByDescending(u => u.Name) : query.OrderBy(u => u.Name), "email" => sortDirection?.ToLower() == "desc" ? query.OrderByDescending(u => u.Email) : query.OrderBy(u => u.Email), _ => sortDirection?.ToLower() == "desc" ? query.OrderByDescending(u => u.Id) : query.OrderBy(u => u.Id) }; // 分页逻辑 var totalCount = await query.CountAsync(); var data = await query .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); return Ok(new PagedResult<User> { Data = data, TotalCount = totalCount }); }
3. 可选:封装通用分页扩展方法
如果多个API需要分页逻辑,可以封装复用:
public static class QueryableExtensions { public static async Task<PagedResult<T>> ToPagedResultAsync<T>(this IQueryable<T> query, int pageNumber, int pageSize) { var totalCount = await query.CountAsync(); var data = await query.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(); return new PagedResult<T> { Data = data, TotalCount = totalCount }; } }
使用时可简化分页代码:
var pagedResult = await query.ToPagedResultAsync(pageNumber, pageSize); return Ok(pagedResult);
三、关键注意事项
- 参数对齐:前端分页器
pageIndex从0开始,后端通常页码从1开始,需做+1转换。 - 排序安全:避免直接用反射拼接排序逻辑,通过枚举字段的方式防止无效字段或SQL注入风险。
- 性能优化:给筛选、排序字段添加数据库索引,避免大数据量下的查询性能问题。
- 筛选扩展:可根据业务需求扩展多字段精确匹配、范围筛选等逻辑。
内容的提问来源于stack exchange,提问作者Alex Wright
相关产品推荐
相关产品推荐

