vue-tree 组织架构图/树形图自动生成(含添加、删除、修改)
这个组件是在一个githup项目上增加了一些功能
原项目地址:https://github.com/tower1229/Vue-Tree-Chart
建议把整个安装包下载下来,写成组件使用.这样方便定制自己的业务需求
原项目效果图
修改后效果图,主要是增加了添加,编辑,删除功能.以及样式上的修改
添加,修改的弹框是用的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 这个文件是平时练习的项目,里面还有一些我写的其他博客的源码,有需要可以下载看看
以上是 vue-tree 组织架构图/树形图自动生成(含添加、删除、修改) 的全部内容, 来源链接: utcz.com/z/375226.html