1、效果如下图
实现tag标签单选或多选功能
2、环境准备
1、react18
2、antd 4+
3、功能实现
原理: 封装一个受控组件,接受父组件的参数,数据发现变化后,回传给父组件
1、首先,引入CheckableTag
组件和useEffect, useMemo, useState
钩子:
import { Tag } from 'antd';
import { useEffect, useMemo, useState } from 'react';
const { CheckableTag } = Tag;
2、然后,定义一个状态变量来存储选中的tag:
const [tagsData, setTagsData] = useState<enumItem[]>();
3、组件可接收的props子属性 如下:
- selectedTagsValues: 父组件传入的标签配置枚举列表
- value: 已选中的值
- startIndex:距离左右节点位置,用于是否将左右节点滑动到可视局域
- onChange: 选中的值发生变化时回调
4、创建一个函数来处理tag的选中和取消选中事件:
// handelChange,找到所点击项的索引,并把那一项的checked设置为true
const handleChange = (tag: any, checked: boolean, mode?: string) => {
if (mode !== 'multiple') {
onChange?.(checked ? tag : null);
return;
}
const changeData = (value || []).filter(
(item: any) => item.value !== tag.value,
);
if (checked) {
changeData.push(tag);
}
onChange?.(changeData);
};
5、最后,使用CheckableTag
组件以及tagsData, value值的变化动态来渲染tag列表,并将选中状态和change事件绑定到对应的属性上:
//遍历
const dom = useMemo(() => {
return (
<div id={uniqueKey} className={clsx(['flex'])}>
{(tagsData || []).map((tag: any, index: number) => {
const isHasSelectedTag = isSelectedTag(tag?.value, value);
return (
// eslint-disable-next-line react/jsx-key
<div className={clsx(['self-check-tag'])} key={index}>
<CheckableTag
key={tag.value}
checked={tag.checked || isHasSelectedTag}
onClick={(e: any) => {
scrollIntoViewHandle(
e.target?.parentElement?.parentElement?.parentElement
?.childNodes,
index,
tagsData?.length || 0,
);
}}
onChange={(checked) => {
handleChange(tag, checked, mode);
}}
>
<div className={clsx([mode === 'multiple' ? 'cur' : ''])}>
{tag.label}
{tag.checked || isHasSelectedTag ? <i></i> : ''}
</div>
</CheckableTag>
</div>
);
})}
</div>
);
}, [tagsData, value]);
6、完整代码如下:
/**
* 公共组件:标签组件
*/
import { Tag } from 'antd';
import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react';
import './index.less';
const { CheckableTag } = Tag;
type enumItem = {
label: string;
value: string;
};
// 距离左右节点的位置的默认值,决定开始、结束节点是否滚到可视区域
const START_INDEX = 3;
/**
* 标签属性配置
* selectedTagsValues: 可选中的标签配置选项
* uniqueKey:组件唯一标识key
* value: 已选中的值
* startIndex:距离左右节点位置,用于是否将左右节点滑动到可视局域
* onChange: 选中的值发生变化时回调
*/
interface SelectTagProps {
selectedTagsValues: enumItem[];
uniqueKey?: string;
value?: any;
startIndex?: number;
mode?: string;
onChange?: (values: any) => void;
}
const SelectTag = (props: SelectTagProps) => {
const { selectedTagsValues, uniqueKey, value, startIndex, mode, onChange } =
props;
const [tagsData, setTagsData] = useState<enumItem[]>();
// 点击tag 跳到对应的可视区域
const scrollIntoViewHandle = (node: any, index: number, tagLen: number) => {
const scrollIndex =
startIndex || Math.min(START_INDEX, Math.floor(tagLen / 2));
if (tagLen > 0 && index < scrollIndex) {
node?.[0]?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
});
return;
}
if (tagLen > 0 && tagLen - index <= scrollIndex) {
node?.[tagLen - 1]?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
});
return;
}
node?.[index]?.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
};
useEffect(() => {
setTagsData(selectedTagsValues);
}, [selectedTagsValues]);
useEffect(() => {
// 若枚举过长,可考虑传入一个uniqueKey,自动调到指定位置
if (uniqueKey && value) {
const index: number = (tagsData || []).findIndex(
(item: any) => item.value && item?.value === value?.value,
);
if (index > -1 && document.getElementById(uniqueKey)) {
setTimeout(() => {
scrollIntoViewHandle(
document.getElementById(uniqueKey)?.childNodes,
index,
tagsData?.length || 0,
);
}, 100);
}
}
}, [value]);
// handelChange,找到所点击项的索引,并把那一项的checked设置为true
const handleChange = (tag: any, checked: boolean, mode?: string) => {
if (mode !== 'multiple') {
onChange?.(checked ? tag : null);
return;
}
const changeData = (value || []).filter(
(item: any) => item.value !== tag.value,
);
if (checked) {
changeData.push(tag);
}
onChange?.(changeData);
};
// tag是否选中
const isSelectedTag = (tagValue: string, value: any) => {
if (mode === 'multiple') {
if (Array.isArray(value) && value.length) {
const findIndex = value.findIndex((item) => item?.value === tagValue);
return findIndex > -1;
}
return false;
}
return tagValue === value?.value;
};
//遍历
const dom = useMemo(() => {
return (
<div id={uniqueKey} className={clsx(['flex'])}>
{(tagsData || []).map((tag: any, index: number) => {
const isHasSelectedTag = isSelectedTag(tag?.value, value);
return (
// eslint-disable-next-line react/jsx-key
<div className={clsx(['self-check-tag'])} key={index}>
<CheckableTag
key={tag.value}
checked={tag.checked || isHasSelectedTag}
onClick={(e: any) => {
scrollIntoViewHandle(
e.target?.parentElement?.parentElement?.parentElement
?.childNodes,
index,
tagsData?.length || 0,
);
}}
onChange={(checked) => {
handleChange(tag, checked, mode);
}}
>
<div className={clsx([mode === 'multiple' ? 'cur' : ''])}>
{tag.label}
{tag.checked || isHasSelectedTag ? <i></i> : ''}
</div>
</CheckableTag>
</div>
);
})}
</div>
);
}, [tagsData, value]);
return <>{dom}</>;
};
export default SelectTag;
样式文件:
.self-check-tag {
display: flex;
.ant-tag {
display: inline-block;
height: 32px;
font-size: 14px;
font-family: 'Microsoft YaHei';
color: #fff;
line-height: 32px;
border-radius: 3px;
padding-left: 10px;
padding-right: 10px;
}
div.cur {
position: relative;
// padding: 0 12px;
}
div.cur > i {
display: block;
position: absolute;
border-bottom: 16px solid #1890ff;
border-left: 16px solid transparent;
width: 0;
height: 0;
bottom: 1px;
right: -8px;
content: '';
}
div.cur > i::before {
content: '';
position: absolute;
top: -1px;
right: -2px;
border: 16px solid #1890ff;
border-top-color: transparent;
border-left-color: transparent;
}
div.cur > i::after {
content: '';
width: 6px;
height: 10px;
position: absolute;
right: 0;
top: 3px;
border: 1px solid #fff;
border-top-color: transparent;
border-left-color: transparent;
transform: rotate(40deg);
}
.ant-tag-checkable-checked {
color: #2eb3ff;
background-color: rgba(46, 179, 255, 10%);
}
.ant-tag-checkable:hover {
color: #2eb3ff;
background-color: rgba(46, 179, 255, 10%);
}
}
组件调用:
const selectedTagsValues: any[] = [
{ label: '特大型', value: '1' },
{ label: '大型2级', value: '2' },
{ label: '大型3级', value: '3' },
{ label: '中型4级', value: '4' },
{ label: '中型5级', value: '5' },
{ label: '小型', value: '6' },
]
<SelectTag selectedTagsValues={selectedTagsValues} />
下一节将分享多层级的标签选中功能,同时支持多选和单选功能