练习树形穿梭框

import React, {

  ReactNode,

  FC,

  useState,

  useEffect,

  useContext,

  createContext,

  useCallback,

  useMemo

} from 'react'

import styled from 'styled-components'

import { Checkbox, Input, Tree, TreeProps } from 'antd'

import { LeftOutlined, RightOutlined } from '@ant-design/icons'

const { Search } = Input

export type treeDataType = Required<Pick<TreeProps, 'treeData'>>

export type onCheckType = Required<Pick<TreeProps, 'onCheck'>>

export interface FunEntryType {

  treeData: any[]

  callback: (

    item: {

      title: string

      key: string

      children: any[]

      [key: string]: any

    },

    index: number,

    parentId: string | null | number

  ) => void

  replaceName?: string

  parentId?: string | null | number

}

export interface FlatEntryType extends treeDataType {

  treeData: any[]

  callback?: (

    item: {

      title: string

      key: string

      children: any[]

      [key: string]: any

    },

    index: number

  ) => void

  replaceName?: string

}

export interface reorganizeEntryType {

  treeArr: Array<any>

  id?: string | null | number

  link?: string

  idName?: string

}

export interface TreeShuttleBoxRightPropsType {}

export interface shuttleLeftProps {

  handleChange?: (value: string[]) => void

}

const treeData_ = [

  {

    key: '1',

    title: '1',

    children: [

      {

        key: '89083790301',

        title: '(Newogbed',

        children: [

          {

            key: '2553780501',

            title: 'VH-Dogobile',

            children: null

          },

          {

            key: '30140750601',

            title: 'VH-Dtop',

            children: [

              {

                key: '255120501',

                title: 'VH-Dogbed',

                children: null

              },

              {

                key: '30140601',

                title: 'VH-Dktop',

                children: null

              }

            ]

          }

        ]

      }

    ]

  },

  {

    key: '60941980901',

    title: 'VHktop',

    children: null

  },

  {

    key: '6624220001',

    title: 'VMobile',

    children: null

  },

  {

    key: '72095520101',

    title: 'bile',

    children: null

  },

  {

    key: '98673349201',

    title: 'Vktop',

    children: null

  }

]

const TreeShuttleContainer = styled.div`

  width: ${({ isOnlyLeft }: { isOnlyLeft: boolean }) =>

    isOnlyLeft ? 360 : 792}px;

  height: 400px;

  display: flex;

  justify-content: center;

  align-items: center;

`

const CustomTransferBox = styled.div`

  width: 358px;

  height: 398px;

  background: #ffffff;

  border: 1px solid #dddddd;

  border-radius: 5px;

`

const CustomTransferButtonBox = styled.div`

  width: 24px;

  margin: 0 24px;

  display: flex;

  flex-direction: column;

  justify-content: center;

  align-items: center;

  .active {

    background: #ffaa00;

    border: 1px solid #ffaa00;

  }

  .icon-color {

    color: rgba(0, 0, 0, 0.25);

  }

  .icon-color-active {

    color: #ffffff;

  }

`

const CustomTransferButton = styled.div`

  width: 23px;

  height: 23px;

  background: rgba(0, 0, 0, 0.04);

  border: 1px solid rgba(0, 0, 0, 0.15);

  border-radius: 3px;

  display: flex;

  justify-content: center;

  align-items: center;

  cursor: pointer;

  & + & {

    margin-top: 12px;

  }

`

const CustomTransferHead = styled.div`

  height: 39px;

  border-bottom: 1px solid #dddddd;

  padding: 13px 0 12px 8px;

  display: flex;

  justify-content: flex-start;

  align-items: center;

  line-height: 14px;

  .select-count {

    font-size: 12px;

    font-family: Helvetica, Helvetica-Regular;

    font-weight: 400;

    text-align: left;

    color: #555555;

    padding: 0;

    margin: 0 0 0 8px;

  }

`

const CustomTransferBody = styled.div`

  padding: 8px;

`

const CustomTransferTree = styled.div`

  height: 300px;

  overflow: auto;

  margin-top: 6px;

  /* 最多显示两行,超过就隐藏 */

  .ant-tree-title {

    overflow: hidden;

    text-overflow: ellipsis;

    display: -webkit-box;

    -webkit-line-clamp: 2;

    -webkit-box-orient: vertical;

    word-break: break-all;

  }

`

interface TreeShuttleBoxType {

  ({

    children,

    defaultValue,

    handleChange,

    isOnlyLeft

  }: {

    children: ReactNode

    defaultValue?: Array<string>

    handleChange?: (value: string[]) => void

    isOnlyLeft?: boolean

  }): JSX.Element

  TreeShuttleBoxLeft: FC<shuttleLeftProps & treeDataType>

  TreeShuttleBoxRight: FC<TreeShuttleBoxRightPropsType>

  TreeShuttleButton: FC

}

type contextType = ReturnType<typeof useTreeShuttle>

const TreeShuttleContext = createContext<contextType | undefined>(undefined)

interface TreeProviderProps {

  children: ReactNode

  value: any

}

function TreeProvider({ children, value }: TreeProviderProps) {

  return (

    <TreeShuttleContext.Provider value={value}>

      {children}

    </TreeShuttleContext.Provider>

  )

}

const useTreeShuttle = () => {

  const [finallyValue, setFinallyValue] = useState<Array<string>>([]) // 最终返回给用户的key数组

  const handleFinallyValueChange = (value: string[]) => {

    setFinallyValue(value)

  }

  const [defaultValue, setDefaultValue] = useState<string[]>([]) // 主要用于保存默认值

  const handleDefaultValueChange = (arr: string[]) => {

    setDefaultValue(arr)

  }

  const [sonParMap, setSonParMap] = useState<Map<string, string | null>>(

    new Map()

  ) // 保存子节点key和父节点key的Map,主要用于子级查找父级

  const handleSonParMapChange = (map: Map<string, string | null>) => {

    setSonParMap(map)

  }

  const [flatArray, setFlatArray] = useState<any[]>([]) // 保存展平的树数据,用于重组树形结构

  const handleFlatArrayChange = (value: any[]) => {

    setFlatArray(value)

  }

  // 左边的树形选择框

  const [checkListLeft, setCheckListLeft] = useState<string[]>([]) // 存储左侧选择框选中的key

  const handleCheckListLeftChange = (value: string[]) => {

    setCheckListLeft(value)

  }

  const [isBrightLeft, setIsBrightLeft] = useState<boolean>(false) // 控制左侧按钮是否高亮

  const handleIsBrightLeftChange = (isBright: boolean) => {

    setIsBrightLeft(isBright)

  }

  const [readOnlyAllKeyLeft, setReadOnlyAllKeyLeft] = useState<string[]>([]) // readonly 左边全部的key

  const handleReadOnlyAllKeyLeftChange = (arr: string[]) => {

    setReadOnlyAllKeyLeft(arr)

  }

  const [indeterminateLeft, setIndeterminateLeft] = useState<boolean>(false) // 主要处理左侧的半选

  const handleIndeterminateLeftChange = (boo: boolean) => {

    setIndeterminateLeft(boo)

  }

  const [dataSourceLeft, setDataSourceLeft] = useState<treeDataType>({

    treeData: []

  }) // 左侧树形选择框的数据源

  const handleDataSourceLeftChange = ({ treeData }: treeDataType) => {

    setDataSourceLeft({ treeData })

  }

  const [readOnlyTreeDataSource, setReadOnlyTreeDataSource] =

    useState<treeDataType>({

      treeData: []

    }) // 保存左侧树的数据源,用于只读,搜索

  const handleReadOnlyTreeDataSourceChange = ({ treeData }: treeDataType) => {

    setReadOnlyTreeDataSource({ treeData })

  }

  // 右边的树形选择框

  const [checkListRight, setCheckListRight] = useState<string[]>([]) // 存储在右侧选择框中的key

  const handleCheckListRightChange = (value: string[]) => {

    setCheckListRight(value)

  }

  const [dataSourceRight, setDataSourceRight] = useState<treeDataType>({

    treeData: []

  }) // 右侧树形选择框的数据源

  const handleDataSourceRightChange = ({ treeData }: treeDataType) => {

    setDataSourceRight({ treeData })

  }

  const [readOnlyAllKeyRight, setReadOnlyAllKeyRight] = useState<string[]>([]) // 右侧所有的key

  const handleReadOnlyAllKeyRightChange = (arr: string[]) => {

    setReadOnlyAllKeyRight(arr)

  }

  const [isBrightRight, setIsBrightRight] = useState<boolean>(false) // 控制右侧按钮是否高亮

  const handleIsBrightRightChange = (isBright: boolean) => {

    setIsBrightRight(isBright)

  }

  const [readOnlyDataSourceRight, setReadOnlyDataSourceRight] =

    useState<treeDataType>({

      treeData: []

    }) // 右侧只读的树形数据,主要用于搜索

  const handleReadOnlyDataSourceRightChange = ({ treeData }: treeDataType) => {

    setReadOnlyDataSourceRight({ treeData })

  }

  return {

    finallyValue,

    checkListLeft,

    checkListRight,

    dataSourceRight,

    dataSourceLeft,

    defaultValue,

    flatArray,

    sonParMap,

    isBrightLeft,

    isBrightRight,

    indeterminateLeft,

    readOnlyAllKeyRight,

    readOnlyDataSourceRight,

    readOnlyAllKeyLeft,

    readOnlyTreeDataSource,

    handleFinallyValueChange,

    handleCheckListLeftChange,

    handleCheckListRightChange,

    handleDataSourceRightChange,

    handleDataSourceLeftChange,

    handleFlatArrayChange,

    handleSonParMapChange,

    handleReadOnlyAllKeyRightChange,

    handleReadOnlyDataSourceRightChange,

    handleIsBrightLeftChange,

    handleIsBrightRightChange,

    handleIndeterminateLeftChange,

    handleDefaultValueChange,

    handleReadOnlyAllKeyLeftChange,

    handleReadOnlyTreeDataSourceChange

  }

}

const useTreeContext = () => {

  const context = useContext(TreeShuttleContext)

  if (!context) {

    throw new Error('context必须在Provider中使用!!')

  }

  return context

}

const traverseTreeData = ({

  treeData = [],

  callback = () => {},

  parentId = null,

  replaceName = 'children'

}: FunEntryType) => {

  // 遍历树形数据

  treeData.forEach((item: any, index) => {

    callback(item, index, parentId)

    if (item[replaceName] && item[replaceName].length > 0) {

      traverseTreeData({

        treeData: item[replaceName],

        callback,

        parentId: item.key,

        replaceName

      })

    }

  })

}

const flatTree = ({

  treeData = [],

  callback = () => {},

  replaceName = 'children'

}: FlatEntryType) => {

  // 扁平化树形数据结构

  let flatArr: any[] = []

  treeData.forEach((item, index) => {

    const copyItem = { ...item }

    if (item[replaceName]) {

      delete copyItem[replaceName]

    }

    callback(copyItem, index)

    flatArr.push(copyItem)

    if (item[replaceName] && item[replaceName].length > 0) {

      flatArr = flatArr.concat(

        flatTree({ treeData: item[replaceName], replaceName, callback })

      )

    }

  })

  return flatArr

}

const searchTree = (searchValue = '', treeArr: any[] = []) => {

  // 树形数据搜索

  let searTreeArr: any[] = []

  treeArr?.forEach((treeItem: { title: string; children: any[] }) => {

    if (treeItem.title.includes(searchValue)) {

      searTreeArr.push(treeItem)

    } else {

      if (treeItem.children && treeItem.children.length) {

        const chr = searchTree(searchValue, treeItem.children)

        const obj = {

          ...treeItem,

          children: chr

        }

        if (chr && chr.length) {

          searTreeArr.push(obj)

        }

      }

    }

  })

  return searTreeArr

}

const reorganizeTree = ({

  treeArr = [],

  id = null,

  link = 'parentId',

  idName = 'key'

}: reorganizeEntryType): any[] =>

  // 重组树形数据

  treeArr

    .filter((itemF) => itemF[link] === id)

    .map((itemM) => ({

      ...itemM,

      children: reorganizeTree({

        treeArr,

        id: itemM[idName],

        link,

        idName

      })

    }))

const searchAllParents = (

  arr: string[],

  sonParMap: Map<string, string | null>

): string[] => {

  let allKey: string[] = []

  function search(key: string) {

    allKey.push(key)

    const nextKey = sonParMap.get(key)

    if (nextKey) {

      search(nextKey)

    }

  }

  arr.forEach((key) => {

    search(key)

  })

  return Array.from(new Set(allKey))

}

const TreeShuttleButton: FC = () => {

  const {

    checkListLeft,

    checkListRight,

    flatArray,

    sonParMap,

    dataSourceLeft,

    finallyValue,

    handleDataSourceRightChange,

    handleReadOnlyAllKeyRightChange,

    handleFinallyValueChange,

    handleDataSourceLeftChange,

    handleReadOnlyDataSourceRightChange,

    isBrightRight,

    handleIsBrightRightChange,

    isBrightLeft,

    handleIsBrightLeftChange,

    handleCheckListRightChange,

    handleCheckListLeftChange,

    indeterminateLeft,

    handleIndeterminateLeftChange,

    defaultValue,

    readOnlyAllKeyLeft,

    readOnlyAllKeyRight,

    readOnlyTreeDataSource,

    readOnlyDataSourceRight

  } = useTreeContext()

  const [handleClickToRight, handleClickToLeft] = useMemo(() => {

    const handleClickToRight = () => {

      if (checkListLeft.length === 0 && !isBrightRight) {

        return

      }

      // 取消左侧的半选

      handleIndeterminateLeftChange(false)

      if (readOnlyAllKeyLeft.length === checkListLeft.length) {

        // 左侧全选逻辑

        // 右侧数据源保存

        handleDataSourceRightChange(

          JSON.parse(JSON.stringify(readOnlyTreeDataSource))

        )

        // 保存右侧只读的数据源所有的key

        handleReadOnlyAllKeyRightChange([...checkListLeft])

        // 保存最终值的key

        handleFinallyValueChange([...checkListLeft])

        // 保存右侧只读的数据源用于搜索

        handleReadOnlyDataSourceRightChange({

          treeData: JSON.parse(JSON.stringify(checkListLeft))

        })

        // 禁用左侧树已经选择的节点

        const treeData_ = JSON.parse(

          JSON.stringify(readOnlyTreeDataSource.treeData)

        )

        traverseTreeData({

          treeData: treeData_,

          callback(item) {

            item['disabled'] = true

          }

        })

        handleDataSourceLeftChange({

          treeData: treeData_

        })

      } else {

        // 左侧不全选逻辑

        // 得到子级的key 和 半选状态的 父级的key 用于重组数组

        let allKey = searchAllParents(checkListLeft, sonParMap)

        allKey = Array.from(new Set(allKey.concat(finallyValue)))

        // 得到选中的key 所对应的节点

        const selectNode = flatArray.filter(({ key }) => allKey.includes(key))

        // 重组树形数据结构

        const treeData = reorganizeTree({

          treeArr: selectNode

        })

        // 右侧数据源保存

        handleDataSourceRightChange({ treeData })

        // 保存右侧只读的数据源所有的key

        handleReadOnlyAllKeyRightChange(allKey)

        // 保存最终值的key

        handleFinallyValueChange(allKey)

        // 保存右侧只读的数据源用于搜索

        handleReadOnlyDataSourceRightChange({

          treeData: JSON.parse(JSON.stringify(treeData))

        })

        // 禁用左侧树已经选择的节点

        traverseTreeData({

          treeData: dataSourceLeft.treeData,

          callback(item) {

            item['disabled'] = allKey.includes(item.key)

          }

        })

        handleDataSourceLeftChange({

          treeData: [...dataSourceLeft.treeData]

        })

      }

      // 右侧按钮取消高亮

      handleIsBrightRightChange(false)

    }

    const handleClickToLeft = () => {

      if (checkListRight.length === 0 && !isBrightLeft) {

        return

      }

      if (checkListRight.length === readOnlyAllKeyRight.length) {

        // 右侧全选逻辑

        // 保存左侧选择的key

        handleCheckListLeftChange([])

        // 保存右侧的数据源

        handleDataSourceRightChange({ treeData: [] })

        // 保存右侧所有的key

        handleReadOnlyAllKeyRightChange([])

        // 更改最终的key值

        handleFinallyValueChange([])

        // 更改右侧选中的key值

        handleCheckListRightChange([])

        // 保存右侧只读的数据源

        handleReadOnlyDataSourceRightChange({

          treeData: []

        })

        // 保存左侧的数据源

        handleDataSourceLeftChange(

          JSON.parse(JSON.stringify(readOnlyTreeDataSource))

        )

      } else {

        // 右侧不全选逻辑

        // 在最终值里删除右边勾选的key

        const finallyArr = finallyValue.filter(

          (key) => !checkListRight.includes(key)

        )

        // 筛选右侧的树形数据

        const selectNode = flatArray.filter(({ key }) =>

          finallyArr.includes(key)

        )

        // 重组右侧树形数据

        const treeData = reorganizeTree({

          treeArr: selectNode

        })

        // 更新左侧选择的key

        const selectKeyLeft = checkListLeft.filter(

          (key) => !checkListRight.includes(key)

        )

        // 保存左侧选择的key

        handleCheckListLeftChange(selectKeyLeft)

        // 保存右侧的数据源

        handleDataSourceRightChange({ treeData })

        // 保存右侧所有的key

        handleReadOnlyAllKeyRightChange(finallyArr)

        // 更改最终的key值

        handleFinallyValueChange(finallyArr)

        // 更改右侧选中的key值

        handleCheckListRightChange([])

        // 保存右侧只读的数据源

        handleReadOnlyDataSourceRightChange({

          treeData: JSON.parse(JSON.stringify(treeData))

        })

        // 取消左侧树数据源的禁用

        traverseTreeData({

          treeData: dataSourceLeft.treeData,

          callback(item) {

            item['disabled'] = finallyArr.includes(item.key)

          }

        })

        // 保存左侧的数据源

        handleDataSourceLeftChange({

          treeData: JSON.parse(JSON.stringify(dataSourceLeft.treeData))

        })

      }

      // 取消左侧高亮显示

      handleIsBrightLeftChange(false)

    }

    return [handleClickToRight, handleClickToLeft]

  }, [

    checkListLeft,

    checkListRight,

    flatArray,

    dataSourceLeft,

    finallyValue,

    sonParMap,

    indeterminateLeft,

    defaultValue,

    readOnlyAllKeyLeft,

    readOnlyAllKeyRight,

    readOnlyTreeDataSource,

    readOnlyDataSourceRight

  ])

  useEffect(() => {

    if (defaultValue.length) {

      handleClickToRight()

    }

  }, [defaultValue])

  return (

    <CustomTransferButtonBox>

      {useMemo(

        () => (

          <CustomTransferButton

            className={isBrightRight ? 'active' : ''}

            onClick={handleClickToRight}

          >

            <RightOutlined

              className={isBrightRight ? 'icon-color-active' : 'icon-color'}

            />

          </CustomTransferButton>

        ),

        [

          isBrightRight,

          checkListLeft,

          indeterminateLeft,

          defaultValue,

          readOnlyAllKeyLeft,

          readOnlyAllKeyRight,

          readOnlyTreeDataSource,

          readOnlyDataSourceRight

        ]

      )}

      {useMemo(

        () => (

          <CustomTransferButton

            className={isBrightLeft ? 'active' : ''}

            onClick={handleClickToLeft}

          >

            <LeftOutlined

              className={isBrightLeft ? 'icon-color-active' : 'icon-color'}

            />

          </CustomTransferButton>

        ),

        [

          isBrightLeft,

          checkListRight,

          indeterminateLeft,

          defaultValue,

          readOnlyAllKeyLeft,

          readOnlyAllKeyRight,

          readOnlyTreeDataSource,

          readOnlyDataSourceRight

        ]

      )}

    </CustomTransferButtonBox>

  )

}

const TreeShuttleBoxRight: FC<TreeShuttleBoxRightPropsType> = ({}) => {

  const {

    dataSourceRight,

    checkListRight,

    readOnlyAllKeyRight,

    handleCheckListRightChange,

    handleDataSourceRightChange,

    readOnlyDataSourceRight,

    handleIsBrightLeftChange

  } = useTreeContext()

  const [indeterminate, setIndeterminate] = useState<boolean>(false) // 控制半选

  const [checkAll, setCheckAll] = useState<boolean>(false) // 控制全选

  const [searchText, setSearchText] = useState<string>('') // 用于搜索title

  useEffect(() => {

    if (checkListRight.length === 0) {

      // 无选择数据是 全选置空

      setCheckAll(false)

      // 无选择数据是 半选选置空

      setIndeterminate(false)

    }

  }, [checkListRight])

  const [handleCheckBoxChange, handleSelectNode] = useMemo(() => {

    const handleCheckBoxChange = (e: any) => {

      // 处理全选和半选

      handleCheckListRightChange(e.target.checked ? readOnlyAllKeyRight : [])

      // 设置全选状态

      setCheckAll(e.target.checked)

      // 设置半选状态

      setIndeterminate(false)

      // 设置to Left 按钮高亮

      handleIsBrightLeftChange(e.target.checked)

    }

    const handleSelectNode = (keyArr: any) => {

      // 设置右侧选项

      handleCheckListRightChange(keyArr)

      // 设置全选状态

      setCheckAll(keyArr.length && readOnlyAllKeyRight.length === keyArr.length)

      // 设置半选状态

      setIndeterminate(

        keyArr.length && readOnlyAllKeyRight.length !== keyArr.length

      )

      // 设置to Left 按钮高亮

      handleIsBrightLeftChange(!!keyArr.length)

    }

    return [handleCheckBoxChange, handleSelectNode]

  }, [checkAll, readOnlyAllKeyRight])

  const [handleSearchTree, handleSearchChange] = useMemo(() => {

    const handleSearchTree = (value: string) => {

      if (value === '') {

        // 点击清除搜索或者手动删除文字按下回车

        const treeData = JSON.parse(JSON.stringify(readOnlyDataSourceRight))

        // 保存右侧数据源

        handleDataSourceRightChange(treeData)

      } else {

        // 搜索树

        handleDataSourceRightChange({

          treeData: searchTree(value, dataSourceRight.treeData)

        })

      }

    }

    const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {

      // 搜索树形结构数据

      setSearchText(e.target.value.trim())

    }

    return [handleSearchTree, handleSearchChange]

  }, [searchText, readOnlyDataSourceRight, dataSourceRight.treeData])

  return (

    <CustomTransferBox>

      <CustomTransferHead>

        {useMemo(

          () => (

            <Checkbox

              indeterminate={indeterminate}

              checked={checkAll}

              onChange={handleCheckBoxChange}

            />

          ),

          [indeterminate, checkAll, readOnlyAllKeyRight]

        )}

        {useMemo(

          () => (

            <p className="select-count">

              {checkListRight.length}/{readOnlyAllKeyRight.length}items

            </p>

          ),

          [readOnlyAllKeyRight, checkListRight]

        )}

      </CustomTransferHead>

      <CustomTransferBody>

        <Search

          placeholder="search here"

          value={searchText}

          onSearch={handleSearchTree}

          onChange={handleSearchChange}

          allowClear

          enterButton

        />

        <CustomTransferTree>

          <Tree

            height={300}

            selectable={false}

            checkable

            checkedKeys={checkListRight}

            onCheck={handleSelectNode}

            treeData={dataSourceRight.treeData}

          />

        </CustomTransferTree>

      </CustomTransferBody>

    </CustomTransferBox>

  )

}

const TreeShuttleBoxLeft: FC<shuttleLeftProps & treeDataType> = ({

  treeData,

  handleChange // 当只需要左侧选择框时就可以传递此回调函数,获取所选择的值

}) => {

  const {

    checkListLeft,

    handleCheckListLeftChange,

    dataSourceLeft,

    handleDataSourceLeftChange,

    handleFlatArrayChange,

    handleSonParMapChange,

    finallyValue,

    handleIsBrightRightChange,

    indeterminateLeft,

    handleIndeterminateLeftChange,

    readOnlyAllKeyLeft,

    handleReadOnlyAllKeyLeftChange,

    readOnlyTreeDataSource,

    handleReadOnlyTreeDataSourceChange

  } = useTreeContext()

  const [checkAll, setCheckAll] = useState<boolean>(false) // 控制全选

  const [searchText, setSearchText] = useState<string>('') // 用于搜索title

  useEffect(() => {

    // 遍历树形数据设置parentId

    const sonParMap: Map<string, string | null> = new Map()

    traverseTreeData({

      treeData,

      callback(item, index, parentId) {

        item.parentId = parentId

        sonParMap.set(item.key, item.parentId)

      }

    })

    handleSonParMapChange(sonParMap)

    // 将所以树形数据拉平,为后续组装右侧的树形树形使用

    const flatArray = flatTree({

      treeData: JSON.parse(JSON.stringify(treeData))

    })

    handleFlatArrayChange(flatArray)

    // 得到左侧所有的key,主要用于全选

    const allKey: string[] = flatArray.map(({ key }) => key)

    // 保存左侧数据源所有的key

    handleReadOnlyAllKeyLeftChange(allKey)

    // 保存到左侧的数据源

    handleDataSourceLeftChange({

      treeData: JSON.parse(JSON.stringify(treeData))

    })

    // 保存左侧树的数据源,只读,用于搜索

    handleReadOnlyTreeDataSourceChange({

      treeData: JSON.parse(JSON.stringify(treeData))

    })

  }, [treeData])

  useEffect(() => {

    // 左侧选择数据时回调change

    handleChange && handleChange(checkListLeft)

  }, [checkListLeft])

  useEffect(() => {

    if (checkListLeft.length !== readOnlyAllKeyLeft.length) {

      setCheckAll(false)

    }

  }, [checkListLeft, readOnlyAllKeyLeft])

  const [handleCheckBoxChange, handleSelectNode] = useMemo(() => {

    const handleCheckBoxChange = (e: any) => {

      // 处理全选和半选

      handleCheckListLeftChange(e.target.checked ? readOnlyAllKeyLeft : [])

      // 处理全选状态

      setCheckAll(e.target.checked)

      // 处理半选状态

      handleIndeterminateLeftChange(false)

      // 处理to Right 高亮

      handleIsBrightRightChange(e.target.checked)

    }

    const handleSelectNode = (keyArr: any) => {

      // 处理左侧选中的key

      handleCheckListLeftChange(keyArr)

      // 处理全选状态

      setCheckAll(readOnlyAllKeyLeft.length === keyArr.length)

      // 处理半选状态

      handleIndeterminateLeftChange(

        keyArr.length && readOnlyAllKeyLeft.length !== keyArr.length

      )

      // 处理 to Right 高亮

      handleIsBrightRightChange(!!keyArr.length)

    }

    return [handleCheckBoxChange, handleSelectNode]

  }, [readOnlyAllKeyLeft, checkAll, checkListLeft])

  const [handleSearchTree, handleSearchChange] = useMemo(() => {

    const handleSearchTree = (value: string) => {

      if (value === '') {

        // 点击清除搜索或者手动删除文字按下回车

        const treeData = JSON.parse(

          JSON.stringify(readOnlyTreeDataSource.treeData)

        )

        // 处理左侧数据源选中数据的禁用

        traverseTreeData({

          treeData,

          callback(item) {

            item['disabled'] = finallyValue.includes(item.key)

          }

        })

        // 保存左侧的数据源

        handleDataSourceLeftChange({

          treeData

        })

      } else {

        // 搜索树

        handleDataSourceLeftChange({

          treeData: searchTree(value, dataSourceLeft.treeData)

        })

      }

    }

    const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {

      setSearchText(e.target.value.trim())

    }

    return [handleSearchTree, handleSearchChange]

  }, [

    searchText,

    readOnlyTreeDataSource.treeData,

    dataSourceLeft.treeData,

    finallyValue

  ])

  return (

    <CustomTransferBox>

      <CustomTransferHead>

        {useMemo(

          () => (

            <Checkbox

              indeterminate={indeterminateLeft}

              checked={checkAll}

              onChange={handleCheckBoxChange}

            />

          ),

          [indeterminateLeft, checkAll, readOnlyAllKeyLeft]

        )}

        {useMemo(

          () => (

            <p className="select-count">

              {checkListLeft.length}/{readOnlyAllKeyLeft.length}items

            </p>

          ),

          [checkListLeft, readOnlyAllKeyLeft]

        )}

      </CustomTransferHead>

      <CustomTransferBody>

        <Search

          placeholder="search here"

          value={searchText}

          onSearch={handleSearchTree}

          onChange={handleSearchChange}

          allowClear

          enterButton

        />

        <CustomTransferTree className="homeScrollBar">

          <Tree

            height={300}

            selectable={false}

            checkable

            checkedKeys={checkListLeft}

            onCheck={handleSelectNode}

            treeData={dataSourceLeft.treeData}

          />

        </CustomTransferTree>

      </CustomTransferBody>

    </CustomTransferBox>

  )

}

const TreeShuttleBox: TreeShuttleBoxType = ({

  children,

  defaultValue = [],

  handleChange,

  isOnlyLeft = false // 主要控制是否是需要左侧树形选择框

}) => {

  const {

    finallyValue,

    handleDefaultValueChange,

    handleCheckListLeftChange,

    handleIsBrightRightChange,

    ...rest

  } = useTreeShuttle()

  useEffect(() => {

    if (defaultValue.length) {

      // 有默认数据时

      // 保存默认数据

      handleDefaultValueChange(defaultValue)

      // 保存左侧选中的数据

      handleCheckListLeftChange(defaultValue)

      // to Right 高亮

      handleIsBrightRightChange(true)

    }

  }, [defaultValue])

  useEffect(() => {

    handleChange && handleChange(finallyValue)

  }, [finallyValue])

  return (

    <TreeProvider

      value={{

        finallyValue,

        handleDefaultValueChange,

        handleCheckListLeftChange,

        handleIsBrightRightChange,

        ...rest

      }}

    >

      <TreeShuttleContainer isOnlyLeft={isOnlyLeft}>

        {children}

      </TreeShuttleContainer>

    </TreeProvider>

  )

}

TreeShuttleBox.TreeShuttleBoxLeft = TreeShuttleBoxLeft

TreeShuttleBox.TreeShuttleBoxRight = TreeShuttleBoxRight

TreeShuttleBox.TreeShuttleButton = TreeShuttleButton

function App() {

  const [treeData, settreeData] = useState<any[]>([])

  const [defaultArr, setdefaultArr] = useState<any[]>([])

  useEffect(() => {

    settreeData(treeData_)

    let defaultArr = [

      '9745585390301',

      '8908033790301',

      '2551253780501',

      '3014087750601',

      '6093241980901',

      '6624581220001',

      '7209552490101',

      '9867334920901'

    ]

    setdefaultArr(defaultArr)

  }, [])

  const handleChange = useCallback((finallyValue: string[]) => {

    console.log('handleChange -> finallyValue', finallyValue)

  }, [])

  return (

    <div>

      <TreeShuttleBox defaultValue={defaultArr} handleChange={handleChange}>

        <TreeShuttleBox.TreeShuttleBoxLeft treeData={treeData} />

        <TreeShuttleBox.TreeShuttleButton />

        <TreeShuttleBox.TreeShuttleBoxRight />

      </TreeShuttleBox>

    </div>

  )

}

export default App

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值