今日分享——前端使用jsplumbs手撸一个思维导图(流程图)

一、jsplumbs的基本使用,例子在vue2项目中实现的

  • 初始化
    mounted() {
            this.container = document.getElementById('container')
    
            this.jsPlumbInstance = jsPlumb.getInstance({
                //特别注意container容器需要设置绝对定位,不要设置宽高,否则会出现画线位置不准确和滚                    
                //动条滚动后位置错乱问题
                Container: 'container' // 线条挂载区域,不设置会渲染在 body 里
            })
        },

  • 画线
    this.jsPlumbInstance.connect(
                        {
                            source: id1, //需要连线的dom元素或元素id
                            target: id2, //需要连线的dom元素或元素id
                            endpoint: 'Blank',
                            anchor: 'Continuous',
                            connector: 'Flowchart', // 连接线的曲线
                            anchors:[[0.5, 1, 0, 1],[0, 0.5, -1, 0]] // 锚点位置
                        }
                    )
    
    //锚点位置[x,y,x1,y1],x,y代表锚点的位置,x1,y1表示锚点的方向
    //例[0.5, 1, 0, 1]表示下边框中点位置,方向向下
    //[0, 0.5, -1, 0]表示左边框中点位置,方向向左
  • 删除连接线
            //删除单个节点线
            delNodeLine(node) {
                //node为元素节点或元素id
                this.jsPlumbInstance.deleteConnectionsForElement(node)
            },
  • 删除所有连接线
            //删除所有节点和画线
            delAllLine() {
                // 这个方法删除所有连线,不需要传入参数
                this.jsPlumbInstance.deleteEveryConnection()
                // 这个方法删除所有节点,不需要传入参数
                this.jsPlumbInstance.deleteEveryEndpoint()
            },

    案例:

二、父组件

<template>
    <div class="wrap">
        <div id="container">
            <div class="serviceNavigation" id="serviceNavigation">
                <div class="item_wrap" :style="{ 'margin-right': item.children.length ? 0 : '80px' }" v-for="(item, index) in dataList" :key="item.myId">
                    <div>
                        <div class="item" :id="item.myId" :class="{ active: curFocus === item.myId }">
                            <div class="main" @click="handleItemClick(item)">
                                <el-input @change="changeValue($event, item)" v-model="item.name"></el-input>
                            </div>
                            <div class="plusIcon" v-if="curFocus === item.myId && index === dataList.length - 1">
                                <i style="color: #008ad6" @click="pushItem(item)" class="el-icon-circle-plus"></i>
                                <i style="color: #ff4545" v-if="index" @click="removeItem(index)" class="el-icon-remove"></i>
                                <i style="width: 16px; display: inline-block" v-else></i>
                            </div>
                            <div class="pushChildIcon" v-if="curFocus === item.myId && index && !item.children.length">
                                <i style="color: #008ad6" @click="pushChildItem(item)" class="el-icon-circle-plus"></i>
                            </div>
                        </div>
                        <ChildList
                            :item="item"
                            :curFocus.sync="curFocus"
                            :menuList="menuList"
                            @getNavigationList="getNavigationList"
                            @handleItemClick="handleItemClick"
                            @pushChildItem="pushChildItem"
                            @removeChildItem="removeChildItem" />
                    </div>
                </div>
            </div>
        </div>
        <div class="save_btn">
            <el-button type="primary" :loading="loading" @click="saveBtn">保存</el-button>
        </div>
    </div>
</template>
<script>
import { jsPlumb } from 'jsplumb'
import ChildList from './components/ChildList.vue'
import { getOrgMenuList, getNavigationList, delNavigation, addNavigation, editNavigation } from './api'
export default {
    name: 'ServiceNavigation',
    components: {
        ChildList
    },
    data() {
        return {
            curFocus: '',
            jsPlumbInstance: null,
            defaultNavigation: { name: '业务导航', children: [], myId: String(Date.now()), level: 1 },
            dataList: [this.defaultNavigation],
            menuList: [],
            container: null,
            loading: false
        }
    },
    deactivated() {
        this.delAllLine()
    },
    destroyed() {
        this.delAllLine()
    },
    activated() {
        console.log('actived')
        this.initAllLine(this.dataList)
    },
    mounted() {
        this.container = document.getElementById('container')

        this.jsPlumbInstance = jsPlumb.getInstance({
            Container: 'container' // 线条挂载区域,不设置会渲染在 body 里
        })
        // this.jsPlumbInstance.setContainer('contanin')
        this.getOrgMenuList()
        this.getNavigationList()
    },
    methods: {
        //初始化所有连接线
        initAllLine(list) {
            console.log(2222)
            list.forEach((item) => {
                item.children?.forEach((child) => {
                    if (item.children?.length) {
                        this.initAllLine(item.children)
                    }
                    this.canvasLine(item.myId, child.myId)
                })
            })
            list.reduce((cur, pre) => {
                if (cur.level === 1 && pre.level === 1) {
                    console.log('levellevellevel')
                    this.canvasLine(cur.myId, pre.myId, [
                        [1, 0.5, 1, 0],
                        [0, 0.5, -1, 0]
                    ])
                }
                return pre
            })
        },
        //删除所有节点和画线
        delAllLine() {
            // 这个方法删除所有连线,不需要传入参数
            this.jsPlumbInstance.deleteEveryConnection()
            // 这个方法删除所有节点,不需要传入参数
            this.jsPlumbInstance.deleteEveryEndpoint()
        },
        //画线
        canvasLine(
            id1,
            id2,
            anchors = [
                [0.5, 1, 0, 1],
                [0, 0.5, -1, 0]
            ]
        ) {
            console.log(id1, id2)
            // 确保 jsPlumb 初始化后再进行操作
            this.jsPlumbInstance.ready(() => {
                this.jsPlumbInstance.connect(
                    {
                        source: id1,
                        target: id2,
                        // options: {},
                        endpoint: 'Blank',
                        anchor: 'Continuous',
                        connector: 'Flowchart', // 连接线的曲线
                        anchors // 锚点位置
                    }
                )
            })
        },
        //删除单个节点线
        delNodeLine(node) {
            this.jsPlumbInstance.deleteConnectionsForElement(node)
        },
        handleItemClick(item, flag = true) {
            if (item.myId === this.curFocus && flag) {
                this.curFocus = ''
            } else {
                this.curFocus = item.myId
            }
        },
        pushItem(item) {
            const item2 = { name: '', myId: String(Date.now()), children: [], level: 1 }
            this.dataList.push(item2)
            console.log(item.myId, item2.myId)
            this.$nextTick(() => {
                this.canvasLine(item.myId, item2.myId, [
                    [1, 0.5, 1, 0],
                    [0, 0.5, -1, 0]
                ])
            })
        },
        removeItem(index) {
            const item = this.dataList.splice(index, 1)[0]
            console.log(item, 'delItem')
            this.delNodeLine(item.myId)
        },
        pushChildItem(item) {
            item.children.push({ name: '', myId: String(Date.now()), children: [] })

            this.jsPlumbInstance.reset() //清除之前的连接关系 否则无法重新绘制

            this.$nextTick(() => {
                this.initAllLine(this.dataList)
            })
        },
        removeChildItem(item, index) {
            item.children.forEach((child) => {
                this.delNodeLine(child.myId)
            })
            item.children.splice(index, 1)
            this.jsPlumbInstance.reset() //清除之前的连接关系 否则无法重新绘制

            this.$nextTick(() => {
                this.initAllLine(this.dataList)
            })
        },
        //获取机构服务下所有菜单
        getOrgMenuList() {
            getOrgMenuList({ status: 0, visible: 0, parentName: '机构服务' }).then((res) => {
                this.menuList = res.data || []
                console.log(this.menuList, 'this.menuList000')
            })
        },
        //获取业务导航列表
        getNavigationList() {
            this.delAllLine()
            getNavigationList({}).then((res) => {
                res.data.forEach((item) => {
                    item.level = 1
                })
                this.dataList = [this.defaultNavigation, ...res.data]
                this.mapDataList(this.dataList)
                this.$nextTick(() => {
                    this.initAllLine(this.dataList)
                    console.log(this.dataList, 'datalist')
                })
            })
        },
        mapDataList(list) {
            let key = Date.now()
            list.forEach((item) => {
                if (!item.children || !item.children?.length) {
                    item.children = []
                }
                item.myId = String(key + 1 + Math.random())
                item.name = item.menuId ? item.menuId.split(',').map((i) => Number(i)) : item.name
                if (item.children.length) {
                    this.mapDataList(item.children)
                }
            })
        },
        saveBtn() {
            this.loading = true
            function mapDataList(list) {
                return list.map((item) => {
                    if (item.children?.length) item.children = mapDataList(item.children)
                    if (!item.level) {
                        return {
                            menuId: item.menuId,
                            id: item.id,
                            parentId: item.parentId,
                            children: item.children,
                            navigateUrl: item.navigateUrl
                        }
                    } else {
                        return {
                            name: item.name,
                            id: item.id,
                            children: item.children
                        }
                    }
                })
            }
            const children = mapDataList(JSON.parse(JSON.stringify(this.dataList.slice(1))))
            console.log(children, 'children')
            addNavigation({ children })
                .then((res) => {
                    this.getNavigationList()
                    this.$notify({
                        type: 'success',
                        message: '保存成功'
                    })
                })
                .finally(() => {
                    this.loading = false
                })
        }
    }
}
</script>
<style lang='scss' scoped>
#container {
    position: relative;
}
::-webkit-scrollbar {
    display: none;
}
.wrap {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: auto;
    scrollbar-width: none;
    .save_btn {
        position: fixed;
        bottom: 20px;
        right: 20px;
    }
}
::v-deep .el-input__inner {
    position: relative;
    border: 0;
    border-radius: 0;
    // // vertical-align: bottom;
    height: 20px;
    padding-left: 0;
    text-align: center;
}
::v-deep .el-input__icon {
    height: 20px;
}
::v-deep .el-icon-arrow-up {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    right: 0px;
    transition: all 0s !important;
}
::v-deep .el-select__caret.is-reverse {
    transform: rotate(360deg) !important;
    transform: translateY(-100%) !important;
    // right: 0px;
}

.serviceNavigation {
    display: flex;
    height: 100%;
    .active {
        border: 2px solid #008ad6 !important;
    }
    .item_wrap {
        display: flex;
        flex-direction: column;
        // min-width: 300px;
        .item,
        .childItem {
            position: relative;
            display: inline-block;
            padding: 5px;
            width: 150px;
            // height: 52px;
            box-sizing: border-box;
            border: 2px solid transparent;
            // margin-right: 80px;
            .main {
                border: 1px solid #979797;
                border-radius: 20px;
                text-align: center;
                background-color: #fff;
                padding: 7px 15px;
                cursor: pointer;
                user-select: none;
                -moz-user-select: none;
                -webkit-user-select: none;
                -ms-user-select: none;
                ::v-deep .el-input__inner {
                    font-size: 17px;
                    font-weight: bold;
                }
            }
            .plusIcon {
                position: absolute;
                top: 50%;
                right: -40px;
                transform: translateY(-50%);
                cursor: pointer;
            }
            .pushChildIcon {
                position: absolute;
                left: 50%;
                transform: translateX(-50%);
                bottom: -25px;
                cursor: pointer;
            }
        }
        .childList {
            margin-top: 40px;
            margin-left: 110px;
            .childItem {
                display: block;
                .main {
                    border-radius: 5px;
                    ::v-deep .el-input__inner {
                        font-size: 13px;
                    }
                }
                .plusIcon {
                    right: -40px;
                }
            }
        }
    }
}
</style>

三、子组件

子组件中引用自身组件实现循环引用可无限嵌套

<template>
    <div class="childList">
        <div :style="{ height: child.children.length ? 'auto' : '52px' }" v-for="(child, index) in item.children" :key="child.myId">
            <div class="childItem" :id="child.myId" :class="{ active: curFocus === child.myId }">
                <div class="main" @click="handleItemClick(child)">
                    <el-cascader
                        ref="refCascader"
                        v-model="child.name"
                        @change="changeValue($event, item, child, index)"
                        :options="menuList"
                        :props="cas_props"
                        :show-all-levels="false"
                        clearable></el-cascader>
                </div>
                <div class="plusIcon" v-if="curFocus === child.myId">
                    <i style="color: #008ad6" @click="pushChildItem(child)" class="el-icon-circle-plus"></i>
                    <i style="color: #ff4545" @click="removeChildItem(item, index)" class="el-icon-remove"></i>
                </div>
                <div class="pushChildIcon" v-if="curFocus === child.myId && index === item.children.length - 1">
                    <i style="color: #008ad6" @click="pushChildItem(item)" class="el-icon-circle-plus"></i>
                </div>
            </div>
            <child-list
                :item="child"
                :menuList="menuList"
                :curFocus="curFocus"
                @getNavigationList="emitGetNavigationList"
                @handleItemClick="handleItemClick"
                @pushChildItem="pushChildItem"
                @removeChildItem="removeChildItem" />
        </div>
    </div>
</template>
<script>
import { addNavigation, editNavigation } from '../api'
export default {
    name: 'ChildList',
    props: {
        item: {
            type: Object,
            required: true
        },
        curFocus: {
            type: String | Number,
            required: true
        },
        menuList: {
            type: Array,
            required: true
        }
    },
    data() {
        return {
            cas_props: { checkStrictly: true, label: 'menuName', value: 'menuId' }
        }
    },
    mounted() {},
    methods: {
        handleItemClick(item, flag) {
            this.$emit('handleItemClick', item, flag)
        },
        pushChildItem(child) {
            this.$emit('pushChildItem', child)
        },
        removeChildItem(item, index) {
            this.$emit('removeChildItem', item, index)
        },
        emitGetNavigationList() {
            this.$emit('getNavigationList')
        },
        changeValue(val, parent, child, index) {
            child.menuId = val.join(',')
            child.parentId = parent.id
            child.navigateUrl = '/orgNoService'
            this.$refs.refCascader[index].getCheckedNodes()[0].pathNodes.forEach((item) => {
                child.navigateUrl += '/' + item.data.path
            })
            console.log(child.navigateUrl, 'path')
        }
    }
}
</script>
<style lang='scss' scoped>
::v-deep .el-cascade,
.el-cascader--medium {
    height: 20px !important;
    line-height: 20px !important;
}
::v-deep .el-input__inner {
    position: relative;
    border: 0;
    border-radius: 0;
    // // vertical-align: bottom;
    padding-left: 0;
    text-align: center;
    padding-right: 5px !important;
}
::v-deep .el-input__icon {
    height: 20px;
}
::v-deep .el-icon-arrow-down {
    position: absolute;
    top: 50%;
    transform: translateY(-85%);
    right: -1px;
    transition: all 0s !important;
}
::v-deep .el-input__suffix-inner {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    height: 35px;
    right: -20px;
    // top: -5px;
}
::v-deep .el-select__caret.is-reverse {
    transform: rotate(360deg) !important;
    transform: translateY(-100%) !important;
    // right: 0px;
}
.active {
    border: 2px solid #008ad6 !important;
}
.childItem {
    position: relative;
    display: inline-block;
    padding: 5px;
    width: 150px;
    // height: 52px;
    box-sizing: border-box;
    border: 2px solid transparent;
    // margin-right: 80px;
    .main {
        border: 1px solid #979797;
        border-radius: 20px;
        text-align: center;
        background-color: #fff;
        padding: 7px 15px;
        cursor: pointer;
        user-select: none;
        -moz-user-select: none;
        -webkit-user-select: none;
        -ms-user-select: none;
        ::v-deep .el-input__inner {
            font-size: 17px;
            font-weight: bold;
        }
    }
    .plusIcon {
        position: absolute;
        top: 50%;
        right: -40px;
        transform: translateY(-50%);
        cursor: pointer;
    }
    .pushChildIcon {
        position: absolute;
        left: 50%;
        // transform: translateX(-50%);
        bottom: -25px;
        cursor: pointer;
    }
}
.childList {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    margin-top: 40px;
    margin-left: 110px;
    .childItem {
        display: block;
        .main {
            border-radius: 5px;
            ::v-deep .el-input__inner {
                font-size: 13px;
            }
        }
        .plusIcon {
            right: -40px;
        }
    }
}
</style>

四、实现效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值