vue2 中 el-table 实现树形列表,支持增删改等操作

发布时间 2023-12-14 15:31:29作者: scallop
  1. 需求场景:el-table构造一个树形列表,支持新增节点,删除,修改等操作。
  2. 实现效果
  3. 思路

    一般的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:

// 数据示例
      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
        }
      ]
View Code

构造基础树形数据,如果自己实现,可以忽略这一步。

// 数据准备 生成 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
    }
View Code

  这里设定,列表里有数据类型列,如果当前为object类型,就可以添加子节点。

  新增,修改,删除中,需要先处理新增数据的情况,有3种:新增根节点数据、新增子节点数据、新增同级节点数据。

  •        新增根节点

       直接Array.push()

  •   新增子节点

       先找到当前节点,然后再判断是否存在子节点,如果存在,直接在当前行的child上添加一条,如果不存在,则直接给child赋值。

  •   新增同级节点

      找到当前节点的父节点,然后在父节点的child属性上追加一条。

  •   删除节点

  如果是根节点,可以直接删除;如果是子节点,则需要先找到父节点,然后再从父节点child中删除当前的节点。

// 删除当前节点及对应子节点数据
    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(() => {})
    }
View Code
  •       编辑节点

       编辑节点,比较好实现,直接使用$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>