最近碰见一个需求,要实现权限树左右树穿梭的功能。Ant Design Vue 官网中有穿梭框 Transfer 组件,但提供的示例是左侧是树,右侧则是平铺列表。只能在其基础上进行修改实现了。

效果图

image-20210521092503598

Options API 实现

新建文件 options-transfer.vue

<template>
  <a-transfer
    :data-source="dataSource"
    :target-keys="targetKeys"
    :render="item => item.title"
    :show-select-all="false"
    @change="onChange"
  >
    <template #children="{ direction, selectedKeys, onItemSelect }">
      <template v-if="direction==='left'">
        <a-tree
          v-if="leftTreeData.length"
          blockNode
          checkable
          defaultExpandAll
          :tree-data="leftTreeData"
          :checked-keys="leftCheckedKey"
          @check="(_, props) => { handleLeftChecked(_, props, [...selectedKeys, ...targetKeys], onItemSelect) }"
        />
        <a-empty v-else>
          <template #description>暂无数据</template>
        </a-empty>
      </template>
      <template v-else-if="direction==='right'">
        <a-tree
          v-if="rightTreeData.length"
          blockNode
          checkable
          defaultExpandAll
          :tree-data="rightTreeData"
          v-model:checked-keys="rightCheckedKey"
          v-model:expanded-keys="rightExpandedKey"
          @check="(_, props) => { handleRightChecked(_, props, [...selectedKeys, ...targetKeys], onItemSelect) }"
        />
        <a-empty v-else>
          <template #description>暂无数据</template>
        </a-empty>
      </template>
    </template>
  </a-transfer>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import {
  cloneDeep,
  flatten,
  getDeepList,
  getTreeKeys,
  handleLeftTreeData,
  handleRightTreeData,
  isChecked,
  uniqueTree
} from '@/utils'
import type { TreeDataItem } from '@/types'

export default defineComponent({
  name: 'OptionsTransfer',
  props: {
    /** 树数据 */
    treeData: {
      type: Array as PropType<TreeDataItem[]>,
      default: () => []
    },
    /** 编辑 key */
    editKey: {
      type: Array as PropType<string[]>,
      default: () => []
    }
  },
  data () {
    return {
      targetKeys: [] as string[], // 显示在右侧框数据的 key 集合
      dataSource: [] as TreeDataItem[], // 数据源,其中的数据将会被渲染到左边一栏

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

      rightCheckedKey: [] as string[], // 右侧树选中集合
      rightCheckedAllKey: [] as string[], // 右侧树选中集合,包括半选与全选
      rightExpandedKey: [] as string[], // 右侧展开数集合
      rightTreeData: [] as TreeDataItem[], // 右侧树

      emitKeys: [] as string[], // 往父级组件传递的数据

      deepList: [] as string[] // 深层列表
    }
  },
  watch: {
    treeData: {
      deep: true,
      handler (val) {
        this.processTreeData()
      }
    },
    editKey: {
      deep: true,
      handler (val) {
        this.processTreeData()
      }
    }
  },
  created () {
    this.processTreeData()
  },
  methods: {
    // 处理树数据
    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 => this.leftCheckedKey.indexOf(item) === -1)
      this.leftTreeData = handleLeftTreeData(cloneDeep(this.treeData), this.leftCheckedKey)

      this.emitKeys = this.rightExpandedKey
    },
    // 穿梭更改
    onChange (targetKeys: string[], 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')
      } 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 => this.rightCheckedKey.indexOf(item) === -1)
        this.targetKeys = this.targetKeys.filter(item => this.rightCheckedKey.indexOf(item) === -1)
        this.leftHalfCheckedKeys = this.leftHalfCheckedKeys.filter(item => this.rightCheckedKey.indexOf(item) === -1)
        this.rightCheckedKey = []
      }
      this.rightExpandedKey = getTreeKeys(this.rightTreeData)
      this.emitKeys = this.rightExpandedKey
    },
    // 左侧选择
    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, !isChecked(checkedKeys, eventKey))
    },
    // 右侧选择
    handleRightChecked (_: string[], {
      node,
      halfCheckedKeys
    }: any, checkedKeys: any, itemSelect: (arg0: any, arg1: boolean) => void) {
      this.rightCheckedKey = _
      this.rightCheckedAllKey = [...halfCheckedKeys, ..._]
      const { eventKey } = node
      itemSelect(eventKey, isChecked(_, eventKey))
    }
  }
})
</script>

<style scoped lang="less">
.ant-transfer {
  ::v-deep(.ant-transfer-list) {
    width: 300px;
  }
}
</style>

Composition API 实现

新建 use-tree-transfer.ts 文件

import { onMounted, reactive, watch } from 'vue'
import {
  cloneDeep,
  flatten,
  getDeepList,
  getTreeKeys,
  handleLeftTreeData,
  handleRightTreeData,
  isChecked,
  uniqueTree
} from '@/utils'
import type { TreeDataItem } from '@/types'

interface Props {
  /** 树数据 */
  treeData: TreeDataItem[];
  /** 编辑 key */
  editKey: string[];
}

interface State {
  /** 显示在右侧框数据的 key 集合 */
  targetKeys: string[];
  /** 数据源,其中的数据将会被渲染到左边一栏 */
  dataSource: TreeDataItem[];
  /** 左侧树选中 key 集合 */
  leftCheckedKey: string[];
  /** 左侧半选集合 */
  leftHalfCheckedKeys: string[];
  /** 左侧树选中的 key 集合,包括半选与全选 */
  leftCheckedAllKey: string[];
  /** 左侧树 */
  leftTreeData: TreeDataItem[];
  /** 右侧树选中集合 */
  rightCheckedKey: string[];
  /** 右侧树选中集合,包括半选与全选 */
  rightCheckedAllKey: string[];
  /** 右侧展开数集合 */
  rightExpandedKey: string[];
  /** 右侧树 */
  rightTreeData: TreeDataItem[];
  /** 往父级组件传递的数据 */
  emitKeys: string[];
  /** 深层列表 */
  deepList: string[];
}

interface UseTreeTransfer {
  state: State;
  /** 穿梭更改 */
  onChange: (targetKeys: string[], direction: string) => void;
  /** 左侧选择 */
  handleLeftChecked: (_: string[], { node, halfCheckedKeys }: any, checkedKeys: any, itemSelect: (arg0: any, arg1: boolean) => void) => void;
  /** 右侧选择 */
  handleRightChecked: (_: string[], { node, halfCheckedKeys }: any, checkedKeys: any, itemSelect: (arg0: any, arg1: boolean) => void) => void;
}

export function useTreeTransfer (props: Props): UseTreeTransfer {
  const state = reactive<State>({
    targetKeys: [],
    dataSource: [],
    leftCheckedKey: [],
    leftHalfCheckedKeys: [],
    leftCheckedAllKey: [],
    leftTreeData: [],
    rightCheckedKey: [],
    rightCheckedAllKey: [],
    rightExpandedKey: [],
    rightTreeData: [],
    emitKeys: [],
    deepList: []
  })

  watch(props, () => {
    processEditData()
  })

  onMounted(() => {
    processTreeData()
  })

  /** 处理树数据 */
  function processTreeData (): void {
    flatten(cloneDeep(props.treeData), state.dataSource)
    if (props.editKey.length) {
      processEditData()
    } else {
      state.leftTreeData = handleLeftTreeData(cloneDeep(props.treeData), state.leftCheckedKey)
    }
  }

  /** 处理编辑数据 */
  function processEditData (): void {
    state.leftCheckedAllKey = props.editKey
    state.rightExpandedKey = props.editKey
    state.targetKeys = props.editKey
    state.rightTreeData = handleRightTreeData(cloneDeep(props.treeData), props.editKey)

    getDeepList(state.deepList, props.treeData)

    state.leftCheckedKey = uniqueTree(props.editKey, state.deepList)
    state.leftHalfCheckedKeys = state.leftCheckedAllKey.filter(item => state.leftCheckedKey.indexOf(item) === -1)
    state.leftTreeData = handleLeftTreeData(cloneDeep(props.treeData), state.leftCheckedKey)

    state.emitKeys = state.rightExpandedKey
  }

  /** 穿梭更改 */
  function onChange (targetKeys: string[], direction: string) {
    if (direction === 'right') {
      state.targetKeys = state.leftCheckedAllKey
      state.rightCheckedKey = []
      state.rightTreeData = handleRightTreeData(cloneDeep(props.treeData), state.leftCheckedAllKey, 'right')
      state.leftTreeData = handleLeftTreeData(cloneDeep(props.treeData), state.leftCheckedKey, 'right')
    } else if (direction === 'left') {
      state.rightTreeData = handleRightTreeData(state.rightTreeData, state.rightCheckedKey, 'left')
      state.leftTreeData = handleLeftTreeData(state.leftTreeData, state.rightCheckedKey, 'left')
      state.leftCheckedKey = state.leftCheckedKey.filter(item => state.rightCheckedKey.indexOf(item) === -1)
      state.targetKeys = state.targetKeys.filter(item => state.rightCheckedKey.indexOf(item) === -1)
      state.leftHalfCheckedKeys = state.leftHalfCheckedKeys.filter(item => state.rightCheckedKey.indexOf(item) === -1)
      state.rightCheckedKey = []
    }
    state.rightExpandedKey = getTreeKeys(state.rightTreeData)
    state.emitKeys = state.rightExpandedKey
  }

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

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

  return {
    state,

    onChange,
    handleLeftChecked,
    handleRightChecked
  }
}

新建 composition-transfer.vue 文件

<template>
  <a-transfer
    :data-source="dataSource"
    :target-keys="targetKeys"
    :render="item => item.title"
    :show-select-all="false"
    @change="onChange"
  >
    <template #children="{ direction, selectedKeys, onItemSelect }">
      <template v-if="direction==='left'">
        <a-tree
          v-if="leftTreeData.length"
          blockNode
          checkable
          defaultExpandAll
          :tree-data="leftTreeData"
          :checked-keys="leftCheckedKey"
          @check="(_, props) => { handleLeftChecked(_, props, [...selectedKeys, ...targetKeys], onItemSelect) }"
        />
        <a-empty v-else>
          <template #description>暂无数据</template>
        </a-empty>
      </template>
      <template v-else-if="direction==='right'">
        <a-tree
          v-if="rightTreeData.length"
          blockNode
          checkable
          defaultExpandAll
          :tree-data="rightTreeData"
          v-model:checked-keys="rightCheckedKey"
          v-model:expanded-keys="rightExpandedKey"
          @check="(_, props) => { handleRightChecked(_, props, [...selectedKeys, ...targetKeys], onItemSelect) }"
        />
        <a-empty v-else>
          <template #description>暂无数据</template>
        </a-empty>
      </template>
    </template>
  </a-transfer>
</template>

<script lang="ts">
import { defineComponent, PropType, toRefs } from 'vue'
import { useTreeTransfer } from './use-tree-transfer'
import type { TreeDataItem } from '@/types'

export default defineComponent({
  name: 'CompositionTransfer',
  props: {
    /** 树数据 */
    treeData: {
      type: Array as PropType<TreeDataItem[]>,
      default: () => []
    },
    /** 编辑 key */
    editKey: {
      type: Array as PropType<string[]>,
      default: () => []
    }
  },
  setup (props) {
    const { state, onChange, handleLeftChecked, handleRightChecked } = useTreeTransfer(props)

    return {
      ...toRefs(state),

      onChange,
      handleLeftChecked,
      handleRightChecked
    }
  }
})
</script>

<style scoped lang="less">
.ant-transfer {
  ::v-deep(.ant-transfer-list) {
    width: 300px;
  }
}
</style>

页面中使用组件

部分代码:

<template>
  <div class="transfer-wrapper">
    <transfer
      ref="transferRef"
      :tree-data="treeData"
      :edit-key="editKey"
    />
  </div>
  <a-space>
    <a-button
      type="primary"
      @click="getValue"
    >
      获取值
    </a-button>
    <a-button
      @click="setValue"
    >
      设置值
    </a-button>
  </a-space>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import Transfer from '@/components/composition-transfer.vue'
import data from '@/assets/data'
import type { TreeDataItem } from '@/types'

interface State {
  /** 组件实例 */
  transferRef: any;
  /** 树数据 */
  treeData: TreeDataItem[];
  /** 编辑 key */
  editKey: string[];
}

export default defineComponent({
  name: 'Composition',
  components: {
    Transfer
  },
  setup () {
    const state = reactive<State>({
      transferRef: undefined,
      treeData: data.treeData,
      editKey: []
    })

    /** 设置值 */
    function getValue () {
      console.log(state.transferRef.emitKeys)
    }

    /** 获取值 */
    function setValue () {
      state.editKey = data.editKey
    }

    return {
      ...toRefs(state),

      getValue,
      setValue
    }
  }
})   
</script>

源码地址

Logo

前往低代码交流专区

更多推荐