这个组件是在一个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 这个文件是平时练习的项目,里面还有一些我写的其他博客的源码,有需要可以下载看看