自定移动端树级联选择

558 阅读1分钟

组件说明

本组件依赖于vant开发,实现树状菜单的自定义展示和选择,可以进行多自定义内容 使用插槽方便用户自定义展示内容

插槽内容规范

title : 弹窗显示的标题内容

默认值: 请选择内容

closeBtn: 确认按钮

默认值: 确认

leftContent :左侧内容插槽

显示左侧内容要显示的信息

rightContent : 右侧内容插槽

显示右侧内容要显示的信息

参数规范说明

treeDatas : 所有树状结构数组

[ { // 关键信息 text: "#C5341B", status: "circle", // 其他可选信息 ext:'112', children: [ { // 关键信息 text: "学生", status: "circle", } ] } ]

selectData 已选中的值

{selectData: { "#5B8DF5": [ { status: "checked", text: "李四", }, ], }}

props : 树节点key定义

props: { default: { label: "text", children: "children", status: "status", }, },

组件代码

<template>
  <van-popup
    v-model="show"
    round
    position="bottom"
    class="pom"
    @click-overlay="close"
  >
    <div class="header">
      <div class="line-box">
        <div class="line"></div>
      </div>
      <div class="title">
        <slot name="title"> 选择内容 </slot>
        <div class="title-close" @click="close">
          <slot name="closeBtn"> 确认 </slot>
        </div>
      </div>
    </div>
    <div class="list-item">
      <div class="main">
        <div class="left">
          <div
            v-for="(item, index) in treeDatas"
            :key="item.text"
            class="checkbox"
            :class="{ active: active === item[props.label] }"
            @click="toggle(index, item)"
          >
            <slot name="leftContent" :data="item">
              {{ item[props.label] }}
            </slot>
          </div>
        </div>
        <div class="right">
          <div class="all" @click="toggleAllCheckBox(checkedAll)">
            <span>全部</span>
            <span
              ><van-icon
                :name="checkedAll ? 'checked' : 'circle'"
                color="green"
            /></span>
          </div>
          <div class="list">
            <div
              v-for="(subItem, subindex) in showChildrens"
              :key="subItem.text"
              class="checkbox"
              @click="subToggle(subindex, subItem)"
            >
              <slot name="rightContent" :data="subItem">
                <span>{{ subItem[props.label] }}</span>
                <van-icon :name="subItem[props.status]||'circle'" color="green" />
              </slot>
            </div>
          </div>
        </div>
      </div>
    </div>
  </van-popup>
</template>
<script>
export default {
  props: {
    treeDatas: {
      default: [],
    },
    selectData: {
      default: [],
    },
    props: {
      default: {
        label: "text",
        children: "children",
        status: "status",
      },
    },
  },
  data() {
    return {
      show: true,
      locationCodes: [],
      active: "",
      activeIndex: 0,
      checkedAll: false,
      showChildrens: [],
      subSelectItem: {},
      hasSelectSubItem: [],
    };
  },
  created() {
    this.subSelectItem = this.selectData;
    // 初始化选择对象状态
    this.initCheckedObj();
  },
  methods: {
    /**
     * 初始化选择对象状态
     */
    initCheckedObj() {
      const fristKeys = Object.keys(this.selectData);
      if (fristKeys && fristKeys.length > 0) {
        this.treeDatas.forEach((item, index) => {
          // 如果选中的对象存在,则设置一级对象的状态
          if (fristKeys.indexOf(item[this.props.label]) > -1) {
            if (!this.activeIndex) {
              this.activeIndex = index;
            }
            if (this.selectData[item[this.props.label]].length > 0) {
              item[this.props.status] =
                this.selectData[item[this.props.label]].length === item.children.length
                  ? "checked"
                  : "certificate";
              item.children.forEach((children) => {
                // 判断二级菜单存在不存在,如果存在则被选中
                let isExits = this.selectData[item[this.props.label]].find(
                  (selectItem) => selectItem[this.props.label] === children[this.props.label]
                );
                if (isExits) {
                  children[this.props.status] = "checked";
                }
              });
            } else {
              item[this.props.status] = "circle";
            }
          }
        });
      }

      if (this.treeDatas[this.activeIndex]) {
        this.toggle(this.activeIndex, this.treeDatas[this.activeIndex]);
      }
    },
    /**
     * 点击一级菜单设置相关信息
     * @param {*} index
     * @param {*} item
     */
    toggle(index, item) {
      // 显示当前点击的一级对象为当前选中状态
      this.active = item[this.props.label];
      this.activeIndex = index;
      // 为选中的一级对象初始化值
      if (!this.subSelectItem[this.active]) {
        this.subSelectItem[this.active] = [];
      }
      // 如果一级对象的状态已经被全部选中,这二级状态的全部选中按钮为选中状态
      this.checkedAll = item[this.props.status] === "checked";
      // 切换二级对象为当前一级对象关联的二级对象
      this.showChildrens = item.children;
    },
    /**
     * 点击二级菜单获取相关信息
     * @param {*} index
     */
    subToggle(index, subitem) {
      // 获取选中状态的二级对象中是否包含当前选择的二级对象
      const extisArray = this.subSelectItem[this.active];
      if (extisArray.find((obj) => obj[this.props.label] === subitem[this.props.label])) {
        // 如果存在则删除当前选项,将当前二级对象作为非选中状态
        subitem[this.props.status] = "circle";
        const index = this.subSelectItem[this.active].findIndex(
          (i) => i[this.props.label] === subitem[this.props.label]
        );
        this.subSelectItem[this.active].splice(index, 1);
      } else {
        // 如果不存在则添加该二级对象为选中状态
        subitem[this.props.status] = "checked";
        this.subSelectItem[this.active].push(subitem);
      }
      // 根据当前选择的二级对象判断一级对象的状态
      const newSubSelect = Array.from(this.subSelectItem[this.active]);
      if (this.showChildrens.length === newSubSelect.length) {
        // 全部选中
        this.treeDatas[this.activeIndex][this.props.status] = "checked";
        this.checkedAll = true;
      } else if (newSubSelect.length > 0) {
        // 部分选中
        this.treeDatas[this.activeIndex][this.props.status] = "certificate";
        this.checkedAll = false;
      } else {
        // 没有选中
        this.treeDatas[this.activeIndex][this.props.status] = "circle";
        this.checkedAll = false;
      }
    },
    /**
     * 切换全部选择和非全部选择
     */
    toggleAllCheckBox() {
      // 全选按钮切换
      this.checkedAll = !this.checkedAll;
      if (this.checkedAll) {
        // 如果为全部选中
        this.treeDatas.forEach((item) => {
          if (item[this.props.label] === this.active) {
            // 设置当前一级对象为全部选中状态
            item[this.props.status] = "checked";
          }
        });
        // 当前一级对象的二级对象为全部选中状态
        this.subSelectItem[this.active] = [];
        this.showChildrens.forEach((showItem) => {
          showItem[this.props.status] = "checked";
          this.subSelectItem[this.active].push(showItem);
        });
      } else {
        //如果为非全选状态
        this.treeDatas.forEach((item) => {
          if (item[this.props.label] === this.active) {
            // 一级对象为非选中状态
            item[this.props.status] = "circle";
          }
        });
        // // 当前二级对象设置为空
        this.subSelectItem[this.active] = [];
        this.showChildrens.forEach((showItem) => {
          showItem[this.props.status] = "circle";
        });
      }
    },
    /**
     * 关闭窗口
     *  */
    close() {
      this.show=false
      // 返回选中的对象
      this.$emit("close", this.subSelectItem);
    },
  },
};
</script>
<style lang="less" scoped>
.pom {
  max-height: 80%;
  z-index: 99999999 !important;
  .header {
    // margin-top: 37px;
    background: #ffffff;
    position: fixed;
    height: 50px;
    width: 100%;
    padding-bottom: 20px;
    z-index: 99;
    border-top-left-radius: 16px;
    border-top-right-radius: 16px;
    .line-box {
      margin-top: 10px;
      display: flex;
      justify-content: center;
      align-items: center;
      .line {
        width: 40px;
        height: 5px;
        left: 168px;
        top: 172px;

        background: #ebebeb;
        border-radius: 100px;
      }
    }

    .title {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin: 22px 15px 0 15px;
      .title-text {
        font-family: PingFang SC;
        font-style: normal;
        font-weight: 500;
        font-size: 16px;
        line-height: 16px;
        color: #323232;
      }
      .title-close {
        font-family: PingFang SC;
        font-style: normal;
        font-weight: normal;
        font-size: 16px;
        line-height: 16px;
        color: #6a6a6a;
      }
    }
    .subtitle {
      font-family: PingFang SC;
      font-style: normal;
      font-weight: normal;
      font-size: 14px;
      line-height: 14px;
      /* identical to box height, or 100% */

      display: flex;
      align-items: center;

      color: #5b5b5b;
      margin: 8px 15px;
    }
  }
  .list-item {
    margin-top: 72px;
    background: linear-gradient(
      180deg,
      #fbfbfb -51.25%,
      rgba(251, 251, 251, 0) 100%
    );
    .main {
      display: flex;
      .left {
        width: 120px;
        height: 50vh;
        border-right: 1px solid #f2f2f2;
        overflow-y: auto;
        background: #ffffff;

        .checkbox {
          width: 120px;
          display: flex;
          // background: #f2f2f2;
          height: 40px;
          padding: 10px;
          box-sizing: border-box;
          justify-content: space-between;
          // border-bottom: 1px solid#ffffff;
          position: relative;
          .colorItem {
            width: 20px;
            height: 20px;
            display: block;
          }
        }
        .active {
          background: #ffffff;
        }
        .active::before {
          background: red;
          content: "";
          width: 5px;
          position: absolute;
          top: 0;
          left: 0;
          height: 40px;
        }
      }
      .right {
        width: calc(100% - 120px);
        background: #f2f2f2;
        .all {
          display: flex;
          justify-content: space-between;
          padding: 0px;
          background: #ffffff;
          height: 40px;
          box-sizing: border-box;
          line-height: 40px;
          // border-bottom: 1px solid #f2f2f2;
          padding-bottom: 5px;
          span {
            margin: 0 10px;
          }
        }
        .list {
          height: calc(50vh - 40px);
          overflow-y: auto;
          .checkbox {
            width: 100%;
            display: flex;
            height: 40px;
            background: #ffffff;
            padding: 10px;
            box-sizing: border-box;
            justify-content: space-between;
            // border: 1px solid#f2f2f2;
            .colorItem {
              width: 20px;
              height: 20px;
              display: block;
            }
          }
        }
      }
      .checkbox {
        width: 120px;
        display: flex;
        background: #ffffff;
        padding: 10px;
        box-sizing: border-box;
        justify-content: space-between;
        // border: 1px solid#f2f2f2;
        .colorItem {
          width: 20px;
          height: 20px;
          display: block;
        }
      }

      .van-cell {
        position: relative;
        // padding: 0px !important;
        // padding-right: 3px !important;
      }
    }
  }
}
</style>

使用案例

  <div class="main">
    <TreeSelect
      :treeDatas="treeDatas"
      :selectData="selectData"
      :props="props"
      @close="close"
    >
      <template #title>
        <span class="title">选择城市</span>
      </template>
      <template #closeBtn> 确定 </template>
    </TreeSelect>
  </div>
</template>

<script>
import TreeSelect from "@/components/common/TreeSelect";
export default {
  name: "DemoCity",
  props: ["pageInfo"],
  components: {
    TreeSelect,
  },
  data() {
    return {
      treeDatas: [
        {
          // 导航名称
          name: "北京",
          checked: "circle",
          children: [
            {
              name: "昌平",
              checked: "circle",
            },
            {
              name: "顺义",
              checked: "circle",
            },
          ],
        },
        {
          // 导航名称
          name: "天津",
          checked: "circle",
          children: [
            {
              name: "北辰",
              checked: "circle",
            },
            {
              name: "武清",
              checked: "circle",
            },
          ],
        },
      ],
      selectData: {},
      props: {
        label: "name",
        children: "children",
        status: "checked",
      },
    };
  },
  methods: {
    close(params) {
      this.selectData = params;
      console.log(params);
    },
  },
};
</script>

<style lang="less" scoped>
.main {
  margin: 10px;
  .title {
    font-size: 16px;
    color: #6a6a6a;
  }
  .colorItem {
    width: 20px;
    height: 20px;
    display: block;
  }
}
</style>