antd 树型穿梭 自定义
<template>
<div>
<a-transfer
:target-keys="targetKeys"
class="tree-transfer"
:data-source="dataSource"
:render="(item) => item.title"
:show-search="false"
:show-select-all="false"
@change="onChange"
>
<template #leftSelectAllLabel>
<div class="transfer-title">
<div class="title">点位列表</div>
</div>
</template>
<template #rightSelectAllLabel>
<div class="transfer-title">
<div class="title">选中点位</div>
</div>
</template>
<template #children="{ direction, selectedKeys, onItemSelect }">
<template v-if="direction === 'left'">
<a-tree
v-if="leftTreeData.length"
block-node
checkable
:tree-data="leftTreeData"
:checked-keys="leftCheckedKey"
:field-names="{ title: 'title', key: 'id' }"
default-expand-all
@check="
(_, props) => {
handleLeftChecked(
_,
props,
[...selectedKeys, ...targetKeys],
onItemSelect
);
}
"
>
<template #title="{ title }">
<span v-if="title.indexOf(leftSearchValue) > -1">
{{ title.substr(0, title.indexOf(leftSearchValue)) }}
<span style="color: #f50">{{ leftSearchValue }}</span>
{{
title.substr(
title.indexOf(leftSearchValue) + leftSearchValue.length
)
}}
</span>
<span v-else>{{ title }}</span>
</template>
</a-tree>
<a-empty v-else>
<template #description>暂无数据</template>
</a-empty>
</template>
<template v-else-if="direction === 'right'">
<a-tree
v-if="rightTreeData.length"
block-node
checkable
default-expand-all
:tree-data="rightTreeData"
:checked-keys="rightCheckedKey"
:field-names="{ title: 'title', key: 'id' }"
@check="
(_, props) => {
handleRightChecked(
_,
props,
[...selectedKeys, ...targetKeys],
onItemSelect
);
}
"
>
<template #title="{ title }">
<span v-if="title.indexOf(rightSearchValue) > -1">
{{ title.substr(0, title.indexOf(rightSearchValue)) }}
<span style="color: #f50">{{ rightSearchValue }}</span>
{{
title.substr(
title.indexOf(rightSearchValue) + rightSearchValue.length
)
}}
</span>
<span v-else>{{ title }}</span>
</template>
</a-tree>
<a-empty v-else>
<template #description>暂无数据</template>
</a-empty>
</template>
</template>
</a-transfer>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import {
cloneDeep,
flatten,
getDeepList,
getTreeKeys,
handleLeftTreeData,
handleRightTreeData,
isChecked,
uniqueTree,
} from "@/utils/utils.js";
const treeData = ref([
{
id: 1,
parentId: 0,
key: "0",
title: "工作台",
},
{
id: 2,
parentId: 0,
key: "2",
title: "仪表盘",
children: [
{
id: 3,
parentId: 2,
key: "3",
title: "欢迎页",
},
{
id: 4,
parentId: 2,
key: "4",
title: "分析页",
},
{
id: 5,
parentId: 2,
key: "5",
title: "监控页",
},
],
},
{
id: 6,
parentId: 0,
key: "6",
title: "表单页",
children: [
{
id: 7,
parentId: 6,
key: "7",
title: "基础表单",
},
{
id: 8,
parentId: 6,
key: "8",
title: "分步表单",
},
{
id: 9,
parentId: 6,
key: "9",
title: "高级表单",
},
],
},
]);
const editKey = ref([]);
const targetKeys = ref([]); // 显示在右侧框数据的 key 集合
const dataSource = ref([]); // 数据源,其中的数据将会被渲染到左边一栏
const leftCheckedKey = ref([]); // 左侧树选中 key 集合
const leftHalfCheckedKeys = ref([]); // 左侧半选集合
const leftCheckedAllKey = ref([]); // 左侧树选中的 key 集合,包括半选与全选
const leftTreeData = ref([]); // 左侧树
const rightCheckedKey = ref([]); // 右侧树选中集合
const rightCheckedAllKey = ref([]); // 右侧树选中集合,包括半选与全选
const rightExpandedKey = ref([]); // 右侧展开数集合
const rightTreeData = ref([]); // 右侧树
const emitKeys = ref([]); // 往父级组件传递的数据
const deepList = ref([]); // 深层列表
const leftSearchValue = ref(); // 左边搜索关键字
const rightSearchValue = ref(); // 右边搜索关键字
const rightTreeDataHis = ref([]) // 历史右侧树
const pointTypeRight = ref() // 点位类型右边
const pointTypeLeft = ref() // 点位类型左边
const rightCheckedKeyHis = ref([]) // 左侧树选中 key 历史集合
onMounted(() => {
processTreeData()
});
// 处理树数据
function processTreeData() {
dataSource.value = [];
flatten(cloneDeep(treeData.value), dataSource.value);
if (editKey.value.length) {
processEditData();
} else {
leftTreeData.value = handleLeftTreeData(
cloneDeep(treeData.value),
leftCheckedKey.value
);
}
}
// 处理编辑数据
function processEditData() {
leftCheckedAllKey.value = editKey.value;
rightExpandedKey.value = editKey.value;
targetKeys.value = editKey.value;
rightTreeData.value = handleRightTreeData(
cloneDeep(treeData.value),
editKey.value
);
getDeepList(deepList.value, treeData.value);
leftCheckedKey.value = uniqueTree(editKey.value, deepList.value);
leftHalfCheckedKeys.value = leftCheckedAllKey.value.filter(
(item) => leftCheckedKey.value.indexOf(item) === -1
);
leftTreeData.value = handleLeftTreeData(
cloneDeep(treeData.value),
leftCheckedKey.value
);
emitKeys.value = rightExpandedKey.value;
}
// 穿梭更改
function onChange(targetKeyList, direction) {
if (direction === 'right') {
pointTypeLeft.value = null
rightCheckedKey.value = []
let rightTreeKeys = getTreeKeys(rightTreeData.value)
let checkedKey = [...new Set([...rightTreeKeys, ...leftCheckedAllKey.value])]
leftCheckedKey.value = checkedKey
targetKeys.value = checkedKey
rightTreeData.value = handleRightTreeData(cloneDeep(treeData.value), checkedKey, 'right')
rightTreeDataHis.value = rightTreeData.value
leftTreeData.value = handleLeftTreeData(cloneDeep(treeData.value), checkedKey, 'right')
} else if (direction === 'left') {
pointTypeRight.value = null
rightTreeData.value = handleRightTreeData(cloneDeep(rightTreeDataHis.value), rightCheckedKey.value, 'left')
rightTreeDataHis.value = rightTreeData.value
leftTreeData.value = handleLeftTreeData(cloneDeep(leftTreeData.value), rightCheckedKey.value, 'left')
leftCheckedKey.value = leftCheckedKey.value.filter(item => rightCheckedKey.value.indexOf(item) === -1)
targetKeys.value = targetKeyList.filter(item => rightCheckedKey.value.indexOf(item) === -1)
leftHalfCheckedKeys.value = leftHalfCheckedKeys.value.filter(item => rightCheckedKey.value.indexOf(item) === -1)
rightCheckedKey.value = []
leftCheckedAllKey.value = []
}
rightExpandedKey.value = getTreeKeys(rightTreeData.value)
emitKeys.value = rightExpandedKey.value
}
// 左侧选择
function handleLeftChecked(list, { node, halfCheckedKeys }, checkedKeys, itemSelect) {
leftCheckedKey.value = list
leftHalfCheckedKeys.value = [...new Set([...leftHalfCheckedKeys.value, ...halfCheckedKeys])]
leftCheckedAllKey.value = [...new Set([...leftHalfCheckedKeys.value, ...halfCheckedKeys, ...list])]
const { eventKey } = node
itemSelect(eventKey, !isChecked(checkedKeys, eventKey))
}
// 右侧选择
function handleRightChecked(list, { node, halfCheckedKeys }, checkedKeys, itemSelect) {
let rightTreeKeys = [...new Set(getTreeKeys(rightTreeDataHis.value))]
rightCheckedKeyHis.value = rightCheckedKey.value
rightCheckedKey.value = list
rightCheckedAllKey.value = [...halfCheckedKeys, ...list]
// let arr = sameArr(rightTreeKeys, rightCheckedAllKey.value)
// if (arr.length !== rightTreeKeys.length) {
// rightCheckedAllKey.value = rightCheckedAllKey.value.filter(x => x !== '510000')
// rightCheckedKey.value = rightCheckedKey.value.filter(x => x !== '510000')
// }
const { eventKey } = node
itemSelect(eventKey, isChecked(list, eventKey))
}
</script>
<style lang="less">
.tree-transfer {
:deep {
.ant-transfer-list-body {
max-height: 500px;
overflow: hidden;
overflow-y: auto;
}
}
.ant-transfer-list:first-child {
width: 50%;
flex: none;
}
}
</style>
utils代码
```javascript
// import type { TreeDataItem } from '@/types'
/**
* 深拷贝
* @param data
*/
export function cloneDeep (data) {
return JSON.parse(JSON.stringify(data))
}
/**
* 树转数组
* @param tree
* @param hasChildren
*/
export function treeToList (tree = [], hasChildren = false) {
let queen = []
const out = []
queen = queen.concat(JSON.parse(JSON.stringify(tree)))
while (queen.length) {
const first = queen.shift()
if (first?.children) {
queen = queen.concat(first.children)
if (!hasChildren) delete first.children
}
out.push(first)
}
return out
}
/**
* 数组转树
* @param list
* @param tree
* @param parentId
* @param key
*/
export function listToTree (list = [], tree = [], parentId = 0, key = 'parentId') {
list.forEach(item => {
if (item[key] == parentId) {
const child = {
...item,
children: []
}
listToTree(list, child.children, item.id, key)
if (!child.children?.length) delete child.children
tree.push(child)
}
})
return tree
}
/**
* 获取树节点 key 列表
* @param treeData
*/
export function getTreeKeys (treeData) {
const list = treeToList(treeData)
return list.map(item => item.id)
}
/**
* 循环遍历出最深层子节点,存放在一个数组中
* @param deepList
* @param treeData
*/
export function getDeepList (deepList, treeData) {
treeData?.forEach(item => {
if (item?.children?.length) {
getDeepList(deepList, item.children)
} else {
deepList.push(item.id)
}
})
return deepList
}
/**
* 将后台返回的含有父节点的数组和第一步骤遍历的数组做比较,如果有相同值,将相同值取出来,push到一个新数组中
* @param uniqueArr
* @param arr
*/
export function uniqueTree (uniqueArr, arr) {
const uniqueChild = []
for (const i in arr) {
for (const k in uniqueArr) {
if (uniqueArr[k] === arr[i]) {
uniqueChild.push(uniqueArr[k])
}
}
}
return uniqueChild
}
/**
* 是否选中
* @param selectedKeys
* @param eventKey
*/
export function isChecked (selectedKeys, eventKey = '') {
return selectedKeys.indexOf(eventKey) !== -1
}
/**
* 处理左侧树数据
* @param data
* @param targetKeys
* @param direction
*/
export function handleLeftTreeData (data, targetKeys, direction = 'right') {
data.forEach(item => {
if (direction === 'right') {
item.disabled = targetKeys.includes(item.id)
} else if (direction === 'left') {
if (item.disabled && targetKeys.includes(item.id)) item.disabled = false
}
if (item.children) handleLeftTreeData(item.children, targetKeys, direction)
})
return data
}
/**
* 处理右侧树数据
* @param data
* @param targetKeys
* @param direction
*/
export function handleRightTreeData (data, targetKeys, direction = 'right') {
const list = treeToList(data)
const arr = []
const tree = []
list.forEach(item => {
if (direction === 'right') {
if (targetKeys.includes(item.id)) {
const content = { ...item }
if (content.children) delete content.children
arr.push({ ...content })
}
} else if (direction === 'left') {
if (!targetKeys.includes(item.id)) {
const content = { ...item }
if (content.children) delete content.children
arr.push({ ...content })
}
}
})
listToTree(arr, tree, 0)
return tree
}
/**
* 树数据展平
* @param list
* @param dataSource
*/
export function flatten (list, dataSource) {
list.forEach(item => {
dataSource.push(item)
if (item.children) flatten(item.children, dataSource)
})
return dataSource
}