「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
本文主要使用react+antd库,打造一个单、复选框联动的标签管理组件如图↓↓↓
父组件
控制model的显示隐藏、标签选择后的保存事件
修改标签的事件方法
editTag = async a => {
// 获取子组件内部选择的值,调用保存接口
let tag = this.TagOperation.state.currentSelectTag
await API.edit_tag(请求参数);
// 关闭model弹窗
this.setState({
isShowAddTag: false
});
};
Model弹窗
<Modal
title="添加标签"
visible={this.state.isShowAddTag}
onOk={this.onedit_clue_tag}
onCancel={a =>
this.setState({
isShowAddTag: false
})
}
>
<TagOperation
// 用于获取子组件的值
onRef={ref => this.TagOperation = ref}
// 已选中/当前选中的标签
currentSelectTag={this.state.currentSelectTag}
/>
</Modal>
TagOperation子组件
标签左右联动的核心在于【以下操作后,重新渲染页面标签列表】 - 清空✅ - 默认值:✅ - 切换选中:✅ - 设置已选中:✅ - 点击删除已选中✅ - 重新获取标签列表时✅
核心逻辑
代码貌似有点多;简单介绍以下核心事件
设置标签是否选中的属性:selectedTag
- 单选;直接设置标签组选中标签的ID即可;取消选中也是同理;将selectedTag属性设置为空即可;下方完整代码有具体体现;
groupItem.selectedTag =(选中标签的ID);
- 复选;选中的标签ID是一个数组;;取消选中也是同理;将selectedTag属性设置为空数组即可;下方完整代码有具体体现;
groupItem.selectedTag =(选中标签的ID数组)
动态选中标签 tagGroupList 列表
- 通过三层for循环,将分好类别、小组的标签渲染为DOM元素;
- Radio.Group、Checkbox.Group的value属性控制单、复选框的选中值
{this.state.tagGroupList[v].map((gv, gi) => (
<div className="group-wrap" key={gi}>
<div className="bq-group-title">
{gi + 1}, {gv.name} <span>({gv.option})</span>
</div>
<div className="tag-wrap">
{gv.option == "单选" ? (
<Radio.Group
onChange={tagid =>
this.onTagSelect(v, gi, tagid, "单选")
}
value={gv.selectedTag}
>
{gv.tag.map(tv => (
<Radio
value={tv.id}
key={tv.id}
onClick={e => this.onClickRadio(e)}
>
{tv.content}
</Radio>
))}
</Radio.Group>
) : (
<Checkbox.Group
onChange={tagid =>
this.onTagSelect(v, gi, tagid, "多选")
}
value={gv.selectedTag}
>
{gv.tag.map(tv => (
<Checkbox value={tv.id} attr={tv} key={tv.id}>
{tv.content}
</Checkbox>
))}
</Checkbox.Group>
)}
</div>
</div>
))}
下方为完整代码;可以复制体验;
import {
Input,
Select,
Button,
Radio,
Icon,
Tag,
Checkbox
} from "antd";
const Option = Select.Option;
const { Search } = Input;
import _ from "underscore";
import './style/TagOperation.less'
class TagOperation extends React.Component {
constructor(props) {
super(props);
this.state = {
...props,
tagGroupList: [], //全部标签组列表
tagCount: "",
}
}
componentDidMount() {
this.props.onRef(this);
this.onGetGroupTypeList(groupType);
}
onGetGroupTypeList = async (ogroupType, tagName) => {
// 获取数据接口;使用静态数据模拟
let data = {
"家庭信息": [
{
"id": 1,
"name": "性别",
"type": "家庭信息",
"option": "单选",
"weight": 1,
"tag": [
{
"id": 1,
"content": "男",
"color": "#FFA94C",
"score": 10,
"group_id": 1,
"use_count": 51
},
{
"id": 10,
"content": "人妖",
"color": "#54C4D8",
"score": null,
"group_id": 1,
"use_count": 8
},
{
"id": 35,
"content": "女",
"color": "#D9132F",
"score": 2,
"group_id": 1,
"use_count": 14
}
]
},
{
"id": 5,
"name": "资产",
"type": "家庭信息",
"option": "单选",
"weight": null,
"tag": [
{
"id": 20,
"content": "10W",
"color": "#FFA94C",
"score": 1,
"group_id": 5,
"use_count": 5
},
{
"id": 21,
"content": "50W",
"color": "#54C4D8",
"score": null,
"group_id": 5,
"use_count": 11
},
{
"id": 23,
"content": "花不完",
"color": "#50BF19",
"score": null,
"group_id": 5,
"use_count": 10
},
{
"id": 28,
"content": "100W",
"color": "#FFA94C",
"score": null,
"group_id": 5,
"use_count": 8
}
]
}
],
"兴趣偏好": [
{
"id": 3,
"name": "游戏",
"type": "兴趣偏好",
"option": "多选",
"weight": null,
"tag": [
{
"id": 24,
"content": "王者",
"color": "#D9132F",
"score": null,
"group_id": 3,
"use_count": 19
},
{
"id": 25,
"content": "吃鸡",
"color": "#50BF19",
"score": null,
"group_id": 3,
"use_count": 22
},
{
"id": 26,
"content": "斗地主",
"color": "#54C4D8",
"score": null,
"group_id": 3,
"use_count": 23
},
{
"id": 27,
"content": "钢琴快儿",
"color": "#8167F5",
"score": null,
"group_id": 3,
"use_count": 19
}
]
},
{
"id": 11,
"name": "书画",
"type": "兴趣偏好",
"option": "多选",
"weight": 3,
"tag": [
{
"id": 38,
"content": "言情书",
"color": "#BE27E4",
"score": -1,
"group_id": 11,
"use_count": 9
},
{
"id": 39,
"content": "科幻书",
"color": "#54C4D8",
"score": -20,
"group_id": 11,
"use_count": 11
}
]
},
{
"id": 12,
"name": "乐器",
"type": "兴趣偏好",
"option": "多选",
"weight": 4,
"tag": [
{
"id": 40,
"content": "钢琴",
"color": "#50BF19",
"score": -20,
"group_id": 12,
"use_count": 5
},
{
"id": 41,
"content": "小提琴",
"color": "#FFA94C",
"score": -19,
"group_id": 12,
"use_count": 5
},
{
"id": 42,
"content": "笛子",
"color": "#54C4D8",
"score": -18,
"group_id": 12,
"use_count": 5
}
]
},
{
"id": 13,
"name": "运动",
"type": "兴趣偏好",
"option": "多选",
"weight": 15,
"tag": []
},
{
"id": 14,
"name": "游玩",
"type": "兴趣偏好",
"option": "多选",
"weight": 12,
"tag": [
{
"id": 43,
"content": "爬山",
"color": "#FFA94C",
"score": -1,
"group_id": 14,
"use_count": 2
},
{
"id": 44,
"content": "游乐园",
"color": "#FFA94C",
"score": -2,
"group_id": 14,
"use_count": 2
},
{
"id": 45,
"content": "品尝美食",
"color": "#D9132F",
"score": -3,
"group_id": 14,
"use_count": 2
}
]
}
],
"报名信息": [
{
"id": 7,
"name": "测试",
"type": "报名信息",
"option": "单选",
"weight": null,
"tag": [
{
"id": 29,
"content": "第一次报名",
"color": "#BE27E4",
"score": 1,
"group_id": 7,
"use_count": 27
},
{
"id": 30,
"content": "第二次报名",
"color": "#54C4D8",
"score": 1,
"group_id": 7,
"use_count": 3
},
{
"id": 36,
"content": "第三次报名",
"color": "#D9132F",
"score": 2,
"group_id": 7,
"use_count": 1
},
{
"id": 37,
"content": "第四次报名",
"color": "#8167F5",
"score": 4,
"group_id": 7,
"use_count": 2
}
]
}
],
"到店信息": [
{
"id": 9,
"name": "喝茶",
"type": "到店信息",
"option": "单选",
"weight": 1,
"tag": [
{
"id": 32,
"content": "喝红茶",
"color": "#FFA94C",
"score": 11,
"group_id": 9,
"use_count": 24
}
]
}
],
}
this.setState({
tagGroupList: this.onSetSelectTag(
[...this.state.currentSelectTag],
data
)
});
};
onSetSelectTag = (tag, tagGroupList = this.state.tagGroupList) => {
let checkedTag = [];
for (const tagItem of tag) {
for (const key in tagGroupList) {
if (Object.hasOwnProperty.call(tagGroupList, key)) {
const groupList = tagGroupList[key];
if (groupList.length == 0) continue;
for (const groupItem of groupList) {
if (tagItem.group_id == groupItem.id) {
if (groupItem.option == "单选") {
groupItem.selectedTag = tagItem.id;
} else {
checkedTag.push(tagItem.id);
groupItem.selectedTag = checkedTag;
}
}
}
}
}
}
return tagGroupList;
};
onTagSelect = async (type, gi, tagid, opiton = "单选") => {
let tagInfo = [];
let groupId = [];
if (opiton == "单选") {
tagInfo.push(
this.state.tagGroupList[type][gi].tag.find(
v => v.id == tagid.target.value
)
);
this.state.tagGroupList[type][gi].selectedTag = tagid.target.value;
this.setState({
tagGroupList: this.state.tagGroupList
});
} else {
for (const v of tagid) {
tagInfo.push(
this.state.tagGroupList[type][gi].tag.find(cv => cv.id == v)
);
groupId.push(v);
}
if (tagid.length == 0) {
for (const v of this.state.tagGroupList[type][gi].tag) {
console.log(this.state.currentSelectTag);
for (const cv of this.state.currentSelectTag) {
if (v.id == cv.id) {
tagInfo.push(v);
}
}
}
}
this.state.tagGroupList[type][gi].selectedTag = groupId;
this.setState({
tagGroupList: this.state.tagGroupList
});
}
console.log(type, gi, tagid);
// console.log(this.state.tagGroupList);
console.log(tagInfo);
// 设置已选中
let a = [];
for (const v of this.state.currentSelectTag) {
if (tagInfo.length && tagInfo[0].group_id != v.group_id) {
a.push(v);
}
}
if (tagid.length == 0) {
tagInfo.length = 0;
}
this.setState({
currentSelectTag: [].concat(a, tagInfo)
});
};
// 双击取消标签选中效果
onClickRadio = e => {
let tagid = e.target.value;
let a = this.state.currentSelectTag;
let isChecked = a.findIndex(v => v.id == tagid);
isChecked == -1 || this.onDelPageClueTag(tagid);
};
onDelPageClueTag = async id => {
let a = this.state.currentSelectTag;
let delGroupId = a.find(v => v.id == id).group_id;
let onSetSelectTag = () => {
let tagGroupList = this.state.tagGroupList;
for (const key in tagGroupList) {
if (Object.hasOwnProperty.call(tagGroupList, key)) {
const groupList = tagGroupList[key];
if (groupList.length == 0) continue;
for (const groupItem of groupList) {
if (delGroupId == groupItem.id) {
if (groupItem.option == "单选") {
groupItem.selectedTag = "";
} else {
groupItem.selectedTag = groupItem.selectedTag.filter(
v => v != id
);
}
}
}
}
}
console.log(tagGroupList);
return tagGroupList;
};
a.splice(
a.findIndex(v => v.id == id),
1
);
this.setState({
currentSelectTag: a,
tagGroupList: onSetSelectTag()
});
};
onClearAllCheckedTAg = () => {
let clearAll = () => {
let tagGroupList = this.state.tagGroupList;
for (const key in tagGroupList) {
if (Object.hasOwnProperty.call(tagGroupList, key)) {
const groupList = tagGroupList[key];
if (groupList.length == 0) continue;
for (const groupItem of groupList) {
if (groupItem.option == "单选") {
groupItem.selectedTag = "";
} else {
groupItem.selectedTag = [];
}
}
}
}
};
clearAll();
this.setState({
currentSelectTag: []
});
};
render() {
return <>
<div className="bq-select-left">
<div className="bq-head lyh-fsb">
<div className="title">全部标签({this.state.tagCount})</div>
</div>
<div className="bq-btn-wrap lyh-fsb p20">
<Select
className="mr20"
placeholder="请选择类别"
style={{ width: "166px" }}
onChange={v => this.onGetGroupTypeList([v])}
>
<Option value="全部" key="全部">
全部
</Option>
{groupType.map(v => (
<Option value={v} key={v}>
{v}
</Option>
))}
</Select>
<Search
placeholder="请输入标签名"
// enterButton
onSearch={v => this.onGetGroupTypeList(groupType, v)}
/>
</div>
<div className="bq-list-wrap">
{Object.keys(this.state.tagGroupList).map((v, i) => (
<div className="type-wrap" key={i}>
{this.state.tagGroupList[v].length == 0 ? (
""
) : (
<div className="bq-type-title">{v}</div>
)}
{this.state.tagGroupList[v].map((gv, gi) => (
<div className="group-wrap" key={gi}>
<div className="bq-group-title">
{gi + 1}, {gv.name} <span>({gv.option})</span>
</div>
<div className="tag-wrap">
{gv.option == "单选" ? (
<Radio.Group
onChange={tagid =>
this.onTagSelect(v, gi, tagid, "单选")
}
value={gv.selectedTag}
>
{gv.tag.map(tv => (
<Radio
value={tv.id}
key={tv.id}
onClick={e => this.onClickRadio(e)}
>
{tv.content}
</Radio>
))}
</Radio.Group>
) : (
<Checkbox.Group
onChange={tagid =>
this.onTagSelect(v, gi, tagid, "多选")
}
value={gv.selectedTag}
>
{gv.tag.map(tv => (
<Checkbox value={tv.id} attr={tv} key={tv.id}>
{tv.content}
</Checkbox>
))}
</Checkbox.Group>
)}
</div>
</div>
))}
</div>
))}
</div>
</div>
<div className="bq-select-right">
<div className="bq-head lyh-fsb">
<div className="title">
已选({this.state.currentSelectTag.length})
</div>
<Button onClick={a => this.onClearAllCheckedTAg()}>
清空
<Icon type="delete" theme="filled" />
</Button>
</div>
<div className="select-tag-wrap p20">
{this.state.currentSelectTag.map(v => (
<Tag
key={v.id}
closable
color={v.color}
onClose={a => this.onDelPageClueTag(v.id)}
className="mb20"
style={{
borderRadius: 20,
padding: "1px 15px"
}}
>
{v.content}
</Tag>
))}
</div>
</div>
</>
}
}
export default TagOperation;
css样式:TagOperation.less
.bq-select-left,
.bq-select-right {
height: 614px;
overflow: auto;
background: #ffffff;
border: 1px solid #d9d9d9;
.bq-head {
padding: 16px 24px;
border-bottom: 1px solid #d9d9d9;
font-size: 18px;
font-weight: bold;
color: #5a5a5a;
}
}
.bq-select-left {
width: 533px;
.bq-list-wrap {
padding: 24px;
.bq-type-title {
font-size: 16px;
font-weight: bold;
color: #5a5a5a;
padding-bottom: 15px;
}
.bq-group-title {
font-size: 18px;
font-weight: 400;
color: #5a5a5a;
span {
font-size: 14px;
}
}
.tag-wrap {
padding: 15px;
display: flex;
justify-content: space-between;
}
}
}
.bq-select-right {
width: 244px;
}