代码块中的“……”代表省略了对本篇文章无用、过多的业务代码
需求
16列表格,均可以进行编辑和值输入
其中第一列为自动匹配框,获取鼠标焦点后展示下拉框,点击后渲染对应行的其他部分数据
第9列为一个图片上传,可以预览和删除
最后一列为操作按钮,可以复制和删除
表格下方有一个新建按钮,可以添加一行空数据
不同的列列宽不一样,需要单独设置
表格对接接口,可以保存,下次进入的时候读取
PS:为啥我会来写这个东西呢,因为这个页面是被调到安卓去的一个同事以前写的
他给每一行都设置了一个FormGroup,每个单元格设置了一个FormConctol,数据一旦多起来直接页面卡死
于是优化的重任就到我头上了
最恐怖的是所有的命名都是id+数字的格式,整个代码毫无有效注释
我还必须按着这样的数据结构去优化……
实现思路
MatTable(也就是Angular-Material的表格组件简称)没有某一行的索引或者指示器,于是我先给到来的数组中每条数据添加一个索引position,后续的复制、删除、新增都需要基于索引上使用
开始实现
先声明数组的结构(在这里我使用的是ngFor去渲染表格,当然也可以一列一列渲染)
// 声明表格类型
mlDataSource = {
// 存放数据
mlData: [...ELEMENT_DATA],
// 定义表头
TableHeaderColumns: ['', '面料', '编号', '材料编号', '品名'……],
// 列宽
FlexData: [0, 40, 2, 8, 7, 5, 5, 8, 8, 4, 4, 5, 4, 4, 4, 5, 5, 5],
// 列模板
displayedColumns: [……]
};
给数据添加索引:
// 获取面料数组
const MLDATA = res['data']['products_materials'].ml_text;
let i = 0;
// 用于给表格数组赋值的临时数组,在forEach中直接将数据push进表格数组,表格不会加载
const MLDATASRC = [];
// 若没有数据,则push五条空数据
if (MLDATA.length === 0) {
MLDATASRC.push(……); // ……中是五行空数据
} else {
// 将数组数据一一添加索引字段position
MLDATA.forEach(index => {
MLDATASRC.push({
position: i,
……
});
i++;
});
}
在HTML中渲染,因为是ngFor循环渲染所以看起来会有些复杂,但其实逻辑非常简单
大部分单元格都是input框,所以根据表头字段判断是否为自动匹配、图片、操作即可
在这里我就直接贴上我的最终代码了,因为中间实现的步骤我并没有记录 被同事的代码恶心的头晕,外加周五想快点结束工时8小时硬是写了7个小时代码
<div class="tableBox mat-elevation-z8">
<h2 style="margin-left: 15px;"> 面料 <span style="font-size:14px;margin-left: 10px;">(
模板要求不超过15行,超过行数无法保存到excel )</span></h2>
<mat-table [dataSource]="mlDataSource['mlData']" class="ml_table" fusePerfectScrollbar>
<ng-container matColumnDef="{{column}}"
*ngFor="let column of mlDataSource['displayedColumns']; let indexNum = index;">
<!-- fxFlex也是mlDataSource中的一个数组给的 -->
<mat-header-cell *matHeaderCellDef
fxFlex="{{mlDataSource['FlexData'][indexNum]}}">{{mlDataSource['TableHeaderColumns'][indexNum]}}</mat-header-cell>
<mat-cell *matCellDef="let row" fxFlex="{{mlDataSource['FlexData'][indexNum]}}">
<div fxFlex="97" *ngIf="column == 'id0'">
<!-- isSelected用于判断id0是否有值,若没有值则显示自动匹配框 -->
<self-autocomplete *ngIf="isSelected(row[column])" [searchList]="searchList"
[options]="materialList"
(receiveList)="receiveCompleteML($event,row)"></self-autocomplete>
<!-- isSelected用于判断id0是否有值,有值则将对应的值放进input框显示 -->
<div style="display: flex; justify-content: center;align-items: center; ">
<input style="width: 90%;" class="input_box" *ngIf="!isSelected(row[column])"
[(ngModel)]="row[column]" [ngModelOptions]="{standalone: true}"
[value]="row[column]" ngDefaultControl>
<!-- 该按钮和input框绑定同时出现和消失,可以清空id0的值,这样input框就会变为自动匹配框 -->
<button *ngIf="!isSelected(row[column])" matSuffix mat-icon-button aria-label="Clear"
(click)="reset(row,'ml')">
<mat-icon style="width: 16px;height: 16px;">close</mat-icon>
</button>
</div>
</div>
<input class="input_box" *ngIf="column !== 'button' && column !== 'id0' && column !== 'id14'"
[(ngModel)]="row[column]" [ngModelOptions]="{standalone: true}" [value]="row[column]"
ngDefaultControl>
<div *ngIf="column == 'id14'" class="img_box" style="width: 70px; height: 30px;">
<!-- 图片上传组件 -->
<upload-img *ngIf="showImgBox" [options]="row[column]" [detailType]="" [multiple]=false
(receiveImg)="importImg($event,row.position,'ml')">
</upload-img>
</div>
<div *ngIf="column == 'button'">
<span style="color:#039BE5;cursor: pointer;" (click)="copyData(row,'ml')">复制</span>
<span style="color:red;cursor: pointer; margin-left: 15px;"
(click)="removeData(row.position,'ml')">删除</span>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef=" displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;" matRipple>
</mat-row>
</mat-table>
<div fxLayout="row" fxLayoutAlign="center center" style="margin-top:15px;">
<button mat-raised-button class="mat-accent mt-5 mb-8" (click)="addData('ml')"> +
添加数据</button>
</div>
</div>
其中self-autocomplete组件非常简单就不贴代码了,就是一个自动匹配框,点击后返回对应的值(但同事写的这个组件复用性极差,我最开始用他写的,但到后面如果更换工作量太大,不得不一直用他的)
但自动匹配的触发事件需要渲染对应的值到同一行的框中:
// 监听输入匹配下拉
receiveCompleteML(list, row): void {
if (list && list.value) {
const arr = this.toArry(list.value);
// 将下拉框对应的数据覆盖对应position的数据
this.mlDataSource['mlData'].splice(row['position'], 1, {
position: row['position'],
id0: list['value'],
id15: row['id15'],
id16: arr[0],
……
});
// 刷新表格
this.mlTable.renderRows();
}
}
这时候出现了一个非常严重的问题,就是第一列自动匹配框虽然选中了,但渲染另外几个单元格后,表格刷新,自动匹配框没有传值(同事写的垃圾组件),自动匹配框就不会显示值了,所以这里我采用了一个组件切换的方式:
也就是当id0(第一列的字段)为空时,显示自动匹配框,当不为空时,显示input框,input框可以设置[(ngmodel)]绑定id0显示,这样就既可以使用自动匹配,又可以显示对应的值了。
html中
<!-- isSelected用于判断id0是否有值,若没有值则显示自动匹配框 -->
<self-autocomplete *ngIf="isSelected(row[column])" [searchList]="searchList
[options]="materialList" (receiveList)="receiveCompleteML($event,row)"></self-autocomplete>
<!-- isSelected用于判断id0是否有值,有值则将对应的值放进input框显示 -->
<div style="display: flex; justify-content: center;align-items: center; ">
<input style="width: 90%;" class="input_box" *ngIf="!isSelected(row[column])"
[(ngModel)]="row[column]" [ngModelOptions]="{standalone: true}"
[value]="row[column]" ngDefaultControl>
<!-- 该按钮和input框绑定同时出现和消失,可以清空id0的值,这样input框就会变为自动匹配框 -->
<button *ngIf="!isSelected(row[column])" matSuffix mat-icon-button aria-label="Clear"
(click)="reset(row,'ml')">
<mat-icon style="width: 16px;height: 16px;">close</mat-icon>
</button>
</div>
函数实现
// 当自动匹配框选中后,切换为input框
// 之所以这么做是因为自动匹配框没有字段,选中后再刷新表格就会显示为空值
isSelected(row): boolean {
let result: boolean;
if (row === '' || row === undefined || row === null) {
result = true;
} else {
result = false;
}
return result;
}
后面的X是重置按钮,重置的思路也很简单,用索引判断数据的位置,使用splice删除对应的数据,再新增一条id0为空其他不变的数据即可。
复制、删除、新增的思路
复制:获取当前索引的数据,将一条索引为当前数组length的相同数据push进数组
// 面料表格的复制按钮
copyData(row, type): void {
// 复制一条数据push进去并设置好对应的索引
this.mlDataSource['mlData'].push({
position: this.mlDataSource['mlData'].length,
id0: row['id0'],
id15: row['id15'],
……
});
// 刷新表格
this.mlTable.renderRows();
}
删除:获取当前索引的数据,删除,数据前半部分的数据索引不变,后半部分的数据索引每条都-1
// 面料表格的删除按钮
removeData(position, type): void {
// 用于存储在索引之前的数据
const mlData1 = [];
// 用于存储在索引之后的数据
const mlData2 = [];
// 删除对应索引的数据
this.mlDataSource['mlData'].splice(position, 1);
// 将索引前后的数据分别临时存储起来
this.mlDataSource['mlData'].forEach(index => {
if (Number(index['position']) < Number(position)) {
mlData1.push(index);
} else {
mlData2.push(index);
}
});
// 先将索引前的数据放进表格数组里
this.mlDataSource['mlData'] = [...mlData1];
// 将索引后的数据索引依次-1后放进数组
mlData2.forEach(index => {
this.mlDataSource['mlData'].push({
position: index['position'] - 1,
id0: index['id0'],
……
});
});
// 刷新表格
this.mlTable.renderRows();
}
新增:直接在数据末尾push一条索引为当前表格length的空数据即可,很简单所以就不贴代码了
总结
这次优化让我领悟到了写注释和命名规范的重要性(虽然我平时确实写,但以后会写的更详细了)以及,工作时间久并不能代表思路和能力就强,也有可能是真的水货。
在技术上,MatTable我觉得一直不太好用的地方就是无法进行实时操作,只能用于只读渲染,原来只是当时自己的能力不足,现在经过这次优化等下一次类似需求的出现就可以更加熟练了!