在工作中难免会碰到各种奇葩的问题。最近遇到一个需求,要实现树左右树穿梭的功能。对于有经验的小伙伴这个需求不难,而且Ant Design Vue 官网中有穿梭框 Transfer 组件。但提供的示例是左侧是树,右侧则是平铺列表。只能在其基础上进行修改实现了在。。。。在但是在工作当中后台给的数据往往满足不了我们的要求(理想很丰满,现实很骨感),这时就要让我们勤劳的小手动起来了,不再是ctrl+c和ctrl+v了。
效果图
1、先创建一个父组件,文件名看自己喜好
<template>
<div class="about transfer-wrapper">
<transfer ref="transferRef" class="tree" :tree-data="treeData" :edit-key="editKey" />
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import transfer from "@/components/transfer-plugin/transfer.vue";
let editKey = ref([]);
const treeData = ref([
{
key: "1002",
title: "海上平台",
children: [
{
key: "100201",
title: "航母",
children: [
{
key: "10020101",
mblb: "100201",
title: "尼米兹号",
},
{
key: "10020102",
mblb: "100201",
title: "阿波罗号",
},
{
key: "10020103",
mblb: "100201",
title: "里根号",
},
],
},
{
key: "100202",
title: "舰队",
children: [
{
key: "10020201",
mblb: "100202",
title: "罗斯福驱逐舰",
},
{
key: "10020202",
mblb: "100202",
title: "奥巴马驱逐舰",
},
{
key: "10020203",
mblb: "100202",
title: "拜登驱逐舰",
},
],
},
{
key: "100203",
title: "潜艇"
},
{
key: "100204",
title: "蛟龙号"
}
],
},
{
key: "1003",
title: "空中平台",
children: [
{
key: "100301",
title: "战机",
children: [
{
key: "10030101",
mblb: "100301",
title: "米格-21",
},
{
key: "10030102",
mblb: "100301",
title: "歼-20",
},
{
key: "10030103",
mblb: "100301",
title: "f-15",
},
],
},
{
key: "100302",
title: "无人机",
children: [
{
key: "10030201",
mblb: "100302",
title: "座山雕",
},
{
key: "10030202",
mblb: "100302",
title: "夜鹰号",
},
{
key: "10030203",
mblb: "100302",
title: "猫头鹰号",
},
],
},
],
},
{
key: "1004",
title: "陆地平台",
children: [
{
key: "100401",
title: "坦克",
children: [
{
key: "10040101",
mblb: "100401",
title: "700-MinT",
},
{
key: "10040102",
mblb: "100401",
title: "T26-轻型坦克",
},
{
key: "10040103",
mblb: "100401",
title: "T90",
},
],
},
{
key: "100402",
title: "装甲车",
children: [
{
key: "10040201",
mblb: "100402",
title: "比亚迪",
},
{
key: "10040202",
mblb: "100402",
title: "吉普",
},
{
key: "10040203",
mblb: "100402",
title: "猛虎",
},
],
},
],
},
]);
</script>
<style>
@media (min-width: 1024px) {
.about {
/* min-height: 100vh; */
/* display: flex; */
align-items: center;
}
}
</style>
2、创建穿梭框组件的文件名transfer.vue
<template>
<div>
<a-transfer
v-model:target-keys="targetKeys"
class="tree-transfer"
:data-source="dataSource"
:render="(item) => item.title"
:show-select-all="false"
@change="onChange"
:titles="['source', 'target']"
>
<template #children="{ direction, selectedKeys, onItemSelect }">
<template v-if="direction === 'left'">
<a-tree
block-node
checkable
default-expand-all
:checked-keys="leftCheckedKey"
:tree-data="leftTreeData"
@check="
(_, props) => {
handleLeftChecked(
_,
props,
[...selectedKeys, ...targetKeys],
onItemSelect
);
}
"
/>
</template>
<template v-if="direction === 'right'">
<a-tree
block-node
checkable
default-expand-all
:checked-keys="rightCheckedKey"
:tree-data="rightTreeData"
@check="
(_, props) => {
handleRightChecked(
_,
props,
[...selectedKeys, ...targetKeys],
onItemSelect
);
}
"
/>
</template>
</template>
</a-transfer>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch, onMounted } from "vue";
import type { TransferProps, TreeProps } from "ant-design-vue";
import type { title } from "process";
import {
cloneDeep,
addParentIdTreeData,
isChecked,
flatten,
handleLeftTreeData,
handleRightTreeData,
getTreeKeys
} from "./index.ts";
const props = defineProps({
/** 树数据 */
treeData: {
type: Array as PropType<TreeDataItem[]>,
default: () => [],
},
/** 编辑 key */
editKey: {
type: Array as PropType<string[]>,
default: () => [],
},
});
const tData: TransferProps["dataSource"] = [];
const transferDataSource: TransferProps["dataSource"] = [];
const dataSource = ref(transferDataSource);
const targetKeys = ref<string[]>([]);
let leftTreeData = ref<any>([]); // 左侧树
let leftCheckedKey = ref([]); // 左侧树选中key的集合
let leftHalfCheckedKeys = ref([]); // 左侧半选集合
let leftCheckedAllKey = ref([]); // 左侧树选中的key集合,包括半选和全选
let rightTreeData = ref<any>([]); // 右侧树
let rightCheckedKey = ref([]); // 右侧树选中key的集合
let rightCheckedAllKey = ref([]); // 右侧树选中的key集合,包括半选和全选
let rightExpandedKey = ref([]); // 右侧展开树集合
let emitKeys = ref([]); // 往父级组件传递的数据
let deepList = ref([]); // 深层列表
// 处理树数据
function processTreeData() {
flatten(addParentIdTreeData(cloneDeep(props.treeData)), dataSource.value);
if (props.editKey.length) {
processEditData();
} else {
leftTreeData.value = handleLeftTreeData(
addParentIdTreeData(cloneDeep(props.treeData)),
leftCheckedKey.value
);
}
}
/** 处理编辑数据 */
function processEditData(): void {
debugger;
}
// 穿梭更改
function onChange(targetKeys: string[], direction: string) {
console.log(targetKeys, direction, "穿梭更改");
if (direction === "right") {
targetKeys.value = leftCheckedAllKey.value;
rightCheckedKey.value = [];
rightTreeData.value = handleRightTreeData(
addParentIdTreeData(cloneDeep(props.treeData)),
leftCheckedAllKey.value,
"right"
);
leftTreeData.value = handleLeftTreeData(addParentIdTreeData(cloneDeep(props.treeData)), leftCheckedKey.value, 'right')
console.log(rightTreeData.value,'rightTreeData.value');
} else if (direction === "left") {
rightTreeData.value = handleRightTreeData(rightTreeData.value, rightCheckedKey.value, 'left')
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 = []
}
rightExpandedKey.value = getTreeKeys(rightTreeData.value)
emitKeys.value = rightExpandedKey.value
}
// 左侧选择
function handleLeftChecked(
_: string[],
{ node, halfCheckedKeys }: any,
checkedKeys: any,
itemSelect: (arg0: any, arg1: boolean) => void
): void {
console.log(
_,
{ 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));
}
// 右侧选择
function handleRightChecked(_: string[], { node, halfCheckedKeys }: any, checkedKeys: any, itemSelect: (arg0: any, arg1: boolean) => void): void {
rightCheckedKey.value = _
rightCheckedAllKey.value = [...halfCheckedKeys, ..._]
const { eventKey } = node
itemSelect(eventKey, isChecked(_, eventKey))
}
onMounted(() => {
processTreeData();
});
</script>
<style scoped>
.tree-transfer .ant-transfer-list:first-child {
width: 50%;
flex: none;
}
</style>
3、创建index.ts文件,放一些工具类
/**
* 深拷贝
* @param data
*/
export function cloneDeep(data) {
return JSON.parse(JSON.stringify(data));
}
/**
* 给每一级添加parentId
* parentId
*/
export function addParentIdTreeData(data: any) {
data.forEach((item) => {
if(item.key.length === 4) {
item.parentId = Number(0)
}else if(item.key.length === 6) {
item.parentId = Number(item.key.slice(0,4))
}else {
item.parentId = Number(item.mblb)
}
if (item.children) addParentIdTreeData(item.children)
});
// console.log(data,'添加pid');
return data;
}
/**
* 是否选中
* @param selectedKeys
* @param eventKey
*/
export function isChecked (selectedKeys: string[], eventKey: string): boolean {
return selectedKeys.indexOf(eventKey) !== -1
}
/**
* 树数据展平
* @param list
* @param dataSource
*/
export function flatten (list: TreeDataItem[], dataSource: TreeDataItem[]): TreeDataItem[] {
list.forEach(item => {
dataSource.push(item)
if (item.children) flatten(item.children, dataSource)
})
return dataSource
}
/**
* 树转数组
* @param tree
* @param hasChildren
*/
export function treeToList (tree: TreeDataItem[], hasChildren = false): TreeDataItem[] {
let queen: TreeDataItem[] = []
const out: TreeDataItem[] = []
queen = queen.concat(JSON.parse(JSON.stringify(tree)))
while (queen.length) {
const first = queen.shift() as TreeDataItem
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: TreeDataItem[], tree: TreeDataItem[], parentId, key = 'parentId'): TreeDataItem[] {
list.forEach(item => {
if (item[key] === parentId) {
const child: TreeDataItem = {
...item,
children: []
}
listToTree(list, child.children as TreeDataItem[], Number(item.key), key)
if (!child.children?.length) delete child.children
tree.push(child)
}
})
return tree
}
/**
* 获取树节点 key 列表
* @param treeData
*/
export function getTreeKeys (treeData: TreeDataItem[]): string[] {
const list = treeToList(treeData)
return list.map(item => item.key)
}
/**
* 处理左侧树数据
* @param data
* @param targetKeys
* @param direction
*/
export function handleLeftTreeData (data: TreeDataItem[], targetKeys: string[], direction = 'right'): TreeDataItem[] {
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: TreeDataItem[], targetKeys: string[], direction = 'right'): TreeDataItem[] {
console.log(data, targetKeys, direction, `处理${direction}侧树数据`);
const list = treeToList(data)
const arr: TreeDataItem[] = []
const tree: TreeDataItem[] = []
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
}