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

7,695 阅读10分钟

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

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

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

需求:

动态列表格,就是一个表格,存在默认列,但是支持我们操控,实现动态效果

实现效果

默认表格配置

image.png

默认列配置

image.png

动态列组件支持查询

image.png

动态列组件支持勾选

image.png

动态列组件支持清空

image.png

动态列组件支持一键全选

image.png

动态列组件支持一键清空

image.png

功能点划分

  • 表格默认列和动态列组件默认选中项 实现双向绑定
  • 动态列组件 增删改与表格 实现双向绑定
  • 动态列组件 实现搜索
  • 动态列组件 实现单点控制 添加与删除
  • 动态列组件 实现一键控制功能 全选与清空
  • 动态列组件 实现恢复初始态

使用到组件(Antd 组件库哈)

  • Table
  • Pagination
  • Modal
  • Input
  • Button
  • Checkbox

动态列组件区域划分

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

动态列组件最终可支持配置项

  open?: boolean // Modal状态
  setOpen?: React.Dispatch<React.SetStateAction<boolean>> // 控制Modal状态
  modalTitle?: string | React.ReactNode
  modalWidth?: number
  modalHeadContent?: React.ReactNode
  leftHeadContent?: React.ReactNode | string
  rightHeadContent?: React.ReactNode | string
  modalBodyStyle?: any
  searchPlaceholder?: string
  modalOk?: (val, isUseDefaultData?: boolean) => void // 第二个参数 内部数据处理支持
  enableSelectAll?: boolean // 是否开启全选功能
  selectData: SelectItem[] // 下拉框数据
  isOutEmitData?: boolean
  defaultSelectKeyList?: string[] // 默认选中的key(当前表格列)自定义数据(外部做逻辑处理)
  initSelectKey?: string[] // 初始表格选中的key 自定义数据(外部做逻辑处理)
  curColumns?: any[] // 当前表格列 内部做逻辑处理
  originColumns?: any[] // 原始表格列 内部做逻辑处理
  isDelHeadCol?: boolean // 删除头部columnKey 例如序号 只有内部处理数据时生效
  isDelTailCol?: boolean // 删除尾部columnKey 例如操作 只有内部处理数据时生效
  isDelHeadAndTail?: boolean // 删除头尾部columnKey 只有内部处理数据时生效

动态列组件布局

    <div>
        // 头部内容区
        <div>{modalHeadContent}</div> 
        // 以下维核心区
        <div className="content-box flex">
          <div className="content-left-box flex-1 flex flex-col">
              // 核心区-左边
            <div className="content-left-head flex">
                // 核心区-功能按钮 - 一键全选
              {enableSelectAll && (
                <div>
                  <Checkbox onChange={e => onCheckBoxChange([], e)} checked={checkAll} indeterminate={indeterminate}>
                    全选
                  </Checkbox>
                </div>
              )}
              <div className="right-head-content">{leftHeadContent || ''}</div>
            </div>
            <div className="content-left-main flex-1 flex flex-col">
                // 核心区-左搜索
              <div>{childSearchRender({ curData: leftSelectData })}</div>
                 // 核心区-左列表区域
              <div className="flex-1 left-select-box">{selectItemRender(leftSelectData)}</div>
            </div>
          </div>
           // 核心区-右边
          <div className="content-right-box flex-1 flex flex-col">
            <div className="content-right-head flex">
              <div className="flex-1 right-head-content">{rightHeadContent || ''}</div>
              // 核心区-功能按钮 - 一键清空
              <div className="content-right-head-clear" onClick={() => handleRightClearSelectData()}>
                清空
              </div>
            </div>
            <div className="content-right-main flex-1 flex flex-col">
                // 核心区-右搜索
              <div>{childSearchRender({ curData: rightSelectData }, true)}</div>
              // 核心区-右列表区域
              <div className="flex-1 right-select-box">{selectItemRender(rightSelectData, true)}</div>
            </div>
          </div>
        </div>
      </div>

动态列组件-列表渲染

const selectItemRender = (listArr = [], isRight = false) => {
    return (
      <div className="h-full overflow-y-auto">
        <Checkbox.Group onChange={onCheckChange} className="flex-col h-full flex" value={selectKey}>
            // 数据遍历形式
          {listArr?.map(({ label, value, disabled = false }) => (
            <div className="mt-2 flex w-full" key={value}>
              {!isRight && (
                <Checkbox value={value} className="flex w-full" disabled={disabled}>
                  <span className="flex-1 inline-block">{label}</span>
                </Checkbox>
              )}
                 // 判断是否是 右边列表区域 添加删除按钮
              {isRight && <span className="flex-1 display-box">{label}</span>}
              {isRight && (
                <div className="text-right display-box">
                  <Button type="text" className="text-right" onClick={() => deleteRightData(value)} size="small">
                    <DeleteOutlined className="icon-box" />
                  </Button>
                </div>
              )}
            </div>
          ))}
        </Checkbox.Group>
      </div>
    )
  }

动态列组件-搜索渲染

const childSearchRender = (childSearchProps: any, isRight = false) => {
    // eslint-disable-next-line react/prop-types
    const { curData } = childSearchProps
    return (
      <Search
        style={{ marginBottom: 8 }}
        placeholder={searchPlaceholder || '请输入'}
        onSearch={e => {
          onSearch(e, curData, isRight)
        }}
        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;
    height: calc(100% - 50px);
    box-sizing: border-box;
  }
  .content-right-main {
    padding: 10px 20px 0 20px;
    height: calc(100% - 50px);
    box-sizing: border-box;
  }
  .right-head-content {
    font-weight: 700;
    color: #151e29;
    font-size: 14px;
  }
  .modal-head-box {
    color: #151e29;
    font-size: 14px;
    height: 30px;
  }
  .icon-box {
    color: #f4513b;
  }
  .ant-checkbox-group {
    flex-wrap: nowrap;
  }
  .left-select-box {
    height: 440px;
    padding-bottom: 10px;
  }
  .right-select-box {
    height: 440px;
    padding-bottom: 10px;
  }
  .ant-checkbox-wrapper {
    align-items: center;
  }
  .display-box {
    height: 22px;
  }

功能点逐一拆解实现

点1:表格默认列和动态列组件默认选中项 实现双向绑定

  • 首先,先写一个表格啦,确定列,这个就不用代码展示了吧,CV大法
  • 其次,把表格原始列注入动态列组件当中,再者注入当前表格列当前能选择的所有项
  • 当前能选择所有项内容参数示例
[
    { label: '项目编码', value: 'projectCode' },
    { label: '项目名称', value: 'projectName' },
    { label: '项目公司', value: 'company' },
    { label: '标段', value: 'lot' },
 ]
<DynamicColumnModal
      selectData={selectDataArr} // 当前能选择的所有项
      curColumns={columns} // 当前表格列
      originColumns={originColumns} // 表格原始列
          />
  • 动态组件内部默认选中当前表格列
  • 这里需要把表格列数据 进行过滤 映射成 string[]
   内部是通过checkbox.Grop 实现选中 我们只需要 通过一个状态去控制即可 `selectKey`
   <Checkbox.Group onChange={onCheckChange} className="flex-col h-full flex" value={selectKey}>
       ...
   </Checkbox.Group>

动态列组件 单点控制 增删改

  • 增,删,改就是实现 左右边列表的双向绑定
  • 监听 左边勾选事件 + 右边删除事件 + 一键清空事件
  • 通过左右两边的状态 控制数据即可
  • 状态
  const [originSelectData, setOriginSelectData] = useState([]) // 下拉原始数据 搜索需要
  const [originRightSelectData, setOriginRightSelectData] = useState([])
  const [rightSelectData, setRightSelectData] = useState([])
  const [selectKey, setSelectKey] = useState([])
  const [transferObj, setTransferObj] = useState({})
  const [indeterminate, setIndeterminate] = useState(false)
  const [checkAll, setCheckAll] = useState(false)
  const [leftSelectData, setLeftSelectData] = useState([])
  const [defaultSelectKey, setDefaultSelectKey] = useState([])
  const [originSelectKey, setOriginSelectKey] = useState([])
const onCheckChange = checkedValues => {
    // 往右边追加数据
    const selectResArr = checkedValues?.map(val => transferObj[val])
    setSelectKey(checkedValues) // 我们选中的key (选项)
    setRightSelectData(selectResArr) // 右边列表数据
    setOriginRightSelectData(selectResArr) // 右边原数据(搜索时候需要)
  }
const deleteRightData = key => {
    const preRightData = rightSelectData
    const filterResKeyArr = preRightData?.filter(it => it.value !== key).map(it => it.value) // 数据处理 只要你不等于删除的key 保留
    const filterResItemArr = preRightData?.filter(it => it.value !== key)
    setRightSelectData(filterResItemArr) // 更新右边数据 即可刷新右边列表
    setOriginRightSelectData(filterResItemArr) // 更新右边数据
    setSelectKey(filterResKeyArr) // 更新选中的key 即可刷新左边选中项
  }
  const handleRightClearSelectData = () => {
      // 这就暴力了塞
    setSelectKey([])
    setRightSelectData([])
    setOriginRightSelectData([])
  }

动态列组件 实现搜索

  • 搜索,就是改变一下数据 视觉上看起来有过滤的效果塞
  • 刚才我们不是多存了一份数据源嘛
  • 出来见见啦~
const onSearch = (val, curData, isRight = false) => {
    const searchKey = val
    // 这个是同时支持 左右两边
    // 做个判断
    if (!isRight) {
     // 在判断一下是否有搜索内容 因为也需要清空的啦
      if (searchKey) {
          // 有,我就过滤呗
        const searchResArr = curData?.filter(item => item.label.includes(searchKey))
        setLeftSelectData(searchResArr)
      }
      if (!searchKey) {
          // 没有 我就把原本数据还给你呗
        setLeftSelectData(originSelectData)
      }
    }
    // 右边 一样
    if (isRight) {
      if (searchKey) {
        const searchResArr = curData?.filter(item => item.label.includes(searchKey))
        setRightSelectData(searchResArr)
      }
      if (!searchKey) {
        setRightSelectData(originRightSelectData)
      }
    }
  }

动态列组件 增删改与表格 实现数据绑定

  • 里面的数据 处理好了 直接再关闭的时候 丢给外面嘛
  • 把右边的内容(也就是选中的key)返回给表格
  • 表格再自己构造
  const handleOk = (colVal, isUseDefaultCol) => {
      `colVal` : 选中的列key
      `isUseDefaultCol`:是否使用默认列
      // table column字段组装
    const normalColConstructor = (
      title,
      dataIndex,
      isSort = true,
      colWidth = 150,
      isEllipsis = false,
      render = null
    ) => {
      const renderObj = render ? { render } : {}
      return {
        title,
        dataIndex,
        sorter: isSort,
        width: colWidth,
        ellipsis: isEllipsis,
        key: dataIndex,
        ...renderObj,
      }
    }
    const statusRender = text => approvalStatusRender(text)
    const dateRender = (text, record) => <span>{dayjs(text).format('YYYY-MM-DD')}</span>
    const newColArr = []
    // 定制化处理 (其实还有2.0)
    colVal?.forEach(({ label, value }, index) => {
      let isSort = false
      let renderFn = null
      const isSubmissionAmount = value === 'submissionAmount'
      const isApprovalAmount = value === 'approvalAmount'
      const isReductionRate = value === 'reductionRate'
      const isInitiationTime = value === 'initiationTime'
      
      // 特定的业务场景 特殊状态渲染
      const isStatus = value === 'status'
      // 特定的业务场景 时间类型 加上排序
      if (isApprovalAmount || isInitiationTime || isReductionRate || isSubmissionAmount) {
        isSort = true
      }
      if (isStatus) {
        renderFn = statusRender
      }
      // 普通列 已就绪
      // 普通列 标题 拿label就ok
      newColArr.push(normalColConstructor(label, value, isSort, 100, true, renderFn))
    })

    // 最后在头部追加一个序号
    newColArr.unshift({
      title: '序号',
      dataIndex: 'orderCode',
      width: 45,
      render: (_: any, record: any, index) => tablePageSize * (tablePage - 1) + index + 1,
    })
    // 最后在尾部部追加一个操作
    newColArr.push({
      title: '操作',
      dataIndex: 'action',
      fixed: 'right',
      width: 50,
      render: (text, row: DataType) => (
        <div>
          <Button type="link" className="tableBtn" onClick={() => console.log('详情')}>
            详情
          </Button>
        </div>
      ),
    })

    if (colVal?.length) {
      if (isUseDefaultCol) {
        setColumns([...originColumns])
      } else {
        setColumns([...newColArr])
      }
    } else {
      setColumns([...originColumns])
    }
  }
// 解决表格存在子列 -- 先搞定数据结构 -- 在解决表格列内容填充 -- 无需捆绑
// 遍历拿到新增列数组 匹配上代表 需要子数组 进行转换
    // eslint-disable-next-line consistent-return
    colVal?.forEach(({ label, value }, index) => {
      // DesignHomeDynamicCol[value] 返回的值能匹配到 说明存在 嵌套列
      const validVal = DesignHomeDynamicClassify[DesignHomeDynamicCol[value]]
      const isHasChild = newColChildObj[validVal]
      const titleText = DesignHomeDynamicLabel[value]
      if (validVal) {
        // 如果已经有孩子 追加子列
        if (isHasChild) {
          newColChildObj[validVal] = [...isHasChild, normalColConstructor(titleText, value)]
        } else {
          // 则 新增
          newColChildObj[validVal] = [normalColConstructor(titleText, value)]
        }
      } else {
        // 普通列 已就绪
        // 普通列 标题 拿label就ok
        newColArr.push(normalColConstructor(label, value, false, 100, true))
      }
    })

动态列组件 实现恢复初始态 实现双向绑定

  • 这个就更简单啦 再点击确定的时候 传一个 isUseDefaultData:true
  • 只是这个isUseDefaultData 的逻辑判断问题
  • 当动态列组件 点击恢复默认列 我们只需把 当初传进来的 原始列数据 更新到 selectKey 状态即可
const handleDefaultCol = () => {
    // 这里是考虑到组件灵活性 数据可由自己处理好在传入
    if (isOutEmitData) {
        setSelectKey(initSelectKey) 
      } else {
      // 这里是使用 内部数据处理逻辑
        setSelectKey(originSelectKey) 
    }
}
const handleOk = () => { 
// 数据比对 是否使用默认校验
// originColumnMapSelectKey 源数据与传出去的数据 进行比对
const originRightMapKey = originRightSelectData?.map(it => it.value) 
// 采用 lodash isEqual 方法
const isSame = isEqual(originSelectKey, originRightMapKey) 
// 判断外部是否有传 确定事件 handleOk
if (modalOk) { 
modalOk(originRightSelectData, isSame) 
} 
setOpen(false) 
}
const handleOk = (colVal, isUseDefaultCol) => {
... 一堆代码
// 当用户清空以后 还是恢复表格默认状态
 if (colVal?.length) {
     // 恢复默认列
     if (isUseDefaultCol) {
         setColumns([...originColumns]) 
       } else {
           // 否则就拿新数据更新
         setColumns([...newColArr])
       }
 } else { 
 setColumns([...originColumns]) 
 }
 }

动态列组件 实现一键控制功能 全选与清空

  • 这就是Vip版本的噻
  • 但是也简单 无非就是操作多选框 无非多选框就三种态
  • 未选 半选 全选
  • 既然我们下面的逻辑已处理好 这个其实也很快的锅
  • 首先,就是下面数据变化的时候 我们上面需要去感应
  • 其次就是 上面操作的时候 下面也需要感应
  • 最后 双向数据绑定 就能搞定 没有那么神秘
  • 一步一步来 先分别把 上下事件处理好
const onCheckBoxChange = (dataArr = [], e = null) => {
// 判断所有数据长度
const allLen = originSelectData?.length 
// 根据当前选中数据长度 判断是 多选框三种状态当中的哪一种
const checkLen = e ? selectKey?.length : dataArr?.length // 全选 
const isAllSelect = allLen === checkLen // 半选 
const isHalfSelect = allLen > checkLen 
// 然后再判断一下是点击一键全选事件 触发还是 点击下面选项的时候触发
// 点击一键全选 能拿到事件的 e.target 从而来判断
// 这里是操作下面按钮的时候 触发
if (!e) { 
// 如果没有选中
if (checkLen === 0) { 
// 恢复未选状态
setCheckAll(false)
setIndeterminate(false)
return ''
} 
if (isAllSelect) { 
// 如果是全选 改为全选态
setCheckAll(true)
setIndeterminate(false) 
}
if (isHalfSelect) {
// 半选态
setIndeterminate(true) // 这个控制 多选框的半选态
setCheckAll(false) 
}
}
// 这个就是用户操作 一键全选按钮触发
if (e) {
// 如果当前长度为0 那么应该更新为全选
if (checkLen === 0) { 
setCheckAll(true)
setIndeterminate(false)
setSelectKey(originSelectData?.map(it => it.value))
} 
// 如果已经全选 就取消全选
if (isAllSelect) { 
setCheckAll(false)
setIndeterminate(false)
setSelectKey([]) 
} 
// 如果是半选态 就全选
if (isHalfSelect) { 
setCheckAll(true)
setIndeterminate(false) 
setSelectKey(originSelectData?.map(it => it.value))
}
} 
}
const onCheckChange = checkedValues => {
// 往右边追加数据 
const selectResArr = checkedValues?.map(val => transferObj[val]) setSelectKey(checkedValues) 
setRightSelectData(selectResArr) 
setOriginRightSelectData(selectResArr) 
}
  • 我们两个事件都处理好 那么开始进行联动
  • 意思就是 我们拿什么去控制这两个机关 核心是不是就是 选中的选项啊
  • 有两种解法,第二种可能有点绕
    • 一、就是在下面每个选项改变的时候 都去调一下 上面一键全选事件;然后上面变化的时候 也去调一下 下面选项的事件 (常规)缺点:容易漏 一变多改
    • 二、监听选项key的变化 去触发开关 避免我们第一种缺点 (优解)同时解决了确保外面传进来的 表格列 使得下拉框选中这些项 生效
// 这里通过 useEffect() 实现监听 确保外面传进来的 表格列 使得下拉框选中这些项 生效 
useEffect(() => { 
onCheckBoxChange(selectKey) 
onCheckChange(selectKey) 
// eslint-disable-next-line react-hooks/exhaustive-deps },
[selectKey]
)

结束

都看到这里了,不留点痕迹,是怕我发现么?

下一篇

juejin.cn/post/726679…