当前位置: 首页 > 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.jsqmd.com/news/1014981/

相关文章:

  • Windows 11右键菜单自定义指南:3步打造你的专属高效工作流
  • 2026年中山专利申请与无效律师哪家好?5位实战专家推荐 - 本地品牌推荐
  • 认准正规老字号!古籍拓片变现如何从源头杜绝仿冒套路、安心交易 - 深鉴新闻
  • Java 转大模型开发:后端程序员的升级路线:从踩坑到可复用方案
  • 2026 盐城空调维修 线路老化排查 家电上门抢修 本地口碑推荐 - 金修达家庭维修
  • 2026年6月市面上单级反渗透纯水设备厂家哪家靠谱推荐:工业净水系统、反渗透设备、纯水机、去离子水设备公司选择指南 - 海棠依旧大
  • OmenSuperHub:开源免费的惠普游戏本终极性能控制工具
  • 深耕东莞环保产业|武科环保打造研发 - 设计 - 施工 - 运维全链条一体化绿色治理标杆 - 广东科技观察
  • 邵阳空调专业维修、线路隐患排查,家电维修优选指南2026年6月最新 - 金修达家庭维修
  • SAP与国产ERP:三层本质差异 - 智慧园区
  • 2026年中西安家庭防水补漏指南:沣东靠谱的家里渗水修补电话与专业服务商解析 - 品牌鉴赏官2026
  • MPC8533E勘误文档深度解析:寄存器级编程避坑与实战指南
  • java:Math类
  • 深蓝词库转换:打破20+输入法壁垒的技术架构深度解析
  • 从手动刷本到智能托管:ok-ww如何用3000行Python代码重构《鸣潮》自动化体验
  • 2026年江苏新房装修怎么选?多维度横评南京本土装修公司,附真实案例与避坑指南 - 优质品牌商家
  • LangChain 实战指南:从调用模型到构建 AI 应用:一次项目复盘里的真实取舍
  • 闭包概念、特性、使用场景与注意事项
  • 2026年哈尔滨茅台酒回收靠谱渠道怎么选?实测7家实体店真实体验与避坑指南 - 优质品牌商家
  • 国内大容量商用消毒柜厂家实力排行及实测对比 - 互联网科技品牌测评
  • 沧州空调应急维修、线路故障排查,家电维修甄选指南2026年6月最新 - 金修达家庭维修
  • 低代码平台的 AI 逻辑编排:从自然语言到业务流程的工程化方案
  • 保姆级教程:用ENVI+Erdas从Landsat数据反演地表温度(附完整模型与避坑指南)
  • 2026排插什么牌子性价比高 实用选购参考 - 品牌排行榜
  • X1nput终极指南:一键解锁Xbox手柄完整震动体验
  • 数据分析转大模型:从报表到智能分析 Agent:从最小 Demo 到上线检查
  • 2026成都林德伯格镜框授权指南:6家靠谱服务商横向对比与选购建议 - 优质品牌商家
  • 2026年6月热门的阿尔卑斯饮品官网怎么选推荐,瓶装即饮茶招商、天然矿泉水代理、区域经销加盟选择指南 - 海棠依旧大
  • 程序员职业规划:大模型时代如何重新设计路线:从踩坑到可复用方案
  • 2026年餐饮设计行业深度观察:正规餐馆设计工作室如何选?真实案例与趋势分析 - 优质品牌商家