效果图
在这里插入图片描述
实现原理
1.放弃div的flex布局, 利用table可以更好地控制新增子节点的对齐,利用::before和::after实现虚线,不需要多余的div
2.使用组件递归,利用子传父 e m i t 传 值 , v − o n = " emit传值,v-on=" emitvon="listeners"实现跨层级事件监听

父组件.vue

<template>
  <div id="app">
    <TreeChart :model="tree" @on-add="add" @on-update="update" @on-remove="remove"/>
  </div>
</template>

<script>
/**
 * 使用方式
 * <TreeChart :model="data" >
 * data 参数格式为树形结构
 * 
 * {
 *  resName: "xxx",
 *  extend": true, 是否展开
 *  children: [{
 *    resName: "xxx",
 *    children: []
 *  }]
 * }
*/
import TreeChart from './components/TreeChart'
const dataTree= {
    "resId":"root",
    "resName":"跟节点",
    "extend": true,
    "children": [{
        "resId":"07fe2e8c976047e186bb6bcb8f4d6574",
        "resName":"跟节点1",
        "extend": true,
        "children": []
    }]
}
export default {
  data() {
    return {
      tree: dataTree
    }
  },
  components: {
    TreeChart
  },
  methods:{
    // 添加
    add: function(node){
      const newNode = {
        resName: '222',
        resId: new Date().getTime(),
        message: '',
        extend: true,
        children: []
      }
      node.children.push(newNode)
    },
    // 修改
    update: function(node){
      node.resName = new Date().getTime()
    },
    // 删除
    remove: function(node){
      if (node.resId == 'root'){
        alert('跟节点不能删除')
        return console.log('跟节点不能删除')
      }
      const deepSearch = (tree) => {
        for (let i = tree.length - 1; i >= 0; i--) {
          if (tree[i].resId == node.resId){
            console.log(tree[i])
            tree.splice(i, 1)
          } else if(tree[i].children){
            deepSearch(tree[i].children)
          }
        }
      }
      deepSearch(this.tree.children)
    }
  },
}
</script>

<style lang="scss">
*{
  padding: 0;
  margin: 0;
  #app{
    height: 100vh;
    width: 100vw;
  }
}
</style>

递归组件TreeChart.vue

<template>
  <table>
    <tr>
      <td
        :colspan="hasChild ? model.children.length * 2 : 1"
        :class="{ extend: hasChild && model.extend }"
      >
        <div class="card">
          <div class="title">{{ model.resName }}</div>
          <div class="body">{{ model.message }}</div>
          <div class="footer">
            <div @click="$emit('on-add', model)">添加</div>
            <div @click="$emit('on-update', model)">修改</div>
            <div @click="$emit('on-remove', model)">删除</div>
          </div>
        </div>
        <div class="extend_handle" v-if="hasChild" @click="toggleExtend()">{{ model.extend ? '展开' : '隐藏' }}</div>
      </td>
    </tr>
    <tr v-if="hasChild && model.extend">
      <td
        v-for="(item, index) in model.children"
        :key="index"
        colspan="2"
        class="child"
      >
        <!--跨层级监听事件 v-on="$listeners".native原生事件无效) -->
        <TreeChart :model="item" v-on="$listeners"/>
      </td>
    </tr>
  </table>
</template>

<script>
export default {
  name: 'TreeChart',
  props: ['model'],
  computed: {
    hasChild () {
      return this.model.children && this.model.children.length
    }
  },
  methods: {
    toggleExtend () {
      this.model.extend = !this.model.extend
    }
  }
}
</script>

<style lang="scss">
.card {
  background: rgb(227, 236, 247);
  border: 2px solid #ffffff;
  border-radius: 5px;
  margin: 0 auto;
  width: 200px;
  .title {
    padding: 10px 0;
    font-size: 12px;
  }

  .body {
    height: 100px;
    background: #ffffff;
    width: auto;
    margin: 0 15px;
  }

  .footer {
    display: flex;
    margin: 5px 15px;
    justify-content: space-between;
    div {
      font-size: 12px;
      color: rgb(20, 126, 192);
      cursor: pointer;
    }
  }
}
.extend_handle {
  cursor: pointer;
}
table {
  border-collapse: separate !important;
  border-spacing: 0 !important;
  td {
    position: relative;
    vertical-align: top;
    padding: 0 0 50px 0;
    text-align: center;
    &.extend {
      &::after {
        content: '';
        position: absolute;
        left: 50%;
        bottom: 18px;
        height: 30px;
        border-left: 2px dashed rgb(159, 186, 202);
        transform: translate3d(-1px, 0, 0);
      }
    }
    &.child {
      &::before {
        content: '';
        position: absolute;
        left: 50%;
        bottom: 100%;
        height: 15px;
        border-left: 2px dashed rgb(159, 186, 202);
        transform: translate3d(-1px, 0, 0);
      }
      // 横线
      &::after {
        content: '';
        position: absolute;
        left: 0;
        right: 0;
        top: -15px;
        border-top: 2px dashed rgb(159, 186, 202);
      }
      &:first-child:before,
      &:last-child:before {
        display: none;
      }
      &:first-child:after {
        left: 50%;
        height: 15px;
        border: 2px dashed;
        border-color: rgb(159, 186, 202) transparent transparent
          rgb(159, 186, 202);
        border-radius: 6px 0 0 0;
        transform: translate3d(1px, 0, 0);
      }
      &:last-child:after {
        right: 50%;
        height: 15px;
        border: 2px dashed;
        border-color: rgb(159, 186, 202) rgb(159, 186, 202) transparent
          transparent;
        border-radius: 0 6px 0 0;
        transform: translate3d(-1px, 0, 0);
      }
      &:first-child.child:last-child::after {
        left: auto;
        border-radius: 0;
        border-color: transparent rgb(159, 186, 202) transparent transparent;
        transform: translate3d(1px, 0, 0);
      }
    }
  }
}
</style>
Logo

前往低代码交流专区

更多推荐