vue3实现自定义树形权限穿梭框

53 阅读7分钟

mmexport1765114180502.jpg 核心:对el-tree进行操作以及对数据的处理

本例子中左侧为单列用户、(原为给某个文档目录下添加可查看文档的用户,现在需要精细化的控制用户对文档的操作权限,**产品),穿梭到右侧后在每个用户下添加权限节点用于勾选权限

自定义树形穿梭框组件(理论上左右均可自定义数据结构)

<template>
        <div class="tree-transfer">
        <!-- 穿梭框左侧 -->
        <div class="tree-transfer-left">
            <!-- 左侧采用element-ui的el-tree -->
            <el-input placeholder="输入关键字进行过滤" v-model="filterText" style="padding:10px;" />
            <div v-show="dataLeft.length > 0" style="width:95%;height:230px;overflow-y:auto;margin:0 auto;">
                <el-tree ref="treeLeft" :data="dataLeft" show-checkbox node-key="id" :props="defaultProps"
                    :filter-node-method="filterNode" @check-change="leftCheckChange" default-expand-all>
                </el-tree>
            </div>
            <div v-show="!dataLeft.length > 0" style="margin:120px auto 0;width:100%;height:30px;text-align:center;">
                暂无数据
            </div>
          </div>
        <!-- 穿梭框中间按钮区域 -->
        <div class="tree-transfer-middle">
            <!--  -->
            <el-button circle type="info" icon="el-icon-arrow-left"
                :style="selectRightList.length > 0 ? 'background:#409EFF;border-color:#409EFF' : ''"
                @click="removeClick"></el-button>
            <el-button circle type="info" icon="el-icon-arrow-right"
                :style="selectLeftList.length > 0 ? 'background:#409EFF;border-color:#409EFF' : ''"
                @click="addClick"></el-button>
        </div>
        <!-- 穿梭框右侧 -->
        <div class="tree-transfer-right">
            <!-- 右侧直接放置结果 -->
            <!-- 这里也采用tree结构,默认是对数据进行处理使得没有树形结构,也可以选择有树形结构 -->
            <div style="height:50px;width:100%;line-height:50px;margin-bottom:10px;border-bottom: 1px solid #ccc;">
                已选择 </div>
            <div v-show="dataRight.length > 0">
                <el-tree ref="treeRight" :data="dataRight" show-checkbox node-key="id" :props="defaultProps"
                    @check-change="rightCheckChange">
                </el-tree>
            </div>
            <div v-show="!dataRight.length > 0" style="margin:120px auto 0;width:100%;height:30px;text-align:center;">
                暂无数据
            </div>
        </div>
    </div>
    <el-button @click="submit">提交</el-button>
</template>

定义需要用到的变量

let treeLeft = ref(null); //左侧树
let treeRight = ref(null); //右侧树
let defaultProps = {
    children: "child",
    label: "name",
}
let yuanlaiData = [] //保存一个原始树

let dataLeft = ref([]) //左侧树--数据
let selectLeftList = [] //左侧树-勾选
let filterText = ref('') //左侧的树进行过滤

let dataRight = ref([]) //右侧树--数据
let selectRightList = [] //右侧树-勾选

let setUserstreeData = [] //获取的一个完整树
let currentData = [] //这个树是剔除右侧数据的树
let dataListData = [{
    id: "11",
    name: "集团公司"
}, {
    id: "22",
    name: "美丽公司"
}]
let permissionData = [
    {
        id: "1111",
        name: "管理权限",
        child: null,
        type: 1,
    },
    {
        id: "2222",
        name: "阅读权限",
        child: null,
        type: 2,
    }
]

左侧树初始化数据,方法中添加对数据回显时的处理

function getReViewOfficeTree() {
    setUserstreeData = dataListData;
    if (Object.keys(setUserstreeData).length > 0) {
        dataLeft.value = setUserstreeData;
        let data = JSON.parse(JSON.stringify(setUserstreeData));
        yuanlaiData = data;
    }
}

数据向右侧流动方法

function addClick() {
    // 定义一个递归过滤的方法,用来删掉父级中给的元素
    let list = treeLeft.value.getCheckedNodes(); //勾选的节点
    // 选中的数据list,遍历树,找到对应的id,将这条数据进行删除
    list.forEach((item, index) => {
        getTreeName(dataLeft.value, item.id);
    });
    setRightList(list)
    selectLeftList = treeLeft.value.getCheckedNodes(); //用于控制右侧按钮颜色
    selectRightList = treeRight.value.getCheckedNodes(); //用于控制左侧按钮颜色
}
function setRightList(list) {
    for (let i of list) {
        let a = JSON.parse(JSON.stringify(permissionData))
        for (let j of a) {
            j.parentId = i.id
            j.id = generateRandomId()
        }
        i.child = a
    }
    // 3.回显设置右侧选中
    let arr = []
    dataRight.value.push(...list);
    dataRight.value.map(item => {
        props.data.map(item2 => {
            if (item.id == item2.userId) {
                item.child.map(item3 => {
                    if (item2.permiss.includes(item3.type)) {
                        console.log("item3", item3)
                        arr.push(item3)
                    }
                })
            }
        })
    })
    nextTick(() => {
        treeRight.value.setCheckedNodes(arr);
    })
}

// 循环遍历树形结构,删除符合条件的叶子节点
function getTreeName(list, id) {
    for (let i = 0; i < list.length; i++) {
        let a = list[i];
        if (a.id === id) {
            const index = list.findIndex((item) => item.id === id);
            list.splice(index, 1);
            return a.name;
        } else {
            if (a.child && a.child.length > 0) {
                let res = getTreeName(a.child, id);
                if (res) {
                    return res;
                }
            }
        }
    }
}

数据向左侧流动方法

function removeClick() {
    // 从右侧移除时的方法
    // 1.从右侧删除选中的数据
    let list = treeRight.value.getCheckedNodes();
    for (let item of list) {
        let index = dataRight.value.findIndex((item2) => {
            return item.id == item2.id;
        });
        if (index >= 0) {
            dataRight.value.splice(index, 1);
        }
    }
    // 2.遍历右边的数据,将左边树对应的数据删除---右边存在的数据不能在左边树存在
    if (dataRight.value.length > 0) {
        currentData = JSON.parse(JSON.stringify(yuanlaiData));
        dataRight.value.forEach((item, index) => {
            getTreeName(currentData, item.id);
            dataLeft.value = currentData;
        });
    } else {
        dataLeft.value = JSON.parse(JSON.stringify(yuanlaiData));
    }
    selectLeftList = treeLeft.value.getCheckedNodes(); //用于控制右侧按钮颜色
    selectRightList = treeRight.value.getCheckedNodes(); //用于控制左侧按钮颜色
}

完整代码

<template>
    <!-- 自定义树形穿梭框组件、理论上左右均可选择是否为树形结构,目前固定为左侧树形、右侧无层级结构 -->
    <div class="tree-transfer">
        <!-- 穿梭框左侧 -->
        <div class="tree-transfer-left">
            <!-- 左侧采用element-ui的el-tree -->
            <el-input placeholder="输入关键字进行过滤" v-model="filterText" style="padding:10px;" />
            <div v-show="dataLeft.length > 0" style="width:95%;height:230px;overflow-y:auto;margin:0 auto;">
                <el-tree ref="treeLeft" :data="dataLeft" show-checkbox node-key="id" :props="defaultProps"
                    :filter-node-method="filterNode" @check-change="leftCheckChange" default-expand-all>
                </el-tree>
            </div>
            <div v-show="!dataLeft.length > 0" style="margin:120px auto 0;width:100%;height:30px;text-align:center;">
                暂无数据
            </div>

        </div>
        <!-- 穿梭框中间按钮区域 -->
        <div class="tree-transfer-middle">
            <!--  -->
            <el-button circle type="info" icon="el-icon-arrow-left"
                :style="selectRightList.length > 0 ? 'background:#409EFF;border-color:#409EFF' : ''"
                @click="removeClick"></el-button>
            <el-button circle type="info" icon="el-icon-arrow-right"
                :style="selectLeftList.length > 0 ? 'background:#409EFF;border-color:#409EFF' : ''"
                @click="addClick"></el-button>
        </div>
        <!-- 穿梭框右侧 -->
        <div class="tree-transfer-right">
            <!-- 右侧直接放置结果 -->
            <!-- 这里也采用tree结构,默认是对数据进行处理使得没有树形结构,也可以选择有树形结构 -->
            <div style="height:50px;width:100%;line-height:50px;margin-bottom:10px;border-bottom: 1px solid #ccc;">
                已选择 </div>
            <div v-show="dataRight.length > 0">
                <el-tree ref="treeRight" :data="dataRight" show-checkbox node-key="id" :props="defaultProps"
                    @check-change="rightCheckChange">
                </el-tree>
            </div>
            <div v-show="!dataRight.length > 0" style="margin:120px auto 0;width:100%;height:30px;text-align:center;">
                暂无数据
            </div>
        </div>
    </div>
    <el-button @click="submit">提交</el-button>
</template>

<script setup>
import { nextTick, onMounted, ref, watch } from 'vue'

let props = defineProps({
    data: {
        type: Array,
        default: () => [{ userId: 11, permiss: [1, 2] }, { userId: 22, permiss: [2] }]
    }
})
let treeLeft = ref(null); //左侧树
let treeRight = ref(null); //右侧树
let defaultProps = {
    children: "child",
    label: "name",
}
let yuanlaiData = [] //保存一个原始树

let dataLeft = ref([]) //左侧树--数据
let selectLeftList = [] //左侧树-勾选
let filterText = ref('') //左侧的树进行过滤

let dataRight = ref([]) //右侧树--数据
let selectRightList = [] //右侧树-勾选

let setUserstreeData = [] //获取的一个完整树
let currentData = [] //这个树是剔除右侧数据的树
let dataListData = [{
    id: "11",
    name: "集团公司"
}, {
    id: "22",
    name: "美丽公司"
}]
let permissionData = [
    {
        id: "1111",
        name: "管理权限",
        child: null,
        type: 1,
    },
    {
        id: "2222",
        name: "阅读权限",
        child: null,
        type: 2,
    }
]
onMounted(() => {
    getReViewOfficeTree();
})
// 监控data中的数据变化
watch(filterText, (newVal, oldVal) => {
    treeLeft.value.filter(newVal);
})
// 获取左侧的树
function getReViewOfficeTree() {
    setUserstreeData = dataListData;
    if (Object.keys(setUserstreeData).length > 0) {
        dataLeft.value = setUserstreeData;
        let data = JSON.parse(JSON.stringify(setUserstreeData));
        yuanlaiData = data;
        // 如果要回显数据,则调用这个方法
        let arr = []
        if (props.data.length > 0) {
            props.data.forEach((item, index) => {
                dataLeft.value.forEach((item2, index2) => {
                    if (item.userId == item2.id) {
                        arr.push(dataLeft.value[index2])
                        dataLeft.value.splice(index2, 1);
                    }
                })
            });
            setRightList(arr)
        }
    }
}
function addClick() {
    list.forEach((item, index) => {
        getTreeName(dataLeft.value, item.id);
    });
    setRightList(list)
    selectLeftList = treeLeft.value.getCheckedNodes(); //用于控制右侧按钮颜色
    selectRightList = treeRight.value.getCheckedNodes(); //用于控制左侧按钮颜色
}
function setRightList(list) {
    for (let i of list) {
        let a = JSON.parse(JSON.stringify(permissionData))
        for (let j of a) {
            j.parentId = i.id
            j.id = generateRandomId()
        }
        i.child = a
    }
    // 3.回显设置右侧选中
    let arr = []
    dataRight.value.push(...list);
    dataRight.value.map(item => {
        props.data.map(item2 => {
            if (item.id == item2.userId) {
                item.child.map(item3 => {
                    if (item2.permiss.includes(item3.type)) {
                        console.log("item3", item3)
                        arr.push(item3)
                    }
                })
            }
        })
    })
    console.log("arr", arr)
    nextTick(() => {
        treeRight.value.setCheckedNodes(arr);
        //  treeRight.value.setCheckedNodes([]);
    })
}
function generateRandomId() {
    const randomId = Math.random().toString(36).substring(2, 10);
    return randomId;
}
function submit() {
    let list = treeRight.value.getCheckedNodes();
    console.log("提交", dataRight.value);
    let params = dataRight.value.map(item => {
        let perArry = list.map(item2 => {
            if (item2.parentId == item.id) {
                console.log("perArry", item2)
                return item2.type
            }
        })

        return {
            id: item.id,
            name: item.name,
            permiss: perArry
        }
    })
    for (let i of dataRight.value) {
        i.permiss = []
        for (let j of list) {
            if (i.id == j.parentId) {
                console.log("j", j)
                i.permiss.push(j.type)
            }
        }
    }
    console.log("params", dataRight.value)
}

// 循环遍历树形结构,删除符合条件的叶子节点
function getTreeName(list, id) {
    for (let i = 0; i < list.length; i++) {
        let a = list[i];
        if (a.id === id) {
            const index = list.findIndex((item) => item.id === id);
            list.splice(index, 1);
            return a.name;
        } else {
            if (a.child && a.child.length > 0) {
                let res = getTreeName(a.child, id);
                if (res) {
                    return res;
                }
            }
        }
    }
}
// 将所有根节点禁选
function getTreeDisable(list) {
    for (let i = 0; i < list.length; i++) {
        let a = list[i];
        if (a.child && a.child.length > 0) {
            a.disabled = true;
            getTreeDisable(a.child);
        }
    }
}
function removeClick() {
    // 从右侧移除时的方法
    // 1.从右侧删除选中的数据
    let list = treeRight.value.getCheckedNodes();
    for (let item of list) {
        let index = dataRight.value.findIndex((item2) => {
            return item.id == item2.id;
        });
        if (index >= 0) {
            dataRight.value.splice(index, 1);
        }
    }
    // 2.遍历右边的数据,将左边树对应的数据删除---右边存在的数据不能在左边树存在
    if (dataRight.value.length > 0) {
        currentData = JSON.parse(JSON.stringify(yuanlaiData));
        dataRight.value.forEach((item, index) => {
            getTreeName(currentData, item.id);
            dataLeft.value = currentData;
        });
    } else {
        dataLeft.value = JSON.parse(JSON.stringify(yuanlaiData));
    }
    selectLeftList = treeLeft.value.getCheckedNodes(); //用于控制右侧按钮颜色
    selectRightList = treeRight.value.getCheckedNodes(); //用于控制左侧按钮颜色
}
function getResult() {
    return dataRight.value;
}
function leftCheckChange(data, checked, indeterminate) {
    console.log("1212dsageddsa", data, checked, indeterminate);
    selectLeftList = treeLeft.value.getCheckedNodes(); //用于控制右侧按钮颜色
    selectRightList = treeRight.value.getCheckedNodes(); //用于控制左侧按钮颜色
}
function rightCheckChange(data, checked, indeterminate) {
    selectRightList = treeRight.value.getCheckedNodes(); //用于控制左侧按钮颜色
    selectLeftList = treeLeft.value.getCheckedNodes(); //用于控制右侧按钮颜色
}
//  左侧树  支持过滤
function filterNode(value, data) {
    if (!value) return true;
    return data.name.indexOf(value) !== -1;
}
</script>

<style scoped lang="scss">
:deep(.el-tree) {
    background-color: rgba(0,0,0,0);
    color: #ffff;
     .el-checkbox__inner{
        background:rgba(0,0,0,0);// checkbox 背景透明
    }
    .el-tree-node__content:hover {
        background: rgba(104,147,215,0.4);// hover 节点背景色,否则是白色
    }
    .el-tree-node:focus>.el-tree-node__content {
        background-color: rgba(0,0,0,0); // focus 节点背景透明,否则是白色
    }

}

.tree-transfer {
    display: flex;
    min-height: 250px;

    .tree-transfer-left {
        width: 300px;
        border: 1px #E5E5E5 solid;
        border-radius: 10px;
        height: 330px;
        overflow-y: auto;

    }

    .tree-transfer-middle {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 14%;
    }

    .tree-transfer-right {
        width: 300px;
        border: 1px #E5E5E5 solid;
        border-radius: 10px;
        height: 330px;
        overflow-y: auto;
    }
}
</style>