注册
web

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

现在市面的组件库,我们用的越发熟练,越发爆嗨,只需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> // 控制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 只有内部处理数据时生效

动态列组件布局



    

// 头部内容区
{modalHeadContent}

// 以下维核心区


// 核心区-左边

// 核心区-功能按钮 - 一键全选
{enableSelectAll && (

onCheckBoxChange([], e)} checked={checkAll} indeterminate={indeterminate}>
全选


)}
{leftHeadContent || ''}



// 核心区-左搜索
{childSearchRender({ curData: leftSelectData })}

// 核心区-左列表区域
{selectItemRender(leftSelectData)}



// 核心区-右边


{rightHeadContent || ''}

// 核心区-功能按钮 - 一键清空
handleRightClearSelectData()}>
清空



// 核心区-右搜索
{childSearchRender({ curData: rightSelectData }, true)}

// 核心区-右列表区域
{selectItemRender(rightSelectData, true)}






动态列组件-列表渲染


const selectItemRender = (listArr = [], isRight = false) => {
return (


// 数据遍历形式
{listArr?.map(({ label, value, disabled = false }) => (

{!isRight && (

{label}

)}
// 判断是否是 右边列表区域 添加删除按钮
{isRight && {label}}
{isRight && (

deleteRightData(value)} size="small">



)}

))}


)
}

动态列组件-搜索渲染


const childSearchRender = (childSearchProps: any, isRight = false) => {
// eslint-disable-next-line react/prop-types
const { curData } = childSearchProps
return (
{
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' },
]




  • 动态组件内部默认选中当前表格列
  • 这里需要把表格列数据 进行过滤 映射成 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) => {dayjs(text).format('YYYY-MM-DD')}
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) => (

console.log('详情')}>
详情


),
})

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/7266463919139684367

0 个评论

要回复文章请先登录注册