适用场景:有若干个表格,前面几列格式不一致,但是后面几列格式皆为占一个单元格,所以需要封装表格,表格元素自动根据数据结构生成即可;并且用户可新增列数据。
分类:
固定数据部分 就是根据数据结构传参设置table单元格内容及格式,数据结构由前端定义;
可新增删除部分 是由用户操作,格式统一为占一格,返回数据结构以列为单位,其中,删除列以判断对应列是否有表头为依据。
展示效果如下:
封装原生表格演示
目录
- 固定表格部分
- 数据格式
- 元素设置
- 可新增删除部分
- 数据格式
- 元素设置
- 新增的方法
- 删除的方法
- 数据转换
- 将格式转化为以列
- 将格式转化为以表格
- 全部代码
固定表格部分
需要确定的是固定表格中的需要给单元格元素传哪些值:
- 单元格的内容;
- 单元格格式会有很多种情况,可能会占几行,也可能会占几列,所以就需要控制着每个单元格的colspan和rowspan;
- 单元格的内容长短不一致所以也需要控制着单元格的宽度;
其它参数可有可无,需要依照自身需求添加逻辑。
数据格式
数据格式有四大字段,分别控制着出表头以外的行数(tdRows),为了便于控制除表头的tr循环次数;表格名称(title);表头内容(ths)和表身内容(tds)。
SafetyTableData:
{
tdRows: 3,//除表头的tbody所占行数
title: '表格名称表格名称表格名称表格名称表格名称',
ths: [
{
thName: 'Safety/安全',
colspan: 1,
rowspan: 4,
isEdit: false,
width: 100,
},
{
thName: '关键指标KPI',
colspan: 2,
rowspan: 1,
isEdit: false,
width: 100,
},
{
thName: '输出部门',
colspan: 1,
rowspan: 1,
isEdit: false,
width: 100,
},
{
thName: '公式',
colspan: 1,
rowspan: 1,
isEdit: false,
width: 180,
},
{
thName: '单位',
colspan: 1,
rowspan: 1,
isEdit: false,
width: 100,
},
],
tds: [
[
{ tdName: '伤害', colspan: 1, rowspan: 3, isEdit: false, width: 30, },
{ tdName: '占比%', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '游戏', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '(损失工时/ 投入工时)*100%', colspan: 1, rowspan: 1, isEdit: false, width: 180, },
{ tdName: '%', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
],
[
{ tdName: '一些值', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '游戏', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: 'ABCDEFG', colspan: 1, rowspan: 1, isEdit: false, width: 180, },
{ tdName: 'absolute', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
],
[
{ tdName: '实际占比%', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '游戏', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '(实际损失工时/实际投入工时)*100%', colspan: 1, rowspan: 1, isEdit: false, width: 180, },
{ tdName: '%', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
],
// Repeat similar structure for other rows as needed
],
},
元素设置
detailTableData为表格固定格式需要显示的数据,前面已设置了SafetyTableData的数据格式,可以直接将SafetyTableData赋值给detailTableData。若还有其他固定表格格式同理视情况赋值即可。
<table border="1">
<tr ref="tableHeader">
<th v-for="(thItem, index) in detailTableData.ths" :key="'th-' + index" :rowspan="thItem.rowspan" :colspan="thItem.colspan" :style="'width:'+thItem.width+'px'">
{{thItem.thName}}
</th>
</tr>
<tr v-for="rowIndex in detailTableData.tdRows" :key="rowIndex">
<td v-for="(tdItem, colIndex) in detailTableData.tds[rowIndex-1]" :key="'td-' + rowIndex + '-' + colIndex" :rowspan="tdItem.rowspan" :colspan="tdItem.colspan" :style="'width:'+tdItem.width+'px'">
{{tdItem.tdName}}
</td>
</tr>
</table>
可新增删除部分
既然可新增删除部分都是占一个单元格,那么可以确定的是colspan和rowspan都为1。
数据格式
全局参数(newTableData)用于存放所有用户新增的数据;newThObj为新增表头时的数据;newTdObj为新增表身时的数据。
newTableData: {
ths: [],
tds: [], // 初始包含一个空数组
},
newThObj: {
thName: '',
colspan: 1,
rowspan: 1,
isEdit: true,
},
newTdObj: {
tdName: '',
colspan: 1,
rowspan: 1,
isEdit: true,
},
元素设置
<table border="1">
<tr ref="tableHeader">
<th v-for="(newThItem,newindex) in newTableData.ths" :key="newindex" class="item" :id="'test'+newindex">
<el-input v-if="newThItem.isEdit && editTableSate" v-model="newThItem.thName" placeholder="请输入时间"></el-input>
<span v-else>{{newThItem.thName}}</span>
</th>
</tr>
<tr v-for="rowIndex in detailTableData.tdRows" :key="rowIndex">
<td v-for="(newTdItem, newColIndex) in newTableData.tds[rowIndex-1]" :key="newColIndex">
<el-input v-if="newTdItem.isEdit && editTableSate" v-model="newTdItem.tdName" placeholder="请输入"></el-input>
<span v-else>{{newTdItem.tdName}}</span>
</td>
</tr>
</table>
新增的方法
// 新增列的点击事件
addTableColBtn() {
// 向 newTableData.ths 添加一个新的表头对象
this.newTableData.ths.push({ ...this.newThObj });
// 如果 tds 为空,需要初始化它
if (this.newTableData.tds.length === 0) {
for (let i = 0; i < this.detailTableData.tdRows; i++) {
this.newTableData.tds.push([]);
}
}
// 遍历每一行,添加空单元格以匹配表头列数
this.newTableData.tds.forEach(row => {
row.push({ ...this.newTdObj });
});
},
删除的方法
// 反向遍历以避免删除元素时影响索引 -- 以表头为准,若表头为空,则提交后对应列为空
for (let i = this.newTableData.ths.length - 1; i >= 0; i--) {
if (!this.newTableData.ths[i].thName) {
let shouldDeleteColumn = true;
for (let row = 0; row < this.newTableData.tdRows; row++) {
if (this.newTableData.tds[row][i].tdName) {
shouldDeleteColumn = false;
break;
}
}
// 如果该列满足删除条件,则删除
if (shouldDeleteColumn) {
this.newTableData.ths.splice(i, 1);
this.newTableData.tds.forEach(row => {
row.splice(i, 1);
});
}
}
}
数据转换
将格式转化为以列
将newTableData{ths: [], tds: [],}转化为data[{SORT:‘’,TIME:‘’,TLLRGOAL:‘’,AF:‘’,ACTUALTLLR:‘’,}]格式
/**
* 用于表格封装方法
* @param {*object} newTableData:{ths:[],tds:[],}
* @param {*string} type
* @returns {*array} array:[{SORT:'',TIME:'',Cells1:'',Cells2:'',Cells3:'',}]
*/
transformTableData(newTableData, type) {
const transformedData = [];
newTableData.ths.forEach((th, index) => {
const thName = th.thName;
const colIndex = index;
var transformedObj = {};
switch (type) {
case 'Safety'://安全
transformedObj = {
SORT: (colIndex + 1).toString(),//列数
TIME: thName,//表头内容
Cells1: newTableData.tds[0][colIndex].tdName,//单元格内容1
Cells2: newTableData.tds[1][colIndex].tdName,//单元格内容2
Cells3: newTableData.tds[2][colIndex].tdName//单元格内容3
};
break;
default:
break;
}
transformedData.push(transformedObj);
});
return transformedData;
}
将格式转化为以表格
将data[{SORT:‘’,TIME:‘’,TLLRGOAL:‘’,AF:‘’,ACTUALTLLR:‘’,}]格式转化为newTableData{ths: [], tds: [],}
/**
* 用于将获取数据返回至符合表格的封装方法
* @param {*array} data:[{SORT:'',TIME:'',Cells1:'',Cells2:'',Cells3:'',}]
* @returns {*object} newTableData:{ths:[],tds:[],}
*/
transformData(data) {
// 初始化 newTableData 结构
let newTableData = {
ths: [],
tds: []
};
// 提取所有的列名,除了 "SORT" 和 "TIME"、"ID",因为这三个是固定的
const columns = Object.keys(data[0]).filter(key => key !== "SORT" && key !== "TIME" && key !== "ID");
// 填充 ths 数组
newTableData.ths = data.map(item => ({
thName: item.TIME,
colspan: 1,
rowspan: 1,
isEdit: true
}));
// 填充 tds 数组
for (let column of columns) {
let columnData = data.map(item =>
(
{
tdName: item[column],
colspan: 1,
rowspan: 1,
isEdit: true
}
)
);
// 将每个字段的值按顺序插入 tds 数组
newTableData.tds.push([...columnData]);
}
return newTableData;
}
全部代码
<template>
<div class="testbox">
<div class="bartype-class">
<div class="verticalbar-class"></div>
<h4>Details</h4>
<el-button v-if="!editTableSate" @click="editTableBtn" size="mini" icon="el-icon-edit" style="margin-left:2%;">编 辑</el-button>
<div v-else style="margin-left: auto;display:flex;">
<el-button @click="addTableColBtn" size="mini" icon="el-icon-plus">新增列</el-button>
<el-button @click="saveTableBtn" size="mini" icon="el-icon-upload">提 交</el-button>
</div>
</div>
<div class="tabletitle-class" v-if="detailTableData.title">{{detailTableData.title}}</div>
<div class="bar-class mytable" style="overflow: auto;">
<table border="1">
<tr ref="tableHeader">
<th v-for="(thItem, index) in detailTableData.ths" :key="'th-' + index" :rowspan="thItem.rowspan" :colspan="thItem.colspan" :style="'width:'+thItem.width+'px'">
{{thItem.thName}}
</th>
<th v-for="(newThItem,newindex) in newTableData.ths" :key="newindex" class="item" :id="'test'+newindex">
<el-input v-if="newThItem.isEdit && editTableSate" v-model="newThItem.thName" placeholder="请输入时间"></el-input>
<span v-else>{{newThItem.thName}}</span>
</th>
</tr>
<tr v-for="rowIndex in detailTableData.tdRows" :key="rowIndex">
<td v-for="(tdItem, colIndex) in detailTableData.tds[rowIndex-1]" :key="'td-' + rowIndex + '-' + colIndex" :rowspan="tdItem.rowspan" :colspan="tdItem.colspan" :style="'width:'+tdItem.width+'px'">
{{tdItem.tdName}}
</td>
<td v-for="(newTdItem, newColIndex) in newTableData.tds[rowIndex-1]" :key="newColIndex">
<el-input v-if="newTdItem.isEdit && editTableSate" v-model="newTdItem.tdName" placeholder="请输入"></el-input>
<span v-else>{{newTdItem.tdName}}</span>
</td>
</tr>
</table>
</div>
</div>
</template>
<script>
export default {
components: {},
data() {
return {
detailTableData: {},//表格显示的数据
// 安全表格固定数据;ID: 6
SafetyTableData:
{
tdRows: 3,//除表头的tbody所占行数
title: '表格名称表格名称表格名称表格名称表格名称',
ths: [
{
thName: 'Safety/安全',
colspan: 1,
rowspan: 4,
isEdit: false,
width: 100,
},
{
thName: '关键指标KPI',
colspan: 2,
rowspan: 1,
isEdit: false,
width: 100,
},
{
thName: '输出部门',
colspan: 1,
rowspan: 1,
isEdit: false,
width: 100,
},
{
thName: '公式',
colspan: 1,
rowspan: 1,
isEdit: false,
width: 180,
},
{
thName: '单位',
colspan: 1,
rowspan: 1,
isEdit: false,
width: 100,
},
],
tds: [
[
{ tdName: '伤害', colspan: 1, rowspan: 3, isEdit: false, width: 30, },
{ tdName: '占比%', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '游戏', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '(损失工时/ 投入工时)*100%', colspan: 1, rowspan: 1, isEdit: false, width: 180, },
{ tdName: '%', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
],
[
{ tdName: '一些值', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '游戏', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: 'ABCDEFG', colspan: 1, rowspan: 1, isEdit: false, width: 180, },
{ tdName: 'absolute', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
],
[
{ tdName: '实际占比%', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '游戏', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
{ tdName: '(实际损失工时/实际投入工时)*100%', colspan: 1, rowspan: 1, isEdit: false, width: 180, },
{ tdName: '%', colspan: 1, rowspan: 1, isEdit: false, width: 100, },
],
// Repeat similar structure for other rows as needed
],
},
newTableData: {
ths: [],
tds: [], // 初始包含一个空数组
},
newThObj: {
thName: '',
colspan: 1,
rowspan: 1,
isEdit: true,
},
newTdObj: {
tdName: '',
colspan: 1,
rowspan: 1,
isEdit: true,
},
editTableSate: false,//表格编辑状态
};
},
created() {
},
computed: {
},
mounted() {
this.detailTableData = this.SafetyTableData;
},
methods: {
// 编辑表格按钮
editTableBtn() {
this.editTableSate = true;
},
// 新增列的点击事件
addTableColBtn() {
// 向 newTableData.ths 添加一个新的表头对象
this.newTableData.ths.push({ ...this.newThObj });
// 如果 tds 为空,需要初始化它
if (this.newTableData.tds.length === 0) {
for (let i = 0; i < this.detailTableData.tdRows; i++) {
this.newTableData.tds.push([]);
}
}
// 遍历每一行,添加空单元格以匹配表头列数
this.newTableData.tds.forEach(row => {
row.push({ ...this.newTdObj });
});
},
// 保存表格的点击事件
saveTableBtn() {
// 反向遍历以避免删除元素时影响索引 -- 以表头为准,若表头为空,则提交后对应列为空
for (let i = this.newTableData.ths.length - 1; i >= 0; i--) {
if (!this.newTableData.ths[i].thName) {
let shouldDeleteColumn = true;
for (let row = 0; row < this.newTableData.tdRows; row++) {
if (this.newTableData.tds[row][i].tdName) {
shouldDeleteColumn = false;
break;
}
}
// 如果该列满足删除条件,则删除
if (shouldDeleteColumn) {
this.newTableData.ths.splice(i, 1);
this.newTableData.tds.forEach(row => {
row.splice(i, 1);
});
}
}
}
this.saveTableDataFun();
},
// 保存表格数据函数
async saveTableDataFun() {
var data = [];
if (this.newTableData.ths.length) {
data = this.transformTableData(this.newTableData, 'Safety');
}
console.log('data:', data);
this.editTableSate = false;
},
/**
* 用于表格封装方法
* @param {*object} newTableData:{ths:[],tds:[],}
* @param {*string} type
* @returns {*array} array:[{SORT:'',TIME:'',Cells1:'',Cells2:'',Cells3:'',}]
*/
transformTableData(newTableData, type) {
const transformedData = [];
newTableData.ths.forEach((th, index) => {
const thName = th.thName;
const colIndex = index;
var transformedObj = {};
switch (type) {
case 'Safety'://安全
transformedObj = {
SORT: (colIndex + 1).toString(),//列数
TIME: thName,//表头内容
Cells1: newTableData.tds[0][colIndex].tdName,//单元格内容1
Cells2: newTableData.tds[1][colIndex].tdName,//单元格内容2
Cells3: newTableData.tds[2][colIndex].tdName//单元格内容3
};
break;
default:
break;
}
transformedData.push(transformedObj);
});
return transformedData;
},
/**
* 用于将获取数据返回至符合表格的封装方法
* @param {*array} data:[{SORT:'',TIME:'',Cells1:'',Cells2:'',Cells3:'',}]
* @returns {*object} newTableData:{ths:[],tds:[],}
*/
transformData(data) {
// 初始化 newTableData 结构
let newTableData = {
ths: [],
tds: []
};
// 提取所有的列名,除了 "SORT" 和 "TIME"、"ID",因为这三个是固定的
const columns = Object.keys(data[0]).filter(key => key !== "SORT" && key !== "TIME" && key !== "ID");
// 填充 ths 数组
newTableData.ths = data.map(item => ({
thName: item.TIME,
colspan: 1,
rowspan: 1,
isEdit: true
}));
// 填充 tds 数组
for (let column of columns) {
let columnData = data.map(item =>
(
{
tdName: item[column],
colspan: 1,
rowspan: 1,
isEdit: true
}
)
);
// 将每个字段的值按顺序插入 tds 数组
newTableData.tds.push([...columnData]);
}
return newTableData;
}
},
watch: {
}
};
</script>
<style scoped>
.testbox {
width: 100%;
height: 100%;
}
.bartype-class {
display: flex;
align-items: center;
height: 25px;
margin: 10px 0;
}
.verticalbar-class {
width: 4px;
height: 100%;
background: #555555;
margin-right: 9px;
}
.mytable table {
border-collapse: collapse;
width: 100%;
font-size: 12px;
}
.mytable table td:first-child,
th:first-child {
/* font-weight: bold; */
/* background-color: pink; */
width: 15%;
}
.mytable table th,
td {
border: 1px solid #ddd;
text-align: center;
padding: 8px;
}
.mytable table th {
background-color: #f2f2f2;
}
.text-left {
text-align: left;
}
.mytable .el-input {
width: 80px;
}
.mytable .el-input__inner {
padding: 0 5px;
}
.item {
cursor: pointer;
}
.tabletitle-class {
background: #0070c0;
color: #fff;
margin: 0.5% 0;
display: flex;
justify-content: center;
align-items: center;
height: 30px;
font-weight: bold;
letter-spacing: 2px;
}
</style>