自建”IT兵器库”,你值得一看!第二篇

现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!

常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。

接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步

需求:

  • 完成一个树形的数据左右动态筛选,就是我们组件当中的 穿梭框

效果展示

左右侧 进行数据联动

右侧数据 单点操作 删除

右侧数据 一键清空

左右侧 支持搜索

左侧 支持一键全选

功能点划分

  • 左侧数据单点操作
  • 右侧数据单点操作
  • 左侧数据 一键全选-反选-取消全选
  • 右侧数据一键清空
  • 左右侧支持搜索

使用到的组件(Antd Design组件库~)

  • Modal
  • Tree
  • Input
  • Checkbox
  • Button

树形穿梭框组件区域划分

  • 头部标题
  • 头部提示语
  • 核心内容区
    • 核心区域头部功能按钮
    • 搜索区域
    • 左边所有内容项
    • 待选内容项

树形穿梭框最终支持配置项

支持配置项 复制代码
interface TreeTransferModalProps {
  open: boolean
  setOpen: React.Dispatch<React.SetStateAction<boolean>>
  modalTitle?: string | React.ReactNode // 标题
  modalWidth?: number // 宽度
  modalHeadContent?: React.ReactNode // 头部内容
  leftHeadContent?: React.ReactNode | string // 内容区域-左侧头部
  rightHeadContent?: React.ReactNode | string // 内容区域-右侧头部
  modalBodyStyle?: any // modal body样式
  searchPlaceholder?: string // 搜索框提示语
  modalOk?: (val) => void // 外部确认函数
  isReturnTileData?: boolean // 是否需要返回平铺数据
}

组件布局

组件布局 复制代码
<Modal
      title={modalTitle}
      centered
      width={modalWidth}
      open={open}
      onOk={handleOk}
      confirmLoading={confirmLoading}
      onCancel={handleCancel}
      destroyOnClose
      bodyStyle={{ height: '600px', ...modalBodyStyle }}
    >
      <div>
         // 头部内容-提示语
        <div>{modalHeadContent}</div>
        <div className="content-box flex">
          // 核心区域-左侧
          <div className="content-left-box flex-1">
            <div className="content-left-head">
                // 核心区域-左侧-一键操作功能区
              <div>
                <Checkbox onChange={e => onCheckBoxChange([], e)} checked={checkAll} indeterminate={indeterminate}>
                  全选
                </Checkbox>
              </div>
              // 核心区域-左侧-头部 -- 业务场景 可以是 外部注入一下数据操作 例如 单选组
              // 每切换一个 更换注入的数据原 重新渲染
              <div>{leftHeadContent || ''}</div>
            </div>
              // 核心区域 左侧-内容区域
            <div className="content-left-main">
                // 搜索区域
              {childSearchRender({
                treeData: leftTreeData,
                dataKeyList: dataList,
                setExpandedKeyFn: setExpandedKeys,
                setAutoExpandParentFn: setAutoExpandParent,
              })}
                // 核心区域-左侧树
              <Tree
                onExpand={onExpand}
                expandedKeys={expandedKeys}
                autoExpandParent={autoExpandParent}
                checkable
                switcherIcon={<DownOutlined />}
                showIcon
                onCheck={onCheck}
                checkedKeys={checkedKeys}
                treeData={leftTreeData}
                blockNode
              />
            </div>
          </div>
          <div className="content-right-box flex-1">
              // 右侧
            <div className="content-right-head flex">
                // 右侧头部
              <div className="flex-1">{rightHeadContent || ''}</div>
                  // 右侧-一键操作按钮
              <div className="content-right-head-clear" onClick={() => handleRightClearSelectData()}>
                清空
              </div>
            </div>
            <div className="content-right-main">
                // 右侧-搜索
              {childSearchRender({
                treeData: rightTreeData,
                dataKeyList: dataRightList,
                setExpandedKeyFn: setExpandedRightKeys,
                setAutoExpandParentFn: setAutoExpandRightParent,
              })}
                  // 右侧-树
              <Tree
                onExpand={onExpandRight}
                expandedKeys={expandedRightKeys}
                autoExpandParent={autoExpandRightParent}
                switcherIcon={<DownOutlined />}
                showIcon
                treeData={rightTreeData}
                blockNode
              />
            </div>
          </div>
        </div>
      </div>
    </Modal>

组件-搜索渲染

搜索渲染 复制代码
 const childSearchRender = (childSearchProps: any) => {
    // eslint-disable-next-line react/prop-types
    const { treeData, dataKeyList, setExpandedKeyFn, setAutoExpandParentFn } = childSearchProps
    return (
      <Search
        style={{ marginBottom: 8 }}
        placeholder={searchPlaceholder || '请输入'}
        onSearch={e => {
          onSearch(e, treeData, dataKeyList, setExpandedKeyFn, setAutoExpandParentFn)
        }}
        allowClear
      />
    )
  }

组件样式

组件样式 复制代码
  .content-box {
    width: 100%;
    height: 550px;
    border: 1px solid #d9d9d9;
  }
  .content-left-box {
    border-right: 1px solid #d9d9d9;
  }
  .content-left-head {
    padding: 16px 20px;
    background: #f5f8fb;
    height: 48px;
    box-sizing: border-box;
  }
  .content-right-head {
    padding: 16px 20px;
    background: #f5f8fb;
    height: 48px;
    box-sizing: border-box;

    &-clear {
      color: #f38d29;
      cursor: pointer;
    }
  }
  .content-right-box {
  }
  .content-left-main {
    padding: 10px 20px 0 20px;
    overflow-y: auto;
    box-sizing: border-box;
  }
  .content-right-main {
    padding: 10px 20px 0 20px;
    overflow-y: auto;
    box-sizing: border-box;
  }
  .ant-tree-node-content-wrapper {
    display: flex;
    width: 100%;
  }
  .ant-tree-title {
    flex: 1;
  }
  .right-head-content {
    font-weight: 700;
    color: #151e29;
    font-size: 14px;
  }

组件状态-定义

组件状态-定义 复制代码
  const [confirmLoading, setConfirmLoading] = useState(false)
  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([])
  const [leftTreeAllKey, setLeftTreeAllKey] = useState<React.Key[]>([])
  const [expandedRightKeys, setExpandedRightKeys] = useState<React.Key[]>([])
  const [autoExpandParent, setAutoExpandParent] = useState(true)
  const [autoExpandRightParent, setAutoExpandRightParent] = useState(true)
  const [leftTreeData, setLeftTreeData] = useState([
      // 这里只是测试数据 看自己后端数据 需要进行转化~
    {
      title: 'okok',
      titleText: 'okok',
      key: '0-0',
      icon: textIcon,
      children: [
        {
          title: '0-0-0',
          key: '0-0-0',
          icon: folderIcon,
          children: [{ title: 'ok1', titleText: 'ok1', key: '0-0-0-0', icon: <MehOutlined /> }],
        },
        {
          title: '0-0-1',
          key: '0-0-1',
          icon: <MehOutlined />,
          children: [
            { title: '0-0-1-0', key: '0-0-1-0', titleText: '0-0-1-0' },
            { title: '优程', titleText: '优程', key: '0-0-1-1' },
            { title: '0-0-1-2', key: '0-0-1-2', titleText: '0-0-1-2' },
          ],
        },
        {
          title: '0-0-2',
          titleText: '0-0-2',
          key: '0-0-2',
          // eslint-disable-next-line react/no-unstable-nested-components
          icon: ({ selected }) => (selected ? <FrownFilled /> : <FrownOutlined />),
        },
      ],
    },
    {
      title: 'haha1',
      titleText: 'haha1',
      key: '0-1',
      children: [{ title: '你好', key: '0-1-0-0', titleText: '你好' }],
    },
    {
      title: '0-2',
      titleText: '0-2',
      key: '0-2',
    },
  ])
  const [rightTreeData, setRightTreeData] = useState([])
  const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([])
  const [dataList, setDataList] = useState([])
  const [dataRightList, setDataRightList] = useState([])
  const [indeterminate, setIndeterminate] = useState(false)
  const [checkAll, setCheckAll] = useState(false)

功能点逐一拆解

左右侧数据进行联动

  • 核心: 过 checkKey 状态 进行控制左侧选中数据 右侧追加数据
  • 左侧数据支持勾选,` 组件里面配置一下就得
左侧数据支持勾选 复制代码
   <Tree
       onExpand={onExpand}
       expandedKeys={expandedKeys}
       autoExpandParent={autoExpandParent}
       checkable // 配置即可能勾选
       switcherIcon={<DownOutlined />}
       showIcon
       onCheck={onCheck}
       checkedKeys={checkedKeys}
       treeData={leftTreeData}
       blockNode
   />
  • 勾选以后 进行事件监听啦
勾选事件 复制代码
  const onCheck = (checkedKeysValue: React.Key[], e) => {
      // 通过 checkKey 进行控制左侧选中数据 右侧追加数据
    setCheckedKeys(checkedKeysValue)
  }
  • 再通过监听 checkKey 去处理数据
监听 复制代码
  useEffect(() => {
    const filterData = filterTreeData(leftTreeData, checkedKeys) // 获取左边选中的数据
    addLabelButton(filterData) // 把原本的标题 改造成 文本+删除按钮
    setRightTreeData(filterData) // 更新右侧数据
    handleAddDataList(filterData, setDataRightList) // 多存一份 改变数据结构 {key, title}用于搜索
    setExpandedRightKeys(checkedKeys) // 设置右边树 展开的节点
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [checkedKeys, leftTreeData])
filterTreeData 复制代码
const filterTreeData = (data, keys) => {
    // 据选中的key 进行数据转成一棵树
    return data.reduce((filteredData, currentItem) => {
      const { children, ...rest } = currentItem
       // 如果当前item key 或者是 当前的item 含有children
      if (keys.includes(currentItem.key) || children) {
          // 进行下一步 有 children 递归 ... 这里就是 一直递归到子节点 返回一个子树
        const filteredChildren = filterTreeData(children || [], keys)
        // 如果 当前的 item 符合 或者 递归返回的数据存在children 进行数据组装
        if (keys.includes(currentItem.key) || filteredChildren.length > 0) {
          rest.children = filteredChildren
          filteredData.push(rest)
        }
      }
      return filteredData
    }, [])
  }
addLabelButton 复制代码
  const addLabelButton = nodes => {
    const arr = nodes
    arr.forEach((node, idx) => {
      if (node.children?.length) addLabelButton(node.children)
      arr[idx].title = (
        <div className="flex justify-between text-right">
          <span>{node.title}</span>
          <Button type="text" className="text-right" onClick={() => deleteRightData(node.key)} size="small">
            <DeleteOutlined />
          </Button>
        </div>
      )
    })
    return arr
  }
handleAddDataList 复制代码
const handleAddDataList = (searchOriginData, setChangeList) => {
    const tempArr = []
    const generateList = (data = []) => {
      data?.forEach(item => {
        const { key, titleText } = item
        tempArr.push({ key, titleText: titleText as string })

        if (item.children && item.children.length) {
          generateList(item.children)
        }
      })
    }
    generateList(searchOriginData)
    setChangeList(tempArr)
  }

右侧数据单点操作

  • 刚才我们已经对 标题进行改造 新增删除按钮
  • 每个删除·按钮绑定了 deleteRightData事件
deleteRightData 复制代码
  const deleteRightData = key => {
    const list = flattenData(leftTreeData)
    const delKeys = Array.from(new Set([...getDelKeysByParentId([key], list), ...getDelKeysByChildren(key, list)]))
    // 最终更新选择的key
    setCheckedKeys(preArr => {
      const curList = preArr.filter(item => !delKeys.includes(item))
      return curList
    })
  }
  • 用户点击的时候 也只是能拿到当前的节点key,由于是树结构 所以要考虑它所处在的节点父子级关系
  • 这里是分开操作 找父级 找子级 独立出来
  • 往父级递归得到要删除的所有key
  • PS:这里没有考虑当前删除的节点的兄弟节点 一股脑把父节点干掉 是因为在前面 选择key的时候 已经重新构造树数据,所以无需考虑
getDelKeysByParentId 复制代码
// 往父级递归得到要删除的所有key
  const getDelKeysByParentId = (initialKeys, list) => {
    const delKeys = [...initialKeys]
    const stack = [...delKeys]
    while (stack.length > 0) {
    // 把删除的key数据最后一个丢出来
      const key = stack.pop()
      // 去平铺数组当中找 满足条件的树节点
      const delRow = list.find(item => item.folderId === key)
      // 如果找到的树节点 存在父级
      if (delRow?.parentId) {
      // 也把它父级找出来
        const parentRow = list.find(item => item.folderId === delRow.parentId)
        if (parentRow) {
          const parentKey = parentRow.folderId
          // 只要父节点不存在于 删除的key push进去
          if (!delKeys.includes(parentKey)) {
            stack.push(parentKey)
            delKeys.push(parentKey)
          }
        }
      }
    }
    return Array.from(new Set(delKeys))
  }
  • 往子级children递归得到要删除的所有key
getDelKeysByChildren 复制代码
 // 往子级children递归得到要删除的所有key
  const getDelKeysByChildren = (key, list, childList = []) => {
    let delKeys = [...childList, key]
    // 找到符合条件的 节点
    const delRow = list.find(item => item.key === key)
    // 如果有children 就一直递归到最后一个叶子节点
    if (delRow.children?.length) {
      delRow.children.forEach(item => {
        delKeys = delKeys.concat(getDelKeysByChildren(item.key, list, delKeys))
      })
    }
    // 去重
    return Array.from(new Set(delKeys))
  }

右侧数据一键清空

  • 全部恢复初始值即可
暴力一键清空 复制代码
  // 清空选择的内容
  const handleRightClearSelectData = () => {
    setCheckedKeys([])
    setRightTreeData([])
    setExpandedRightKeys([])
  }

左右侧支持搜索

onSearch 复制代码
  // 搜索逻辑
  const onSearch = (value: string, originTree, dataOriginList, setExpand, setAutoExpand) => {
      // 没有搜索值 不展开
    if (!value) {
      setExpand([])
      setAutoExpand(false)
      return ''
    }
    const newExpandedKeys = dataOriginList // 已经经过处理的平铺 {titleText: string, key: string}
      .map(item => {
      // 如果文本符合 找找其父级
        if (item.titleText?.indexOf(value) > -1) {
          return getParentKey(item.key, originTree)
        }
        return null
      })
      .filter((item, i, self) => item && self.indexOf(item) === i) // 去重&&过滤掉null 
    setExpand(newExpandedKeys as React.Key[]) // 更新展开key
    setAutoExpand(true) // 开启自动展开
    return ''
  }
getParentKey 复制代码
  const getParentKey = (key: React.Key, tree: DataNode[]): React.Key => {
    let parentKey: React.Key
    // 遍历整个树
    for (let i = 0; i < tree.length; i++) {
      const node = tree[i]
      // 如果某个树 存在孩子
      // 不存在孩子 不做判断 是因为他是一级节点 无需展开处理
      if (node.children) {
          // 看看孩子是否包含 search 传下来的key
        if (node.children.some(item => item.key === key)) {
          parentKey = node.key
          // 看看 孩子里面的孩子
          // 外面是平铺处理 即使返回的一级/二级树 等到下一次 遍历的时候 也能找到 一级作为父级展开喔
        } else if (getParentKey(key, node.children)) {
          parentKey = getParentKey(key, node.children)
        }
      }
    }

    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    return parentKey
  }

左侧数据 一键全选-反选-取消全选

  • 核心:就是当selectKey变化的时候 我们去做判断
  • 有两种方式 改变selectKey
  • 直接操作核心区域选项
  • 直接操作一键全选按钮
  • 那我们直接监听 selectKey 不就完成了嘛
  • 在此之前 先把一键全选逻辑补充
onCheckBoxChange 复制代码
  const onCheckBoxChange = (dataArr = [], e = null) => {
    const allLen = leftTreeAllKey?.length
    const checkLen = e ? checkedKeys?.length : dataArr?.length
    // 全选
    const isAllSelect = allLen === checkLen
    // 半选
    const isHalfSelect = allLen > checkLen
    // 这个就是 区分用户在哪里操作 selectKey了 
    // 如果存在这个e 代表在操作 一键全选按钮
    // 否则就在操作 选项
    if (!e) {
      // 如果长度为0 未选态
      if (checkLen === 0) {
        setCheckAll(false)
        setIndeterminate(false)
        return ''
      }
      // 如果为全选 全选态
      if (isAllSelect) {
        setCheckAll(true)
        setIndeterminate(false)
      }
      // 半选态
      if (isHalfSelect) {
        setIndeterminate(true)
        setCheckAll(false)
      }
    }
    if (e) {
    // 反选
      if (checkLen === 0) {
        setCheckAll(true)
        setIndeterminate(false)
        setCheckedKeys(leftTreeAllKey)
      }
      // 取消全选
      if (isAllSelect) {
        setCheckAll(false)
        setIndeterminate(false)
        setCheckedKeys([])
      }
        // 反选
      if (isHalfSelect) {
        setCheckAll(true)
        setIndeterminate(false)
        setCheckedKeys(leftTreeAllKey)
      }
    }
  }

结束

都第二篇了,还不留点痕迹,是怕我发现么?

上一篇

下一篇

相关推荐
GIS程序媛—椰子28 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00135 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端37 分钟前
Content Security Policy (CSP)
前端·javascript·面试
木舟100941 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习
攻城狮_Dream1 小时前
“探索未来医疗:生成式人工智能在医疗领域的革命性应用“
人工智能·设计·医疗·毕业