Vue中实现树状表格结构编辑与版本对比的详细技术实现
在Vue中,创建一个可编辑的树状表格并实施版本对比功能是一种需求较为常见的场景。在本教程中,我们将使用Vue结合Element UI的el-table
组件,来构建一个树状表格,其中包含添加、编辑功能,并通过特定的方法展示数据变更。本文会详继解析每一步的代码实现。
图中,黄色为修改的数据,绿色为新增,红色是被删除的数据。
初始设置与组件
首先,我们使用el-table
组件创建基础的表格,并通过tree-props
属性指定了如何展示树形数据的结构。
<template><div class="h-station"><el-card class="h-card"><el-button type="primary" @click="addScheme">添加一级分类</el-button><el-button type="primary">批量导入</el-button><el-table:data="tableData":row-class-name="getRowClass"style="width: 100%; margin-top: 15px"borderheight="calc(100vh - 260px)"row-key="id":tree-props="{ children: 'children' }"><el-table-column align="center" prop="name" label="维修方案名称" min-width="100px"><template slot-scope="scope"><el-inputv-if="scope.row.type !== 'Button'"style="width: calc(100% - 50px)"v-model="scope.row.name":disabled="scope.row.type === 'delete'"></el-input></template></el-table-column></el-table></el-card></div>
</template>
数据模型和方法定义
在data
函数中定义的tableData
数组,包含了表格数据和结构信息。此外,我们会备份原始数据以供版本对比之用。
<script>
export default {data() {return {rawData: [],tableData: [...],curMateral: { children: [] },materials: [],materialsTypeIds: [],materialsTypeNames: [],};},created() {this.rawData = JSON.parse(JSON.stringify(this.tableData));this.loadMaterialsInfo(); // 模拟加载物料信息},methods: {enrichDataWithLevel(data, level = 1, parent = null) {return data.map(item => ({...item,level,children: item.children ? this.enrichDataWithLevel(item.children, level + 1, item) : [],parent,}));},// 示例方法:模拟加载物料信息loadMaterialsInfo() {this.materials = [{ materialsTypeId: 1, materialsTypeName: '物料1' }];this.curMateral = this.materials[0];},}
};
</script>
版本对比的展示实现
我们通过getRowClass
方法为表格行动态赋予样式,标识数据的更改状态:新添加、更改或删除。
methods: {getRowClass({ row, rowIndex }) {let rawNode = this.findNodeById(this.rawData, row.id);if (row.type === 'delete') {return 'deleted-row';} else if (row.id.includes && row.id.includes('cwx-') && row.type !== 'Button') {return 'new-row';} else if (rawNode) {let flag = true;if (rawNode&&!(row.id+'').includes('cwx-')) {let keys = Object.keys(rawNode);keys.push('materialsTypeIds')for (let key of keys) {if(key==='materialsTypeIds'){if((!rawNode.materialsTypeIds||rawNode.materialsTypeIds.length===0)&&(!row.materialsTypeIds||row.materialsTypeIds.length===0)){}else{flag=false}}else if (rawNode[key] !== row[key]&&(key!=='parent')&&(key!=='children')) {flag = false;}}}if(!flag){return 'change-row';}}},
}
样式定义
使用SCSS来定义不同状态下行的样式:
<style scoped>
::v-deep .change-row {background-color: rgba(230,162,60,0.2);
}
::v-deep .el-table--enable-row-hover .el-table__body .change-row:hover > td.el-table__cell {background-color: rgba(230,162,60,0.2);
}
::v-deep .new-row {background-color: rgba(103, 194, 58, 0.2);
}
::v-deep .el-table--enable-row-hover .el-table__body .new-row:hover > td.el-table__cell {background-color: rgba(103, 194, 58, 0.2);
}
::v-deep .deleted-row {background-color: rgba(245, 108, 108, 0.2);
}
::v-deep .deleted-row::after {content: '';position: absolute;left: 0;top: 50%; /* 置于行的中间 */width: 100%; /* 线的宽度为整行 */border-top: 1px solid #000; /* 红色的线,你可以调整颜色和线的样式 */opacity: 0.7; /* 线的透明度,你可以调整它使线更清晰或更隐蔽 */
}
::v-deep .el-table .el-table__row {position: relative;
}
::v-deep .el-table--enable-row-hover .el-table__body .deleted-row:hover > td.el-table__cell {background-color: rgba(245, 108, 108, 0.2);
}
</style>
完整代码实现
<template><div class="h-station"><el-card class="h-card"><el-button type="primary" @click="addScheme">添加一级分类</el-button><el-button type="primary">批量导入</el-button><el-table:row-class-name="getRowClass":data="tableData"style="width: 100%; margin-top: 15px"borderheight="calc(100vh - 260px)"row-key="id":tree-props="{ children: 'children' }"><el-table-column align="center" prop="name" label="维修方案名称" min-width="100px"><template slot-scope="scope"><el-inputv-if="scope.row.type !== 'Button'"style="width: calc(100% - 50px)"v-model="scope.row.name":disabled="scope.row.type === 'delete'"></el-input><el-button v-else type="primary" :disabled="tableData.find((item) => item.id === scope.row.parent.id).type==='delete'" @click="addSubScheme(scope.row)">添加子方案</el-button></template></el-table-column><el-table-column align="center" prop="state" label="状态" min-width="30px"><template slot-scope="scope"><el-switchv-if="scope.row.level === 0"v-model="scope.row.state"active-color="#199f7e"inactive-color="#eee"></el-switch><span v-else-if="scope.row.type === 'Button'"></span><span v-else>--</span></template></el-table-column><el-table-column align="center" show-overflow-tooltip prop="wlz" label="物料组"><template slot-scope="scope">{{getMaterialsNameStr(getMaterialsName(scope.row.materialsTypeIds || []))}}</template></el-table-column><el-table-column align="center" prop="cjsj" label="创建时间" min-width="60px"> </el-table-column><el-table-column align="center" prop="handle" label="操作" min-width="40px"><template slot-scope="scope"><template v-if="scope.row.type !== 'Button'"><template v-if="scope.row.type === 'delete'"><el-button type="text" @click="revokeDeleteScheme(scope.row)">撤销删除</el-button></template><template v-else><el-button v-if="scope.row.level > 0" type="text" @click="relatedMaterials(scope.row)">关联物料</el-button><el-button type="text" @click="deleteScheme(scope.row)">删除</el-button></template></template></template></el-table-column></el-table><p style="text-align: center;margin-top: 20px;"><el-button type="primary">保存更改</el-button><el-button>返回</el-button></p></el-card><common-dialogtitle="关联物料组":visible="visible"width="700px"confirmText="保存":loading="btnloading":handleClose="handleClose":handleConfirm="handleConfirm"><div style="width: 100%; overflow: hidden"><el-row :gutter="20"><el-col :span="3"><p style="margin-top: 30px">物料组:</p></el-col><el-col :span="10"><p class="mb10">物料组</p><div class="select-box"><p v-for="item in materials" :key="item.materialsTypeId" :class="{'cur-materials': item.materialsTypeId===curMateral.materialsTypeId}" class="materials" @click="selectMateral(item)"><span>{{ item.materialsTypeName }}</span><i class="el-icon-arrow-right"></i></p></div></el-col><el-col :span="11"><p class="mb10">二级分类</p><div class="select-box"><el-checkbox-group v-model="materialsTypeIds" @change="changeMaterialsTypes"><p v-for="item in curMateral.children" :key="item.materialsTypeId"><el-checkbox :label="item.materialsTypeId">{{ item.materialsTypeName }}</el-checkbox></p></el-checkbox-group></div></el-col></el-row><el-row :gutter="20" style="margin-top: 15px"><el-col :span="3"><p>已关联物料组:</p></el-col><el-col :span="21"><div class="select-box h150"><p v-for="(item, index) in materialsTypeNames" :key="index">{{ item }}</p></div></el-col></el-row></div></common-dialog></div>
</template><script>
import commonDialog from '@/components/CommonDialog/index';
import { getSecondaryClassi } from '@/api/sparePartsManagement/basic/materialsInfo.js';
export default {components: { commonDialog },data() {return {rawData: [],tableData: [{id: 1,name: '123',state: true,children: [{ id: 10, name: '10' },{ id: 11, name: '11' },],},{id: 2,name: '222',state: true,children: [],},],visible: false,btnloading: false,curRow: null,curMateral: {children: [],},materials: [],materialsTypeIds: [],materialsTypeNames: [],};},created() {this.tableData = this.enrichDataWithLevel(this.tableData);this.rawData = JSON.parse(JSON.stringify(this.tableData));this.getMaterialsInfo();},methods: {findNodeById(tree, id) {for (let i = 0; i < tree.length; i++) {if (tree[i].id === id) {return tree[i];}if (tree[i].children && tree[i].children.length) {const found = this.findNodeById(tree[i].children, id);if (found) {return found;}}}return null; // 如果在树中找不到该节点},getRowClass({ row, rowIndex }) {let rawNode = this.findNodeById(this.rawData, row.id);if (row.type === 'delete') {return 'deleted-row';} else if (row.id.includes && row.id.includes('cwx-') && row.type !== 'Button') {return 'new-row';} else if (rawNode) {let flag = true;if (rawNode&&!(row.id+'').includes('cwx-')) {let keys = Object.keys(rawNode);keys.push('materialsTypeIds')for (let key of keys) {if(key==='materialsTypeIds'){if((!rawNode.materialsTypeIds||rawNode.materialsTypeIds.length===0)&&(!row.materialsTypeIds||row.materialsTypeIds.length===0)){}else{flag=false}}else if (rawNode[key] !== row[key]&&(key!=='parent')&&(key!=='children')) {flag = false;}}}if(!flag){return 'change-row';}}},changeMaterialsTypes() {this.materialsTypeNames = this.getMaterialsName(this.materialsTypeIds);},getMaterialsNameStr(arr) {return arr.join(';');},getMaterialsName(ids) {let nodes = this.findNodesById({ children: this.materials, materialsTypeId: -1 }, ids);let tree = [];for (let node of nodes) {let parentNode = this.materials.find((item) => item.materialsTypeId === node.parentId);let treeIndex = tree.findIndex((item) => item.materialsTypeId === parentNode.materialsTypeId);if (treeIndex === -1) {tree.push({ ...parentNode, children: [node] });} else {tree[treeIndex].children.push(node);}}let arr = [];for (let parent of tree) {let str = parent.materialsTypeName + ':';let childs = parent.children.map((item) => item.materialsTypeName).join('、');str += childs;arr.push(str);}return arr;},findNodesById(tree, ids) {// 定义一个结果数组来存储找到的节点let result = [];// 定义一个递归函数用于在树中找到具有特定id的节点function searchTree(node, ids) {// 如果当前节点的id在ids数组中,那么把节点按照ids的顺序加入结果数组const index = ids.indexOf(node.materialsTypeId);if (index !== -1) {result[index] = node;}// 如果当前节点有子节点,递归搜索每个子节点if (node.children && node.children.length) {node.children.forEach((child) => searchTree(child, ids));}}// 开始从树的根节点开始递归搜索searchTree(tree, ids);// 过滤掉结果数组中的未定义项,并返回结果return result.filter((node) => node !== undefined);},selectMateral(item) {this.curMateral = item;},getMaterialsInfo() {getSecondaryClassi().then((res) => {this.materials = res.data;});},relatedMaterials(row) {this.curRow = row;this.materialsTypeIds = [...(this.curRow?.materialsTypeIds || [])];this.changeMaterialsTypes();this.visible = true;},handleClose() {this.visible = false;this.curRow = null;this.curMateral = {children: [],};},handleConfirm() {this.$set(this.curRow, 'materialsTypeIds', [...this.materialsTypeIds]);this.handleClose();},deleteScheme(row) {if (row.id.includes && row.id.includes('cwx-')) {this.deleteNode(this.tableData, row.id);} else {this.$set(row, 'type', 'delete');if(row.children){for(let item of row.children){if(item.type!=='Button'){this.$set(item, 'type', 'delete');}}}}},deleteNode(data, id) {for (let i = 0; i < data.length; i++) {if (data[i].id === id) {// 直接从数组中删除data.splice(i, 1);return true; // 表示删除完成}if (data[i].children && data[i].children.length > 0) {// 递归调用删除函数if (this.deleteNode(data[i].children, id)) {if (data[i].children.length === 0) {// 如果子数组为空,则删除子数组属性delete data[i].children;}return true;}}}return false;},revokeDeleteScheme(row) {this.$set(row, 'type', '');if(row.children){for(let item of row.children){if(item.type!=='Button'){this.$set(item, 'type', '');}}}},addScheme() {let now = new Date().valueOf();let item = {id: 'cwx-' + now, //id 为 cwx-xxx这种格式的均为临时id,和后端返回的id隔离name: '',state: true,wlz: '',cjsj: '',level: 0,parent: null,children: [],};item.children.push({id: 'cwx-' + now + 1, //id 为 cwx-xxx这种格式的均为临时id,和后端返回的id隔离type: 'Button',level: 1,parent: item,});this.tableData.push(item);},addSubScheme(row) {let parent = this.tableData.find((item) => item.id === row.parent.id);let now = new Date().valueOf();parent.children.splice(-1, 0, {id: 'cwx-' + now, //id 为 cwx-xxx这种格式的均为临时id,和后端返回的id隔离name: '',state: true,wlz: '',cjsj: '',level: 1,parent: row.parent,});},enrichDataWithLevel(data, level = 0, parent = null) {let list = data.map((item) => ({...item,level: level,children: item.children ? this.enrichDataWithLevel(item.children, level + 1, item) : null,parent: parent,}));if (level === 1) {let now = new Date().valueOf();list.push(//添加按钮节点{id: 'cwx-' + now, //id 为 cwx-xxx这种格式的均为临时id,和后端返回的id隔离type: 'Button',parent: parent,});}return list;},},
};
</script><style lang="scss" scoped>
p {margin: 0;
}
::v-deep .change-row {background-color: rgba(230,162,60,0.2);
}
::v-deep .el-table--enable-row-hover .el-table__body .change-row:hover > td.el-table__cell {background-color: rgba(230,162,60,0.2);
}
::v-deep .new-row {background-color: rgba(103, 194, 58, 0.2);
}
::v-deep .el-table--enable-row-hover .el-table__body .new-row:hover > td.el-table__cell {background-color: rgba(103, 194, 58, 0.2);
}
::v-deep .deleted-row {background-color: rgba(245, 108, 108, 0.2);
}
::v-deep .deleted-row::after {content: '';position: absolute;left: 0;top: 50%; /* 置于行的中间 */width: 100%; /* 线的宽度为整行 */border-top: 1px solid #000; /* 红色的线,你可以调整颜色和线的样式 */opacity: 0.7; /* 线的透明度,你可以调整它使线更清晰或更隐蔽 */
}
::v-deep .el-table .el-table__row {position: relative;
}
::v-deep .el-table--enable-row-hover .el-table__body .deleted-row:hover > td.el-table__cell {background-color: rgba(245, 108, 108, 0.2);
}
.select-box {padding: 10px;height: 200px;border: 1px solid #eee;overflow: auto;
}.materials {line-height: 25px;padding: 0 15px;cursor: pointer;span {display: inline-block;vertical-align: middle;width: calc(100% - 16px);overflow: hidden; //超出的文本隐藏text-overflow: ellipsis; //溢出用省略号显示white-space: nowrap; //溢出不换行}
}
.cur-materials{background-color: rgba(25,159,126,0.41);
}.h150 {height: 150px;
}.mb10 {margin-bottom: 10px;
}
</style>
总结
通过这种方式,我们不仅提供了树状表格数据的编辑功能,还实现了通过颜色和样式标识不同版本之间数据变动的可视化展示。这使得数据的对比和审核变得直观和高效。