展示结果

在这里插入图片描述

前言

最近在做一个员工管理系统的时候,发现自己经常要写表格。每次都要配置列、处理分页、实现搜索、处理排序…这些代码重复性太强了。

我就想,能不能把这些常用的逻辑封装起来,让下次写表格的时候更简单一点?

于是就有了这个二次封装的 DataTable 组件。用起来真的爽多了。


一、原生 DataTable 的痛点

配置太复杂

DevUI 的 DataTable 功能很强大,但配置起来也很复杂。你需要:

// 定义列配置
const columns = [
  { field: 'id', header: 'ID', width: '80px' },
  { field: 'name', header: '姓名', width: '120px' },
  // ... 更多列
];

// 定义数据
const data = [
  { id: 1, name: '张三', ... },
  // ... 更多数据
];

// 配置分页
pageIndex = 0;
pageSize = 10;
pageSizeOptions = [10, 20, 50];

// 配置排序
sortField = '';
sortDirection = 'asc';

// 配置搜索
searchText = '';

// 配置选择
checkOptions = { ... };

这还只是基础配置。如果要实现搜索、排序、分页的联动,代码会更复杂。

逻辑重复

每个表格都要写:

  • 分页逻辑
  • 搜索过滤逻辑
  • 排序逻辑
  • 数据更新逻辑

如果有多个表格,这些代码会重复很多次。

数据操作麻烦

添加、删除、更新数据的时候,需要手动处理数组操作,还要更新表格配置。


二、我的解决方案

想法很直接

既然每次都要做同样的事情,那就把这些重复的逻辑提取出来,做成一个可复用的服务。这样下次需要表格的时候,就不用重复写那些配置代码了。

我的方案分为两层:

第一层是 SimplifiedDataTableService,这是给日常使用的。它提供了一些简单的方法:createTable()addRow()deleteRow() 等。用这个服务,创建一个表格只需要一行代码。

第二层是 SimpleDataTableService,这是给需要更多控制的场景用的。如果 SimplifiedDataTableService 不够灵活,可以用这个来做更复杂的操作。

然后是 DataTableWrapperComponent,这是一个包装器组件,负责处理所有的 UI 逻辑——分页、搜索、排序等等。

最底层就是 DevUI 的原生 DataTable 组件,我们的所有东西都是基于它来构建的。


三、开始编码

先定义接口

首先要定义两个接口。一个是列的配置,一个是表格的配置。

列配置接口很直接:

export interface DataTableColumnConfig {
  field: string;
  header: string;
  width?: string;
  sortable?: boolean;
  filterable?: boolean;
  template?: any;
  align?: 'left' | 'center' | 'right';
}

这些字段都很好理解。field 是数据中的字段名,header 是列的标题。sortable 表示这列是否可以排序,width 是列的宽度。

然后是表格配置接口:

export interface DataTableWrapperConfig {
  columns: DataTableColumnConfig[];
  data: any[];
  pageSize?: number;
  pageSizeOptions?: number[];
  showPagination?: boolean;
  showCheckbox?: boolean;
  loading?: boolean;
  striped?: boolean;
  bordered?: boolean;
  hover?: boolean;
}

这个接口定义了表格需要的所有信息。columns 是列配置数组,data 是表格数据。其他的都是可选的,有默认值。

实现服务

现在来实现 SimplifiedDataTableService。这是整个方案的核心。

@Injectable({
  providedIn: 'root'
})
export class SimplifiedDataTableService {
  constructor(private simpleDataTableService: SimpleDataTableService) {}

  createTable(
    columns: DataTableColumnConfig[],
    data: any[],
    options?: {
      pageSize?: number;
      showPagination?: boolean;
      showCheckbox?: boolean;
    }
  ): DataTableWrapperConfig {
    return this.simpleDataTableService.createConfig(columns, data, {
      pageSize: options?.pageSize || 10,
      showPagination: options?.showPagination !== false,
      showCheckbox: options?.showCheckbox || false,
      striped: true,
      bordered: true,
      hover: true
    });
  }

  addRow(data: any[], newRow: any): any[] {
    return [...data, newRow];
  }

  deleteRow(data: any[], index: number): any[] {
    return data.filter((_, i) => i !== index);
  }

  updateRow(data: any[], index: number, newRow: any): any[] {
    const newData = [...data];
    newData[index] = newRow;
    return newData;
  }

  search(data: any[], keyword: string, fields: string[]): any[] {
    if (!keyword) return data;
    return data.filter(item =>
      fields.some(field =>
        String(item[field]).toLowerCase().includes(keyword.toLowerCase())
      )
    );
  }

  sort(data: any[], field: string, direction: 'asc' | 'desc' = 'asc'): any[] {
    return this.simpleDataTableService.sortData(data, field, direction);
  }

  getStats(data: any[], field: string): any {
    const values = data.map(item => item[field]).filter(v => typeof v === 'number');
    const total = values.reduce((a, b) => a + b, 0);
    
    return {
      total,
      count: values.length,
      average: values.length > 0 ? total / values.length : 0,
      min: values.length > 0 ? Math.min(...values) : 0,
      max: values.length > 0 ? Math.max(...values) : 0
    };
  }
}

这个服务提供了一些常用的方法。createTable() 用来创建表格配置,addRow() 用来添加行,deleteRow() 用来删除行,search() 用来搜索数据,getStats() 用来获取统计信息。

所有这些方法都很简单,但是很实用。


四、实际应用

现在来看看怎么用这个服务。我做了一个员工管理系统的例子。

在组件中注入服务

import { Component } from '@angular/core';
import { SimplifiedDataTableService } from './shared/datatable-wrapper.service';
import { DataTableColumnConfig, DataTableWrapperConfig } from './shared/datatable-wrapper.component';

@Component({
  selector: 'app-root',
  standalone: true,
  providers: [SimplifiedDataTableService],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  tableConfig!: DataTableWrapperConfig;
  tableColumns: DataTableColumnConfig[] = [];
  tableData: any[] = [
    { id: 1, name: '张三', email: 'zhangsan@example.com', department: '技术部', salary: 15000 },
    { id: 2, name: '李四', email: 'lisi@example.com', department: '产品部', salary: 14000 },
    { id: 3, name: '王五', email: 'wangwu@example.com', department: '设计部', salary: 13000 },
    { id: 4, name: '赵六', email: 'zhaoliu@example.com', department: '技术部', salary: 16000 },
    { id: 5, name: '孙七', email: 'sunqi@example.com', department: '销售部', salary: 12000 }
  ];

  constructor(private simplifiedDataTableService: SimplifiedDataTableService) {
    this.initializeTable();
  }

  initializeTable(): void {
    this.tableColumns = [
      { field: 'id', header: 'ID', width: '80px', sortable: true },
      { field: 'name', header: '姓名', width: '120px', sortable: true },
      { field: 'email', header: '邮箱', width: '200px' },
      { field: 'department', header: '部门', width: '120px', sortable: true },
      { field: 'salary', header: '薪资', width: '120px', sortable: true }
    ];

    this.tableConfig = this.simplifiedDataTableService.createTable(
      this.tableColumns,
      this.tableData,
      {
        pageSize: 10,
        showPagination: true,
        showCheckbox: false
      }
    );
  }
}

很简单,就是定义列配置和数据,然后调用 createTable() 方法。就这样,表格就配置好了。

添加员工

添加员工的时候,调用 addRow() 方法:

addEmployee(): void {
  const newEmployee = {
    id: Math.max(...this.tableData.map(e => e.id), 0) + 1,
    name: '新员工',
    email: 'new@example.com',
    department: '技术部',
    salary: 15000
  };
  
  this.tableData = this.simplifiedDataTableService.addRow(this.tableData, newEmployee);
  this.tableConfig.data = this.tableData;
}

就这么简单。添加一行数据,然后更新表格配置。

删除员工

删除员工的时候,调用 deleteRow() 方法:

deleteEmployee(id: number): void {
  const index = this.tableData.findIndex(e => e.id === id);
  this.tableData = this.simplifiedDataTableService.deleteRow(this.tableData, index);
  this.tableConfig.data = this.tableData;
}

同样很简单。找到要删除的行的索引,然后调用 deleteRow() 方法。

搜索员工

搜索员工的时候,调用 search() 方法:

searchEmployees(keyword: string): void {
  if (!keyword) {
    this.tableConfig.data = this.tableData;
  } else {
    this.tableConfig.data = this.simplifiedDataTableService.search(
      this.tableData,
      keyword,
      ['name', 'email', 'department']
    );
  }
}

传入关键词和要搜索的字段,就能得到搜索结果。

获取统计信息

获取统计信息的时候,调用 getStats() 方法:

getStats(): any {
  return this.simplifiedDataTableService.getStats(this.tableData, 'salary');
}

就这一行代码,就能得到薪资的统计信息——总和、平均值、最大值、最小值。


五、模板和样式

HTML 模板

<div class="datatable-demo-section">
  <h2>二次封装 DataTable 组件演示</h2>
  
  <!-- 操作按钮 -->
  <div class="button-section">
    <d-button bsStyle="primary" (click)="addEmployee()">添加员工</d-button>
  </div>

  <!-- DataTable -->
  <div class="datatable-wrapper-container" *ngIf="tableConfig">
    <d-datatable-wrapper 
      [config]="tableConfig"
      [showToolbar]="true"
    ></d-datatable-wrapper>
  </div>

  <!-- 统计信息 -->
  <div class="stats-section" *ngIf="tableData.length > 0">
    <h3>薪资统计</h3>
    <div class="stats-grid">
      <div class="stat-card">
        <div class="stat-label">总薪资</div>
        <div class="stat-value">¥{{ getStats().total | number }}</div>
      </div>
      <div class="stat-card">
        <div class="stat-label">平均薪资</div>
        <div class="stat-value">¥{{ getStats().average | number:'1.0-0' }}</div>
      </div>
      <div class="stat-card">
        <div class="stat-label">最高薪资</div>
        <div class="stat-value">¥{{ getStats().max | number }}</div>
      </div>
      <div class="stat-card">
        <div class="stat-label">最低薪资</div>
        <div class="stat-value">¥{{ getStats().min | number }}</div>
      </div>
    </div>
  </div>
</div>

模板很简洁。主要包括:

  1. 操作按钮区域,用于添加员工
  2. DataTable 组件,展示员工列表
  3. 统计信息区域,展示薪资统计

CSS 样式

.datatable-demo-section {
  margin-top: 40px;
  padding-top: 24px;
  border-top: 2px solid #e1e5eb;
}

.datatable-wrapper-container {
  margin-bottom: 24px;
  padding: 16px;
  background: #fff;
  border: 1px solid #e1e5eb;
  border-radius: 4px;
}

.stats-section {
  margin-bottom: 24px;
  padding: 20px;
  background: #f8f9fa;
  border-radius: 4px;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
}

.stat-card {
  padding: 16px;
  background: #fff;
  border: 1px solid #e1e5eb;
  border-radius: 4px;
  text-align: center;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}

.stat-label {
  color: #a8abb2;
  font-size: 12px;
  margin-bottom: 8px;
  font-weight: 500;
}

.stat-value {
  color: #5e7ce0;
  font-size: 24px;
  font-weight: 700;
}

样式遵循 DevUI 的设计规范,包括统一的颜色、间距和响应式设计。


六、总结一下优势

代码确实少了很多

用原生 DataTable,一个表格需要 100+ 行代码。用我的方案,只需要 20 行左右。

维护也简单了

所有 DataTable 的逻辑都在一个服务里。如果要改样式或改行为,只需要改一个地方。不用在每个组件里都改一遍。

功能更完整

内置了搜索、排序、分页、统计等常用功能。不用自己实现这些逻辑。

开发快

写表格的时候,不用关心 DataTable 怎么配置,只需要定义列和数据就行。

容易扩展

如果要加新的功能,比如导出、打印等,只需要在服务里加一个方法就行。


七、一些小技巧

多字段搜索

搜索的时候,可以搜索多个字段:

this.tableConfig.data = this.simplifiedDataTableService.search(
  this.tableData,
  keyword,
  ['name', 'email', 'department', 'position']
);

排序数据

需要排序的时候,可以用 sort() 方法:

const sortedData = this.simplifiedDataTableService.sort(
  this.tableData,
  'salary',
  'desc'
);

批量删除

如果要删除多行,可以循环调用 deleteRow()

let data = this.tableData;
selectedIndices.forEach(index => {
  data = this.simplifiedDataTableService.deleteRow(data, index);
});
this.tableData = data;

更新行

更新一行数据的时候,用 updateRow() 方法:

const updatedEmployee = { ...employee, salary: 18000 };
this.tableData = this.simplifiedDataTableService.updateRow(
  this.tableData,
  index,
  updatedEmployee
);

八、一些建议

列配置可以复用

如果有多个表格用同样的列,可以把它提取出来:

const employeeColumns: DataTableColumnConfig[] = [
  { field: 'id', header: 'ID', width: '80px', sortable: true },
  { field: 'name', header: '姓名', width: '120px', sortable: true },
  { field: 'email', header: '邮箱', width: '200px' },
  { field: 'department', header: '部门', width: '120px', sortable: true },
  { field: 'salary', header: '薪资', width: '120px', sortable: true }
];

// 在多个地方使用
this.tableConfig1 = this.simplifiedDataTableService.createTable(employeeColumns, data1);
this.tableConfig2 = this.simplifiedDataTableService.createTable(employeeColumns, data2);

要处理空数据的情况

表格可能没有数据,所以要处理这种情况:

<div *ngIf="tableData.length > 0">
  <d-datatable-wrapper [config]="tableConfig"></d-datatable-wrapper>
</div>
<div *ngIf="tableData.length === 0" class="empty-state">
  <p>暂无数据</p>
</div>

操作完成后给个反馈

用户操作完成后,最好给个提示,让他知道操作成功了:

addEmployee(): void {
  // ... 添加员工的逻辑
  this.showSuccessMessage('员工添加成功');
}

九、常见问题

怎样改表格的宽度?

在列配置里设置 width 字段:

{ field: 'name', header: '姓名', width: '200px', sortable: true }

怎样禁用某列的排序?

在列配置里设置 sortable 为 false:

{ field: 'email', header: '邮箱', sortable: false }

怎样自定义列的内容?

使用 template 字段:

{
  field: 'actions',
  header: '操作',
  template: actionTemplate  // 传入自定义模板
}

怎样处理大数据量?

如果数据很多,可以用虚拟滚动或者服务端分页。但这超出了本文的范围。


十、最后说两句

这个方案用了一段时间,感觉效果不错。主要的好处就是:

  • 代码少了很多,从 100+ 行减到 20 行
  • 所有表格的风格都一样,看起来更专业
  • 改样式的时候,只需要改一个地方
  • 开发新功能的时候,不用关心 DataTable 怎么配置,只需要定义列和数据就行

如果你的项目里有很多表格,这个方案应该会很有帮助。


相关资源

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐