DevUI组件库实战:从入门到企业级应用的深度探索,如何实现支持表格扩展和表格编辑功能

DevUI 是一套以「设计系统为灵魂、组件库为核心、工程化工具为支撑」的企业级前端解决方案,核心优势在于「企业级场景适配、全链路一致性设计、高可定制性与工程化效率协同」。

在当今快速迭代的互联网时代,企业级前端开发面临着效率、一致性、可维护性等多重挑战。作为华为内部多年业务沉淀的结晶,DevUI应运而生------这是一款基于Angular框架的开源前端解决方案,以"高效、开放、可信、乐趣"为设计价值观,致力于为企业中后台产品提供开箱即用的前端组件库。

DevUI 为 Web 应用提供了丰富的基础 UI 组件,我们还将持续探索企业级应用的最佳 UI 实践,欢迎尝试使用 DevUI。


真实效果一:

真实效果二:

真实效果三:


真实效果:

js 复制代码
import { Component, OnInit } from '@angular/core';
import { DialogService, EditableTip, FormLayout, TableWidthConfig } from 'ng-devui';
import { Subscription } from 'rxjs';
import { FormConfig } from 'src/app/@shared/components/admin-form';
import { ListDataService } from './list-data.service';

@Component({
  selector: 'da-editable-list',
  templateUrl: './editable-list.component.html',
  styleUrls: ['./editable-list.component.scss'],
})
export class EditableListComponent implements OnInit {
  editableTip = EditableTip.btn;
  nameEditing: boolean;
  busy: Subscription;
  pager = {
    total: 0,
    pageIndex: 1,
    pageSize: 10,
  };

  listData = [];

  headerNewForm = false;

  formConfig: FormConfig = {
    layout: FormLayout.Horizontal,
    items: [
      {
        label: 'Id',
        prop: 'id',
        type: 'input',
      },
      {
        label: 'Title',
        prop: 'title',
        type: 'input',
        required: true,
        rule: {
          validators: [{ required: true }],
        },
      },
      {
        label: 'Priority',
        prop: 'priority',
        type: 'select',
        options: ['Low', 'Medium', 'High'],
        required: true,
        rule: {
          validators: [{ required: true }],
        },
      },
      {
        label: 'Iteration',
        prop: 'iteration',
        type: 'input',
      },
      {
        label: 'Assignee',
        prop: 'assignee',
        type: 'input',
        required: true,
        rule: {
          validators: [{ required: true }],
        },
      },
      {
        label: 'Status',
        prop: 'status',
        type: 'select',
        options: ['Stuck', 'Done', 'Working on it'],
      },
      {
        label: 'Timeline',
        prop: 'timeline',
        type: 'datePicker',
      },
    ],
    labelSize: ''
  };

  defaultRowData = {
    id: '',
    title: '',
    priority: 'Low',
    iteration: '',
    assignee: '',
    status: 'Stuck',
    timeline: new Date(),
  };

  priorities = ['Low', 'Medium', 'High'];

  tableWidthConfig: TableWidthConfig[] = [
    {
      field: 'id',
      width: '150px',
    },
    {
      field: 'title',
      width: '200px',
    },
    {
      field: 'priority',
      width: '100px',
    },
    {
      field: 'iteration',
      width: '100px',
    },
    {
      field: 'assignee',
      width: '100px',
    },
    {
      field: 'status',
      width: '100px',
    },
    {
      field: 'timeline',
      width: '100px',
    },
    {
      field: 'operator',
      width: '100px',
    },
  ];

  constructor(
    private listDataService: ListDataService,
    private dialogService: DialogService
  ) {}

  ngOnInit() {
    this.getList();
  }

  onEditEnd(rowItem, field) {
    rowItem[field] = false;
  }

  getList() {
    this.busy = this.listDataService
      .getListData(this.pager)
      .subscribe((res) => {
        const data = JSON.parse(JSON.stringify(res.pageList));
        data.$expandConfig = { expand: false };
        this.listData = data;
        this.pager.total = res.total;
      });
  }

  beforeEditStart = (rowItem, field) => {
    return true;
  };

  beforeEditEnd = (rowItem, field) => {
    console.log('beforeEditEnd');
    if (rowItem && rowItem[field].length < 3) {
      return false;
    } else {
      return true;
    }
  };

  newRow() {
    this.headerNewForm = true;
  }

  getuuid() {
    return new Date().getTime() + 'CNWO';
  }

  quickRowAdded(e) {
    const newData = { ...e };
    this.listData.unshift(newData);
    this.headerNewForm = false;
  }

  quickRowCancel() {
    this.headerNewForm = false;
  }

  subRowAdded(index, item) {
    this.listData[index].$expandConfig.expand = false;
    const newData = { ...this.defaultRowData };
    this.listData.splice(index + 1, 0, newData);
  }

  subRowCancel(index) {
    this.listData[index].$expandConfig.expand = false;
  }

  toggleExpand(rowItem) {
    if (rowItem.$expandConfig) {
      rowItem.$expandConfig.expand = !rowItem.$expandConfig.expand;
    }
  }

  onPageChange (e) {
    this.pager.pageIndex = e;
    this.getList()
  }

  onSizeChange (e) {
    this.pager.pageSize = e;
    this.getList()
  }

  deleteRow(index) {
    const results = this.dialogService.open({
      id: 'delete-dialog',
      width: '346px',
      maxHeight: '600px',
      title: 'Delete',
      showAnimate: false,
      content: 'Are you sure you want to delete it?',
      backdropCloseable: true,
      onClose: () => {},
      buttons: [
        {
          cssClass: 'primary',
          text: 'Ok',
          disabled: false,
          handler: () => {
            this.listData.splice(index, 1);
            results.modalInstance.hide();
          },
        },
        {
          id: 'btn-cancel',
          cssClass: 'common',
          text: 'Cancel',
          handler: () => {
            results.modalInstance.hide();
          },
        },
      ],
    });
  }
}

这段代码是一个基于 Angular 和 DevUI 组件库实现的可编辑列表组件,主要用于展示和管理任务列表数据。以下是对代码的详细分析:

1. 组件基本信息

js 复制代码
@Component({
  selector: 'da-editable-list',
  templateUrl: './editable-list.component.html',
  styleUrls: ['./editable-list.component.scss'],
})
export class EditableListComponent implements OnInit
  • 选择器‌: 可在模板中使用
  • 模板/样式‌: 分别指向 HTML 和 SCSS 文件
  • 实现接口‌: OnInit 表示组件初始化时执行特定逻辑

2. 核心属性说明

表格配置

  • editableTip: 设置编辑提示方式为按钮触发
  • pager: 分页配置对象,包含总记录数、当前页码、每页条数
  • tableWidthConfig: 表格列宽配置数组,定义每列字段和宽度

数据模型

  • listData: 存储列表数据的数组
  • defaultRowData: 新增行的默认数据结构
  • formConfig: 表单配置对象,定义新增/编辑表单的字段、类型、验证规则等

3. 关键方法解析

生命周期钩子

js 复制代码
ngOnInit() {
  this.getList(); // 组件初始化时获取列表数据
}

数据获取

js 复制代码
getList() {
  // 调用服务获取分页数据并更新本地状态
  this.busy = this.listDataService.getListData(this.pager).subscribe(...)
}

编辑控制

  • beforeEditStart: 编辑开始前的回调函数(始终返回 true)
  • beforeEditEnd: 编辑结束前的验证回调(字段长度需 >=3)
  • onEditEnd: 编辑完成后重置编辑状态

行操作

  • newRow(): 显示新增表单头部
  • quickRowAdded(): 快速添加新行到列表顶部
  • subRowAdded(): 在指定位置插入新行
  • toggleExpand(): 切换行展开/收起状态

分页处理

  • onPageChange(): 页码变更时重新加载数据
  • onSizeChange(): 每页条数变更时重新加载数据

删除确认

js 复制代码
deleteRow(index) {
  // 打开确认对话框,用户确认后从列表中移除指定行
  const results = this.dialogService.open({...})
}

4. 表单配置详情

formConfig 定义了包含以下字段的表单:

  • Id‌: 只读输入框
  • Title‌: 必填输入框
  • Priority‌: 必填下拉选择(低/中/高)
  • Iteration‌: 可选输入框
  • Assignee‌: 必填输入框
  • Status‌: 下拉选择(卡住/完成/进行中)
  • Timeline‌: 日期选择器

总结

这是一个功能完整的可编辑数据列表组件,具备以下特性:

  • 数据展示‌: 支持分页的任务列表展示
  • 行内编辑‌: 可直接在表格中编辑行数据
  • 新增功能‌: 支持头部快速新增和插入新行
  • 删除确认‌: 删除操作需二次确认
  • 表单验证‌: 编辑时进行数据合法性校验
  • 响应式设计‌: 通过 DevUI 表格组件实现列宽控制
js 复制代码
<div class="da-list-wrap">
  <da-layout-row [daGutter]="[24, 24]">
    <da-col-item [daSpan]="24" [daXs]="24">
      <div class="da-list-container" dLoading [loading]="busy">
        <div class="da-list-content">
          <d-data-table
            #dataTable
            [dataSource]="listData"
            [scrollable]="true"
            [tableWidthConfig]="tableWidthConfig"
          >
            <thead dTableHead>
              <tr dTableRow>
                <th dHeadCell>Id</th>
                <th dHeadCell>Title</th>
                <th dHeadCell>Priority</th>
                <th dHeadCell>Iteration</th>
                <th dHeadCell>Assignee</th>
                <th dHeadCell>Status</th>
                <th dHeadCell>Timeline</th>
                <th dHeadCell>Actions</th>
              </tr>
            </thead>
            <tbody dTableBody>
              <ng-template let-rowItem="rowItem" let-rowIndex="rowIndex">
                <tr dTableRow *ngIf="rowIndex === 0">
                  <td dTableCell [attr.colspan]="tableWidthConfig.length">
                    <div
                      *ngIf="!headerNewForm"
                      (click)="newRow()"
                      class="cursor-pointer"
                    >
                      <span class="tips-icon icon-add"></span>
                      <span style="margin-left: 10px">Create new data</span>
                    </div>
                    <div *ngIf="headerNewForm" class="edit-padding-fix">
                      <da-admin-form
                        [formConfig]="formConfig"
                        [formData]="defaultRowData"
                        class="editable-row"
                        (submitted)="quickRowAdded($event)"
                        (canceled)="quickRowCancel()"
                      ></da-admin-form>
                    </div>
                  </td>
                </tr>
                <tr dTableRow>
                  <td
                    dTableCell
                    [editable]="true"
                    [editableTip]="editableTip"
                    [(editing)]="rowItem['idEdit']"
                    [rowItem]="rowItem"
                    [field]="'id'"
                    [beforeEditStart]="beforeEditStart"
                    [beforeEditEnd]="beforeEditEnd"
                  >
                    <span *ngIf="!rowItem['idEdit']">{{ rowItem?.id }}</span>
                    <div *ngIf="rowItem['idEdit']" class="edit-padding-fix">
                      <input
                        class="devui-form-control"
                        name="id"
                        [(ngModel)]="rowItem.id"
                        [attr.maxlength]="100"
                        [attr.minlength]="3"
                      />
                    </div>
                  </td>
                  <td
                    dTableCell
                    [editable]="true"
                    [editableTip]="editableTip"
                    [(editing)]="rowItem['titleEdit']"
                    [rowItem]="rowItem"
                    [field]="'title'"
                    [beforeEditStart]="beforeEditStart"
                    [beforeEditEnd]="beforeEditEnd"
                  >
                    <span *ngIf="!rowItem['titleEdit']"
                      ><d-tag [tag]="'Epic'" [labelStyle]="'epic'"></d-tag>
                      {{ rowItem?.title }}</span
                    >
                    <div *ngIf="rowItem['titleEdit']" class="edit-padding-fix">
                      <input
                        class="devui-form-control"
                        name="title"
                        [(ngModel)]="rowItem.title"
                        [attr.maxlength]="100"
                        [attr.minlength]="3"
                      />
                    </div>
                  </td>
                  <td
                    dTableCell
                    [editable]="true"
                    [editableTip]="editableTip"
                    [(editing)]="rowItem['priorityEdit']"
                    [rowItem]="rowItem"
                    [field]="'priority'"
                    [beforeEditStart]="beforeEditStart"
                    [beforeEditEnd]="beforeEditEnd"
                  >
                    <span *ngIf="!rowItem['priorityEdit']"
                      ><d-tag
                        [tag]="rowItem?.priority"
                        [labelStyle]="rowItem?.priority"
                      ></d-tag
                    ></span>
                    <div
                      *ngIf="rowItem['priorityEdit']"
                      class="edit-padding-fix"
                    >
                      <d-select
                        name="priority"
                        [(ngModel)]="rowItem.priority"
                        [options]="priorities"
                      ></d-select>
                    </div>
                  </td>
                  <td
                    dTableCell
                    [editable]="true"
                    [editableTip]="editableTip"
                    [(editing)]="rowItem['iterationEdit']"
                    [rowItem]="rowItem"
                    [field]="'iteration'"
                    [beforeEditStart]="beforeEditStart"
                    [beforeEditEnd]="beforeEditEnd"
                  >
                    <span *ngIf="!rowItem['iterationEdit']">{{
                      rowItem?.iteration
                    }}</span>
                    <div
                      *ngIf="rowItem['iterationEdit']"
                      class="edit-padding-fix"
                    >
                      <input
                        dTextInput
                        size="sm"
                        [(ngModel)]="rowItem.iteration"
                      />
                    </div>
                  </td>
                  <td
                    dTableCell
                    [editable]="true"
                    [editableTip]="editableTip"
                    [(editing)]="rowItem['assigneeEdit']"
                    [rowItem]="rowItem"
                    [field]="'assignee'"
                    [beforeEditStart]="beforeEditStart"
                    [beforeEditEnd]="beforeEditEnd"
                  >
                    <span *ngIf="!rowItem['assigneeEdit']">
                      <d-avatar
                        [name]="rowItem.assignee"
                        [width]="24"
                        [height]="24"
                      ></d-avatar>
                      <span style="margin-left: 6px">{{
                        rowItem.assignee
                      }}</span>
                    </span>
                    <div
                      *ngIf="rowItem['assigneeEdit']"
                      class="edit-padding-fix"
                    >
                      <input
                        dTextInput
                        size="sm"
                        [(ngModel)]="rowItem.assignee"
                      />
                    </div>
                  </td>
                  <td
                    dTableCell
                    [editable]="true"
                    [editableTip]="editableTip"
                    [(editing)]="rowItem['statusEdit']"
                    [rowItem]="rowItem"
                    [field]="'status'"
                    [beforeEditStart]="beforeEditStart"
                    [beforeEditEnd]="beforeEditEnd"
                  >
                    <span *ngIf="!rowItem['statusEdit']"
                      ><span [ngClass]="rowItem?.status.split(' ')[0]">{{
                        rowItem?.status || "--"
                      }}</span></span
                    >
                    <div *ngIf="rowItem['statusEdit']" class="edit-padding-fix">
                      <d-select
                        name="status"
                        [(ngModel)]="rowItem.status"
                        [options]="['Stuck', 'Done', 'Working on it']"
                      ></d-select>
                    </div>
                  </td>
                  <td
                    dTableCell
                    [editable]="true"
                    [(editing)]="rowItem['dateEdit']"
                  >
                    <span *ngIf="!rowItem['dateEdit']">{{
                      rowItem?.timeline | i18nDate: "short":false
                    }}</span>
                    <form
                      *ngIf="rowItem['dateEdit']"
                      class="form-inline edit-padding-fix"
                    >
                      <div class="devui-form-group">
                        <div class="devui-input-group devui-dropdown-origin">
                          <input
                            class="devui-form-control search"
                            name="date"
                            [(ngModel)]="rowItem.timeline"
                            dDatepicker
                            appendToBody
                            #datePicker="datepicker"
                            [autoOpen]="true"
                            (ngModelChange)="onEditEnd(rowItem, 'dateEdit')"
                          />
                          <div
                            class="devui-input-group-addon"
                            (click)="datePicker.toggle()"
                          >
                            <i class="icon icon-calendar"></i>
                          </div>
                        </div>
                      </div>
                    </form>
                  </td>
                  <td>
                    <d-button
                      icon="icon-delete"
                      bsStyle="text-dark"
                      title="delete"
                      (click)="deleteRow(rowIndex)"
                    ></d-button>
                  </td>
                </tr>
              </ng-template>
            </tbody>
          </d-data-table>
        </div>
        <div class="da-list-footer">
          <d-pagination
            [size]="'sm'"
            [total]="pager.total"
            [(pageSize)]="pager.pageSize"
            [(pageIndex)]="pager.pageIndex"
            [canViewTotal]="true"
            [canChangePageSize]="true"
            [canJumpPage]="true"
            [maxItems]="5"
            (pageIndexChange)="onPageChange($event)"
            (pageSizeChange)="onSizeChange($event)"
          >
          </d-pagination>
        </div>
      </div>
    </da-col-item>
  </da-layout-row>
</div>

这段代码是 Angular 模板的一部分,实现了一个可编辑的数据表格功能。以下是对代码的详细分析:

  • 使用 DevUI 布局组件创建响应式网格布局
  • dLoading 指令在数据加载时显示加载状态
  • 数据表格组件
js 复制代码
<d-data-table
  #dataTable
  [dataSource]="listData"
  [scrollable]="true"
  [tableWidthConfig]="tableWidthConfig"
>
  • #dataTable: 模板引用变量,可在组件中访问表格实例
  • dataSource: 绑定表格数据源
  • scrollable: 启用滚动功能
  • tableWidthConfig: 配置各列宽度

表头定义

js 复制代码
<thead dTableHead>
  <tr dTableRow>
    <th dHeadCell>Id</th>
    <th dHeadCell>Title</th>
    <th dHeadCell>Priority</th>
    <th dHeadCell>Iteration</th>
    <th dHeadCell>Assignee</th>
    <th dHeadCell>Status</th>
    <th dHeadCell>Timeline</th>
    <th dHeadCell>Actions</th>
  </tr>
</thead>

定义了8列表头:ID、标题、优先级、迭代、负责人、状态、时间线、操作

表格主体内容

JS 复制代码
<tbody dTableBody>
  <ng-template let-rowItem="rowItem" let-rowIndex="rowIndex">

使用 Angular 模板语法遍历数据行

  • rowItem: 当前行数据对象
  • rowIndex: 当前行索引

新增行功能

JS 复制代码
<tr dTableRow *ngIf="rowIndex === 0">
  <td dTableCell [attr.colspan]="tableWidthConfig.length">
    <div *ngIf="!headerNewForm" (click)="newRow()" class="cursor-pointer">
      <span class="tips-icon icon-add"></span>
      <span style="margin-left: 10px">Create new data</span>
    </div>
    <div *ngIf="headerNewForm" class="edit-padding-fix">
      <da-admin-form
        [formConfig]="formConfig"
        [formData]="defaultRowData"
        class="editable-row"
        (submitted)="quickRowAdded($event)"
        (canceled)="quickRowCancel()"
      ></da-admin-form>
    </div>
  </td>
</tr>

在第一行显示新增数据入口

  • 点击 "Create new data" 触发 newRow() 方法
  • 显示/隐藏新增表单组件
  • 可编辑单元格示例(ID列)
JS 复制代码
<td
  dTableCell
  [editable]="true"
  [editableTip]="editableTip"
  [(editing)]="rowItem['idEdit']"
  [rowItem]="rowItem"
  [field]="'id'"
  [beforeEditStart]="beforeEditStart"
  [beforeEditEnd]="beforeEditEnd"
>
  <span *ngIf="!rowItem['idEdit']">{{ rowItem?.id }}</span>
  <div *ngIf="rowItem['idEdit']" class="edit-padding-fix">
    <input
      class="devui-form-control"
      name="id"
      [(ngModel)]="rowItem.id"
      [attr.maxlength]="100"
      [attr.minlength]="3"
    />
  </div>
</td>

关键属性说明:

属性 作用
editable 启用单元格编辑功能
editableTip 设置编辑提示方式
editing 双向绑定编辑状态
rowItem 传递行数据对象
field 指定编辑字段名
beforeEditStart/End 编辑前后回调函数

这个表格实现了完整的 CRUD 操作,支持行内编辑、新增数据、数据验证等功能。

DevUI是华为开源的企业级前端解决方案,基于Angular框架,提供丰富的基础UI组件和工程化工具。其核心优势包括企业级场景适配、全链路一致性设计、高可定制性和工程化效率协同。该方案源自华为内部业务沉淀,以"高效、开放、可信、乐趣"为设计价值观,特别适合中后台产品开发。示例代码展示了DevUI的可编辑列表组件实现,包含表单配置、数据绑定等企业级功能。通过组件库和工具链的结合,DevUI有效解决了企业前端开发中的效率、一致性和维护性等痛点问题。


MateChat:https://gitcode.com/DevCloudFE/MateChat

MateChat官网:https://matechat.gitcode.com

DevUI官网:https://devui.design/home

相关推荐
LYFlied2 小时前
从循环依赖检查插件Circular Dependency Plugin源码详解Webpack生命周期以及插件开发
前端·webpack·node.js·编译原理·plugin插件开发
麒qiqi2 小时前
【Linux 系统编程】文件 IO 与 Makefile 核心实战:从系统调用到工程编译
java·前端·spring
IT_陈寒2 小时前
Vue3 性能优化实战:从10秒到1秒的5个关键技巧,让你的应用飞起来!
前端·人工智能·后端
gambool2 小时前
新版chrome Edge浏览器不再支持手动添加cookie
前端·chrome·edge
一只爱吃糖的小羊2 小时前
React 避坑指南:“闭包陷阱“
前端·javascript·react.js
weixin_446260852 小时前
八、微调后模型使用及效果验证-1
前端·人工智能·chrome·微调模型
by__csdn2 小时前
大前端:定义、演进与实践全景解析
前端·javascript·vue.js·react.js·typescript·ecmascript·动画
JS_GGbond3 小时前
前端工具链:从“厨房设备”到“开箱即用”的轻松之旅
前端
7***37453 小时前
前端体验的隐性力量:微交互、认知负担与情绪设计的技术实践思维
前端·交互