先上效果图!
直接上代码,需要的自取
主要代码
vue
<template>
<a-transfer
class="tree-transfer"
:data-source="dataSource"
checkStrictly
:target-keys="targetKeys"
:render="(item) => item.title"
:show-select-all="false"
@change="onChange"
:titles="[leftTitle, rightTitle]">
<template #children="{direction, selectedKeys, onItemSelect}">
<template v-if="direction === 'left'">
<a-input-search class="search-input" placeholder="搜索" @change="onLeftSearch" />
<a-tree
v-if="leftTreeData.length"
blockNode
checkable
:auto-expand-parent="leftAutoExpandParent"
:expanded-keys="leftExpandedKeys"
@expand="onLeftExpand"
:tree-data="leftTreeData"
:checked-keys="leftCheckedKey"
@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-input-search class="search-input" placeholder="搜索" @change="onRightSearch" />
<a-tree
v-if="rightTreeData.length"
blockNode
checkable
:auto-expand-parent="rightAutoExpandParent"
:expanded-keys="rightExpandedKeys"
@expand="onRightExpand"
:tree-data="rightTreeData"
:checked-keys="rightCheckedKey"
@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>
</template>
<script lang="ts" setup>
import {ref, watch, onMounted, computed} from 'vue';
import {findPathById, filterTreeArray, cloneDeep, getDeepList, getTreeKeys, treeToList, handleLeftTreeData, uniqueTree, isChecked} from './index';
// Props 类型定义
interface Props {
originTreeData: Array<any>;
editKey: Array<any>;
}
const props = defineProps<Props>();
// Ref 定义
const targetKeys = ref<Array<any>>([]);
const dataSource = ref<Array<any>>([]);
const leftCheckedKey = ref<Array<any>>([]);
const leftHalfCheckedKeys = ref<Array<any>>([]);
const leftCheckedAllKey = ref<Array<any>>([]);
const leftTreeData = ref<Array<any>>([]);
const rightCheckedKey = ref<Array<any>>([]);
const rightCheckedAllKey = ref<Array<any>>([]);
const rightTreeData = ref<Array<any>>([]);
const emitKeys = ref<Array<any>>([]);
const deepList = ref<Array<any>>([]);
const leftSearchValue = ref<string>('');
const leftAutoExpandParent = ref<boolean>(true);
const leftExpandedKeys = ref<Array<any>>([]);
const rightSearchValue = ref<string>('');
const rightAutoExpandParent = ref<boolean>(true);
const rightExpandedKeys = ref<Array<any>>([]);
const leftTitle = computed(() => {
return `待选`;
});
const rightTitle = computed(() => {
return `已选`;
});
// 处理树数据
const processTreeData = () => {
dataSource.value = treeToList(cloneDeep(props.originTreeData), true);
if (props.editKey.length) {
processEditData();
} else {
leftTreeData.value = handleLeftTreeData(cloneDeep(props.originTreeData), leftCheckedKey.value);
}
};
// 处理编辑数据
const processEditData = () => {
leftCheckedAllKey.value = props.editKey;
rightExpandedKeys.value = props.editKey;
targetKeys.value = props.editKey;
rightTreeData.value = findPathById(cloneDeep(props.originTreeData), props.editKey);
getDeepList(deepList.value, props.originTreeData);
leftCheckedKey.value = uniqueTree(props.editKey, deepList.value);
leftHalfCheckedKeys.value = leftCheckedAllKey.value.filter((item) => leftCheckedKey.value.indexOf(item) === -1);
leftTreeData.value = handleLeftTreeData(cloneDeep(props.originTreeData), leftCheckedKey.value);
emitKeys.value = rightExpandedKeys.value;
};
// 穿梭更改
const onChange = (_, direction: string) => {
if (direction === 'right') {
targetKeys.value = leftCheckedAllKey.value;
rightCheckedKey.value = [];
rightTreeData.value = uniqueObject(findPathById(cloneDeep(props.originTreeData), leftCheckedAllKey.value));
leftTreeData.value = handleLeftTreeData(cloneDeep(props.originTreeData), leftCheckedKey.value, 'right');
} else if (direction === 'left') {
let rightTreeData_Temp = filterTreeArray(rightTreeData.value, rightCheckedKey.value, 'id');
rightTreeData_Temp = rightTreeData_Temp.filter((item) => item.children && item.children.length);
rightTreeData.value = rightTreeData_Temp;
leftTreeData.value = handleLeftTreeData(leftTreeData.value, rightCheckedKey.value, 'left');
leftCheckedKey.value = leftCheckedKey.value.filter((item) => rightCheckedKey.value.indexOf(item) === -1);
targetKeys.value = targetKeys.value.filter((item) => rightCheckedKey.value.indexOf(item) === -1);
leftHalfCheckedKeys.value = leftHalfCheckedKeys.value.filter((item) => rightCheckedKey.value.indexOf(item) === -1);
rightCheckedKey.value = [];
}
rightExpandedKeys.value = getTreeKeys(rightTreeData.value);
emitKeys.value = rightExpandedKeys.value;
};
// 左侧选择
const handleLeftChecked = (_, {node, halfCheckedKeys}, checkedKeys, itemSelect) => {
leftCheckedKey.value = _;
leftHalfCheckedKeys.value = [...new Set([...leftHalfCheckedKeys.value, ...halfCheckedKeys])];
leftCheckedAllKey.value = [...new Set([...leftHalfCheckedKeys.value, ...halfCheckedKeys, ..._])];
const {eventKey} = node;
itemSelect(eventKey, !isChecked(checkedKeys, eventKey));
};
// 右侧选择
const handleRightChecked = (_, {node, halfCheckedKeys}, checkedKeys, itemSelect) => {
rightCheckedKey.value = _;
rightCheckedAllKey.value = [...halfCheckedKeys, ..._];
const {eventKey} = node;
itemSelect(eventKey, isChecked(_, eventKey));
};
// 唯一对象处理
const uniqueObject = (arr: Array<any>) => {
let obj: {[key: string]: any} = {};
arr.forEach((item, index) => {
if (obj.hasOwnProperty(item.id)) {
arr.splice(index, 1);
} else {
obj[item.id] = item.name;
}
});
return arr;
};
// 获取父节点键
const getParentKey = (key: any, tree: Array<any>) => {
let parentKey: any;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey;
};
// 左侧展开
const onLeftExpand = (leftExpandedKeysBase: Array<any>) => {
leftExpandedKeys.value = leftExpandedKeysBase;
leftAutoExpandParent.value = false;
};
// 左侧搜索
const onLeftSearch = (e: Event) => {
const value = (e.target as HTMLInputElement).value;
const leftExpandedKeysBase = treeToList(props.originTreeData, true)
.map((item) => {
if (item.title.includes(value)) {
return getParentKey(item.key, props.originTreeData);
}
return null;
})
.filter((item, i, self) => item && self.indexOf(item) === i);
leftExpandedKeys.value = leftExpandedKeysBase;
leftSearchValue.value = value;
leftAutoExpandParent.value = true;
};
// 右侧展开
const onRightExpand = (rightExpandedKeysBase: Array<any>) => {
rightExpandedKeys.value = rightExpandedKeysBase;
rightAutoExpandParent.value = false;
};
// 右侧搜索
const onRightSearch = (e: Event) => {
const value = (e.target as HTMLInputElement).value;
const rightExpandedKeysBase = treeToList(rightTreeData.value, true)
.map((item) => {
if (item.title.includes(value)) {
return getParentKey(item.key, rightTreeData.value);
}
return null;
})
.filter((item, i, self) => item && self.indexOf(item) === i);
rightExpandedKeys.value = rightExpandedKeysBase;
rightSearchValue.value = value;
rightAutoExpandParent.value = true;
};
// 监视props变化
watch(() => props.originTreeData, processTreeData, {deep: true});
watch(() => props.editKey, processTreeData, {deep: true});
// 组件创建时调用
onMounted(() => {
processTreeData();
// 将左侧树数据展开
setTimeout(() => {
leftExpandedKeys.value = treeToList(props.originTreeData, true)
.map((item) => {
if (item.children && item.children.length) {
return item.key;
}
return null;
})
.filter((item, i, self) => item && self.indexOf(item) === i);
});
});
</script>
<style lang="less" scoped>
.search-input {
width: 90%;
margin: 12px;
}
:deep(.ant-tree) {
height: 500px;
overflow-y: auto;
}
:deep(.ant-transfer-list-header-selected) {
span {
display: none;
}
}
:deep(.ant-transfer-list-header-title) {
display: contents !important;
}
</style>
index.ts
ts
/**
* 深拷贝
* @param data
*/
export function cloneDeep(data) {
return JSON.parse(JSON.stringify(data));
}
/**
* 通过id获取树的完整路径
* @param tree 树型数据
* @param ids id集合
* @param keyName 关键字
*/
export function findPathById(tree, ids, keyName = 'id') {
const result = [];
for (const node of tree) {
if (ids.includes(node[keyName])) {
result.push(node);
}
if (node.children) {
const childResults = findPathById(node.children, ids);
if (childResults.length > 0) {
result.push({...node, children: childResults});
}
}
}
return result;
}
/**
* 通过id过滤树的id所在子节点
* @param tree 树型数据
* @param ids id集合
* @param keyName 关键字
*/
export function filterTreeArray(tree, ids, keyName = 'id') {
const newTree = tree
.filter((item) => {
return ids.indexOf(item[keyName]) == -1;
})
.map((item) => {
item = Object.assign({}, item);
if (item.children) {
item.children = filterTreeArray(item.children, ids);
}
return item;
});
return newTree;
}
/**
* 树转数组
* @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
* @param key
*/
export function getTreeKeys(treeData, key = 'key') {
const list = treeToList(treeData);
return list.map((item) => item[key]);
}
/**
* 循环遍历出最深层子节点,存放在一个数组中
* @param deepList
* @param treeData
*/
export function getDeepList(deepList, treeData) {
treeData?.forEach((item) => {
if (item?.children?.length) {
getDeepList(deepList, item.children);
} else {
deepList.push(item.key);
}
});
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.key);
} else if (direction === 'left') {
if (item.disabled && targetKeys.includes(item.key)) 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.key)) {
const content = {...item};
if (content.children) delete content.children;
arr.push({...content});
}
} else if (direction === 'left') {
if (!targetKeys.includes(item.key)) {
const content = {...item};
if (content.children) delete content.children;
arr.push({...content});
}
}
});
listToTree(arr, tree, 0);
return tree;
}