vue-tree 组织架构图/树形图自动生成(含添加、删除、修改)

时间:2024-02-26 08:41:24
这个组件是在一个githup项目上增加了一些功能
建议把整个安装包下载下来,写成组件使用.这样方便定制自己的业务需求
原项目效果图
 
 
 
修改后效果图,主要是增加了添加,编辑,删除功能.以及样式上的修改
添加,修改的弹框是用的element的<el-popover></el-popover>组件
 
 
因为这个组件是递归组件,所以组件里不要除了组件代码,不要把其他代码写在里面.之前我在组件里添加了一个<el-dialog></el-dialog>弹框组件.这就导致渲染出来的页面里有很多弹框
 
子组件: TreeData.vue
<template>
  <table v-if="treeData && treeData.partnerName">
    <tr>
      <td :colspan="treeData.childers ? treeData.childers.length * 2 : 1" :class="{parentLevel: treeData.childers, extend: treeData.childers && treeData.childers.length && treeData.extend}">
        <div :class="{node: true, hasMate: treeData.mate}">
          <div class="person" @click="$emit(\'click-node\', treeData)">
            <el-popover
              v-if="!isDetail"
              placement="top"
              width="180"
              trigger="hover">
              <div style="margin: 0">
                <el-button size="mini" type="primary" @click="addStock(0)" v-if="treeData.partnerType !== 1 && treeData.partnerType !== 3">添加</el-button>
                <el-button type="primary" size="mini" @click="addStock(1)" v-if="treeData.proportionShares">编辑</el-button>
                <el-button type="primary" size="mini" @click="deleteStock" v-if="treeData.proportionShares">删除</el-button>
              </div>
              <div class="avat" :class="{parent: !treeData.proportionShares, company: Number(treeData.partnerType) === 2, other: Number(treeData.partnerType) === 3}" slot="reference">
                {{treeData.partnerName}}({{treeData.proportionShares ? treeData.proportionShares : 100}}%)
              </div>
            </el-popover>
            <div class="avat" :class="{parent: !treeData.proportionShares, company: Number(treeData.partnerType) === 2, other: Number(treeData.partnerType) === 3}" v-else>
              {{treeData.partnerName}}({{treeData.proportionShares}}%)
            </div>
          </div>
        </div>
        <div class="extend_handle" v-if="treeData.childers && treeData.childers.length" @click="toggleExtend(treeData)"></div>
      </td>
    </tr>
    <!-- 这是一个递归组件,注意,这里还要调用,需要传递的数据这里也要传递,否则操作时拿不到子级的数据 -->
    <tr v-if="treeData.childers && treeData.childers.length && treeData.extend">
      <td v-for="(childers, index) in treeData.childers" :key="index" colspan="2" class="childLevel">
        <TreeChart 
          :json="childers" 
          :isDetail="isDetail"
          @add="$emit(\'add\', $event)"
          @delete="$emit(\'delete\', $event)"
          @click-node="$emit(\'click-node\', $event)"/>
      </td>
    </tr>
  </table>
</template>

<script>

export default {
  name: "TreeChart",
  props: {
    json: {}, // 渲染数据
    isDetail: {
      default: false // 是否是详情
    }
  },

  data() {
    return {
      treeData: {},
    };
  },

  created() {
    // console.log(this.json)
  },

  watch: {
    isDetail: function(val) { // 是否是详情,详情不能添加编辑
      this.isDetail = val;
    },
    json: {
      // 遍历当前的数据
      handler: function(Props) {
        let extendKey = function(jsonData) {
          jsonData.extend =
            jsonData.extend === void 0 ? true : !!jsonData.extend;
          // if (Array.isArray(jsonData.children) && jsonData.children.length) {
          //   jsonData.children.forEach(c => {
          //     extendKey(c);
          //   });
          // }
          return jsonData;
        };
        if (Props) {
          this.treeData = extendKey(Props);
        }
      },
      immediate: true,
      deep: true
    }
  },
  methods: {
    toggleExtend(treeData) {
      treeData.extend = !treeData.extend;
      this.$forceUpdate();
    },

    // 新增编辑股东,val: 0 新增, 1 编辑
    addStock(val) {
      // console.log(this.treeData)
      this.$emit(\'add\', {val: val, data: this.treeData})
    },

    // 删除股东
    deleteStock() {
      this.$emit(\'delete\', this.treeData)
    }
  }
};
</script>

<style lang="less">

  table{border-collapse: separate!important;border-spacing: 0!important;}
  td{position: relative; vertical-align: top;padding:0 0 50px 0;text-align: center; }

  .parent {
    background: #199ed8 !important;
    font-weight: bold;
  }
  .extend_handle{position: absolute;left:50%;bottom:27px; width:10px;height: 10px;padding:10px;transform: translate3d(-15px,0,0);cursor: pointer;}
  .extend_handle:before{content:""; display: block; width:100%;height: 100%;box-sizing: border-box; border:2px solid;border-color:#ccc #ccc transparent transparent;
  transform: rotateZ(135deg);transform-origin: 50% 50% 0;transition: transform ease 300ms;}
  .extend_handle:hover:before{border-color:#333 #333 transparent transparent;}
  .extend .extend_handle:before{transform: rotateZ(-45deg);}

  .extend::after{content: "";position: absolute;left:50%;bottom:15px;height:15px;border-left:2px solid #ccc;transform: translate3d(-1px,0,0)}
  .childLevel::before{content: "";position: absolute;left:50%;bottom:100%;height:15px;border-left:2px solid #ccc;transform: translate3d(-1px,0,0)}
  .childLevel::after{content: "";position: absolute;left:0;right:0;top:-15px;border-top:2px solid #ccc;}
  .childLevel:first-child:before, .childLevel:last-child:before{display: none;}
  .childLevel:first-child:after{left:50%;height:15px; border:2px solid;border-color:#ccc transparent transparent #ccc;border-radius: 6px 0 0 0;transform: translate3d(1px,0,0)}
  .childLevel:last-child:after{right:50%;height:15px; border:2px solid;border-color:#ccc #ccc transparent transparent;border-radius: 0 6px 0 0;transform: translate3d(-1px,0,0)}
  .childLevel:first-child.childLevel:last-child::after{left:auto;border-radius: 0;border-color:transparent #ccc transparent transparent;transform: translate3d(1px,0,0)}

  .node{position: relative; display: inline-block;box-sizing: border-box; text-align: center;padding: 0 5px;}
  .node .person{padding-top: 15px; position: relative; display: inline-block;z-index: 2;width:120px; overflow: hidden;}
  .node .person .avat{
    padding: 5px;
    padding-top: 10px;
    display: block;width:100%;height: 100%;margin:auto;word-break: break-all; background:#ffcc00;box-sizing: border-box;border-radius: 4px;
    .opreate_icon {
      display: none;
    }
    &:hover {
      .opreate_icon {
        display: block;
        position: absolute;
        top: -3px;
        right: -3px;
        padding: 5px;
      }
    }
    
    &.company {
      background:#199ed8;
    }
    &.other {
      background:#ccc;
    }
  }
  .node .person .avat img{cursor: pointer;}
  .node .person .name{height:2em;line-height: 2em;overflow: hidden;width:100%;}
  .node.hasMate::after{content: "";position: absolute;left:2em;right:2em;top:15px;border-top:2px solid #ccc;z-index: 1;}
  .node.hasMate .person:last-child{margin-left:1em;}

  .el-dialog__header {
    padding: 0;
    padding-top: 30px;
    margin: 0 30px;
    border-bottom: 1px solid #F1F1F1;
    text-align: left;
    .el-dialog__title {
      font-size: 14px;
      font-weight: bold;
      color: #464C5B;
      line-height: 20px;
    }
  }
  .tips {
    padding: 0 20px;
    .el-select {
      width: 100%;
    }
    .blue {
      color: #00B5EF;
    }
    .check {
      margin-left: 100px;
    }
    .inquiry {
      font-weight: bold;
    }
    .el-form-item__label {
      display: block;
      float: none;
      text-align: left;
    }
    .el-form-item__content {
      margin-left: 0;
    }
  }
  .el-dialog__body {
    padding: 30px 25px;
    p {
      margin-bottom: 15px;
    }
  }
  .el-dialog__headerbtn {
    top: 30px;
    right: 30px;
  }

  // 竖向
  .landscape {
    transform: translate(-100%,0) rotate(-90deg);
    transform-origin: 100% 0;
    .node{text-align: left;height: 8em;width:8em;}
    .person{
      position: relative; 
      transform: rotate(90deg);
      // padding-left: 4.5em;
      // height: 4em;
      top:35px;
      left: 12px;
      width: 110px;
    }
  }

.el-popover {
  .el-button {
    padding: 8px !important;
    margin-left: 5px !important;
    float: left;
  }
}
</style>

 

父组件调用 tree.vue
<template>
<div>
  <TreeChart 
    :json="treeData"
    :class="{landscape: isVertical}"
    :isDetail="isDetail"
    @add="addStock"
    @delete="deleteStock" 
  />
 <el-dialog
    title="提示"
    :visible.sync="dialogVisible"
    @close="clearDialog"
    :close-on-click-modal="false"
    width="500px">
    <div class="tips">
      <el-form :model="ruleForm" :rules="rules" ref="ruleForm" class="demo-ruleForm">
        <el-form-item label="类型" prop="type">
          <el-select v-model="ruleForm.type" placeholder="类型" @change="changeType">
            <el-option
              v-for="item in shareholderTypeOptions"
              :key="item.value"
              :label="item.labelZh"
              :value="item.value">
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="姓名" prop="partnerName">
          <el-input placeholder="输入姓名" :maxlength="32" v-model="ruleForm.partnerName"></el-input>
        </el-form-item>
        <el-form-item label="占比" prop="proportionShares">
          <el-input placeholder="输入占比" :maxlength="5" v-model="ruleForm.proportionShares"></el-input>
        </el-form-item>
      </el-form>
    </div>
    <span slot="footer" class="dialog-footer">
      <div class="tip-left">
        <el-button type="info" @click="dialogVisible=false">取消</el-button>
        <el-button type="primary" @click="confirm">确定</el-button>
      </div>
    </span>
  </el-dialog>

  <!-- 删除提示弹框 -->
  <el-dialog
    title="提示"
    :visible.sync="dialogVisible2"
    width="30%">
    <div class="tips">
      <p style="text-align: left">确定删除该股东信息?</p>
    </div>
    <span slot="footer" class="dialog-footer">
      <div class="tip-left">
        <el-button type="info" @click="dialogVisible2=false">取消</el-button>
        <el-button type="primary" @click="confimdelete">确定</el-button>
      </div>
    </span>
  </el-dialog>
</div>
</template>

<script>
  import TreeChart from \'@/components/TreeData\'
  import { Loading } from "element-ui";

  export default {
    name: \'tree\',
    components: {
      TreeChart
    },
    data() {
      return {
        treeData: {
          partnerName: \'大米科技公司\',
          proportionShares: \'100\',
          partnerType: 2,
          id: 1,
          childers: [{
            partnerName: \'股东1\',
            proportionShares: \'50\',
            partnerType: 1,
            id: 2,
            partnerCode: 1
          }, {
            partnerName: \'股东2\',
            proportionShares: \'20\',
            partnerType: 1,
            id: 4,
            partnerCode: 1
          }, {
            partnerName: \'股东3\',
            proportionShares: \'20\',
            partnerType: 2,
            id: 5,
            partnerCode: 1
          }, {
            partnerName: \'其他\',
            proportionShares: \'10\',
            partnerType: 3,
            id: 6,
            partnerCode: 1
          }]
        },
        isVertical: false, // 是否是竖方向,只给最外层的添加
        isDetail: false, // 是否是详情,不可编辑操作
        dialogVisible: false, // 添加股东弹框
        dialogVisible2: false, // 删除提示弹框
        ruleForm: {
          type: 1,
          partnerName: "",
          proportionShares: null
        },
        rules: {
          proportionShares: [
            { required: true, message: \'请输入比例\', trigger: "blur" }
          ],
          partnerName: [
            { required: true, message: "请输入股东名称", trigger: "blur" }
          ],
          cardId: [
            { required: true, message: "请输入证件号", trigger: "blur" }
          ],
          type: [
            { required: true, message: "请选择类型", trigger: "blur" }
          ]
        },
        shareholderTypeOptions: [
          {
            labelEn: "Individual",
            labelZh: "个人",
            value: 1
          },
          {
            labelEn: "Company",
            labelZh: "公司",
            value: 2
          },
          {
            labelEn: "Other",
            labelZh: "其他",
            value: 3
          }
        ], // 股东类型
        lastId: 11, // 最后一级id
        currentTreeData: {}
      }
    },
    methods: {
      // 新增编辑股东,val: 0 新增, 1 编辑
      addStock(data) {
        // console.log(data)
        if (data.val) {
          // 不使用=赋值,内存相同,改变后,treeData数据也会改变
          // this.ruleForm = data.data;
          this.ruleForm = Object.assign(this.ruleForm, data.data);
          this.ruleForm.type = data.data.partnerType;
        }
        this.isEdit = data.val
        // 使用=赋值,编辑时改变currentTreeData, 源数据treeData也会改变
        this.currentTreeData = data.data
        this.dialogVisible = true;
      },
      // 删除
      deleteStock(data) {
        // console.log(data)
        this.currentTreeData = data
        this.dialogVisible2 = true
      },
      // 确定删除
      confimdelete() {
        // 前端删除 遍历原数据,删除匹配id数据
        const deleteData = (data) => {
          data.some((item, i) => {
            if (item.id === this.currentTreeData.id) {
              data.splice(i, 1)
              return
            } else if (item.childers) {
              deleteData(item.childers)
            }
          })
        }
        let arr = [this.treeData]
        deleteData(arr)
        this.treeData = arr[0] ? arr[0] : {}
        // console.log(this.treeData)
        this.dialogVisible2 = false
        this.$message({
          type: "success",
          message: "成功"
        });
      },

      // 保存添加股东
      confirm() {
        let loading = Loading.service();
        this.$refs.ruleForm.validate(valid => {
          if (valid) {
            this.sendData();
          } else {
            loading.close();
          }
        });
      },

      // 发送添加股东数据
      sendData() {
        let loading = Loading.service();
        let data = {
          partnerType: this.ruleForm.type,
          partnerName: this.ruleForm.partnerName,
          proportionShares: this.ruleForm.proportionShares
        };
        if (this.isEdit) { // 编辑
          // data.id = this.treeData.id;
          this.currentTreeData.partnerType = data.partnerType
          this.currentTreeData.partnerName = data.partnerName
          this.currentTreeData.proportionShares = data.proportionShares
          // 前端编辑数据
          this.$message({
            type: "success",
            message: "成功"
          });
          this.clearDialog();
          loading.close()
        } else { // 添加
          // 前端添加数据,需要自己生成子级id,可以传数据的时候把最后一级id传过来,进行累加
          data.id = this.lastId ++
          data.partnerCode = this.currentTreeData.id
          data.extend = true
          const render = (formData) => {
            formData.some(item => {
              if (item.id === this.currentTreeData.id) {
                if (item.childers) {
                  item.childers.push(data)
                } else {
                  this.$set(item, \'childers\', [data])
                }
                return
              } else if (item.childers) {
                render(item.childers)
              }

            })
          }
          let arr = [this.treeData]
          render(arr)
          this.treeData = arr[0]
  
          this.$message({
            type: "success",
            message: "成功"
          });
          this.clearDialog();
          loading.close()
        }

      },
    }
  }
</script>

 

 git-hup地址: https://github.com/shengbid/vue-demo  这个文件是平时练习的项目,里面还有一些我写的其他博客的源码,有需要可以下载看看