背景:主流组件库提供的穿梭框并不能满足需求...手动封装扩展性更高

功能:

  1. 具备首次进入页面,分配左右侧穿梭框展示(左侧为树形,右侧为一维数组)
  2. 选中左侧-节点(子节点)通过 >按钮 可将选中节点移动到右侧(移动后会默认选中下一项,右侧同理)
  3. 通过 >>按钮将左侧所有子级节点移动到右侧
  4. 选中右侧-节点,通过 <按钮可将节点移动到左侧该节点的父节点下
  5. 通过<<按钮讲右侧节点全部恢复至左侧穿梭框中
  6. 在右穿梭框中,选中某一节点,通过∧∨按钮可上下移动节点位置
  7. 右穿梭框中,选中某一节点,通过↑↓按钮可将节点移动至最前或最后
  8. 左侧带有搜索框
  9. 双击左侧或右侧节点,则将该节点移动至对侧

思路:

1、左侧移动到右侧:获取选中的左节点push进右侧的数组,左侧的节点隐藏而并非删除

2、右侧移动到左侧:删除右侧选中的节点,左侧的该项恢复显示

注:此封装为子组件,数据通过props获取

/*** 请在父组件设置好数据,只需要传整一份树形结构。* 一级包含 id(唯一)、lable(展示)、selected(false则在左侧)、children(子级)* leftTitle rightTitle 穿梭框的左侧和右侧的顶部title*/

效果图:

H5部分

<template><div><el-card  class="container" shadow="always"><div class="leftBox"><div class="titlePid">{{treeShuttle_config.leftTitle}}</div><div class="treeBox"><el-treenode-key="id"class="filter-tree":data="treedata":props="defaultProps"default-expand-all:filter-node-method="filterNode"@node-click="nodeClick"ref="shuttleTree"><el-tooltip slot-scope="{ node }" effect="light" :open-delay='300' :content="node.label" class="item" placement="top"><span>{{node.label}}</span></el-tooltip></el-tree></div><div class="leftSearching"><el-inputplaceholder="输入关键词搜索指标"v-model="filterText"></el-input></div></div><div class="buttons"><el-button @click="moveAllToRight" icon='el-icon-d-arrow-right'></el-button><el-button @click="moveToRight" icon='el-icon-arrow-right'></el-button><el-button @click="moveToLeft" icon='el-icon-arrow-left' ></el-button><el-button @click="moveAllToLeft" icon='el-icon-d-arrow-left'></el-button></div><div class="rightBox"><div class="titlePid">{{treeShuttle_config.rightTitle}}</div><ul class="list"><liv-for="item in rightArray":key="item.id":class="{ 'selected':selectedRightItem && selectedRightItem.id === item.id }"@click="selectRightItem(item)"@dblclick="dblClickRight(item)">{{item.label}}</li></ul></div><div class="order-buttons"><el-button @click="moveToTop"  icon='el-icon-top'></el-button><el-button @click="moveUp"  icon='el-icon-arrow-up'></el-button><el-button @click="moveDown"  icon='el-icon-arrow-down'></el-button><el-button @click="moveToBottom"  icon='el-icon-bottom'></el-button></div></el-card></div>
</template>

data部分需要用到的数据

      /* tree双击 */preNodeId: null, // 左侧tree点击nodeCount: 0, // 左侧点击次数curNodeId: null,nodeTimer: null,filterText: '', // 左侧模糊过滤treedata: [], // 整棵树数据defaultProps: {children: 'children',label: 'label'},hideNode: [], // 所有需要隐藏的左侧节点selectedLeftItem: {}, // 左侧选中项rightArray: [], // 右侧数据selectedRightItem: {} // 右侧选中项

watch完成左侧的模糊搜索以及作为子组件获取数据后调用分配左右侧的方法

watch: {filterText (val) {this.$refs.shuttleTree.filter(val)},// 监听props数据treeShuttle_config: {immediate: true,deep: true,handler (val) {if (val) {this.fetchDataExecute(val.data)}}}}

methods 整个页面中用到的方法,每个方法会有详细解释

fetchDataExecute方法,会在watch中触发,用到settingsVisible方法和setNodeVisible方法

   // 获取数据后执行fetchDataExecute (data) {if (!data) returnconst treeList = dataconst fItems = this.settingsVisible(treeList)this.rightArray = fItems// 右侧节点的idthis.hideNode = fItems.map(item => item.id)this.$nextTick(() => {// 右侧节点的id,将左侧隐藏this.setNodeVisible(this.hideNode, false)})this.treedata = treeList}
  /*** 根据树形结构的数据,返回所有在右侧的节点* @param {Array} treeArr 树形结构的数据* @returns {Array} 在右侧的节点*/settingsVisible (treeArr) {const farr = [] // 定义空数组// 递归函数function recursive (treeArr) {treeArr.forEach(item => {if (item.children) {item.visible = truerecursive(item.children)} else {if (item.selected === false) {item.visible = true} else {item.visible = falsefarr.push(item)}}})}recursive(treeArr) // 开始递归return farr // 返回数组}

setNodeVisible共用方法,会频繁调用用来修改左侧节点的隐藏显示状态(这个方法只用来修改dom的隐藏显示,还有另外一个方法操作数据的参数值的变化,不知道什么因为dom更改后数据不会变,暂时先两个方法同步修改)

    /*** 控制指定节点的显示与隐藏* @param {Array} ids - 指定节点的id数组* @param {Boolean} fShow - true表示显示,false表示隐藏*/setNodeVisible (ids, fShow) {ids.forEach(item => {// 通过节点的id获取对应的节点对象const node = this.$refs.shuttleTree.getNode(item)// 设置节点的visible属性来控制节点的显示与隐藏this.$set(node, 'visible', fShow)})}

点击左侧穿梭框的节点,保存一个变量为当前点击项,并实现双击逻辑(双击调用移动到右侧方法)

    // 树形点击节点nodeClick (data, node, prop) {// console.log(data, node, prop)this.selectedLeftItem = datathis.nodeCount++if (this.preNodeId && this.nodeCount >= 2) {this.curNodeId = data.idthis.nodeCount = 0if (this.curNodeId === this.preNodeId) { // 第一次点击的节点和第二次点击的节点id相同console.log('双击,执行代码写在这里')this.moveToRight()this.curNodeId = nullthis.preNodeId = nullreturn}}this.preNodeId = data.idthis.nodeTimer = setTimeout(() => { // 300ms内没有第二次点击就把第一次点击的清空this.preNodeId = nullthis.nodeCount = 0}, 300)}

将左侧选中节点移动到右侧。此方法实现了:将左侧点击项移动至右侧,并移动后默认选中下一个,若已为最后一个,则向前设置默认直到为空。所以判断会多一点(默认选择下一个,因为我们树形中只是隐藏节点,并不是真删除,所有要找到当前父节点下当前项下一个可以被显示的项)。这个方法会调用setHide更改数据

/*** 将左侧选中的节点移动到右侧*/moveToRight () {// console.log(this.selectedLeftItem) // 打印选中的节点// 没有选择节点returnif (!this.selectedLeftItem.id) return// 父节点不可移动if (this.selectedLeftItem.children) return// 获取选中节点的父节点const parentLevel = this.getNowNodeFather(this.treedata, this.selectedLeftItem)// 父节点下最后一个visible为true的节点,需要用来判断当前节点是否是最后一个let lastVisibleNodefor (let i = parentLevel.children.length - 1; i >= 0; i--) {const node = parentLevel.children[i]if (node.visible) {lastVisibleNode = nodebreak}}// 将选中的节点添加到右侧数组中this.rightArray.push(this.selectedLeftItem)// 如果选中的节点没有被隐藏(讲移动的节点隐藏)if (!this.hideNode.includes(this.selectedLeftItem.id)) {// 将选中的节点的 id 添加到 hideNode 数组中this.hideNode.push(this.selectedLeftItem.id)// 隐藏选中的节点this.setNodeVisible(this.hideNode, false)this.setHide(this.treedata, this.selectedLeftItem.id)}// 从后往前找有无visible为true的节点if (!lastVisibleNode) return// 获取当前节点在当前父级中的索引 设置下一个默认选中节点parentLevel.children.forEach((item, index) => {if (item.id === this.selectedLeftItem.id) {// 当前点击项是父节点中的最后一项if (item.id === lastVisibleNode.id) {console.log(' 最后一项')// 最后一项为true的索引const topItem = parentLevel.children.findLastIndex(item => item.visible === true)if (topItem === -1) {// 如果没有下一个节点,将选中节点设置为空this.selectedLeftItem = {}} else {const nodeNextTerm = parentLevel.children[topItem]// 设置选中节点this.$refs.shuttleTree.setCurrentKey(nodeNextTerm.id)this.$nextTick(() => {// 执行点击事件document.querySelector('.is-current').firstChild.click()})}} else {// 获取下一个visible为true的节点const bottomItem = parentLevel.children.findIndex((ele, ind) => ele.visible === true && ind > index)const nodeNextTerm = parentLevel.children[bottomItem]// 设置选中节点this.$refs.shuttleTree.setCurrentKey(nodeNextTerm.id)this.$nextTick(() => {// 执行点击事件document.querySelector('.is-current').firstChild.click()})}}})}

获取左侧点击节点的父级所有数据(因为要默认选中他的下一项)

    // 获取当前选中节点的父级所有数据getNowNodeFather (treedata, itemPid, parentN = {}) {let parentNodefunction recursive3 (treedata, itemPid, parentN) {treedata.forEach(item => {if (item.children) {recursive3(item.children, itemPid, item)} else {if (item.id === itemPid.id) {parentNode = parentN}}})}// 开始递归recursive3(treedata, itemPid)return parentNode}

setHide方法就是上方提到的两个方法中的一个,用来同步数据跟随dom的变化

    /*** 设置指定itemId的节点隐藏* @param {Array} treedata - 树形数据,包含所有节点* @param {Number} itemId - 指定要隐藏的节点id*/setHide (treedata, itemId) {function recursive2 (treedata, itemId) {treedata.forEach(item => {if (item.children) {recursive2(item.children, itemId) // 递归遍历子节点} else {if (item.id === itemId) {item.visible = false // 设置目标节点隐藏}}})}// 开始递归recursive2(treedata, itemId)}// 和上一个方法同理,只不过是设置显示的setHideShow (treedata, itemId) {function recursive22 (treedata, itemId) {treedata.forEach(item => {if (item.children) {recursive22(item.children, itemId) // 递归遍历子节点} else {if (item.id === itemId) {item.visible = true // 设置目标节点显示}}})}// 开始递归recursive22(treedata, itemId)}

将左侧全部移动到右侧,用到了getAllChildNodes方法

    // 全部移动到右侧moveAllToRight () {const res = this.getAllChildNodes(this.treedata)this.rightArray = res[0]this.hideNode = res[1]this.setNodeVisible(this.hideNode, false)}

将树形的所有子节点都隐藏

    // 获取所有左侧子级节点、并改变子级的visiblegetAllChildNodes (treedata) {const parentNode = []const parentIds = []function recursive5 (treedata) {treedata.forEach(item => {if (item.children) {recursive5(item.children)} else {item.visible = falseparentNode.push(item)parentIds.push(item.id)}})}// 开始递归recursive5(treedata)return [parentNode, parentIds]}

点击右侧节点,把点击项进行存储

    // 右侧点击节点selectRightItem (node) {this.selectedRightItem = node}

将右侧移动到左侧

    /**将选中的节点从右侧移动到左侧*/moveToLeft () {// 找到选中右侧节点在右侧数组中的索引const itemInd = this.rightArray.findIndex(item => item.id === this.selectedRightItem.id)if (itemInd === -1) return// 从右侧数组中过滤掉选中的节点this.rightArray = this.rightArray.filter(item => item.id !== this.selectedRightItem.id)// 找出选中节点在左侧需要展示的节点中的索引const showNode = this.hideNode.filter(item => item === this.selectedRightItem.id)// 从隐藏节点数组中过滤掉选中的节点(因为展示了)this.hideNode = this.hideNode.filter(item => item !== this.selectedRightItem.id)// 将选中节点在左侧中需要展示的节点设置为可见this.setNodeVisible(showNode, true)// 将选中的节点在左侧中数据变更为truethis.setHideShow(this.treedata, this.selectedRightItem.id)// 如果选择的是最后一项,则选中右侧的最后一项,否则选中当前移动项的下一项if (itemInd === this.rightArray.length) {this.selectedRightItem = this.rightArray[this.rightArray.length - 1]} else {this.selectedRightItem = this.rightArray[itemInd]}// 调用左侧模糊搜索this.$refs.shuttleTree.filter(this.filterText)}

双击右侧项,则直接调用上面moveToLeft

    // 双击右侧移动到左侧dblClickRight () {this.moveToLeft()}

将右侧全部移动至左侧

    // 全部移动到左侧moveAllToLeft () {// 找出选中节点在左侧需要展示的节点中的索引const showNode = this.rightArray.map(item => item.id)// 从隐藏节点数组中过滤掉选中的节点(因为展示了)this.hideNode = []// 将选中节点在左侧中需要展示的节点设置为可见this.setNodeVisible(showNode, true)// 将选中的节点在左侧中数据变更为truethis.aaa(this.treedata)this.rightArray = []// 调用左侧模糊搜索this.$refs.shuttleTree.filter(this.filterText)},// 上部分代码调用了,一个递归用来将整棵树改为trueaaa (treedata) {function recursive6 (treedata) {treedata.forEach(item => {if (item.children) {recursive6(item.children)} else {item.visible = true}})}recursive6(treedata)}

至此,左右穿梭的功能全部完成,由于文章过长,右侧穿梭框的四个上下移动的功能在下一个文章补全,下面是最后的样式部分

<style scoped lang='less'>
// 整体页面
.container {padding: 5px;width: 670px;display: flex;height: 450px;// title.titlePid{background-color: #d7e6fd;color: #333;height: 30px;line-height: 30px;text-align: center;border-bottom: 1px solid #333;}/deep/ .el-card__body{display: flex;}
}
// 左穿梭框
.leftBox{position: relative;height: 430px;width: 255px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);.leftSearching{width: 100%;position: absolute;left: 0;bottom: 0;}/deep/ .el-tree{background-color: #f8fbfe;}/deep/ .el-input__inner{height: 28px;line-height: 28px;}/deep/ .el-tree-node__content{height: 24px;font-size: 14px;}.treeBox{height: 372px;overflow-y: auto;}
}// 右穿梭框
.rightBox{height: 430px;width: 255px;border-radius: 4px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);.list {border-radius: 4px;list-style: none;padding: 0;margin: 0;width: 100%;background-color: #f8fbfe;height: 399px;overflow-y: auto;li {color: #333333;font-size: 14px;padding: 2px 10px;cursor: pointer;}li:hover {background-color: #93afd8;}// 选中项.selected {background-color: #3A85F5;color:#fff !important;}}
}// 左侧按钮
.buttons {display: flex;flex-direction: column;justify-content: center;align-items: center;margin: 0 10px;
}.buttons button {margin: 10px 0;padding: 10px 20px;border: none;border-radius: 5px;background-color: #3A85F5;color: #fff;cursor: pointer;transition: all 0.3s;
}.buttons button:hover {background-color: #66b1ff;
}.buttons button:disabled {opacity: 0.5;cursor: not-allowed;
}// 右侧按钮
.order-buttons {display: flex;flex-direction: column;justify-content: center;align-items: center;margin: 0 5px;
}.order-buttons button {margin: 10px 0;padding: 10px 20px;border: none;border-radius: 5px;background-color: #3A85F5;color: #fff;cursor: pointer;transition: all 0.3s;
}.order-buttons button:hover {background-color: #66b1ff;
}.order-buttons button:disabled {opacity: 0.5;cursor: not-allowed;
}// tree样式
/deep/ .el-tree-node__content:hover{background-color: #93afd8;color: #fff;
}
/deep/ .el-tree-node.is-current > .el-tree-node__content {background: #3A85F5;border-right: 3px solid #1677ff;color: #fff;/deep/ .el-tree-node__expand-icon {color: rgb(0, 112, 255);}/deep/ .is-leaf {color: rgba(0, 0, 0, 0);}
}
</style>

vue项目手写树形穿梭框相关推荐

  1. 一个基于vue和element-ui的树形穿梭框组件

    el-tree-transfer 简介 因为公司业务使用vue框架,ui库使用的element-ui.在市面上找到一个好用的vue树形穿梭框组件都很难,又不想仅仅因为一个穿梭框在element-ui之 ...

  2. Vue使用基于element-ui的el-tree-fransfer树形穿梭框组件

    el-tree-fransfer 是一个基于 VUE 和 element-ui 的树形穿梭框组件,使用前请确认已经引入element-ui 官网npm文档:https://www.npmjs.com/ ...

  3. 在vue项目中使用树形结构的穿梭框

    先看一下最后的效果: 一个基于elementui的穿梭框组件:el-tree-transfer 第一步:安装组件 npm install el-tree-transfer --save 第二步:写代码 ...

  4. vue中手写一个放大镜功能

    vue中手写一个放大镜功能 有的时候需要对图片进行放大,类似于电商的商品放大功能,于是在这个想法上写了一个放大镜的功能,并且在放大镜的基础上新添加了一些小功能,下面开始吧! 放大镜是封装的组件的形式, ...

  5. el-tree与el-transfer结合成树形穿梭框(tree-transfer)

    下载 npm install el-tree-transfer --save <tree-transfer :title="['模块(菜单)访问权限', '拥有的操作权限']" ...

  6. vue纯手写思维导图,拒绝插件(cv即用)

    vue纯手写思维导图,拒绝插件(cv即用) 已完成功能点:折叠.放大.缩小.移动 后续增加功能点:添加.删除 先看结果: 有这么个需求,按照层级关系,把表格放在思维导图上,我第一时间想到用插件,但是找 ...

  7. html5 移动端手写签名,H5移动端项目实现手写签名功能 vue实现手写签名

    vue 移动端实现手写签名效果,功能很完美,保存时保存为base64格式. 刚好项目用到此功能,就网上找了一下,清理了无用代码,简单方便,因为项目中多个地方需要使用,所以我将它整理为组件,通过ref和 ...

  8. H5移动端项目实现手写签名功能 vue实现手写签名

    vue 移动端实现手写签名效果,功能很完美,保存时保存为base64格式. base64转file文件格式 vue中将base64转file文件格式 刚好项目用到此功能,就网上找了一下,清理了无用代码 ...

  9. tree结构穿梭 vue_结合el-tree和el-transfer搞一个树形穿梭框

    最近遇到个需求,穿梭框中的内容是树形结构的数据.查看elementUI的transfer组件是不支持树形结构的数据,也就不能直接使用了.但是el-tree组件支持啊,那如果让tree组件和transf ...

最新文章

  1. 软件工程第二次作业完整版
  2. SQLServer存储过程
  3. MFC软件工程架构模型-模式窗口-非模式窗口
  4. 宝塔服务器搞成虚拟主机,宝塔面板怎么配置虚拟主机
  5. 产品如何解决「发型师」与「消费者」的认知偏差?
  6. vue 调用mutation方法_Vuex白话教程第三讲:Vuex旗下的Mutation
  7. ubuntu 16.04系统中nvidai、cuda、cudnn安装及注意事项
  8. admin.php为什么是乱码,phpadmin和MySQL中文乱码问题的剖析
  9. c语言设计程序实现顺序冒泡_2019年9月全国计算机等级考试《二级C语言程序设计》题库...
  10. win10 dcom服务器进程占用cpu,WIN10CPU百分百,Windows10 DCOM占用过高解决办法 | 帮助信息-动天数据...
  11. Linux中MongoDB创建数据库
  12. @程序员,京东面试官让你谈谈注册中心的问题!
  13. 框架学习笔记:Unity3D的MVC框架——StrangeIoC
  14. 基于OpenPose的人体姿态检测
  15. 有源滤波器设计工具枪战---凯利讯半导体
  16. 链家爬取深圳租房代码(java)
  17. android 目录作用,Android中各级目录的作用说明
  18. java心电图_使用原生js+canvas实现模拟心电图
  19. OnInitDialog()中SetFocus()不起作用解决方法
  20. 关于java集合的查找和删除的小程序

热门文章

  1. js阻止浏览器返回上一页
  2. QT POST/GET HTTP操作
  3. 关于开关软起动(缓启动)电路学习笔记
  4. 如何批量复制word文件名到excel?
  5. Primavera P6 Professional R21.12 正式发布(附下载地址)
  6. D3 二维图表的绘制系列(六)基础折线图
  7. 一步步打造属于自己的私有云服务(Gen8 + Esxi + 群晖 + CentOS + Server2016)
  8. 运行pytorch作业出现错误 RuntimeError: unable to write to file
  9. twisted 多线程并发的相关讨论
  10. 计算机无法启用媒体流,电脑流媒体打不开怎么回事?电脑打开媒体流的方法