Ant Design Vue 实现左右树穿梭框

707 阅读3分钟

需求是这样的,产品想要更直观的展示已选的地区数据,要求弄两个树来展示,不用往下拉就能看到已选的地区。

交互效果说明,全选、省份和城市可以选中和取消,全选选中添加要把全国及以下所有层级数据选中添加,全选选中取消要把全国及以下所有层级数据选中取消;省份选中添加要把省份及以下所有层级数据选中添加,省份选中取消要把省份及以下所有层级数据选中取消。

效果图

image.png

页面布局

  <a-transfer
    :data-source="dataSource"
    :list-style="{
      width: '45%',
      height: '420px',
      marginTop: '20px',
      marginBottom: '20px'
    }"
    :render="item => item.title"
    :show-select-all="false"
    :target-keys="targetKeys"
    @change="onChangeTransfer"
    >
    <template
      slot="children"
      slot-scope="{
        props: { direction, selectedKeys },
        on: { itemSelect, itemSelectAll },
      }"
    >
      <div v-if="direction === 'left'">
        <a-checkbox
          :checked="checkedLeftAll"
          @change="(e) => {
            onChangeLeftAll(e, [...selectedKeys], itemSelectAll, itemSelect)
          }
          "
        >
          全选
        </a-checkbox>
        <a-tree
          class="transfer-tree"
          blockNode
          checkable
          :checked-keys="leftCheckedKey"
          :treeData="leftTreeData"
          @check="
            (_, props) => {
              handleLeftChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
            }
          "
        />
      </div>
      <div v-else-if="direction==='right'">
        <a-checkbox
          :checked="checkedRightAll"
          @change="(e) => {
            onChangeRightAll(e, [...selectedKeys], itemSelectAll, itemSelect)
          }
          "
        >
          全选
        </a-checkbox>
        <a-tree
          id="rightTree"
          class="transfer-tree"
          checkable
          :auto-expand-parent="autoExpandParent"
          :expanded-keys="rightExpandedKey"
          :checked-keys="rightCheckedKey"
          :treeData="rightTreeData"
          @expand="onExpand"
          @check="
            (_, props) => {
              handleRightChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
            }
          "
        />
      </div>
    </template>
  </a-transfer>

js设置

<script lang="ts">
import {
  cloneDeep,
  flatten,
  getDeepList,
  getTreeKeys,
  handleLeftTreeData,
  handleRightTreeData,
  isChecked,
  uniqueTree
} from '@/utils/tree'

export default class AreaSelect extends Vue {
  title = '地区'

  treeData: any = [] // tree地区数据
  dataSource: any[] = [] // 穿梭框数据
  targetKeys: any = [] // 穿梭框已选的key

  leftCheckedKey: any = [] // 左侧树选中 key 集合
  leftHalfCheckedKeys: any = [] // 左侧半选集合
  leftCheckedAllKey: any = [] // 左侧树选中的 key 集合,包括半选与全选
  leftTreeData: any = [] // 左侧树

  rightCheckedKey: any = [] // 左侧树选中 key 集合
  rightHalfCheckedKeys: any = [] // 左侧半选集合
  rightCheckedAllKey: any = [] // 左侧树选中的 key 集合,包括半选与全选
  rightExpandedKey: any = [] // 右侧展开数集合
  rightTreeData: any = [] // 左侧树

  emitKeys: any = [] // 往父级组件传递的数据
  deepList: any = [] // 深层列表
  editKey: any = [] // 编辑的key,areaCode
  areaCodes: any = '' // 编辑回传的地区code,全国'-1'或地区areaCode

  checkedLeftAll: any = false // 左侧全选
  checkedRightAll: any = false // 右侧全选

  autoExpandParent: any = true

  onExpand (expandedKeys: any) {
    this.rightExpandedKey = expandedKeys
    this.autoExpandParent = false
  }

  isChecked (selectedKeys: any, eventKey: any) {
    return selectedKeys.indexOf(eventKey) !== -1
  }

  onChangeLeftAll (
    e: any,
    checkedKeys: string[],
    itemSelectAll: (arg0: any, arg1: boolean) => void) {
    this.checkedLeftAll = e.target.checked
    let diffKeys: string[] = []
    if (this.checkedLeftAll) {
      this.leftCheckedAllKey = getTreeKeys(this.leftTreeData)
      this.leftCheckedKey = this.leftCheckedAllKey
    } else {
      this.leftCheckedAllKey = []
      this.leftCheckedKey = []
      diffKeys = []
    }
    diffKeys = getTreeKeys(this.leftTreeData)
    itemSelectAll(diffKeys, e.target.checked)
  }

  onChangeRightAll (e: any,
    checkedKeys: string[],
    itemSelectAll: (arg0: any, arg1: boolean) => void) {
    this.checkedRightAll = e.target.checked
    let diffKeys: string[] = []
    if (this.checkedRightAll) {
      diffKeys = getTreeKeys(this.rightTreeData)
      this.rightCheckedKey = diffKeys
    } else {
      this.rightCheckedKey = []
      diffKeys = []
    }
    itemSelectAll(diffKeys, e.target.checked)
  }

  // 左侧选择
  handleLeftChecked (_: string[], {
    node,
    halfCheckedKeys
  }: any, checkedKeys: any, itemSelect: (arg0: any, arg1: boolean) => void) {
    this.leftCheckedKey = _
    this.leftHalfCheckedKeys = [...new Set([...this.leftHalfCheckedKeys, ...halfCheckedKeys])]
    this.leftCheckedAllKey = [...new Set([...this.leftHalfCheckedKeys, ...halfCheckedKeys, ..._])]
    const { eventKey } = node
    itemSelect(eventKey, !this.isChecked(checkedKeys, eventKey))
  }

  // 右侧选择
  handleRightChecked (_: string[], {
    node,
    halfCheckedKeys
  }: any, checkedKeys: any, itemSelect: (arg0: any, arg1: boolean) => void) {
    this.rightCheckedKey = _
    this.rightCheckedAllKey = [...new Set([...halfCheckedKeys, ..._])]
    const { eventKey } = node
    itemSelect(eventKey, this.isChecked(_, eventKey))
  }

  // 选项在两栏之间转移时的回调函数
  onChangeTransfer (targetKeys: any, direction: string) {
    if (direction === 'right') {
      this.targetKeys = this.leftCheckedAllKey
      this.rightCheckedKey = []
      this.rightTreeData = handleRightTreeData(cloneDeep(this.treeData), this.leftCheckedAllKey, 'right')
      this.leftTreeData = handleLeftTreeData(cloneDeep(this.treeData), this.leftCheckedKey, 'right')
      this.checkedLeftAll = false
    } else if (direction === 'left') {
      this.rightTreeData = handleRightTreeData(this.rightTreeData, this.rightCheckedKey, 'left')
      this.leftTreeData = handleLeftTreeData(this.leftTreeData, this.rightCheckedKey, 'left')
      this.leftCheckedKey = this.leftCheckedKey.filter((item: any) => this.rightCheckedKey.indexOf(item) === -1)
      this.targetKeys = this.targetKeys.filter((item: any) => this.rightCheckedKey.indexOf(item) === -1)
      this.leftHalfCheckedKeys = this.leftHalfCheckedKeys.filter((item: any) => this.rightCheckedKey.indexOf(item) === -1)
      this.rightCheckedKey = []
      this.checkedRightAll = false
    }
    this.rightExpandedKey = getTreeKeys(this.rightTreeData)
    this.emitKeys = this.rightExpandedKey
  }
  
  // 查看详情反显已选地区数据
  detail (record: any) {
    this.areaCodes = record.areaCodes || ''
    this.queryAreaList()
  }

  // 获取tree地区数据
  queryAreaList () {
    this.dataSource = []
    // 这里是测试数据,实际开发中这里是调接口请求数据
    this.treeData = {
      id: 16004,
      key: '110000',
      title: '北京市',
      level: 1,
      parentId: 0,
      areaCode: '110000',
      children: [{
        id: 16038,
        key: '110100',
        title: '市辖区',
        level: 2,
        parentId: 16004,
        areaCode: '110100'
      },
      {
        id: 16039,
        key: '110200',
        title: '县',
        level: 2,
        parentId: 16004,
        areaCode: '110200'
      }]
    }
    this.getAreaCodes(this.treeData)
    this.processTreeData()
  }
 
  // 根据实际情况处理数据
  getAreaCodes (data: any) {
    if (this.areaCodes !== '-1') {
      this.editKey = this.areaCodes.split(',')
    } else if (this.areaCodes === '-1') {
      this.editKey = areaCodes // 全国的地区code
    } else {
      this.editKey = []
    }
  }

  // 处理树数据
  processTreeData () {
    flatten(cloneDeep(this.treeData), this.dataSource)
    if (this.editKey.length) {
      this.processEditData()
    } else {
      this.leftTreeData = handleLeftTreeData(cloneDeep(this.treeData), this.leftCheckedKey)
    }
  }

  // 处理编辑数据
  processEditData () {
    this.leftCheckedAllKey = this.editKey
    this.rightExpandedKey = this.editKey
    this.targetKeys = this.editKey
    this.rightTreeData = handleRightTreeData(cloneDeep(this.treeData), this.editKey)

    getDeepList(this.deepList, this.treeData)
    this.leftCheckedKey = uniqueTree(this.editKey, this.deepList)
    this.leftHalfCheckedKeys = this.leftCheckedAllKey.filter((item: any) => this.leftCheckedKey.indexOf(item) === -1)
    if (this.areaCodes === '-1') {
      this.leftCheckedKey = [...this.leftCheckedAllKey, ...this.leftHalfCheckedKeys]
    } else {
      this.leftCheckedKey = this.leftCheckedAllKey
    }
    this.leftTreeData = handleLeftTreeData(cloneDeep(this.treeData), this.leftCheckedKey)

    this.emitKeys = this.rightExpandedKey
  }

}
</script>

css样式设置

  .transfer-tree {
    height: 300px;
    overflow-y: auto
  }

utils/tree.ts


/**
 * 根据parentId生成树结构
 * @param array 要操作的数据
 * @param childrenWrapName 包裹子数组的属性值
 * @param isLeaf 是否为最终项(即无子数组了)
 * @returns {boolean} 成功 push 返回 数组,不处理返回 []
 */
export function transferArrayToTree (data: any[], childrenWrapName = 'children', isLeafName = 'isLeaf') {
  let idArray: any[] = []
  let dealData: any[] = []
  let children = childrenWrapName || 'children'
  let isLeaf = isLeafName || 'isLeaf'
  // 获取所有id
  idArray = data.map(item => {
    return item.id
  })

  if (dealData) {
    // 如果允许不存在的parentId升为一级
    dealData = data.map(item => {
      if (!idArray.includes(item.parentId)) {
        item.parentId = 0
      }
      return item
    })
  } else {
    dealData = data
  }

  // 对源数据深度克隆
  let cloneData: any[] = JSON.parse(JSON.stringify(dealData))
  if (cloneData) {
    return cloneData.filter(father => {
      // 返回每一项的子级数组
      let branchArr = cloneData.filter(child => {
        if (father.id === child.parentId) {
          return child
        }
      })
      // 如果存在子级,则给父级添加一个children属性,并赋值
      branchArr.length > 0 ? father[children] = branchArr : father[isLeaf] = true
      // 返回第一层
      return father.parentId === 0
    })
  } else {
    return []
  }
}

// 获取指定层级需要展开的树节点
export function getTreeExpandedKeysByLevel (arr: any[] = [], level = 1) {
  let result: any[] = []
  let func = (list: any[], number: number) => {
    if (number <= level) {
      list.forEach(item => {
        result.push(item.key)
        func(item.children || [], number + 1)
      })
    }
  }
  func(arr, 1)
  return result
}

export interface TreeDataItem {
  key: string;
  title: string;
  disabled?: boolean;
  children?: TreeDataItem[];
  [key: string]: any;
}

/**
 * 深拷贝
 * @param data
 */
export function cloneDeep<T> (data: T): T {
  return JSON.parse(JSON.stringify(data))
}

/**
 * 树转数组
 * @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 = 0, key = 'parentId'): TreeDataItem[] {
  list.forEach(item => {
    if (item[key] === parentId) {
      const child: TreeDataItem = {
        ...item,
        children: []
      }
      listToTree(list, child.children as TreeDataItem[], item.id, 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 deepList
 * @param treeData
 */
export function getDeepList (deepList: string[], treeData: TreeDataItem[]): string[] {
  treeData.forEach(item => {
    if (item.children && item.children.length) {
      getDeepList(deepList, item.children)
    } else {
      deepList.push(item.key)
    }
  })
  return deepList
}

/**
 * 将后台返回的含有父节点的数组和第一步骤遍历的数组做比较,如果有相同值,将相同值取出来,push到一个新数组中
 * @param uniqueArr
 * @param arr
 */
export function uniqueTree (uniqueArr: string[], arr: string[]): string[] {
  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: string[], eventKey: string): boolean {
  return selectedKeys.indexOf(eventKey) !== -1
}

/**
 * 处理左侧树数据
 * @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[] {
  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
}

/**
 * 树数据展平
 * @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
}

参考:blog.csdn.net/qq_35844177…