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