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.结果展示
