【React+antd】做一个自定义列的组件
前言:因为我是半途接手,之前的前端已经做了一部分,所以有些东西是二次修改,代码冗余之类的请勿在意(功能实现就好),只是一个小总结,有空优化~
效果:
基本功能:左侧定位栏(大类),中间checkbox.group用来选择,右侧展示已选择的数据&&排序&删除功能,上面搜索列
首先我们需要的数据先定义一下:
1 2 3 4 5 6 7 8 9 | state: ProState = { active: null , // 用来设置初始化首位 indicatorsListRef: {}, // 用于定位 checkIndicatorsList: [], //用来存储传入格式的数组 defaultIndicatorsList: [], // 用来存储右边选中数组 checkArr: this .props.checkArr || [], // 传入的全部数据,大类里包着对应数据的格式 navList: this .props.navList || [], // 标题数组 itemOrder: this .props.itemOrder || [] // 传入-右边选中数组 }; |
传入的数据格式是这样的:
所以我们为了方便右边已选列表展示,定义一个方法来获取勾选结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * @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是这样滴:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | 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> </> ); }; |
定义一个初始化列表的方法,以及处理数据更新时触发数据重新渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | /** * @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的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /** * @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,所以外层删除的方法就是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * @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: [], }); } }; |
哦还有搜索列功能~:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * 搜索列名称 * @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属性是用来控制是否展示的!(夸一下机智的我)
看看效果:
然后就是提交数据了,这个没有什么好说的,给父组件一个是全部数据,一个是排序数据,父组件那边可以随便选用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /** * @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)组件不是我写的,也附上代码一起学习叭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | // 第三方库 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; |
而左边的定位功能其实体验不太好,因为没有滚动效果,只是直接定位了中间的位置,但也记录一下大佬的代码共同学习:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * @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的代码也贴一下,基本没改什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | // 第三方库 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; |
至此一个复杂的要死套来套去的自定义列组件就做好啦,呜呜呜呜
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具