antdvue+tree+transfer+vue3 实现树形带搜索穿梭框

先上效果图!

直接上代码,需要的自取

主要代码

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;
}
相关推荐
windyrain10 分钟前
ant design pro 模版简化工具
前端·react.js·ant design
浪遏17 分钟前
我的远程实习(六) | 一个demo讲清Auth.js国外平台登录鉴权👈|nextjs
前端·面试·next.js
GISer_Jing40 分钟前
React-Markdown详解
前端·react.js·前端框架
太阳花ˉ41 分钟前
React(九)React Hooks
前端·react.js
拉不动的猪2 小时前
vue与react的简单问答
前端·javascript·面试
污斑兔2 小时前
如何在CSS中创建从左上角到右下角的渐变边框
前端
星空寻流年2 小时前
css之定位学习
前端·css·学习
旭久3 小时前
react+antd封装一个可回车自定义option的select并且与某些内容相互禁用
前端·javascript·react.js
是纽扣也是烤奶3 小时前
关于React Redux
前端