当前位置: 首页 > news >正文

使用e-tree开发树形穿梭框

欢迎来到我的小屋

 

 一、效果图

image

 二、源代码

<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-inputv-model="leftFilterText"placeholder="搜索资源"class="tree-search"clearable/><el-treeref="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-buttontype="primary":icon="ArrowRight"@click="addToRight":disabled="!leftCheckedKeys.length"/><el-buttontype="primary":icon="ArrowLeft"@click="removeFromRight":disabled="!rightCheckedKeys.length"/><el-buttontype="primary":icon="DArrowRight"@click="addAllToRight"/><el-buttontype="primary":icon="DArrowLeft"@click="removeAllFromRight"/></div><!-- 右侧已选资源树 --><div class="transfer-panel"><div class="transfer-header">已选资源</div><el-inputv-model="rightFilterText"placeholder="搜索资源"class="tree-search"clearable/><el-treeref="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 truereturn 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 {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) returnform.resourceIds = [...new Set([...form.resourceIds, ...leftCheckedKeys.value])]if (leftTree.value) {leftTree.value.setCheckedKeys([])}leftCheckedKeys.value = []
}/*** 从右侧移除选中项*/
const removeFromRight = () => {if (rightCheckedKeys.value.length === 0) returnform.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>

 

http://www.rkmt.cn/news/1526802.html

相关文章:

  • 2026 盐城空调维修 线路老化排查 家电上门抢修 本地口碑推荐 - 金修达家庭维修
  • 邵阳空调专业维修、线路隐患排查,家电维修优选指南2026年6月最新 - 金修达家庭维修
  • 2026年中西安家庭防水补漏指南:沣东靠谱的家里渗水修补电话与专业服务商解析 - 品牌鉴赏官2026
  • 从手动刷本到智能托管:ok-ww如何用3000行Python代码重构《鸣潮》自动化体验
  • 2026年江苏新房装修怎么选?多维度横评南京本土装修公司,附真实案例与避坑指南 - 优质品牌商家
  • 闭包概念、特性、使用场景与注意事项
  • 保姆级教程:用ENVI+Erdas从Landsat数据反演地表温度(附完整模型与避坑指南)
  • 低代码平台的 AI 逻辑编排:从自然语言到业务流程的工程化方案
  • 国内大容量商用消毒柜厂家实力排行及实测对比 - 互联网科技品牌测评
  • 数据分析转大模型:从报表到智能分析 Agent:从最小 Demo 到上线检查
  • 2026年行业内优秀职务侵占罪刑事律师排行 - 品牌排行榜
  • 广州正规电工证培训机构盘点 老牌机构资质与服务对比 - 互联网科技品牌测评
  • 数术宇宙:零一无穷创世史诗
  • 2026年四川铝合金门窗品牌实力观察:从技术到服务,谁在定义新标准? - 优质品牌商家
  • 2026年深圳出口包装印刷行业观察:技术升级与FSC认证成竞争关键 - 优质品牌商家
  • 新手应该如何正确地创造类
  • 镇江GEO/SEO优化避坑指南:2026年6月十家主流公司独立权威评测 - 936品牌测评网
  • 点焊机怎么选?搞懂这5点,少花冤枉钱 - 奔跑123
  • 青岛配眼镜推荐,多少钱验光科普指南 - 配眼镜新资讯
  • 如何一键合并B站缓存视频:HLB站缓存合并工具完全指南
  • MouseTester终极指南:5分钟快速掌握鼠标性能测试的完整教程
  • Mythos安全模型:漏洞发现与利用链构建的因果建模范式
  • 广州正规无人机培训机构盘点 资质与实训双维度解析 - 互联网科技品牌测评
  • 终极实战指南:3种高效部署Realtek RTL8125 2.5GbE网卡驱动的完整方案
  • 三步打造个人云游戏主机:Sunshine游戏串流实战指南
  • 2026年阿里云618超速攻略:OpenClaw怎么部署?Token Plan配置及大模型接入指南
  • 微信小程序逆向工程深度解析:wxappUnpacker架构设计与安全分析机制
  • 2026 年阜阳入夏空调故障排查、线路老化检修 正规家电维修服务商推荐指南 - 金修达家庭维修
  • 如何快速实现音频转文字:AsrTools智能语音识别工具的完整解决方案
  • 2026精品古籍拓片留存指南:哪些老书值得留?哪些适合及时出手? - 深鉴新闻