前言:因为我是半途接手,之前的前端已经做了一部分,所以有些东西是二次修改,代码冗余之类的请勿在意(功能实现就好),只是一个小总结,有空优化~
效果:
基本功能:左侧定位栏(大类),中间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={{ 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;
至此一个复杂的要死套来套去的自定义列组件就做好啦,呜呜呜呜