react+next 打造一个标签选项联动的组件

547 阅读5分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

本文主要使用react+antd库,打造一个单、复选框联动的标签管理组件如图↓↓↓

image.png


父组件

控制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;
}