前言:因為我是半途接手,之前的前端已經做了一部分,所以有些東西是二次修改,代碼冗余之類的請勿在意(功能實現就好),只是一個小總結,有空優化~
效果:

基本功能:左側定位欄(大類),中間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;
至此一個復雜的要死套來套去的自定義列組件就做好啦,嗚嗚嗚嗚
