【VUE】Vue中实现树状表格结构编辑与版本对比的详细技术实现

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>

总结

通过这种方式,我们不仅提供了树状表格数据的编辑功能,还实现了通过颜色和样式标识不同版本之间数据变动的可视化展示。这使得数据的对比和审核变得直观和高效。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/830860.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入探究C++四大关键特性:初始化列表、友元函数、内部类与static成员

目录 1. 构造函数不为人知的那些事 1.1 构造函数体赋值与初始化列表对比 1.2 explicit关键字与构造函数隐式转换 2. static成员 2.1 static成员的概念 2.2 static成员的特性与应用 2.3 小结 3. C11 成员变量初始化新用法 4. 友元 4.1 友元函数 4.2 友元类 5. 内部类…

Python 中的花卉矩阵组合

使用场景描述 (rib) 协议编写脚本的基础知识。通过创建在 3D 空间中转换的基本几何图形,解决了 xyz 坐标系的基础知识。初步渲染是使用基本着色完成的,因此可以更容易地看到几何体。RenderMan 图1 图 1 是我作为作业参考的示例图片,并尝试匹配 中的图片。为了完成这项任务…

Python | Leetcode Python题解之第61题旋转链表

题目&#xff1a; 题解&#xff1a; class Solution:def rotateRight(self, head: ListNode, k: int) -> ListNode:if k 0 or not head or not head.next:return headn 1cur headwhile cur.next:cur cur.nextn 1if (add : n - k % n) n:return headcur.next headwhi…

机器学习的两种典型任务

机器学习中的典型任务类型可以分为分类任务&#xff08;Classification&#xff09;和回归任务&#xff08;Regression&#xff09; 分类任务 回归任务 简单的理解&#xff0c;分类任务是对离散值进行预测&#xff0c;根据每个样本的值/特征预测该样本属于类 型A、类型B 还是类…

Django后台项目开发实战四

用户可以浏览工作列表以及工作详情 第四阶段 在 jobs 文件夹下创建 templates 文件夹&#xff0c;在里面创建 base.html 网页&#xff0c;内容如下 <!-- base.html --> <div style"text-align:center;"><h1 style "margin:auto; width:50%;&…

MATLAB - 自定义惯性矩阵

系列文章目录 前言 一、关键惯性约定 Simscape 多体软件在惯性定义中采用了一系列约定。请注意这些约定&#xff0c;因为如果手动进行惯性计算&#xff0c;这些约定可能会影响计算结果。如果您的惯性数据来自 CAD 应用程序或其他第三方软件&#xff0c;这些约定还可能影响到您需…

Mac好用又好看的终端iTerm2 + oh-my-zsh

Mac好用又好看的终端iTerm2 1. iTerm2的下载安装2. oh-my-zsh的安装2.1 官网安装方式2.2 国内镜像源安装方式 3. oh-my-zsh配置3.1 存放主题的路径3.2 存放插件的路径3.3 配置文件路径 1. iTerm2的下载安装 官网下载&#xff1a; iTerm2 2. oh-my-zsh的安装 oh-my-zsh是一…

C语言 | Leetcode C语言题解之第60题排列序列

题目&#xff1a; 题解&#xff1a; char* getPermutation(int n, int k) {int factorial[n];factorial[0] 1;for (int i 1; i < n; i) {factorial[i] factorial[i - 1] * i;}--k;char* ans malloc(n 1);ans[n] \0;int valid[n 1];for (int i 0; i < n; i) {val…

飞书API(6):使用 pandas 处理数据并写入 MySQL 数据库

一、引入 上一篇了解了飞书 28 种数据类型通过接口读取到的数据结构&#xff0c;本文开始探讨如何将这些数据写入 MySQL 数据库。这个工作流的起点是从 API 获取到的一个完整的数据&#xff0c;终点是写入 MySQL 数据表&#xff0c;表结构和维格表结构类似。在过程中可以有不同…

【Leetcode每日一题】 动态规划 - 简单多状态 dp 问题 - 按摩师(难度⭐)(64)

1. 题目解析 题目链接&#xff1a;面试题 17.16. 按摩师 这个问题的理解其实相当简单&#xff0c;只需看一下示例&#xff0c;基本就能明白其含义了。 2.算法原理 一、状态定义 在解决这类动态规划问题时&#xff0c;首先我们需要明确状态的定义。对于本题&#xff0c;我们…

在mac上安装node.js及使用npm,yarn相关命令教程

1、安装node.js 官网&#xff1a;Node.js — Download Node.js 选择需要的版本&#xff0c;点击DownLoad 2、点击继续&#xff0c;直到安装成功。 2.1打开终端输入命令node -v 显示版本号则说明已安装成功 3、全局安装yarn命令 1、sudo npm install --global yarn &#xf…

Git学习笔记(五)IDEA使用Git

在前面几篇文章中&#xff0c;我们已经介绍了git的基础知识&#xff0c;知道了其主要作用是用来进行代码的版本管理&#xff1b;并且已经介绍了Git操作的常用命令。在日常的开发环境下&#xff0c;除了通过Bash命令行来操作Git之外&#xff0c;我们另外一种常用的操作方式则是直…

基于STC12C5A60S2系列1T 8051单片机的Proteus中的单片机发送一帧或一串数据给串口调试助手软件接收区显示出来的串口通信应用

基于STC12C5A60S2系列1T 8051单片机的Proteus中的单片机发送一帧或一串数据给串口调试助手软件接收区显示出来的串口通信应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机串口通信介绍STC12C5A60S2系列1T 8051单片机串口通信的结构基于STC12C5A60S2系列…

Python_GUI框架 PyQt 与 Pyside6的介绍

Python_GUI框架 PyQt 与 Pyside6的介绍 一、简介 在Python的GUI&#xff08;图形用户界面&#xff09;开发领域&#xff0c;PyQt和PySide6是两个非常重要的工具包。它们都基于Qt库&#xff0c;为Python开发者提供了丰富的GUI组件和强大的功能。当然Python也有一些其他的GUI工…

手把手教数据结构与算法:优先级队列(银行排队问题)

队列 基本概念 队列的定义 队列&#xff08;Queue&#xff09;&#xff1a;队列是一种常见的数据结构&#xff0c;遵循先进先出&#xff08;First-In-First-Out, FIFO&#xff09;的原则。在队列中&#xff0c;元素按照进入队列的顺序排列。队列是一个线性的数据结构&#x…

【团体程序设计天梯赛】往年关键真题 L2-036 网红点打卡攻略 模拟 L2-037 包装机 栈和队列 详细分析完整AC代码

【团体程序设计天梯赛 往年关键真题 详细分析&完整AC代码】搞懂了赛场上拿下就稳 【团体程序设计天梯赛 往年关键真题 25分题合集 详细分析&完整AC代码】&#xff08;L2-001 - L2-024&#xff09;搞懂了赛场上拿下就稳了 【团体程序设计天梯赛 往年关键真题 25分题合…

《Redis使用手册之列表》

《Redis使用手册之列表》 目录 **《Redis使用手册之列表》****LPUSH&#xff1a;将元素推入列表左端****LPUSHX、RPUSHX&#xff1a;只对已存在的列表执行推入操作****LPOP&#xff1a;弹出列表最左端的元素****RPOP&#xff1a;弹出列表最右端的元素****RPOPLPUSH&#xff1a;…

ElasticSearch教程入门到精通——第二部分(基于ELK技术栈elasticsearch 7.x新特性)

ElasticSearch教程入门到精通——第二部分&#xff08;基于ELK技术栈elasticsearch 7.x新特性&#xff09; 1. JavaAPI-环境准备1.1 新建Maven工程——添加依赖1.2 HelloElasticsearch 2. 索引2.1 索引——创建2.2 索引——查询2.3 索引——删除 3. 文档3.1 文档——重构3.2 文…

SQL 基础 | BETWEEN 的常见用法

在SQL中&#xff0c;BETWEEN是一个操作符&#xff0c;用于选取介于两个值之间的数据。 它包含这两个边界值。BETWEEN操作符常用于WHERE子句中&#xff0c;以便选取某个范围内的值。 以下是BETWEEN的一些常见用法&#xff1a; 选取介于两个值之间的值&#xff1a; 使用 BETWEEN来…

基于昇腾AI | 英码科技EA500I使用AscendCL实现垃圾分类和视频物体分类应用

现如今&#xff0c;人工智能迅猛发展&#xff0c;AI赋能产业发展的速度正在加快&#xff0c;“AI”的需求蜂拥而来&#xff0c;但AI应用快速落地的过程中仍存在很大的挑战&#xff1a;向下需要适配的硬件&#xff0c;向上需要完善的技术支持&#xff0c;两者缺一不可。 基于此&…