vue2 中 el-table 实现树形列表,支持增删改等操作
- 需求场景:el-table构造一个树形列表,支持新增节点,删除,修改等操作。
- 实现效果
- 思路
一般的el-table 增删改,我们都很熟悉;关键在于实现一个纯前端的树形列表,最终再调接口存列表数据。
树形el-table,需要设置 row-key,一般为 id,所以每新增一条数据,都必须有id。需要一个生成id的方法:
// 生成id 时间戳 + 随机数
generateId() { return `id_${new Date().getTime()}${Math.floor(Math.random() * 10000)}` }
有树形结构,就得有父子关系,因此除了id还需要有parentId。根节点parentId,此处定义为"0"。
这里方便看效果,内置了一条tableData数据,然后再构造一个基础树形数据:
tableData:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 数据示例 tableData: [ { key: 'name', type: 'string', child: null }, { key: 'age', type: 'integer', child: null }, { key: 'response', type: 'object', child: [ { key: 'childrenone', type: 'string', child: null }, { key: 'childrentwo', type: 'boolean', child: null } ] }, { key: 'address', type: 'string', child: null } ]
构造基础树形数据,如果自己实现,可以忽略这一步。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 数据准备 生成 id 和 parentId handleTableDataFormat(data) { const tableFormat = (tableData, parentId) => { tableData.forEach((item) => { item.isEdit = false item.parentId = parentId || '0' item.id = this.generateId() if (item.child && item.child.length > 0) { tableFormat(item.child, item.id) } }) } tableFormat(data) console.log('Format tableData', data) return data }
这里设定,列表里有数据类型列,如果当前为object类型,就可以添加子节点。
新增,修改,删除中,需要先处理新增数据的情况,有3种:新增根节点数据、新增子节点数据、新增同级节点数据。
- 新增根节点
直接Array.push()
- 新增子节点
先找到当前节点,然后再判断是否存在子节点,如果存在,直接在当前行的child上添加一条,如果不存在,则直接给child赋值。
- 新增同级节点
找到当前节点的父节点,然后在父节点的child属性上追加一条。
- 删除节点
如果是根节点,可以直接删除;如果是子节点,则需要先找到父节点,然后再从父节点child中删除当前的节点。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 删除当前节点及对应子节点数据 onDelete(row) { const msg = '<div><span style="color: #F56C6C">删除后将不可恢复</span>,你还要继续吗?</div>' this.$confirm(msg, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', dangerouslyUseHTMLString: true, type: 'warning' }) .then(() => { const { parentId, id } = row // 根节点直接删除 if (parentId === '0') { const delIndex = this.tableData.findIndex((item) => item.id === id) this.tableData.splice(delIndex, 1) } else { // 找到父节点,通过父节点删除 let parentRow = {} const findRow = (data) => { data.forEach((item) => { if (item.id === parentId) { parentRow = { ...item } } if (item.child && item.child.length) { findRow(item.child) } }) } findRow(this.tableData) const { child } = parentRow const delIndex = child.findIndex((item) => item.id === id) child.splice(delIndex, 1) } }) .catch(() => {}) }
- 编辑节点
编辑节点,比较好实现,直接使用$set 重新赋值即可。
下面是完整代码:
1 <template> 2 <div class="custom-tree-table"> 3 <el-table 4 ref="tableDataRef" 5 :data="tableData" 6 max-height="400" 7 row-key="id" 8 border 9 :tree-props="{ children: 'child' }" 10 default-expand-all 11 > 12 <el-table-column width="55" align="center" type="index" label="序号" /> 13 <el-table-column label="参数名" prop="key" min-width="200"> 14 <template #default="{ row }"> 15 <el-input v-if="row.isEdit" v-model="row.key" placeholder="请输入" /> 16 <span v-else>{{ row.key }}</span> 17 </template> 18 </el-table-column> 19 <el-table-column label="数据类型" prop="type" min-width="200"> 20 <template #default="{ row }"> 21 <el-select 22 v-if="row.isEdit" 23 v-model="row.type" 24 filterable 25 clearable 26 placeholder="请选择" 27 style="width: 100%" 28 @change="handleDataTypeChange($event, row)" 29 > 30 <el-option 31 v-for="item in source.dataTypeOptions" 32 :key="item.value" 33 :value="item.value" 34 :label="item.label" 35 /> 36 </el-select> 37 <span v-else>{{ row.type }}</span> 38 </template> 39 </el-table-column> 40 <el-table-column label="操作" min-width="100"> 41 <template slot="header"> 42 <el-tooltip 43 :hide-after="500" 44 class="item" 45 effect="dark" 46 content="添加根节点" 47 placement="top" 48 > 49 <el-button 50 type="text" 51 icon="el-icon-circle-plus-outline" 52 @click="onAddRoot" 53 /> 54 </el-tooltip> 55 </template> 56 <template #default="{ row, $index }"> 57 <el-tooltip 58 :hide-after="hideAfter" 59 :open-delay="openDelay" 60 effect="dark" 61 content="添加" 62 placement="top" 63 > 64 <el-button 65 type="text" 66 icon="el-icon-plus" 67 @click="onAddSibling(row, $index)" 68 /> 69 </el-tooltip> 70 71 <el-tooltip 72 v-if="row.type === 'object'" 73 :hide-after="hideAfter" 74 :open-delay="openDelay" 75 effect="dark" 76 content="添加子节点" 77 placement="top" 78 > 79 <el-button 80 type="text" 81 icon="el-icon-circle-plus-outline" 82 @click="onAddChild(row, $index)" 83 /> 84 </el-tooltip> 85 86 <el-tooltip 87 v-if="!row.isEdit" 88 :hide-after="hideAfter" 89 :open-delay="openDelay" 90 effect="dark" 91 content="编辑" 92 placement="top" 93 > 94 <el-button 95 type="text" 96 icon="el-icon-edit" 97 @click="onEdit(row, $index)" 98 /> 99 </el-tooltip> 100 101 <el-tooltip 102 v-if="row.isEdit" 103 :hide-after="hideAfter" 104 :open-delay="openDelay" 105 effect="dark" 106 content="保存" 107 placement="top" 108 > 109 <el-button 110 type="text" 111 icon="el-icon-circle-check" 112 @click="onSave(row, $index)" 113 /> 114 </el-tooltip> 115 116 <el-tooltip 117 :hide-after="hideAfter" 118 :open-delay="openDelay" 119 effect="dark" 120 content="删除" 121 placement="top" 122 > 123 <el-button 124 type="text" 125 icon="el-icon-delete" 126 @click="onDelete(row, $index)" 127 /> 128 </el-tooltip> 129 </template> 130 </el-table-column> 131 </el-table> 132 </div> 133 </template> 134 135 <script> 136 export default { 137 name: 'CustomTreeTable', 138 data() { 139 return { 140 source: { 141 dataTypeOptions: [ 142 { label: 'Array', value: 'array' }, 143 { label: 'String', value: 'string' }, 144 { label: 'Boolean', value: 'boolean' }, 145 { label: 'Object', value: 'object' }, 146 { label: 'Number', value: 'number' } 147 ] 148 }, 149 hideAfter: 1500, 150 openDelay: 500, 151 // 数据示例 152 tableData: [ 153 { 154 key: 'name', 155 type: 'string', 156 child: null 157 }, 158 { 159 key: 'age', 160 type: 'integer', 161 child: null 162 }, 163 { 164 key: 'response', 165 type: 'object', 166 child: [ 167 { 168 key: 'childrenone', 169 type: 'string', 170 child: null 171 }, 172 { 173 key: 'childrentwo', 174 type: 'boolean', 175 child: null 176 } 177 ] 178 }, 179 { 180 key: 'address', 181 type: 'string', 182 child: null 183 } 184 ] 185 } 186 }, 187 created() { 188 this.tableData = this.handleTableDataFormat(this.tableData) 189 }, 190 methods: { 191 // 数据准备 生成 id 和 parentId 192 handleTableDataFormat(data) { 193 const tableFormat = (tableData, parentId) => { 194 tableData.forEach((item) => { 195 item.isEdit = false 196 item.parentId = parentId || '0' 197 item.id = this.generateId() 198 if (item.child && item.child.length > 0) { 199 tableFormat(item.child, item.id) 200 } 201 }) 202 } 203 204 tableFormat(data) 205 console.log('Format tableData', data) 206 return data 207 }, 208 /** 209 * 生成简单id 树形列表必须有id 210 */ 211 generateId() { 212 return `id_${new Date().getTime()}${Math.floor(Math.random() * 10000)}` 213 }, 214 // 数据类型改变 215 handleDataTypeChange(val, row) { 216 row.type = val 217 this.$set(row, 'type', val) 218 // 对象类型存在子节点 219 if (val === 'object') { 220 this.$set(row, 'child', []) 221 } 222 }, 223 /** 224 * 生成一行数据 225 */ 226 generateRow(parentId) { 227 return { 228 id: this.generateId(), 229 key: '', 230 type: '', 231 isEdit: true, 232 parentId 233 } 234 }, 235 // 添加根节点 236 onAddRoot() { 237 this.tableData.push(this.generateRow('0')) 238 }, 239 /** 240 * 处理添加一行数据 241 * @param {object} row 当前节点 242 * @param {number} index 243 * @param {string} type 操作类型 SIBLING 同级 / CHILD 子级 244 */ 245 handleAddOneRow(row, index, type) { 246 const { parentId, id } = row 247 const curId = type === 'SIBLING' ? parentId : id 248 let curRow = {} 249 // 在 tableData 中,找到当前节点 250 const findRow = (data) => { 251 data.forEach((item) => { 252 if (item.id === curId) { 253 curRow = { ...item } 254 } 255 if (item.child && item.child.length) { 256 findRow(item.child) 257 } 258 }) 259 } 260 261 findRow(this.tableData) 262 263 const { id: generateParentId, child } = curRow 264 265 if (child) { 266 child.push(this.generateRow(generateParentId)) 267 } else { 268 this.$set(curRow, 'child', [this.generateRow(generateParentId)]) 269 } 270 }, 271 // 添加同级节点 272 onAddSibling(row, index) { 273 console.log('onAddSibling', row, index) 274 const { parentId } = row 275 // 先判断是不是根节点 276 if (parentId === '0') { 277 // 当前节点直接添加 278 this.tableData.push(this.generateRow('0')) 279 } else { 280 this.handleAddOneRow(row, index, 'SIBLING') 281 } 282 }, 283 // 添加子节点 todo 284 onAddChild(row, index) { 285 this.handleAddOneRow(row, index, 'CHILD') 286 }, 287 // 编辑 288 onEdit(row) { 289 this.$set(row, 'isEdit', true) 290 }, 291 // 保存 292 onSave(row) { 293 this.$set(row, 'isEdit', false) 294 }, 295 // 删除当前节点及对应子节点数据 296 onDelete(row) { 297 const msg = 298 '<div><span style="color: #F56C6C">删除后将不可恢复</span>,你还要继续吗?</div>' 299 this.$confirm(msg, '提示', { 300 confirmButtonText: '确定', 301 cancelButtonText: '取消', 302 dangerouslyUseHTMLString: true, 303 type: 'warning' 304 }) 305 .then(() => { 306 const { parentId, id } = row 307 // 根节点直接删除 308 if (parentId === '0') { 309 const delIndex = this.tableData.findIndex((item) => item.id === id) 310 this.tableData.splice(delIndex, 1) 311 } else { 312 // 找到父节点,通过父节点删除 313 let parentRow = {} 314 const findRow = (data) => { 315 data.forEach((item) => { 316 if (item.id === parentId) { 317 parentRow = { ...item } 318 } 319 if (item.child && item.child.length) { 320 findRow(item.child) 321 } 322 }) 323 } 324 findRow(this.tableData) 325 326 const { child } = parentRow 327 328 const delIndex = child.findIndex((item) => item.id === id) 329 330 child.splice(delIndex, 1) 331 } 332 }) 333 .catch(() => {}) 334 } 335 } 336 } 337 </script> 338 <style lang="scss" scoped> 339 .custom-tree-table { 340 height: 100%; 341 background-color: #fff; 342 padding: 20px; 343 } 344 </style>