适用场景:有若干个表格,前面几列格式不一致,但是后面几列格式皆为占一个单元格,所以需要封装表格,表格元素自动根据数据结构生成即可;并且用户可新增列数据。
分类:
固定数据部分 就是根据数据结构传参设置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,//单元格内容1Cells2: newTableData.tds[1][colIndex].tdName,//单元格内容2Cells3: 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: 6SafetyTableData:{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,//单元格内容1Cells2: newTableData.tds[1][colIndex].tdName,//单元格内容2Cells3: 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>