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;
}
相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen7 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒8 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端