当前位置:   article > 正文

vue3 + elementPlus 树形穿梭框_vue3使用elementplus穿梭框树形多选

vue3使用elementplus穿梭框树形多选

 HTML

  1. <template>
  2. <div>
  3. <div class="v-transfer">
  4. <!-- 左侧框 -->
  5. <div class="v-transfer-item v-transfer-left">
  6. <div class="header">
  7. <el-checkbox v-model="checkedLeft" :disabled="leftTreeData.length < 1" label="" size="large"
  8. style="margin-right: 12px" @change="leftAllCheck" />
  9. {{ leftTitle }}
  10. </div>
  11. <div class="content">
  12. <!-- 搜索框 -->
  13. <div class="seacrh-box">
  14. <el-input v-model.trem="leftFilterText" placeholder="请输入" clearable @clear="onSearchLeft"
  15. @keyup.enter="onSearchLeft" @change="onSearchLeft"></el-input>
  16. </div>
  17. <!-- 数据区域 -->
  18. <div class="tree-box">
  19. <div class="empty-box" v-if="leftTreeData.length < 1">暂无数据</div>
  20. <el-tree class="tree" v-else ref="leftTreeRef" :data="leftTreeData" show-checkbox :node-key="'id'"
  21. :filter-node-method="filterNode" :props="props.defaultProps" @check="onCheckLeft">
  22. <template #default="{ data }">
  23. <span style="padding-left: 4px; font-size: 16px">{{ data.title }}</span>
  24. </template></el-tree>
  25. </div>
  26. </div>
  27. </div>
  28. <!-- 按钮区域 -->
  29. <div class="v-transfer-middle">
  30. <div class="middle-icon">
  31. <div :disabled="leftOperation.length < 1" @click="addHandle">
  32. <ArrowRight style="width: 1em; height: 1em" />
  33. </div>
  34. <div :disabled="rightOperation.length < 1" @click="delHandle">
  35. <ArrowLeft style="width: 1em; height: 1em" />
  36. </div>
  37. </div>
  38. </div>
  39. <!-- 右侧框 -->
  40. <div class="v-transfer-item v-transfer-right">
  41. <div class="header">
  42. <el-checkbox v-model="checkedRight" :disabled="rightTreeData.length < 1" label="" size="large"
  43. style="margin-right: 12px" @change="rightAllCheck" />{{ rightTitle }}
  44. </div>
  45. <div class="content">
  46. <!-- 搜索框 -->
  47. <div class="seacrh-box">
  48. <el-input v-model.trem="rightFilterText" placeholder="请输入" clearable @clear="onSearchRight"
  49. @keyup.enter="onSearchRight" @change="onSearchRight"></el-input>
  50. </div>
  51. <!-- 数据区域 -->
  52. <div class="tree-box">
  53. <div class="empty-box" v-if="rightTreeData.length < 1">暂无数据</div>
  54. <el-tree class="tree" v-else ref="rightTreeRef" :filter-node-method="filterNode" :data="rightTreeData"
  55. show-checkbox :node-key="'id'" :props="props.defaultProps" @check="onCheckRight">
  56. <template #default="{ data }">
  57. <span style="padding-left: 4px; font-size: 16px">{{ data.title }}</span>
  58. </template>
  59. </el-tree>
  60. </div>
  61. </div>
  62. </div>
  63. </div>
  64. <div class="bottomBtn">
  65. <el-button type="primary" style="background-color:#0068DA;" @click="roleDialogSubmit">确认授权</el-button>
  66. </div>
  67. </div>
  68. </template>

 JS

  1. <script setup lang="ts" name="TreeTransfer">
  2. import { onMounted, ref, reactive, watch, nextTick } from 'vue'
  3. import { ElTree, ElMessage, ElMessageBox } from 'element-plus'
  4. import { grant } from '@/api/roleMangement/index'
  5. import type Node from 'element-plus/es/components/tree/src/model/node'
  6. import lodash from 'lodash'
  7. import { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
  8. import { Right } from '@element-plus/icons-vue'
  9. import { treeToList } from '@/utils/util'
  10. interface treeNode {
  11. className: string
  12. parentId: string
  13. id: string
  14. children?: treeNode
  15. }
  16. const props = defineProps({
  17. leftTitle: {
  18. type: String,
  19. default: () => {
  20. return ''
  21. }
  22. },
  23. rightTitle: {
  24. type: String,
  25. default: () => {
  26. return ''
  27. }
  28. },
  29. leftTree: {
  30. type: Array,
  31. default: () => {
  32. return []
  33. }
  34. },
  35. rightTree: {
  36. type: Array,
  37. default: () => {
  38. return []
  39. }
  40. },
  41. defaultProps: {
  42. type: Object,
  43. default: () => {
  44. return {
  45. children: 'children',
  46. label: 'title'
  47. }
  48. }
  49. },
  50. nodeKey: {
  51. type: String,
  52. default: 'id'
  53. },
  54. leafsArr: {
  55. type: Array,
  56. default: () => {
  57. return []
  58. }
  59. }
  60. })
  61. const checkedLeft = ref(false)
  62. const checkedRight = ref(false)
  63. // 抛出事件
  64. const emits = defineEmits(['change', 'delete', 'add'])
  65. const leftFilterText = ref<string>('')
  66. const leftTreeData = ref<any[]>([])
  67. const leftDefaultCheckedKeys = ref<any[]>([])
  68. const leftDefaultExpandedKeys = ref<any[]>([])
  69. const leftOperation = ref<any[]>([])
  70. const leftTreeRef = ref<InstanceType<typeof ElTree>>()
  71. const rightFilterText = ref<string>('')
  72. const rightTreeData = ref<any[]>([])
  73. const rightDefaultCheckedKeys = ref<any[]>([])
  74. const rightDefaultExpandedKeys = ref<any[]>([])
  75. const rightOperation = ref<any[]>([])
  76. const rightTreeRef = ref<InstanceType<typeof ElTree>>()
  77. const removeNodes = ref<any[]>([])
  78. interface Tree {
  79. id: number
  80. label: string
  81. children?: Tree[]
  82. }
  83. watch(
  84. props,
  85. newVal => {
  86. leftTreeData.value = lodash.cloneDeep(newVal.leftTree)
  87. rightTreeData.value = lodash.cloneDeep(newVal.rightTree)
  88. },
  89. { immediate: true }
  90. )
  91. watch(rightFilterText, val => {
  92. rightTreeRef.value!.filter(val)
  93. })
  94. watch(leftFilterText, val => {
  95. leftTreeRef.value!.filter(val)
  96. })
  97. defineExpose({
  98. leftTreeData,
  99. rightTreeData
  100. })
  101. onMounted(() => {
  102. leftFilterText.value = ''
  103. rightFilterText.value = ''
  104. })
  105. const formatTree = (tree: any[], parentKey: string = 'parentId', idKey: string = 'id') => {
  106. // 格式化选择的树:清除全选下面的子节点
  107. let swap,
  108. parentIds: string[] = []
  109. // 先找出有children的id集合,再把所有的数据做对比,只要parentId和其中一个对上,就把该数据删除;
  110. tree.forEach((item, index) => {
  111. if (item.children) {
  112. parentIds.push(item[idKey])
  113. }
  114. })
  115. swap = tree.filter((item, index) => {
  116. if (parentIds.indexOf(item[parentKey]) == -1) {
  117. return item
  118. }
  119. })
  120. return swap
  121. }
  122. // 左侧选中
  123. const onCheckLeft = () => {
  124. leftOperation.value = formatTree(
  125. leftTreeRef.value!.getCheckedNodes(false) || [],
  126. 'parentId',
  127. 'id'
  128. )
  129. }
  130. // 左侧搜素
  131. // const onSearchLeft = () => {
  132. // leftTreeRef.value!.filter(leftFilterText.value)
  133. // }
  134. // 左侧过滤
  135. // const filterLeftNode = (value: string, data: Tree, node: treeNode) => {
  136. // if (!value) return true
  137. // return chodeNode(value, data, node)
  138. // }
  139. // 右侧选中
  140. const onCheckRight = () => {
  141. rightOperation.value = formatTree(
  142. rightTreeRef.value!.getCheckedNodes(false) || [],
  143. 'parentId',
  144. 'id'
  145. )
  146. }
  147. // 右侧搜素
  148. const onSearchRight = () => {
  149. rightTreeRef.value!.filter(rightFilterText.value)
  150. }
  151. // 左
  152. const onSearchLeft = () => {
  153. leftTreeRef.value!.filter(leftFilterText.value)
  154. }
  155. const filterNode = (value: string, data: any) => {
  156. if (!value) return true
  157. return data.label.includes(value)
  158. }
  159. const leftAllCheck = () => {
  160. const leftTree = leftTreeRef.value
  161. if (checkedLeft.value) {
  162. leftTree?.setCheckedNodes(leftTreeData.value)
  163. checkedLeft.value = true
  164. } else {
  165. leftTree?.setCheckedNodes([])
  166. checkedLeft.value = false
  167. }
  168. }
  169. const rightAllCheck = () => {
  170. const rightTree = rightTreeRef.value
  171. if (checkedRight.value) {
  172. rightTree?.setCheckedNodes(rightTreeData.value)
  173. checkedRight.value = true
  174. } else {
  175. rightTree?.setCheckedNodes([])
  176. checkedRight.value = false
  177. }
  178. }
  179. const removeChecked = (rightList, allCheckedKeys) => {
  180. rightList.forEach((e, index) => {
  181. if (allCheckedKeys.indexOf(e.id) == -1) {
  182. rightList.splice(index, 1)
  183. }
  184. if (e.children) {
  185. removeChecked(e.children, allCheckedKeys)
  186. }
  187. })
  188. return rightList
  189. }
  190. const recursionTree = (parents, allCheckedKeys) => {
  191. parents.forEach(e => {
  192. const list = lodash.filter(e.children, child => allCheckedKeys.indexOf(child.id) >= 0)
  193. e.children = list
  194. if (e.children.length !== 0) {
  195. recursionTree(e.children, allCheckedKeys)
  196. }
  197. })
  198. return parents
  199. }
  200. // 右移
  201. const addHandle = async () => {
  202. const leftTree = leftTreeRef.value
  203. checkedLeft.value = false
  204. if (!leftTree) {
  205. return
  206. }
  207. const leftNodes = leftTree.getCheckedNodes(false, true)
  208. const parents = leftNodes.filter(
  209. el => el.children && el.children.length > 0 && el.parentId == '0'
  210. )
  211. const checkedKeys = leftTree.getCheckedKeys(false)
  212. const halfCheckedKeys = leftTree.getHalfCheckedKeys()
  213. const allCheckedKeys = halfCheckedKeys.concat(checkedKeys)
  214. const rightTree = rightTreeRef.value
  215. if (!rightTree) {
  216. let rightList = parents.map(parent => {
  217. const obj = lodash.omit(parent, ['children'])
  218. obj.children = lodash.filter(parent.children, child => allCheckedKeys.indexOf(child.id) >= 0)
  219. return obj
  220. })
  221. let copyRightList = lodash.cloneDeep(rightList)
  222. removeChecked(copyRightList, allCheckedKeys)
  223. rightTreeData.value.push(...copyRightList)
  224. } else {
  225. let isExist = false
  226. rightTree.data.forEach(e => {
  227. if (parents[0].id === (e as any).id) isExist = true
  228. })
  229. if (!isExist) {
  230. const overall = lodash.cloneDeep(parents)
  231. let rightList = await recursionTree(overall, allCheckedKeys)
  232. let copyRightList = lodash.cloneDeep(rightList)
  233. removeChecked(copyRightList, allCheckedKeys)
  234. rightTreeData.value.push(...copyRightList)
  235. } else {
  236. handleData(leftNodes, rightTree, allCheckedKeys)
  237. }
  238. }
  239. // 移过去之后要删除原本的值
  240. leftNodes.forEach(node => {
  241. leftTree.setChecked(node, false, false)
  242. if (checkedKeys.indexOf(node.id) >= 0) {
  243. leftTree.remove(node)
  244. }
  245. })
  246. leftOperation.value = formatTree(leftTree.getCheckedNodes(false) || [], 'parentId', 'id')
  247. emits('change', leftTreeData.value, rightTreeData.value)
  248. emits('add')
  249. }
  250. // 左移
  251. const delHandle = async () => {
  252. checkedRight.value = false
  253. const rightTree = rightTreeRef.value
  254. if (!rightTree) {
  255. return
  256. }
  257. const rightNodes = rightTree.getCheckedNodes(false, true)
  258. const checkedKeys = rightTree.getCheckedKeys(false)
  259. const halfCheckedKeys = rightTree.getHalfCheckedKeys()
  260. const allCheckedKeys = halfCheckedKeys.concat(checkedKeys)
  261. const parents = lodash.filter(
  262. rightNodes,
  263. item => item.children && item.children.length > 0 && item.parentId == '0'
  264. )
  265. const leftTree = leftTreeRef.value
  266. if (!leftTree) {
  267. let leftList = parents.map(parent => {
  268. const obj = lodash.omit(parent, ['children'])
  269. obj.children = lodash.filter(parent.children, child => allCheckedKeys.indexOf(child.id) >= 0)
  270. return obj
  271. })
  272. let copyLeftList = lodash.cloneDeep(leftList)
  273. removeChecked(copyLeftList, allCheckedKeys)
  274. leftTreeData.value.push(...copyLeftList)
  275. } else {
  276. let isExist = false
  277. leftTree.data.forEach(e => {
  278. if (parents[0].id === (e as any).id) isExist = true
  279. })
  280. if (!isExist) {
  281. const overall = lodash.cloneDeep(parents)
  282. let leftList = await recursionTree(overall, allCheckedKeys)
  283. let copyLeftList = lodash.cloneDeep(leftList)
  284. removeChecked(copyLeftList, allCheckedKeys)
  285. rightTreeData.value.push(...copyLeftList)
  286. } else {
  287. handleData(rightNodes, leftTree, allCheckedKeys)
  288. }
  289. }
  290. rightNodes.forEach(node => {
  291. rightTree.setChecked(node, false, false)
  292. if (checkedKeys.indexOf(node.id) >= 0) {
  293. rightTree.remove(node)
  294. }
  295. })
  296. rightOperation.value = formatTree(rightTree.getCheckedNodes(false) || [], 'parentId', 'id')
  297. emits('change', leftTreeData.value, rightTreeData.value)
  298. emits('delete')
  299. }
  300. const handleData = (rightNodes, leftTree, allCheckedKeys) => {
  301. rightNodes.forEach(item => {
  302. if (item.children) {
  303. let parent = leftTree.getNode(item.parentId)
  304. let node = item
  305. if (!leftTree.getNode(item.id) && allCheckedKeys.indexOf(item.id) > 0) {
  306. const copyNode = lodash.cloneDeep(node)
  307. copyNode.children = []
  308. leftTree.append(copyNode, parent)
  309. }
  310. handleData(item.children, leftTree, allCheckedKeys)
  311. } else {
  312. let parent = leftTree.getNode(item.parentId)
  313. let node = item
  314. if (!leftTree.getNode(item.id) && allCheckedKeys.indexOf(item.id) > 0) {
  315. leftTree.append(node, parent)
  316. }
  317. }
  318. })
  319. }
  320. const roleDialogSubmit = () => {
  321. const rightList = treeToList(rightTreeData.value)
  322. grant({ roleIds: [props.nodeKey], menuIds: rightList.map(item => (item).id) }).then(
  323. res => {
  324. ElMessage({
  325. type: 'success',
  326. message: '授权成功!'
  327. })
  328. }
  329. )
  330. }
  331. </script>

CSS

  1. <style lang="scss">
  2. .v-transfer {
  3. width: 100%;
  4. height: 100%;
  5. .tree {
  6. .ep-tree__empty-block {
  7. text-align: center;
  8. color: #e5e6eb;
  9. position: relative;
  10. &::before {
  11. content: '';
  12. background-image: url(../../assets/img/empty.png);
  13. background-size: cover;
  14. width: 160px;
  15. height: 172px;
  16. position: absolute;
  17. top: 20px;
  18. left: 50%;
  19. transform: translateX(-50%);
  20. }
  21. .ep-tree__empty-text {
  22. position: absolute;
  23. top: 192px;
  24. left: 50%;
  25. transform: translateX(-50%);
  26. }
  27. }
  28. }
  29. }
  30. .bottomBtn {
  31. text-align: center;
  32. }
  33. </style>
  34. <style lang="scss" scoped>
  35. .v-transfer {
  36. display: flex;
  37. justify-content: flex-start;
  38. font-size: 16px;
  39. &-item {
  40. border: 1px solid #e5e6eb;
  41. border-radius: 2px;
  42. min-height: 200px;
  43. display: flex;
  44. flex-direction: column;
  45. .header {
  46. background: #f7f8fa;
  47. height: 40px;
  48. display: flex;
  49. align-items: center;
  50. // justify-content: center;
  51. padding: 0 16px;
  52. border-bottom: 1px solid #e5e6eb;
  53. }
  54. .content {
  55. background-color: #fff;
  56. flex: 1;
  57. padding: 16px;
  58. .seacrh-box {
  59. width: 100%;
  60. display: flex;
  61. align-items: center;
  62. justify-content: flex-start;
  63. margin-bottom: 16px;
  64. .ep-button {
  65. margin-left: 16px;
  66. }
  67. }
  68. .tree-box {
  69. height: 570px;
  70. overflow-y: auto;
  71. }
  72. }
  73. }
  74. &-left {
  75. width: 40%;
  76. height: calc(100% - 60px);
  77. }
  78. &-middle {
  79. width: 20%;
  80. height: calc(100% - 60px);
  81. text-align: center;
  82. margin: auto 0;
  83. position: relative;
  84. // .ep-button {
  85. // margin-top: 10px;
  86. // }
  87. .middle-icon {
  88. position: absolute;
  89. top: 38%;
  90. left: 100px;
  91. div {
  92. width: 40px;
  93. height: 40px;
  94. background: #409eff;
  95. border-radius: 50%;
  96. color: #fff;
  97. cursor: pointer;
  98. &:first-child {
  99. margin-bottom: 40px;
  100. }
  101. svg {
  102. margin-top: 12px;
  103. }
  104. }
  105. }
  106. }
  107. &-right {
  108. width: 40%;
  109. height: calc(100% - 60px);
  110. }
  111. .empty-box {
  112. text-align: center;
  113. color: #86909c;
  114. margin-top: 40px;
  115. }
  116. }
  117. </style>

引用

  1. <TreeTransfer
  2. ref="treeTransferRef"
  3. :nodeKey="roleId"
  4. :left-tree="leftTree"
  5. :right-tree="rightTree"
  6. :default-props="props"
  7. :leafsArr="leafsArr"
  8. :leftTitle="'左侧标题'"
  9. :rightTitle="'右侧标题'"
  10. />

效果

 

很简陋,待优化

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/223922
推荐阅读
相关标签
  

闽ICP备14008679号