使用e-tree开发树形穿梭框
欢迎来到我的小屋
一、效果图
二、源代码
<template> <div style="margin: 100px;width: 1000px;height: 500px;border:1px solid #e4e7ed;border-radius:8px;box-shadow:0 2px 12px 0 rgba(0,0,0,0.08);"> <el-form :model="form" ref="formRef" label-width="100px"> <div class="tree-transfer"> <!-- 左侧可选资源树 --> <div class="transfer-panel"> <div class="transfer-header">可选资源</div> <el-input v-model="leftFilterText" placeholder="搜索资源" class="tree-search" clearable /> <el-tree ref="leftTree" node-key="id" :data="leftTreeData" :props="treeProps" :default-expand-all="true" :show-checkbox="true" :filter-node-method="filterNode" :check-strictly="false" @check-change="handleLeftCheckChange" /> </div> <!-- 中间操作按钮 --> <div class="transfer-buttons"> <el-button type="primary" :icon="ArrowRight" @click="addToRight" :disabled="!leftCheckedKeys.length" /> <el-button type="primary" :icon="ArrowLeft" @click="removeFromRight" :disabled="!rightCheckedKeys.length" /> <el-button type="primary" :icon="DArrowRight" @click="addAllToRight" /> <el-button type="primary" :icon="DArrowLeft" @click="removeAllFromRight" /> </div> <!-- 右侧已选资源树 --> <div class="transfer-panel"> <div class="transfer-header">已选资源</div> <el-input v-model="rightFilterText" placeholder="搜索资源" class="tree-search" clearable /> <el-tree ref="rightTree" node-key="id" :data="rightTreeData" :props="treeProps" :default-expand-all="true" :show-checkbox="true" :filter-node-method="filterNode" :check-strictly="false" @check-change="handleRightCheckChange" /> </div> </div> </el-form> </div> </template> <script setup> // 1. vue基础API import { ref, reactive, computed, watch, nextTick, onMounted } from 'vue' // 2. Element Plus 消息提示 import { ElMessage } from 'element-plus' // 3. 箭头图标 import { ArrowRight, ArrowLeft, DArrowRight, DArrowLeft } from '@element-plus/icons-vue' onMounted(() => { getResourceTree() }) /** 表单数据 */ const form = reactive({ resourceIds: [] }) /** 表单引用 */ const formRef = ref() const allResourceTree = ref([]) /** 树形穿梭框状态 */ const leftTree = ref() const rightTree = ref() const leftFilterText = ref('') const rightFilterText = ref('') const leftCheckedKeys = ref([]) const rightCheckedKeys = ref([]) /** 树配置 */ const treeProps = { label: 'resourceName', children: 'children', disabled: 'disabled' } /** * 获取所有子节点ID * @param tree 树形数据 * @param nodeId 节点ID * @returns {Array} */ const getAllChildrenIds = (tree, nodeId) => { const result = [] const findNode = (nodes, targetId) => { for (const node of nodes) { if (node.id === targetId) { if (node.children && node.children.length > 0) { const collectChildren = (children) => { for (const child of children) { result.push(child.id) if (child.children && child.children.length > 0) { collectChildren(child.children) } } } collectChildren(node.children) } return } if (node.children && node.children.length > 0) { findNode(node.children, targetId) } } } findNode(tree, nodeId) return result } /** * 过滤树节点 * @param value 过滤文本 * @param data 节点数据 * @returns {boolean} */ const filterNode = (value, data) => { if (!value) return true return data.resourceName.toLowerCase().includes(value.toLowerCase()) } /** * 从树中移除指定节点 * @param tree 树形数据 * @param ids 要移除的ID列表 * @returns {Array} */ const removeNodesByIds = (tree, ids) => { const result = [] for (const node of tree) { if (!ids.includes(node.id)) { const newNode = { ...node } if (node.children && node.children.length > 0) { newNode.children = removeNodesByIds(node.children, ids) } result.push(newNode) } else { // 如果父节点被选中,但子节点未被选中,保留父节点结构并显示未被选中的子节点 if (node.children && node.children.length > 0) { const remainingChildren = removeNodesByIds(node.children, ids) if (remainingChildren.length > 0) { // 创建一个临时父节点,标记为已选中状态,显示未被选中的子节点 result.push({ ...node, disabled: true, // 已选中父节点无法再选 children: remainingChildren, isSelectedParent: true // 标记这是一个已选中的父节点 }) } } } } return result } /** * 获取树中所有叶子节点ID * @param tree 树形数据 * @returns {Array} */ const getAllLeafIds = (tree) => { const result = [] const collectLeafs = (nodes) => { for (const node of nodes) { // 可选1、只收集叶子节点ID // if (!node.children || node.children.length === 0) { // result.push(node.id) // } else { // collectLeafs(node.children) // } // 可选2、收集所有节点ID(包括父节点) result.push(node.id) if (node.children && node.children.length > 0) { collectLeafs(node.children) } } } collectLeafs(tree) return result } /** * 根据ID列表构建子树 * @param tree 原始树形数据 * @param ids 选中的ID列表 * @returns {Array} */ const buildSubTreeByIds = (tree, ids) => { const result = [] for (const node of tree) { if (ids.includes(node.id)) { const newNode = { ...node } if (node.children && node.children.length > 0) { newNode.children = buildSubTreeByIds(node.children, ids) } result.push(newNode) } else if (node.children && node.children.length > 0) { const childResult = buildSubTreeByIds(node.children, ids) if (childResult.length > 0) { result.push({ ...node, children: childResult }) } } } return result } // ===================== API 方法 ===================== /** * 获取资源树列表 */ const getResourceTree = () => { request.get('/tree').then(res => { if (res.code === '200' || res.code === 200) { allResourceTree.value = res.data || [] nextTick(() => { syncTreeData() }) } }).catch(() => { ElMessage.error('获取资源列表失败') }) } /** * 同步左右树数据 */ const syncTreeData = () => { if (leftTree.value) { leftTree.value.setCheckedKeys([]) } if (rightTree.value) { rightTree.value.setCheckedKeys([]) } leftCheckedKeys.value = [] rightCheckedKeys.value = [] } /** * 获取左侧树数据(排除已选) */ const leftTreeData = computed(() => { if (!allResourceTree.value.length || !form.resourceIds.length) { return allResourceTree.value } return removeNodesByIds(allResourceTree.value, form.resourceIds) }) /** * 获取右侧树数据(已选资源) */ const rightTreeData = computed(() => { if (!allResourceTree.value.length || !form.resourceIds.length) { return [] } return buildSubTreeByIds(allResourceTree.value, form.resourceIds) }) /** * 左侧树勾选变化处理 */ const handleLeftCheckChange = (data, checked, indeterminate) => { const childIds = getAllChildrenIds(allResourceTree.value, data.id) const allIds = [data.id, ...childIds]
if (checked) {
leftCheckedKeys.value = [...new Set([...leftCheckedKeys.value, ...allIds])]
} else if(indeterminate){
// 半选状态,不处理
} else{
leftCheckedKeys.value = leftCheckedKeys.value.filter(id => !allIds.includes(id))
}
} /** * 右侧树勾选变化处理 */ const handleRightCheckChange = (data, checked, indeterminate) => { const childIds = getAllChildrenIds(allResourceTree.value, data.id) const allIds = [data.id, ...childIds] if (checked) { rightCheckedKeys.value = [...new Set([...rightCheckedKeys.value, ...allIds])] } else { rightCheckedKeys.value = rightCheckedKeys.value.filter(id => !allIds.includes(id)) } } /** * 添加选中项到右侧 */ const addToRight = () => { if (leftCheckedKeys.value.length === 0) return form.resourceIds = [...new Set([...form.resourceIds, ...leftCheckedKeys.value])] if (leftTree.value) { leftTree.value.setCheckedKeys([]) } leftCheckedKeys.value = [] } /** * 从右侧移除选中项 */ const removeFromRight = () => { if (rightCheckedKeys.value.length === 0) return form.resourceIds = form.resourceIds.filter(id => !rightCheckedKeys.value.includes(id)) if (rightTree.value) { rightTree.value.setCheckedKeys([]) } rightCheckedKeys.value = [] } /** * 添加全部到右侧 */ const addAllToRight = () => { const leafIds = getAllLeafIds(leftTreeData.value) form.resourceIds = [...new Set([...form.resourceIds, ...leafIds])] } /** * 移除全部 */ const removeAllFromRight = () => { form.resourceIds = [] rightCheckedKeys.value = [] if (rightTree.value) { rightTree.value.setCheckedKeys([]) } } /** * 监听左侧过滤文本变化 */ watch(leftFilterText, (val) => { if (leftTree.value) { leftTree.value.filter(val) } }) /** * 监听右侧过滤文本变化 */ watch(rightFilterText, (val) => { if (rightTree.value) { rightTree.value.filter(val) } }) // ============ 模拟request对象(mock专用)============ const request = { get: (url) => { // 匹配你请求的 /tree 接口 if (url === '/tree') { // 返回模拟Promise,结构和后端一致 {code, data, msg} return new Promise((resolve) => { // 模拟接口延迟200ms setTimeout(() => { const mockTreeData = [ { id: 1, resourceName: '系统管理', children: [ { id: 11, resourceName: '用户管理', children: [] }, { id: 12, resourceName: '角色管理', children: [] }, { id: 13, resourceName: '菜单权限', children: [ { id: 131, resourceName: '新增菜单', children: [] }, { id: 132, resourceName: '编辑菜单', children: [] } ] } ] }, { id: 2, resourceName: '订单模块', children: [ { id: 21, resourceName: '全部订单', children: [] }, { id: 22, resourceName: '退款订单', children: [] } ] }, { id: 3, resourceName: '财务中心', children: [] } ] resolve({ code: '200', data: mockTreeData, msg: '查询成功' }) }, 200) }) } // 其他接口可继续扩展 return Promise.reject({ msg: '接口不存在' }) } } </script> <style scoped> /* 树形穿梭框样式 */ .tree-transfer { display: flex; align-items: flex-start; gap: 10px; width: 100%; } .transfer-panel { flex: 0 0 45%; /* flex: 1; */ border: 1px solid #e4e7ed; border-radius: 4px; overflow: hidden; display: flex; flex-direction: column; } .transfer-header { padding: 12px 15px; background-color: #f5f7fa; border-bottom: 1px solid #e4e7ed; font-weight: 500; } .tree-search { padding: 10px; border-bottom: 1px solid #e4e7ed; } .transfer-panel :deep(.el-tree) { flex: 1; max-height: 400px; overflow-y: auto; } .transfer-buttons { /* 垂直居中核心 */ align-self: center; display: flex; flex-direction: column; gap: 12px; padding: 10px; } .transfer-buttons :deep(.el-button) { width: 40px; height: 40px; padding: 0; } .transfer-buttons :deep(.el-button + .el-button) { margin-left: 0; } </style>

使用e-tree开发树形穿梭框
浙公网安备 33010602011771号