> 文章列表 > 树形穿梭框组件

树形穿梭框组件

树形穿梭框组件

树形穿梭框组件

1. 在component文件中新建 TreeTransfer 文件夹,在该文件夹下新建 index.vue 文件,代码如下:

<template><div class="v-transfer"><!-- 左侧框 --><div class="v-transfer-item v-transfer-left"><div class="header">可选列表</div><div class="content"><!-- 搜索框 --><div class="seacrh-box"><el-input v-model.trem="leftFilterText" placeholder="请输入" clearable @clear="onSearchLeft" @keyup.enter.native="onSearchLeft"></el-input><el-button type="primary" @click="onSearchLeft">搜索</el-button></div><!-- 数据区域 --><div class="tree-box"><div class="empty-box" v-if="leftTreeData.length < 1">暂无数据</div><el-treeclass="tree"v-elseref="leftTreeRef":data="leftTreeData"show-checkbox:node-key="props.nodeKey":filter-node-method="filterLeftNode":props="props.defaultProps"@check="onCheckLeft"></el-tree></div></div></div><!-- 按钮区域 --><div class="v-transfer-middle"><el-button type="primary"  style="margin-bottom:10px" :disabled="leftOperation.length < 1" @click="addHandle">添加</el-button><el-button type="danger" style="margin:0" :disabled="rightOperation.length < 1" @click="delHandle">删除</el-button></div><!-- 右侧框 --><div class="v-transfer-item v-transfer-right"><div class="header">已选列表</div><div class="content"><!-- 搜索框 --><div class="seacrh-box"><el-input v-model.trem="rightFilterText" placeholder="请输入" clearable @clear="onSearchRight" @keyup.enter.native="onSearchRight"></el-input><el-button type="primary" @click="onSearchRight">搜索</el-button></div><!-- 数据区域 --><div class="tree-box"><div class="empty-box" v-if="rightTreeData.length < 1">暂无数据</div><el-treeclass="tree"v-elseref="rightTreeRef":filter-node-method="filterRightNode":data="rightTreeData"show-checkbox:node-key="props.nodeKey":props="props.defaultProps"@check="onCheckRight"></el-tree></div></div></div></div>
</template>
<script setup lang="ts" name="VTransfer">
import { onMounted, ref, reactive, watch, nextTick } from 'vue'
import { ElTree } from 'element-plus'
import type Node from 'element-plus/es/components/tree/src/model/node'
import { ArrowLeft, ArrowRight, Expande } from '@element-plus/icons-vue'
import lodash from 'lodash'
import { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'interface treeNode {className: stringparentId: stringid: stringchildren?: treeNode
}const props = defineProps({leftTree: {type: Array,default: () => {return []},},rightTree: {type: Array,default: () => {return []},},defaultProps: {type: Array,default: () => {return {children: 'children',label: 'label'}},},nodeKey: {type: String,default: 'id',},
})// 抛出事件
const emits = defineEmits(['change', 'delete','add'])const leftFilterText = ref<string>('')
const leftTreeData = ref<treeNode[]>([])
const leftDefaultCheckedKeys = ref<any[]>([])
const leftDefaultExpandedKeys = ref<any[]>([])
const leftOperation = ref<any[]>([])
const leftTreeRef = ref<InstanceType<typeof ElTree>>()const rightFilterText = ref<string>('')
const rightTreeData = ref<treeNode[]>([])
const rightDefaultCheckedKeys = ref<any[]>([])
const rightDefaultExpandedKeys = ref<any[]>([])
const rightOperation = ref<any[]>([])
const rightTreeRef = ref<InstanceType<typeof ElTree>>()interface Tree {id: numberlabel: stringchildren?: Tree[]
}watch(props,newVal => {leftTreeData.value = lodash.cloneDeep(newVal.leftTree)rightTreeData.value = lodash.cloneDeep(newVal.rightTree)},{ immediate: true }
)defineExpose({leftTreeData,rightTreeData,
})onMounted(() => {leftFilterText.value = ''rightFilterText.value = ''
})const formatTree = (tree: any[], parentKey: string = 'parentId', idKey: string = 'id') => {//格式化选择的树:清除全选下面的子节点let swap = [],parentIds: string[] = []//先找出有children的id集合,再把所有的数据做对比,只要parentId和其中一个对上,就把该数据删除;tree.forEach((item, index) => {if (item.children) {parentIds.push(item[idKey])}})swap = tree.filter((item, index) => {if (parentIds.indexOf(item[parentKey]) == -1) {return item}})return swap
}//左侧选中
const onCheckLeft = () => {leftOperation.value = formatTree(leftTreeRef.value!.getCheckedNodes(false) || [], 'parentId', 'id')
}
//左侧搜素
const onSearchLeft = () => {leftTreeRef.value!.filter(leftFilterText.value)
}
//左侧过滤
const filterLeftNode = (value: string, data: Tree, node: treeNode) => {if (!value) return truereturn chodeNode(value, data, node)
}//右侧选中
const onCheckRight = () => {rightOperation.value = formatTree(rightTreeRef.value!.getCheckedNodes(false) || [], 'parentId', 'id')
}
//右侧搜素
const onSearchRight = () => {rightTreeRef.value!.filter(rightFilterText.value)
}//右侧过滤
const filterRightNode = (value: string, data: Tree, node: treeNode) => {if (!value) return truereturn chodeNode(value, data, node)
}const chodeNode = (value: string, data: Tree, node: treeNode) => {if (data.className.indexOf(value) !== -1) {return true}const level = node.level// 如果传入的节点本身就是一级节点就不用校验了if (level === 1) {return false}// 先取当前节点的父节点let parentData = node.parent// 遍历当前节点的父节点let index = 0while (index < level - 1) {// 如果匹配到直接返回if (parentData.data.className.indexOf(value) !== -1) {return true}// 否则的话再往上一层做匹配parentData = parentData.parentindex++}// 没匹配到返回falsereturn false
}//添加
const addHandle = () => {const leftTree = leftTreeRef.valueif (!leftTree) {return}const leftNodes = leftTree.getCheckedNodes(false, true)const parents = leftNodes.filter(el => el.children && el.children.length > 0)const checkedKeys = leftTree.getCheckedKeys(false)const rightTree = rightTreeRef.valueif (!rightTree) {const rightList = parents.map(parent => {const obj = lodash.omit(parent, ['children'])obj.children = lodash.filter(parent.children, child => checkedKeys.indexOf(child.id) >= 0)return obj})rightTreeData.value = lodash.cloneDeep(rightList)} else {leftNodes.forEach(el => {const leftData = lodash.omit(el, ['children'])const rightParent = rightTree.getNode(leftData.parentId)if (!rightTree.getNode(leftData.id)) {rightTree.append(leftData, rightParent)}})}// 删除原值leftNodes.forEach(node => {leftTree.setChecked(node, false, false)if (checkedKeys.indexOf(node.id) >= 0) {leftTree.remove(node)}})leftOperation.value = formatTree(leftTree.getCheckedNodes(false) || [], 'parentId', 'id')emits('change', leftTreeData.value, rightTreeData.value)emits('add')}//删除
const delHandle = () => {const rightTree = rightTreeRef.valueif (!rightTree) {return}const rightNodes = rightTree.getCheckedNodes(false, true)const checkedKeys = rightTree.getCheckedKeys(false)const parents = lodash.filter(rightNodes, item => item.children && item.children.length > 0)const leftTree = leftTreeRef.valueif (!leftTree) {parents.forEach(item => {item.chidren = lodash.filter(item.chidren, ele => checkedKeys.indexOf(ele.id) >= 0)leftTreeData.value.push(item)})} else {rightNodes.forEach(item => {let parent = leftTree.getNode(item.parentId)let node = lodash.omit(item, 'children')if (!leftTree.getNode(item.id)) {leftTree.append(node, parent)}})}checkedKeys.forEach(key => {rightTree.setChecked(key, false, false)rightTree.remove(key)})rightOperation.value = formatTree(rightTree.getCheckedNodes(false) || [], 'parentId', 'id')emits('change', leftTreeData.value, rightTreeData.value)emits('delete')
}
</script><style lang="scss">
.v-transfer {.tree {.ep-tree__empty-block {text-align: center;color: #e5e6eb;position: relative;&::before {content: '';background-image: url(../../assets/img/empty.png);background-size: cover;width: 160px;height: 172px;position: absolute;top: 20px;left: 50%;transform: translateX(-50%);}.ep-tree__empty-text {position: absolute;top: 192px;left: 50%;transform: translateX(-50%);}}}
}
</style><style lang="scss" scoped>
.v-transfer {display: flex;align-items: center;justify-content: flex-start;&-item {border: 1px solid #e5e6eb;border-radius: 2px;min-height: 200px;display: flex;flex-direction: column;.header {background: #f7f8fa;height: 40px;display: flex;align-items: center;justify-content: center;padding: 0 16px;border-bottom: 1px solid #e5e6eb;}.content {background-color: #fff;flex: 1;padding: 16px;.seacrh-box {width: 100%;display: flex;align-items: center;justify-content: flex-start;margin-bottom: 16px;.ep-button {margin-left: 16px;}}.tree-box {height: 300px;overflow-y: auto;}}}&-left {width: calc(50% - 30px);}&-middle {width: 100px;text-align: center;.ep-button {margin-top: 10px;}}&-right {width: calc(50% - 30px);}.empty-box {text-align: center;color: #86909c;margin-top:40px;}
}
</style>

2. 组件使用示例

<template><TreeTransfer ref="transfer" :default-props="props" :left-tree="leftTree" :right-tree="rightTree" />
</template><script setup lang="ts" name="demo">
import { onMounted, ref, reactive } from 'vue'
import TreeTransfer from './TreeTransfer/index.vue'
const leftTree = ref([{name: '三一设计',id: '1',parentId: '-1',children: [{name: '三一设计高三新高考',id: '1-1',parentId: '1',},{name: '三一设计高三老高考',id: '1-2',parentId: '1',},],},{name: '金太阳考案',id: '2',parentId: '-1',children: [{name: '金太阳考案高三(SHX)',id: '2-1',parentId: '2',},{name: '金太阳考案高三(广西)',id: '2-2',parentId: '2',},],},
])
const rightTree = ref([])const props = ref({label: 'name',
})
</script><style lang="scss"></style>