先上效果图~
import React, {
useState,
useCallback,
useMemo,
useEffect,
useRef,
} from 'react';
import {
Select,
SelectProps,
Space,
Checkbox,
Input,
Empty,
Tooltip,
} from 'antd';
import { debounce, isArray, isEmpty, isFunction } from 'lodash-es';
import classnames from 'classnames';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { useIntl } from 'umi';
type Key = string | number;
type OptionType = {
label: string | React.ReactNode;
value: Key;
};
type TypeOptMap = {
[key: string]: OptionType;
};
type CustomTagProps = {
label: React.ReactNode;
value: any;
disabled?: boolean;
onClose?: (event?: React.MouseEvent<HTMLElement, MouseEvent>) => void;
closable?: boolean;
};
type PluralSelectProps = Omit<
SelectProps,
'onChange' | 'options' | 'value' | 'mode'
> & {
onChange?: (value: Key[]) => void;
options?: OptionType[];
value?: Key[];
showSearch?: boolean;
};
const transformMap = (params: OptionType[]) => {
const result: { [key: string]: OptionType } = {};
if (isEmpty(params) || !isArray(params)) return result;
params.forEach((item) => {
result[item.value] = item;
});
return result;
};
const labelVisible = (searchValue: string, label: string) => {
return !searchValue || (searchValue && label.indexOf(searchValue) > -1);
};
const PluralSelect: React.FC<PluralSelectProps> = ({
options,
maxTagCount = 'responsive',
value = [],
onChange,
placeholder,
className,
dropdownClassName,
showSearch = true,
...props
}) => {
const { formatMessage } = useIntl();
const [currentValues, setCurrentValues] = useState<Key[]>(value);
const [searchValue, setSearchValue] = useState<string>('');
const searchInputRef: any = useRef();
const valueCacheRef = useRef<string>();
const optMap: TypeOptMap = useMemo(() => {
return transformMap(options || []) || {};
}, [options]);
const allValues = useMemo<Key[]>(() => {
return options?.map((opt) => opt?.value) || [];
}, [options]);
// 半选状态
const indeterminate = useMemo((): boolean => {
return (
Boolean(currentValues?.length) &&
isArray(options) &&
currentValues?.length < options.length
);
}, [currentValues, options]);
// 全选状态
const checkedAll = useMemo((): boolean => {
return Boolean(
options?.length && currentValues?.length === options?.length,
);
}, [currentValues, options]);
// 选择checkbox
const handleChange = useCallback(
(event: CheckboxChangeEvent) => {
const {
target: { value, checked },
} = event;
let mergedValues: Key[] = [];
if (checked) {
mergedValues = [...currentValues, value];
setCurrentValues(mergedValues);
} else {
mergedValues = currentValues.filter(
(each) => String(each) !== String(value),
);
setCurrentValues(mergedValues);
}
isFunction(onChange) && onChange?.(mergedValues);
},
[currentValues, onChange],
);
// 输入搜索条件
const handleInputKeyword = useCallback(() => {
const value = searchInputRef.current?.input?.value || '';
setSearchValue(value);
}, []);
// 搜索(防抖)
const debounceSearch = useMemo(() => debounce(handleInputKeyword, 200), [
handleInputKeyword,
]);
// 全选或者全不选
const handleCheckAllChange = useCallback(
(event: CheckboxChangeEvent) => {
const {
target: { checked },
} = event;
isFunction(onChange) && onChange?.(checked ? allValues : []);
setCurrentValues(checked ? allValues : []);
},
[onChange, allValues],
);
// select 变化
const handleSelectChange = useCallback(
(values: Key[]) => {
setCurrentValues(values);
isFunction(onChange) && onChange?.(values);
},
[onChange],
);
useEffect(() => {
// 当 value 发生变化时,更新 currentValues
const currentValueStr = value ? JSON.stringify(value) : '';
if (currentValueStr === valueCacheRef.current) return;
valueCacheRef.current = currentValueStr;
setCurrentValues(value);
}, [value]);
// 卸载时清空
useEffect(() => {
return () => {
setSearchValue('');
};
}, []);
const renderTitle = (title: string | React.ReactNode) => {
if (!searchValue) {
return <span>{title}</span>;
}
const strTitle = String(title);
const index = strTitle.indexOf(searchValue);
const beforeStr = strTitle.substring(0, index);
const afterStr = strTitle.substring(index + searchValue.length);
// 高亮搜索关键字
const currentTitle =
index > -1 ? (
<>
{beforeStr}
<span className="highlight-value">{searchValue}</span>
{afterStr}
</>
) : (
strTitle
);
return <span>{currentTitle}</span>;
};
const CheckboxOptions = () => {
if (isEmpty(options) || !isArray(options))
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
return (
<Space direction="vertical" className="content-inner-wapper">
<Checkbox
indeterminate={indeterminate}
onChange={handleCheckAllChange}
checked={checkedAll}
>
{formatMessage({ id: 'ACTION_SELECT_ALL' })}
</Checkbox>
<Checkbox.Group value={currentValues}>
<Space direction="vertical">
{isArray(options) &&
options.map(
({ label, value }, idx) =>
labelVisible(searchValue, String(label)) && (
<Checkbox
onChange={handleChange}
value={value}
key={`${idx}_${value}`}
>
{renderTitle(label)}
</Checkbox>
),
)}
</Space>
</Checkbox.Group>
</Space>
);
};
const tagRender = (props: CustomTagProps) => {
const { value } = props;
const { label } = optMap[value];
return (
<Tooltip placement="bottom" title={label}>
<span
className="ant-select-selection-item"
style={{ maxWidth: '80px' }}
>
{label}
</span>
</Tooltip>
);
};
const dropdownRender = () => (
<div className="customer-select-wrapper">
{showSearch && (
<Input
ref={searchInputRef}
className="customer-select-input"
placeholder={formatMessage({ id: 'SEARCH' })}
onChange={debounceSearch}
onPressEnter={debounceSearch}
/>
)}
<CheckboxOptions />
</div>
);
return (
<Select
placeholder={placeholder || formatMessage({ id: 'PLEASE_SELECT' })}
tagRender={tagRender}
maxTagCount={maxTagCount}
dropdownRender={dropdownRender}
showArrow
allowClear
dropdownMatchSelectWidth
{...props}
mode="multiple"
showSearch={false}
value={currentValues}
onChange={handleSelectChange}
className={classnames(['checkbox-selector', className])}
dropdownClassName={classnames([
'checkbox-selector-dropdown',
dropdownClassName,
])}
/>
);
};
export default PluralSelect;