【React+antd】做一个自定义列的组件
前言:因为我是半途接手,之前的前端已经做了一部分,所以有些东西是二次修改,代码冗余之类的请勿在意(功能实现就好),只是一个小总结,有空优化~
效果:

基本功能:左侧定位栏(大类),中间checkbox.group用来选择,右侧展示已选择的数据&&排序&删除功能,上面搜索列
首先我们需要的数据先定义一下:
state: ProState = {
active: null, // 用来设置初始化首位
indicatorsListRef: {}, // 用于定位
checkIndicatorsList: [], //用来存储传入格式的数组
defaultIndicatorsList: [], // 用来存储右边选中数组
checkArr: this.props.checkArr || [], // 传入的全部数据,大类里包着对应数据的格式
navList: this.props.navList || [], // 标题数组
itemOrder: this.props.itemOrder || [] // 传入-右边选中数组
};
传入的数据格式是这样的:

所以我们为了方便右边已选列表展示,定义一个方法来获取勾选结果:
/**
* @description 获取勾选结果
* @param {array} data 勾选的数据
*/
getDefaultList = (data: any) => {
const resultArr: any[] = [];
data && data.forEach((check: any) => {
if (check.defaultCheckedList) {
resultArr.push(check.defaultCheckedList);
}
});
const list: any[] = resultArr && resultArr.length > 0&& resultArr.reduce((pre: any, next: any) => {
return pre.concat(next);
});
return list;
};
格式是这样的:

jsx是这样滴:
renderContent = () => {
const {
active,
checkIndicatorsList,
indicatorsListRef,
defaultIndicatorsList,
navList,
} = this.state;
return (
<>
<div style={{ display: 'flex' }}>
<div className={styles['m-add-indicators']}>
<TheListTitle title="可添加的指标">
<Search
placeholder="请输入列名称搜索"
// onChange={this.searchColumnChange}
onSearch={this.searchColumn}
style={{ width: 300 }}
/>
</TheListTitle>
<div style={{ display: 'flex' }}>
<div className={styles['m-add-indicators-nav']}>
{navList.map((item: any) => {
return (
<div className={styles['m-add-indicators-nav-item']} key={item.id}>
<div className={styles['m-add-indicators-nav-title']}> {item.value}</div>
{item.subList &&
item.subList.map((el: any) => {
return (
<div
className={`${styles['m-add-indicators-nav-subtitle']} ${
active && active.id === el.id
? styles['m-add-indicators-nav-subtitle-active']
: ''
}`}
key={el.id}
onClick={() => this.handleAnchor(el)}
>
{el.label}
</div>
);
})}
</div>
);
})}
</div>
<div
className={styles['m-select-content']}
ref={(el: any) => {
this.checkboxRef = el;
}}
style={{position:'relative'}}
>
{checkIndicatorsList.map((item: any) => {
return (
<div
ref={(el: any) => {
indicatorsListRef[item.id] = el;
}}
key={item.id}
>
<div className={`${styles['m-select-item']}${item.list.filter((item:any)=>item.hidden === false).length > 0 ? ` m-select-item-show` : ` m-select-item-hidden`}`}>
<BasisCheckbox
allName={item.label}
plainOptions={item.list}
defaultCheckedList={item.defaultCheckedList || []}
getGroupCheckedResult={(values) => {
this.getGroupCheckedResult(values, item);
}}
/>
</div>
</div>
);
})}
</div>
</div>
</div>
<DragResultList list={defaultIndicatorsList} deleteItem={this.deleteItem} />
</div>
</>
);
};
定义一个初始化列表的方法,以及处理数据更新时触发数据重新渲染:
/**
* @description 页面初始化加载
*/
componentDidMount() {
this.initList();
}
/**
* @description 页面props更新
*/
componentWillReceiveProps(nextProps:any) {
// 如果不是第一次数据改变,不触发重初始化
const { checkArr } =this.state;
if(JSON.stringify(nextProps.checkArr) === JSON.stringify(checkArr)) return;
this.setState({
checkArr: nextProps.checkArr,
navList: nextProps.navList,
itemOrder: nextProps.itemOrder
},() => {
this.initList();
});
}
initList = () =>{
const {navList , checkArr, itemOrder } = this.state;
if (navList && navList.length > 0 && navList[0].subList) {
// 设置初始化首位
this.setState({
active: navList[0].subList[0],
});
}
// 默认选中项列表
checkArr && checkArr.length > 0 && checkArr.forEach((item: any) => {
item.defaultCheckedList = item.list && item.list.filter((el: any) => el.state === 1);
item.list && item.list.forEach((el: any) => { el.hidden = false });
});
let initItemOrder:any = [];
if(itemOrder && itemOrder.length && itemOrder.length > 0){
initItemOrder = itemOrder && itemOrder.map((el: any) => {
return {...el,hidden: false}
})
}else {
initItemOrder = this.getDefaultList(checkArr) && this.getDefaultList(checkArr).map((el: any) => {
return {...el,hidden: false}
})
}
this.setState({
checkIndicatorsList: checkArr,
defaultIndicatorsList: initItemOrder || []
});
}
关于中间那块的数据改变,页面会返回对应的values(之前的大佬写的),这个values包括了两个数组,defaultList和checkedList,但是它defaultList就是当前这个group的对象数组,checkedList就是id的数组,如果这个group没有选中东西,它就是空的,对外层的使用略不友好,尽管如此因为实在没时间去重构了所以我直接用了QAQ,外层定义一个checkbox.group改变时的callback的方法:
/**
* @description checkbox 结果
* @param {any} values
* @param {any} item 项
*/
getGroupCheckedResult = (values: any, item: any) => {
const { checkIndicatorsList, defaultIndicatorsList } = this.state;
checkIndicatorsList.forEach((el: any) => {
if (el.id === item.id) {
el.defaultCheckedList = values.defaultList;
}
});
// 新的数组>旧的数组 => add
if(this.getDefaultList(checkIndicatorsList).length > defaultIndicatorsList.length){
const resIDs = defaultIndicatorsList.map(item => item.id)
// 新增的
const diff = this.getDefaultList(checkIndicatorsList).filter(item => !resIDs.includes(item.id))
this.setState({
defaultIndicatorsList: defaultIndicatorsList.concat([...diff]),
});
}else{
const resIDs = this.getDefaultList(checkIndicatorsList).map(item => item.id)
// 删掉的
const diff = defaultIndicatorsList.filter(item => !resIDs.includes(item.id))
const diffIDs = diff.map(item => item.id)
const newArr = defaultIndicatorsList.filter(item => !diffIDs.includes(item.id))
this.setState({
defaultIndicatorsList: newArr,
});
}
};
也就是其实实际右侧的展示其实是直接通过返回的values.defaultList跟源数据进行了处理,然后通过getDefaultList获取到了目前整个中间选中的项。
因为每次操作都只是增加或者删除单独的一项,我们就得到了onChange之后的值并且进行了处理和展示,所以我的逻辑很简单:
如果是增加,那么新旧数组之差集就是新增的,通过filter筛选出来新数组返回来的这条新增的数据(因为是条对象),concat接到旧数组后面,这样就可以按照添加的顺序依次展示在右侧了~
如果是删除,那么新旧数组之差集就是删除的,把这一项的id获取到,filter出旧数组id不等于这个id的其他项保存,就正常删除了~如果是普通数组就更方便了,splice(id,1)舒服的要死 TAT
实现了中间对已选栏的增删后,就是已选栏自己的删除和排序了,在大佬封装的DragResultList这个组件里,删除返回的是当前item对象,清空返回的是-1,所以外层删除的方法就是这样:
/**
* @description 删除项
* @param {object | number } item 项 -1全部
*/
deleteItem = (item: any) => {
const { checkIndicatorsList, defaultIndicatorsList } = this.state;
// -1 === 清空
if (item !== -1) {
checkIndicatorsList.forEach((check: any) => {
if (check.defaultCheckedList) {
check.defaultCheckedList = check.defaultCheckedList.filter((el: any) => el.id !== item.id);
}
});
this.setState({
defaultIndicatorsList: defaultIndicatorsList && defaultIndicatorsList.filter((element:any)=> element.id !== item.id ),
});
} else {
checkIndicatorsList.forEach((check: any) => {
check.defaultCheckedList = [];
});
this.setState({
defaultIndicatorsList: [],
});
}
};
哦还有搜索列功能~:
/**
* 搜索列名称
* @param {string} value - 搜索的值
*/
searchColumn = (value: string) => {
this.setState({
searchColumnValue: value
})
const { checkIndicatorsList } = this.state;
const newCheckArr = JSON.parse(JSON.stringify(checkIndicatorsList))
// 默认选中项列表
newCheckArr && newCheckArr.length > 0 && newCheckArr.forEach((item: any) => {
item.defaultCheckedList = item.list && item.list.filter((el: any) => el.state === 1);
item.list && item.list.forEach((el: any) => {
if(el.value.indexOf(value) !== -1){
el.hidden = false
}else{
el.hidden = true
}
});
});
this.setState({
checkIndicatorsList: newCheckArr,
});
};
这里就是我们为什么初始化的时候全部统一给数据加上hidden为false的原因了,因为这个hidden属性是用来控制是否展示的!(夸一下机智的我)
看看效果:

然后就是提交数据了,这个没有什么好说的,给父组件一个是全部数据,一个是排序数据,父组件那边可以随便选用:
/**
* @description 确定
*/
onSubmit = async () => {
const { channelId } = this.props;
if (!channelId) return;
const { checkIndicatorsList, defaultIndicatorsList } = this.state;
// 处理当前选中的指标项数据,提交
const newList = checkIndicatorsList.map((item:any) => {
let newObj = { id: item.id, label: item.label, list: [] as any };
item.list && item.list.forEach((listItem:any) => {
const newItem = {
id: listItem.id,
value: listItem.value,
state: 0,
isOrder: listItem.isOrder,
};
defaultIndicatorsList && defaultIndicatorsList.forEach((obj:any) => {
if (newItem.id == obj.id) {
newItem.state = 1;
}
})
newObj.list.push(newItem)
})
return newObj
});
if(this.props.onSubmitCallback) this.props.onSubmitCallback(newList, defaultIndicatorsList)
};
至于右侧的拖拽列表(DragResultList)组件不是我写的,也附上代码一起学习叭:
// 第三方库
import React, { useEffect, useState } from 'react';
import { Space } from 'antd';
import {
UnorderedListOutlined,
LockOutlined,
DeleteOutlined,
VerticalAlignTopOutlined,
} from '@ant-design/icons';
// 组件
import { BasisEmpty } from '@/components/index';
import { TheCardDragList, TheListTitle } from '@/modules';
// 类型声明
import { Props } from './index.type';
// 样式
import styles from './style.less';
// 交换数组索引位置位置
function swapPositions(arr: any[], preIndex: number, nextIndex: number) {
arr[preIndex] = arr.splice(nextIndex, 1, arr[preIndex])[0];
return arr;
}
// 拖拽列表
const DragResultList: React.FC<Props> = (props) => {
const { list, deleteItem } = props;
// 结果列表
const [resultList, setResultList] = useState<any[]>([]);
// 置顶Id
const [unTopId, setUnTopId] = useState<number>(-1);
/**
* 设置为置顶
* @param {array} data -数据
*/
const setTopId = (data: any[]) => {
if(data.length !== 0){
// 非固定数组
const unfixArr: any[] = data.filter((el: any) => !el.disabled);
if (unfixArr && unfixArr.length > 0) {
setUnTopId(unfixArr[0].id);
}
}else{
setUnTopId(-1);
}
};
useEffect(() => {
// if (list && list.length > 0) {
setResultList(list);
setTopId(list);
// }
}, [list]);
/**
* 删除
* @param {object} data -删除的项
*/
const handleDelete = (data: any) => {
deleteItem && deleteItem(data);
};
/**
* 向上置顶
* @param {object} data -置顶的项
*/
const handlePlaceTop = (data: any) => {
// 需要置顶的项
const preIndex: number = resultList.findIndex((el: any) => el.id === data.id);
// 当前置顶
const nowIndex: number = resultList.findIndex((el: any) => el.id === unTopId);
if (nowIndex !== -1) {
const arr: any = swapPositions(resultList, preIndex, nowIndex);
setResultList(arr);
const unfixArr: any[] = arr.filter((el: any) => !el.disabled);
setUnTopId(unfixArr[0].id);
}
};
/**
* 获取拖拽结果
* @param {array} data -拖拽数据
*/
const getDrageList = (data: any[]) => {
setResultList(data);
setTopId(data);
};
/**
* 构建卡片
* @param {object} item -卡片项
*/
const renderCardItem = (item: any) => {
return (
<div className={styles['m-card-item']}>
{!item.disabled ? <UnorderedListOutlined style={{ cursor: 'move' }} /> : <LockOutlined />}
<div className={`${styles['m-card-item-title']} ${!item.disabled ? 'cursor_move' : ''}`}>
{item.value}
</div>
{!item.disabled && (
<Space>
{unTopId !== item.id && (
<VerticalAlignTopOutlined
style={{ cursor: 'pointer' }}
onClick={() => handlePlaceTop(item)}
/>
)}
<DeleteOutlined style={{ cursor: 'pointer' }} onClick={() => handleDelete(item)} />
</Space>
)}
</div>
);
};
// 构建已选结果
const renderList = () => {
return resultList && resultList.length > 0 ? (
<TheCardDragList
className={styles['m-drag-list']}
list={resultList}
renderCardItem={renderCardItem}
getDrageList={getDrageList}
/>
) : (
<BasisEmpty />
);
};
return (
<div className={styles['m-drag-result']}>
<TheListTitle list={resultList} clearItems={handleDelete} />
{renderList()}
</div>
);
};
export default DragResultList;
而左边的定位功能其实体验不太好,因为没有滚动效果,只是直接定位了中间的位置,但也记录一下大佬的代码共同学习:
/**
* @description 定位
* @param {object} item 项
*/
handleAnchor = (item: any) => {
this.setState({
active: item,
});
const { indicatorsListRef } = this.state;
const keysList: any[] = Object.keys(indicatorsListRef);
if (indicatorsListRef[item.id] && keysList.length > 0) {
this.checkboxRef.scrollTop =
indicatorsListRef[item.id].offsetTop - indicatorsListRef[keysList[0]].offsetTop;
}
};
关于这个我觉得也许可以考虑用antd的锚点组件,但是实在没空了QAQ回头有时间试一下
补充:
经过师傅的提醒,在中间的包裹层加scroll-behavior: smooth;这个样式,即可有滚动效果,滚动中间高亮左边可用 IntersectionObserver。
因为我是点击左边定位,没有滚动包裹层高亮的需求,因此不做赘述。
e.g.
学习文档:https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
阮一峰:http://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html
顺便大佬的BaseCheckbox的代码也贴一下,基本没改什么:
// 第三方库
import _ from 'lodash';
import { Checkbox, Tooltip } from 'antd';
import React, { useState, useEffect } from 'react';
// 类型声明
import { Props, CheckProps } from './index.type';
// 样式
import styles from './style.less';
const { Group } = Checkbox;
const BaseCheckbox: React.FC<Props> = (props) => {
// 父级数据
const {
plainOptions,
defaultCheckedList,
showAll,
showTip,
limit,
disabled,
allName,
showDefault,
defaultSystem,
getGroupCheckedResult,
} = props;
// 默认的id
const formattedCheckedList = defaultCheckedList && defaultCheckedList.map((item) => item.id);
// disabled 的 id
const disabledCheckedList =
plainOptions && plainOptions.filter((item: any) => item.disabled).map((item) => item.id);
// 初始化数据
const [state, setState] = useState<CheckProps>({
plainOptions: [],
checkedList: [],
indeterminate: false,
checkAll: false,
});
// 全选
const [allValuesChecked, setAllValuesChecked] = useState([]);
//默认
const [checkDefault, setDefaultCheck] = useState<any>(true);
/**
* @description 依赖默认项 与初始化数据
*/
useEffect(() => {
if (plainOptions) {
const formattedValues = plainOptions.map((item) => {
return { label: item.value, value: item.id, ...item };
});
// @ts-ignore
setAllValuesChecked(() => {
return plainOptions.map((item) => {
return item.id;
});
});
setState({
plainOptions: formattedValues,
checkedList: formattedCheckedList,
indeterminate:
!!formattedCheckedList.length && formattedCheckedList.length < plainOptions.length,
checkAll: formattedCheckedList.length === plainOptions.length,
});
}
}, [defaultCheckedList, plainOptions]);
// 监听点击事件 超过个数禁止点击
useEffect(() => {
if (!limit) return;
// 如果超过个数 未选中的 disabled true
if (state.checkedList && state.checkedList.length === limit) {
_.forEach(plainOptions, (o: any) => {
if (_.includes(state.checkedList, o.id)) {
o.disabled = false;
} else {
o.disabled = true;
}
});
} else {
_.forEach(plainOptions, (o: any) => {
o.disabled = false;
});
}
}, [state.checkedList, plainOptions]);
/**
* 改变选择项
* @param {array} checkedList - 已选的项
* @return 已选项为选择的项
*/
const onChange = (checkedList: any[]) => {
// console.log(checkedList,'--checkedList-', plainOptions)
// 传递给父级
const defaultList = plainOptions
.map((item: any) => {
if (checkedList.includes(item.id)) {
return item;
}
return null;
})
.filter((item) => item != null);
const checkInfo = {
...state,
checkedList,
indeterminate: !!checkedList.length && checkedList.length < plainOptions.length,
checkAll: checkedList.length === plainOptions.length,
};
setState(checkInfo);
if (getGroupCheckedResult) {
getGroupCheckedResult({
defaultList,
checkedList,
});
}
};
/**
* @description 依赖全选 与所选项
*/
useEffect(() => {
if (showDefault) {
setDefaultCheck(_.isEqual(state.checkedList, defaultSystem));
}
}, [state]);
/**
* @description 默认数据
* @return 已选项为默认数据
*/
const onCheckDefaultChange = () => {
if (!defaultSystem) return;
// props.changeDefautUse();
return setState({
...state,
indeterminate: !!defaultSystem.length && defaultSystem.length < plainOptions.length,
checkAll: defaultSystem.length === plainOptions.length,
checkedList: defaultSystem,
});
};
/**
* 全选
* @param {object} event - 原生项
* @return 已选项为全部选项
*/
const onCheckAllChange = (e: any) => {
// 控制全选的选项项
const checkedArr: any[] = e.target.checked
? allValuesChecked
: _.intersection(allValuesChecked, disabledCheckedList);
// 传递给父级
const defaultList = plainOptions
.map((item: any) => {
if (checkedArr.includes(item.id)) {
return item;
}
return null;
})
.filter((item) => item != null);
// 传递给父级
if (getGroupCheckedResult) {
getGroupCheckedResult({
defaultList,
checkedList: checkedArr,
checkAll: true,
});
}
setState({
...state,
checkedList: checkedArr,
indeterminate: false,
checkAll: e.target.checked,
});
};
// 全选与默认
const handleCheckbox = () => {
const unHideList:any[] = plainOptions && plainOptions.filter((item:any)=>item.hidden === false)
const hiddenGroup = unHideList && unHideList.length > 0 ? false : true;
return (
!hiddenGroup &&
<div>
{showAll && (
<Checkbox
indeterminate={state.indeterminate}
onChange={onCheckAllChange}
checked={state.checkAll}
>
{allName || (state.checkAll ? '取消全选' : '全选')}
</Checkbox>
)}
{showDefault && (
<Checkbox onChange={onCheckDefaultChange} checked={checkDefault}>
系统默认
</Checkbox>
)}
</div>
);
};
// checkbox组
const renderGroup = () => {
const unHideList:any[] = plainOptions && plainOptions.filter((item:any)=>item.hidden === false)
const hiddenGroup = unHideList && unHideList.length > 0 ? false : true;
return (
<>
{/* 带有提示Tooltip */}
{showTip ? (
!hiddenGroup &&
<Group disabled={disabled} value={state.checkedList} onChange={onChange}>
{plainOptions.map((o) => (
<Tooltip title={o.nouns} key={o.id} >
<Checkbox disabled={o.disabled} key={o.id} value={o.id} style={{display: o.hidden ? 'none' : 'inline-block'}}>
{o.value}
</Checkbox>
</Tooltip>
))}
</Group>
) : (
!hiddenGroup &&
<Group disabled={disabled} value={state.checkedList} onChange={onChange}>
{plainOptions.map((o) => (
<Checkbox disabled={o.disabled} key={o.id} value={o.id} style={{display: o.hidden ? 'none' : 'inline-block'}}>
{o.value}
{o.lock}
</Checkbox>
))}
</Group>
)}
</>
);
};
return (
<div className={styles['c-base-checkbox']}>
{state.plainOptions.length > 0 && (
<>
{showAll && handleCheckbox()}
{renderGroup()}
</>
)}
</div>
);
};
// 默认
BaseCheckbox.defaultProps = {
showAll: true,
showTip: false,
defaultCheckedList: [],
};
export default BaseCheckbox;
至此一个复杂的要死套来套去的自定义列组件就做好啦,呜呜呜呜

浙公网安备 33010602011771号