vue-tree-chart 组织架构 树状图组件

1、安装插件

npm i vue-tree-chart --save

2.使用插件

1.子组件  treechar.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,
              }"
            >
              {{ 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);
          //   console.log(this.treeData);
        }
      },
      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>

2.父组件  index.vue

<template>
  <div>
    <TreeChart
      :json="treeData"
      :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 "./treechar.vue";
//   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,
            childers: [
              {
                partnerName: "子节点",
                proportionShares: "10",
                partnerType: 3,
                id: 6,
                partnerCode: 1,
              },
            ],
          },
        ],
      },
      isDetail: true, // 是否是详情,不可编辑操作
      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>

3.结果展示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值