使用原生 JS 实现树形表格 ?
【 如需查看完整示例请点击:tree-table_JS: 使用原生 JS 实现,树形表格案例。支持多级数据渲染,新增,删除,编辑等操作。 】
- 请求本地 JSON 文件获取数据 。
- 问题:cors ,跨域问题 。
- 解决:使用 jsonp 方式,请求相关数据 。
- 注意:在 json 文件中,json 数据使用 callback( json数据 ) 包裹,才能保持浏览器控制台打印出数据无报错。我这里回调函数是 getData( json数据 )。
- 代码:
<script type="text/javascript"> /* getData 函数: JSONP 的函调函数,解决跨域问题。请求本地 JSON 文件 参数: data---》拿到的 JSON 数据 */ function getData(data) { // initData---》存放当前操作完的全局树形数据 var initData = [] initData = JSON.parse(JSON.stringify(formatData(data.menuTree,''))) initTable(initData) } </script> <script type="text/javascript" src="./data.json?callback=getData"></script>
- 一维数组数据转换为树形数据 (递归)。
- 思路:使用递归的方式将一维数组转换成树形数据,根据 menuParentid 和 menuId 的对比,对数据进行分类,添加到合适的位置上。
- 代码:
/* formatData 函数:数据转换,将一维数组数据转换为树形数据 参数:data---》原始一维数组数据 返回值:resultArr---》格式化好的树形数据 */ function formatData(data, pid) { var resultArr = [], temp; for (var i = 0; i < data.length; i++) { if (data[i].menuParentid == pid) { var obj = JSON.parse(JSON.stringify(data[i])); temp = formatData(data, data[i].menuId); if (temp.length > 0) { obj.children = temp; } resultArr.push(obj); } } return resultArr; }
- JS 中初始化表格 。
- 思路:在 initTR_DG 函数中递归的根据数据,动态新增 tr 到 table 中。根据 当前数据项的 menuType 是否为菜单,决定是否有前缀的展开和收起按钮。
- 代码:
/* initTable 函数: 初始化表格 参数: data---》转换的树形数据 */ function initTable(data) { globalData = JSON.parse(JSON.stringify(data)) var tableEL = document.createElement('table') var containerEL = document.getElementById('container') containerEL.appendChild(tableEL) tableEL.appendChild(createTH()) initTR_DG(data,tableEL,'') } function initTR_DG(data,tableEL,index){ if(data && data.length != 0){ for(var i=0; i<data.length; i++){ if(data[i].menuType == '菜单') { var trEL = createTR(data[i], 1, index+'.'+(i+1)) } else { var trEL = createTR(data[i], 0, index+'.'+(i+1)) } tableEL.appendChild(trEL) if(data[i].children && data[i].children.length != 0){ initTR_DG(data[i].children,tableEL,index+'.'+(i+1)) } } } } /* createTH 函数: 创建表头 返回值: trEL---》表头元素 */ function createTH() { var trEL = document.createElement('tr') var thEL0 = document.createElement('th') thEL0.innerHTML = '序号' trEL.appendChild(thEL0) var thEL1 = document.createElement('th') thEL1.innerHTML = '功能名称' trEL.appendChild(thEL1) var thEL2 = document.createElement('th') thEL2.innerHTML = '是否启用' trEL.appendChild(thEL2) var thEL3 = document.createElement('th') thEL3.innerHTML = '功能类型' trEL.appendChild(thEL3) var thEL4 = document.createElement('th') thEL4.innerHTML = '创建时间' trEL.appendChild(thEL4) var thEL5 = document.createElement('th') thEL5.innerHTML = '功能备注' trEL.appendChild(thEL5) var thEL6 = document.createElement('th') thEL6.innerHTML = '相关操作' trEL.appendChild(thEL6) return trEL } /* createTR 函数: 创建每一列 参数1: currentData---》当前列的数据 参数2: type---》类型,决定有没有展开折叠按钮,1 有,其他无 参数3 index---》计算的当前列的标号 参数4: tier---》第几层 返回值: trEL---》创建的当前 tr */ function createTR(currentData, type, index) { var trEL = document.createElement('tr') if (type == 1) { var tdEL0 = document.createElement('td') tdEL0.innerHTML = index tdEL0.style.fontWeight = 'bold' tdEL0.style.fontSize = '8px' trEL.appendChild(tdEL0) var spanEL = document.createElement('span') spanEL.classList.add('span') spanEL.title = '展开' spanEL.onclick = clickZK var spanEL1 = document.createElement('span') spanEL1.classList.add('span1') spanEL1.title = '收起' spanEL1.onclick = clickSQ var tdEL1 = document.createElement('td') tdEL1.innerHTML = currentData.menuText tdEL0.style.textAlign = 'left' tdEL0.appendChild(spanEL) tdEL0.appendChild(spanEL1) trEL.appendChild(tdEL1) var tdEL2 = document.createElement('td') tdEL2.innerHTML = currentData.menuIsmodify trEL.appendChild(tdEL2) var tdEL3 = document.createElement('td') tdEL3.innerHTML = currentData.menuType currentData.menuType == '菜单' ? tdEL3.style.color = '#409EFF' : tdEL3.style.color = 'green' trEL.appendChild(tdEL3) var tdEL4 = document.createElement('td') tdEL4.innerHTML = datefomate(currentData.menuCreatetime) trEL.appendChild(tdEL4) var tdEL5 = document.createElement('td') tdEL5.innerHTML = currentData.menuNote trEL.appendChild(tdEL5) var tdEL6 = document.createElement('td') var editBtn = document.createElement('button') editBtn.innerText = '编辑' editBtn.classList.add('button1') editBtn.onclick = editTableData tdEL6.appendChild(editBtn) var delBtn = document.createElement('button') delBtn.innerText = '删除' delBtn.classList.add('button2') delBtn.onclick = delTableData tdEL6.appendChild(delBtn) var addBtn = document.createElement('button') addBtn.innerText = '新增' addBtn.onclick = addTableData tdEL6.appendChild(addBtn) addBtn.classList.add('button0') trEL.appendChild(tdEL6) } else { var tdEL0 = document.createElement('td') var strongEL = document.createElement('strong') strongEL.innerHTML = index tdEL0.appendChild(strongEL) tdEL0.style.fontSize = '8px' trEL.appendChild(tdEL0) var tdEL1 = document.createElement('td') tdEL1.innerHTML = currentData.menuText trEL.appendChild(tdEL1) var tdEL2 = document.createElement('td') tdEL2.innerHTML = currentData.menuIsmodify trEL.appendChild(tdEL2) var tdEL3 = document.createElement('td') tdEL3.innerHTML = currentData.menuType currentData.menuType == '菜单' ? tdEL3.style.color = '#409EFF' : tdEL3.style.color = 'green' trEL.appendChild(tdEL3) var tdEL4 = document.createElement('td') tdEL4.innerHTML = datefomate(currentData.menuCreatetime) trEL.appendChild(tdEL4) var tdEL5 = document.createElement('td') tdEL5.innerHTML = currentData.menuNote trEL.appendChild(tdEL5) var tdEL6 = document.createElement('td') var editBtn = document.createElement('button') editBtn.innerText = '编辑' editBtn.classList.add('button1') editBtn.onclick = editTableData tdEL6.appendChild(editBtn) var delBtn = document.createElement('button') delBtn.innerText = '删除' delBtn.classList.add('button2') delBtn.onclick = delTableData tdEL6.appendChild(delBtn) trEL.appendChild(tdEL6) } return trEL }
- 列收起和列展开。
- 思路:根据 clickSQ 的点击事件,确定点击的数据项,在根据 clickSQ_DG 函数,递归的找出当前数据项,让它的 children = [ ],重新初始化表格。根据 clickZK 的点击事件,清空原始 table 表格结构,使用缓存的 initData 数据去重新初始化表格。
- 代码:
// clickSQ 函数: 列收起 function clickSQ() { initData = JSON.parse(JSON.stringify(globalData)) var currentText = this.parentNode.parentNode.children[1].innerText clickSQ_DG(globalData,currentText) document.getElementById('container').innerHTML = '' initTable(globalData) SQstatus = false } function clickSQ_DG(globalData,currentText){ for (var i = 0; i < globalData.length; i++) { if (globalData[i].menuText == currentText) { globalData[i].children = [] SQstatus = true break } if(SQstatus == false){ if(globalData[i].children && globalData[i].children.length != 0){ clickSQ_DG(globalData[i].children,currentText) } } } } // clickZK 函数: 列展开 function clickZK() { document.getElementById('container').innerHTML = '' initTable(initData) }
- 删除操作 。
- 思路:根据 delTableData 确定当前操作的数据列,使用 delTableData_DG 函数递归来找到当前的数据,进行删除 ,重新初始化表格 。
- 表格:
// delTableData函数: 删除操作 function delTableData() { var currentText = this.parentNode.parentNode.children[1].innerText delTableData_DG(globalData,currentText) document.getElementById('container').innerHTML = '' initTable(globalData) SQstatus = false } function delTableData_DG(globalData,currentText){ for (var i = 0; i < globalData.length; i++) { if (globalData[i].menuText == currentText) { globalData.splice(i, 1) SQstatus = true break } if(SQstatus == false){ if(globalData[i].children && globalData[i].children.length != 0){ delTableData_DG(globalData[i].children,currentText) } } } }
- 编辑操作 。
- 思路:根据 editTableData 确定当前操作的数据列,使用 editTableData_DG 函数递归来找到当前的数据,使用 setEditData 函数进行数据回显,使用 getCurrentEdit 函数进行数据缓存,当点击编辑保存事件时进行当前缓存数据的更改,最后重新初始化表格 。
- 代码:
// editTableData 函数: 编辑操作 function editTableData() { document.getElementById('edit').style.display = 'block' var currentText = this.parentNode.parentNode.children[1].innerText editTableData_DG(globalData,currentText) SQstatus = false } function editTableData_DG(globalData,currentText){ for (var i = 0; i < globalData.length; i++) { if (globalData[i].menuText == currentText) { setEditData(globalData[i]) getCurrentEdit(globalData[i]) SQstatus = true return } if(SQstatus == false){ if(globalData[i].children && globalData[i].children.length != 0){ editTableData_DG(globalData[i].children,currentText,currentGlobalData) } } } } function getCurrentEdit(data){ return currentGlobalData = data } /* etEditData 函数: 编辑数据回显 参数: data---》编辑回显的当前数据 */ function setEditData(data) { document.getElementById('menuText').value = data.menuText document.getElementById('menuIsmodify').value = data.menuIsmodify == '启用' ? 1 : 0 document.getElementById('menuType').value = data.menuType == '菜单' ? 1 : 0 document.getElementById('menuCreatetime').value = datefomate(data.menuCreatetime) document.getElementById('menuNote').value = data.menuNote } // 编辑关闭 document.getElementById('editClose').onclick = function() { document.getElementById('edit').style.display = 'none' } // 编辑保存 document.getElementById('editSave').onclick = function() { currentGlobalData.menuText = document.getElementById('menuText').value currentGlobalData.menuIsmodify = document.getElementById('menuIsmodify').value == 1 ? '启用' : '禁用' currentGlobalData.menuType = document.getElementById('menuType').value == 1 ? '菜单' : '链接' currentGlobalData.menuCreatetime = document.getElementById('menuCreatetime').value currentGlobalData.menuNote = document.getElementById('menuNote').value document.getElementById('container').innerHTML = '' initTable(globalData) document.getElementById('edit').style.display = 'none' } // 编辑取消 document.getElementById('editCancle').onclick = function() { document.getElementById('edit').style.display = 'none' }
- 新增操作 。
- 思路:根据 addTableData 确定当前操作的数据列,使用 addTableData_DG 函数递归来找到当前的数据,使用 getCurrentAdd 函数进行数据缓存,当点击新增保存事件时进将数据添加到缓存的当前数据项的 children 中,最后重新初始化表格 。
- 代码:
// addTableData 函数:新增操作 function addTableData() { document.getElementById('add').style.display = 'block' document.getElementById('menuCreatetime1').value = datefomate(Date.now()) var currentText = this.parentNode.parentNode.children[1].innerText addTableData_DG(globalData,currentText) SQstatus = false } function addTableData_DG(globalData,currentText){ for (var i = 0; i < globalData.length; i++) { if (globalData[i].menuText == currentText) { getCurrentAdd(globalData[i]) SQstatus = true return } if(SQstatus == false){ if(globalData[i].children && globalData[i].children.length != 0){ addTableData_DG(globalData[i].children,currentText) } } } } function getCurrentAdd(data){ return currentGlobalData1 = data } // 新增关闭 document.getElementById('addClose').onclick = function() { document.getElementById('menuText1').value = '' document.getElementById('menuNote1').value = '' document.getElementById('add').style.display = 'none' } //新增保存 document.getElementById('addSave').onclick = function() { var obj = {} obj.children = [] obj.menuText = document.getElementById('menuText1').value obj.menuIsmodify = document.getElementById('menuIsmodify1').value == 1 ? '启用' : '禁用' obj.menuType = document.getElementById('menuType1').value == 1 ? '菜单' : '链接' obj.menuCreatetime = document.getElementById('menuCreatetime1').value obj.menuNote = document.getElementById('menuNote1').value if(!currentGlobalData1.children){ currentGlobalData1.children = [] currentGlobalData1.children[0] = obj }else{ currentGlobalData1.children.push(obj) } document.getElementById('container').innerHTML = '' initTable(globalData) document.getElementById('menuText1').value = '' document.getElementById('menuNote1').value = '' document.getElementById('add').style.display = 'none' } // 新增取消 document.getElementById('addCancle').onclick = function() { document.getElementById('menuText1').value = '' document.getElementById('menuNote1').value = '' document.getElementById('add').style.display = 'none' }
- 格式化日期 。
- 思路:使用 new Date(value) 方法返回值提供的一系列方法进行详细日期时间子项的提取,然后在根据合适的格式进行返回。
- 代码:
// datefomate函数: 格式化日期 function datefomate(value) { if (value == null || value == undefined) { return ""; } var date = new Date(value); Y = date.getFullYear(), m = date.getMonth() + 1, d = date.getDate(), H = date.getHours(), i = date.getMinutes(), s = date.getSeconds(); return Y + '-' + m + '-' + d + ' ' + H + ':' + i + ':' + s; };
使用 VUE 和 Element-UI 实现树形表格 ?
【 如需查看完整示例请点击:tree_table_vue_element: 使用 vue 和 element-ui 实现,树形表格案例,支持多级数据渲染,新增,数辑,删除等操作。 】
- 请求本地 JSON 文件获取数据 。
- 思路:使用 axios.get( ) 的方法,请求本地 JSON 文件,获取数据 。
- 注意:cli2 需要把 json 放在 static 目录下,cli3 中静态资源文件目录由 static 变为 public 不能把 .json 文件直接放到 public 下,必须放到 public/js 下(自己创建 js 目录),否则会报 404 错误 。
- 代码:
axios.get('/js/data.json').then((res) => { this.defaultData = res.data.menuTree this.tableData = this.formatData(this.defaultData) })
- 一维数组数据转换为树形数据(三级)。
- 思路:数据格式转换,根据自己需求场景的不同来决定。我这里只涉及三级数据,选择使用 for 循环嵌套去实现(第一层判断 menuParentid 是否为空,第二三层根据比对 menuParentid 和 menuId 的值来确定存放的合适 children 位置)。更多级推荐使用递归方式实现,具体使用参考上面 。
- 代码:
formatData(data) { let resultArr = [] for (let i = 0; i < data.length; i++) { if (data[i].menuParentid == '') { resultArr.push(data[i]) } } for (let i = 0; i < resultArr.length; i++) { let newArr = [] for (let j = 0; j < data.length; j++) { if (data[j].menuParentid == resultArr[i].menuId) { newArr.push(data[j]) } } resultArr[i].children = newArr } for (let i = 0; i < resultArr.length; i++) { for (let k = 0; k < resultArr[i].children.length; k++) { let newArr1 = [] for (let j = 0; j < data.length; j++) { if (data[j].menuParentid == resultArr[i].children[k].menuId) { newArr1.push(data[j]) } } resultArr[i].children[k].children = newArr1 } } return resultArr }
- 数据渲染,使用 Element-UI 中提供的表格组件 。
- 思路:这里使用 Element-UI 中的表格组件去实现,也可以使用 layui 或者其他 UI 库中的组件去实现 。
- 代码:
<el-table :data="tableData" height="500" style="width: 100%;margin-bottom: 20px;" row-key="id" border :expand-row-keys="['1','2','16','17','18']" :tree-props="{children: 'children', hasChildren: 'hasChildren'}"> <el-table-column type="index" width="60" label="序号" align="center"> </el-table-column> <el-table-column prop="menuText" label="功能名称" width="180" align="center"> </el-table-column> <el-table-column prop="menuIsmodify" label="是否启用" width="180" align="center"> </el-table-column> <el-table-column prop="menuType" label="功能类型" align="center"> <template slot-scope="scope"> <el-tag :type="scope.row.menuType === '菜单' ? 'primary' : 'success'" disable-transitions>{{scope.row.menuType}} </el-tag> </template> </el-table-column> <el-table-column prop="menuCreatetime" label="创建时间" align="center"> </el-table-column> <el-table-column prop="menuNote" label="备注" width="200" align="center"> </el-table-column> <el-table-column label="操作" align="center"> <template slot-scope="scope"> <el-button style="color:#67C23A;" @click="addHandle(scope.row)" type="text" size="small" v-if="scope.row.menuType === '菜单'" icon="el-icon-circle-plus-outline">新增</el-button> <el-button type="text" size="small" @click="editHandle(scope.row)" icon="el-icon-edit">编辑</el-button> <el-button style="color:red;" @click="delHandle(scope.row)" type="text" size="small" icon="el-icon-delete">删除 </el-button> </template> </el-table-column> </el-table>
- 删除操作 。
- 思路:遍历 defaultData 中的数据,找到删除的那一项进行删除,在使用 formatData 函数对数据进行格式化树形数据,重新赋值给 tableData ,进行表格的重新渲染。
- 代码:
delHandle(row) { this.$confirm('此操作将永久删除此项, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { for (let i = 0; i < this.defaultData.length; i++) { if (this.defaultData[i].id == row.id) { this.defaultData.splice(i, 1) } } this.tableData = this.formatData(this.defaultData) this.$message({ type: 'success', message: '删除成功!' }); }).catch(() => { this.$message({ type: 'info', message: '已取消删除' }); }); }
- 编辑操作 。
- 思路:遍历 defaultData 中的数据,找到要编辑的那一项进行修改,在使用 formatData 函数对数据进行格式化树形数据,重新赋值给 tableData ,进行表格的重新渲染。
- 代码:
editHandle(row) { this.isExit = true this.timer = new Date().getTime() for (let i = 0; i < this.defaultData.length; i++) { if (this.defaultData[i].id == row.id) { this.currentEdit = this.defaultData[i] } } }, eventHandle($event) { for (let i = 0; i < this.defaultData.length; i++) { if (this.defaultData[i].id == $event.id) { this.defaultData[i] = $event } } this.tableData = this.formatData(this.defaultData) }
- 新增操作 。
- 思路:遍历 defaultData 中的数据,找到要新增项的父级,使得新增项的 menuParentid 等于新增项的父级的 menuId ,同时将新增项添加到 defaultData 中 。在使用 formatData 函数对数据进行格式化树形数据,重新赋值给 tableData ,进行表格的重新渲染。
- 代码:
addHandle(row) { this.isExit1 = true this.timer1 = new Date().getTime()+'1' for (let i = 0; i < this.defaultData.length; i++) { if (this.defaultData[i].id == row.id) { this.currentAdd = this.defaultData[i] } } }, eventHandle1($event) { this.defaultData.push($event) $event.id = this.defaultData.length $event.menuParentid = this.currentAdd.menuId this.tableData = this.formatData(this.defaultData) } }