现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!
常用库有 element、antd、iView、antd pro
,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。
接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步
。
需求:
动态列表格
,就是一个表格,存在默认列,但是支持我们操控,实现动态效果
实现效果
默认表格配置
默认列配置
动态列组件支持查询
动态列组件支持勾选
动态列组件支持清空
动态列组件支持一键全选
动态列组件支持一键清空
功能点划分
- 表格默认列和动态列组件默认选中项 实现双向绑定
- 动态列组件 增删改与表格 实现双向绑定
- 动态列组件 实现搜索
- 动态列组件 实现单点控制 添加与删除
- 动态列组件 实现一键控制功能 全选与清空
- 动态列组件 实现恢复初始态
使用到组件(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 只有内部处理数据时生效
动态列组件布局
- 这里使用了Tailwind CSS
<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的变化 去触发开关 避免我们第一种缺点 (优解)
同时解决了确保外面传进来的 表格列 使得下拉框选中这些项 生效
- 二、监听选项key的变化 去触发开关 避免我们第一种缺点 (优解)
// 这里通过 useEffect() 实现监听 确保外面传进来的 表格列 使得下拉框选中这些项 生效
useEffect(() => {
onCheckBoxChange(selectKey)
onCheckChange(selectKey)
// eslint-disable-next-line react-hooks/exhaustive-deps },
[selectKey]
)
结束
都看到这里了,不留点痕迹,是怕我发现么?