一个树形多选框的实现

100 阅读5分钟

实现功能

  1. 查询
  2. 主从店切换
  3. 多选
  4. 全选/全不选

gitee地址:gitee.com/li_xing150/…

image.png 可以在检索框进行数据的检索

通过全选按钮进行全选/全不选

可实现多选,可实现主从店切换,切换时数据全部刷新

代码功能逻辑

主从店选则.png

调用代码如下

调用代码

AboutView.vue

<template>
  <div class="about">
    <button @click="togglePicker">测试</button>
    <div v-if="data16.showPicker">
      <selectTreeDemo 
        ref="selectTree" 
        :treesData="data16.treeData" 
        :selectedValues="data16.selectedValues" 
        :show="data16.showPicker"
        :isMainStyle="data16.isMain" 
        @selectNodes="onSelectNodes" 
        @changePikerStyle="changePikerStyle" 
        >
      </selectTreeDemo>
    </div>
  </div>
</template>
<script>
import selectTreeDemo from '../components/selectTreeNew/index.vue'
import { data } from './data.js'
export default {
  name: 'Screen-detail',
  components: { selectTreeDemo },
  data () {
    return {
      dataObj: data.second,
      data16:{
        treeData:data.arr,//树形结构
        selectedValues:[],//选中的值
        showPicker:false,//是否展开弹框
        isMain:true,//是否为主数据
      },
    }
  },
  methods: {
    // 打开和关闭弹窗
    togglePicker() {
      this.data16.showPicker = !this.data16.showPicker
    },
    // 确认,返回选中的数据
    onSelectNodes (nodes, isMain) {
      this.data16.selectedValues = nodes.map(node => node.label)
      this.data16.isMain = isMain
    },
    // 取消,重置为默认的数据
    changePikerStyle(){
      this.data16.isMain = this.dataObj.isMain
      this.data16.selectedValues = this.data16.selectedValues.length > 0 ? this.data16.selectedValues : this.dataObj.selected
      this.togglePicker()
    },
  }
}

组件index源码

index.vue

<template>
  <div>
    <div class="van-picker__toolbar" style="display: flex; justify-content: space-between">
      <button type="button" class="van-picker__cancel" style="padding: 0;font-size: 14px;" @click="cancel">取消</button>
      <div class="van-ellipsis van-picker__title">门店</div>
      <button type="button" class="van-picker__confirm" style="padding: 0;font-size: 14px; font-weight: 600;" @click="onConfirm">确认</button>
    </div>
    <div style="display: flex;">
      <input 
          v-model="value" 
          shape="round" 
          :style="{ width: '100%', height: 'fit-content', paddingTop: '0', paddingLeft: '0', paddingRight: '0' }" 
          placeholder="请输入搜索关键词" 
          input-align="center" 
          @change="onChange" 
          @clear="onChange"
      >
      <!-- <van-search  v-model="value" shape="round" style="width: 100%;height: fit-content;padding-top: 0;padding-left: 0;padding-right: 0;" placeholder="请输入搜索关键词" input-align="center" @change="onChange" @clear="onChange" /> -->
      <!-- <div style="width: 20%;" @click="onChange">🔍</div>
      <div style="width: 20%;" @click="reset">重置</div> -->
    </div>
    <div class="changeBox">
      <div class="storeBox">
        <div class="storeStype" :class="{ active: isMain }" @click="changeisMain(true)">主店</div>
        <div class="storeStype" :class="{ active: !isMain }" @click="changeisMain(false)">从属店</div>
      </div>
      <div>
        全选
        <input class="checkbox" style="top: 3px; margin-left: 8px;" type="checkbox" :checked="checkedAll"
          @change="handleCheckedAll" />
      </div>
    </div>
    <div style="overflow-y: auto;height: 200px;margin-top: 5px;">
      <TreeItem1 ref="treeItem" v-for="child in treeData" :node="child" :isMain="isMain" @node-checked="onNodeChecked" :key="child.id"></TreeItem1>
    </div>
  </div>
</template>

<script>
import TreeItem1 from './treeItem.vue'
export default {
  components: {
    TreeItem1
  },
  props: {
    treesData: { type: Array, required: true },
    isMainStyle: { type: Boolean, required: false, default: true },
    selectedValues: { type: Array, required: false },
    show: { type: Boolean, required: false }
  },
  data() {
    return {
      treeData: [],
      tempTreeData: [],
      value: '',
      checkedAll: false,
      isMain: true,
      selectedNodes: []
    }
  },
  watch: {
    show(val) {
      if (val) {
        window.console.log('show', val, this.treesData, this.selectedValues)
        this.reset()
      }
    },
    treesData: {
      deep: true, // 添加深度监听
      handler(newVal) {
        window.console.log('treesData')
        this.reset()
      }
    },
    selectedValues: {
      deep: true, // 添加深度监听
      handler(newVal) {
        window.console.log('selectedValues', this.selectedValues)
        this.reset()
      }
    }
  },
  mounted() {
    this.reset()
  },
  methods: {
    // 数据处理和转换
    transformData(data) {
      // 对数据进行清洗,满足使用标准
      return data.map(shop => {
        const transformedShop = {
          id: shop.shopCode, // 使用 shopCode 作为 id
          label: shop.shopName, // 将 shopName 作为 label
          label1: shop.shopStatus, // 将 shopName 作为 label
          checked: false,
          collapsed: true,
          children: []
        }
        if (shop.rooms && shop.rooms.length > 0) {
          transformedShop.children = shop.rooms.map(room => {
            return {
              id: room.roomCode, // 使用 roomCode 作为 id
              label: room.roomName, // 将 roomName 作为 label
              label1: room.roomStatus, // 将 shopName 作为 label
              parentNode: shop.shopCode, // 将 shopCode 作为 parentNode
              checked: false,
              collapsed: true,
              children: []
            }
          })
        }
        return transformedShop
      })
    },
    formTreeData (tree, isMain) {
      // 根据是否为主店或从店生成树形结构
      if (isMain) {
        return tree
      } else {
        return this.getCoTreeData(tree)
      }
    },
    getCoTreeData(treeData) {
      // 获取从店的数据结构
      let tree = []
      treeData.forEach(node => {
        if (node.children.length) {
          tree.push({ ...node, collapsed: false })
        }
      })
      return tree
    },
    filterTreeData(treeData, value) {
      // 按照value过滤树形结构的数据
      return treeData.map(node => {
        if (node.label.includes(value)) {
          return {
            ...node,
            children: node.children ? this.filterTreeData(node.children, value) : []
          }
        } else if (node.children) {
          const filteredChildren = this.filterTreeData(node.children, value)
          if (filteredChildren.length > 0) {
            return {
              ...node,
              children: filteredChildren
            }
          }
        }
        return null
      }).filter(Boolean)
    },

    // 数据选择和交互
    setSelectedNodes (arr, tree, isMain) {
      // 设置选中节点
      let arrNode = []
      if (isMain) {
        tree.forEach(node => {
          if (arr.includes(node.label) || arr.includes(node.id)) {
            arrNode.push(node)
          }
        })
      } else {
        tree.forEach(node => {
          node.children.forEach(child => {
            if (arr.includes(child.label) || arr.includes(child.id)) {
              arrNode.push(child)
            }
          })
        })
      }
      return arrNode
    },
    setTreeDataChecked (tree, arr, isMain) {
      // 设置树形结构中节点的选中状态
      // tree的节点node的label与arr的arrnode的label匹配,设置node的checked为true
      if (isMain) {
        tree.forEach(node => {
          if (arr.some(arrnode => arrnode.label === node.label)) {
            node.checked = true
          } else {
            node.checked = false
          }
        })
      } else {
        tree.forEach(treeson => {
          treeson.children.forEach(childnode => {
            if (arr.some(arrnode => arrnode.label === childnode.label)) {
              childnode.checked = true
            } else {
              childnode.checked = false
            }
          })
        })
      }
    },
    isAllChecked (tree, selectedNodes, isMain) {
      // 判断是否全选
      let allTree = isMain ? tree : tree.map(node => node.children).flat()
      const isAllChecked = allTree.every(node => {
        const correspondingNode = selectedNodes.find(selectedNode => selectedNode.label === node.label)
        console.log('isAllChecked', correspondingNode, node.checked)
        return correspondingNode && node.checked
      })
      return isAllChecked
    },
    nodeChecked(node) {
      // 若数据未选中,则加入选中列表
      // 若数据选中,则移出选中列表
      if (node.checked) {
        this.selectedNodes.push(node)
      } else {
        this.selectedNodes.splice(this.selectedNodes.findIndex(selectedNode => selectedNode.label === node.label), 1)
      }
    },

    // 用户交互
    reset() {
      // 1.依据输入数据进行重置
      window.console.log('selectedValues', this.selectedValues, this.show)
      this.value = ''
      this.isMain = this.isMainStyle
      this.tempTreeData = this.transformData(this.treesData)
      this.treeData = this.formTreeData(this.tempTreeData, this.isMain)
      if (this.selectedValues && this.selectedValues.length > 0) {
        this.selectedNodes = this.setSelectedNodes(this.selectedValues, this.treeData, this.isMain)
      } else {
        this.selectedNodes = []
      }
      // 设置treeData的选中状态
      this.setTreeDataChecked(this.treeData, this.selectedNodes, this.isMain)
      this.checkedAll = this.isAllChecked(this.treeData, this.selectedNodes, this.isMain)
      window.console.log('selectedValues', this.treeData[0])
    },
    changeisMain (isMain) {
      // 2.主从店切换
      if (this.isMain === isMain) return
      this.isMain = isMain
      console.log('isMain', isMain)

      this.checkedAll = false
      // 重置treeData
      this.treeData = this.formTreeData(this.tempTreeData, isMain)
      // 置空selectedNodes
      this.selectedNodes = []
      this.setTreeDataChecked(this.treeData, this.selectedNodes, isMain)
      // 筛选数据
      this.treeData = this.filterTreeData(this.treeData, this.value)
    },
    onChange() {
      // 3.主从店不切换时的筛选
      // 从tempTreeData中筛选数据
      this.treeData = this.formTreeData(this.tempTreeData, this.isMain)
      this.treeData = this.filterTreeData(this.treeData, this.value)
      this.setTreeDataChecked(this.treeData, this.selectedNodes, this.isMain)
      this.checkedAll = this.isAllChecked(this.treeData, this.selectedNodes, this.isMain)
    },
    handleCheckedAll() {
      // 4.全选/全不选
      this.checkedAll = !this.checkedAll
      // 全选/全不选-设置selecteddNodes
      if (this.isMain) {
        this.treeData.forEach(treenode => {
          treenode.checked = this.checkedAll
          this.nodeChecked(treenode)
        })
      } else {
        this.treeData.forEach(treenode => {
          treenode.children.forEach(child => {
            child.checked = this.checkedAll
            this.nodeChecked(child)
          })
        })
      }
      // 节点去重
      this.selectedNodes = this.selectedNodes.filter((node, index, self) =>
        index === self.findIndex((t) => (
          t.label === node.label
        ))
      )
      this.setTreeDataChecked(this.treeData, this.selectedNodes, this.isMain)
    },
    onNodeChecked (node) {
      // 进行选中数据处理
      this.nodeChecked(node)
      this.checkedAll = this.isAllChecked(this.treeData, this.selectedNodes, this.isMain)
      // console.log('onNodeChecked', this.selectedNodes)
    },
    onConfirm() {
      // 返回查询的数据
      this.$emit('selectNodes', this.selectedNodes, this.isMain)
      this.value = ''
    },
    cancel() {
      // 重置为默认数据
      this.$emit('changePikerStyle')
      this.value = ''
      console.log('cancel')
    },
    
  }
}
</script>
<style  scoped>
.changeBox {
  display: flex;
  justify-content: space-between;
  padding: 0 0 8px;
  border-bottom: 1px solid #F2F4F7;
}
.storeBox{
  height: 24px;
  background: #EBEBEB;
  border-radius: 13px;
  display: flex;
}

.treeBox {
  margin-top: 8px;
  overflow: scroll;
  height: 200px;
}

.treeBox::-webkit-scrollbar {
  display: none;
  /* 隐藏滚动条 */
}

.search-wrapper {
  position: relative;
}

.clear-btn {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
}

.storeStype {
  text-align: center;
  width: 80px;
  height: 24px;
  line-height: 24px;
  color: #999999;
  border-radius: 13px;
}

.active {
  background: #3C60C3;
  color: #fff;
}

.van-search .van-cell {
  padding: 0 !important;
}
.van-search__content--round{
  background-color: #EDF2FC;
}
</style>

treeItem.vue

<template>
  <div>
    <!-- 父节点 -->
    <div class="treeItem">
      <div @click="toggleCollapse" style="display: flex; align-items: center; position: relative;">
        <span style="padding: 3px; border-radius: 10%;display: flex;justify-content: center;align-items: center;">
          <img src="./picture/saling.png" style="width: 48px;" v-if="node.label1 === '营业中'">
          <img src="./picture/saling1.png" style="width: 48px;" v-else-if="node.label1 === '待开业'">
          <img src="./picture/saling2.png" style="width: 48px;" v-else-if="node.label1 === '已撤销'">
        </span>
        {{ node.label }}
        <img src="./picture/angle.png" :class="{ 'downIcon--down': !collapsed }" class="downIcon" v-if="node.children.length">
      </div>
      <div>
        <input v-show="isMain" class="checkbox" type="checkbox" :checked="node.checked" @change="handleCheck('',node)" />
      </div>
    </div>

    <!-- 子节点区域 -->
    <transition name="collapse">
      <ul v-show="!collapsed">
        <li v-for="child in node.children" :key="child.id">
          <!-- 递归渲染子节点 -->
          <div class="treeItem">
            <div style="display: flex;align-items: center;">
              <span style="padding: 3px;padding-left: 15px; border-radius: 10%;display: flex;justify-content: center;align-items: center;">
                <img src="./picture/saling.png" style="width: 48px;" v-if="child.label1 === '营业中'">
                <img src="./picture/saling1.png" style="width: 48px;" v-else-if="child.label1 === '待开业'">
                <img src="./picture/saling2.png" style="width: 48px;" v-else-if="child.label1 === '已撤销'">
              </span>
              {{ child.label }}
            </div>
            <input v-if="!isMain" class="checkbox" type="checkbox" :checked="child.checked"
              @change="handleCheck(child, node)" />
          </div>
        </li>
      </ul>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'TreeItem',
  props: {
    node: { type: Object, required: true },
    isMain: { type: Boolean, default: true }
  },
  data() {
    return {
      collapsed: true
    }
  },
  watch: {
    'node.collapsed'(newVal) {
      // console.log('collapsed',this.node.id,newVal)
      this.collapsed = newVal
    },
    'isMain': {
      handler(newVal) {
      }
    }
  },
  mounted() {
    this.collapsed = this.node.collapsed
  },
  methods: {
    toggleCollapse() {
      this.collapsed = !this.collapsed
    },
    handleCheck(child, node) {
      if (!child) {
        node.checked = !node.checked
        let nodeT = { ...node, checked: node.checked }
        this.$emit('node-checked', nodeT)
      } else {
        child.checked = !child.checked
        child.parentNode = node.id
        this.$emit('node-checked', child)
      }
    }
  }
}
</script>
<style>
.labelStyle {
  font-weight: 400;
  font-family: PingFangSC, PingFang SC;
  font-size: 12px;
  color: #3C60C3;
  line-height: 17px;
  height: 18px;
  text-align: left;
  font-style: normal;
  background: #F0F4FF;
  border-radius: 2px;
  border: 1px solid #3C60C3;
  padding: 0px 2px;
  margin-right: 5px;
}

.labelStyleDyy {
  color: #3CC3C3;
  background: #F0FFFF;
  border: 1px solid #3CC3C3;
}

.labelStyleYcx {
  color: #ABAEB3;
  background: #fff;
  border: 1px solid #ABAEB3;
}

li {
  list-style: none;
  line-height: 25px;
}

.treeItem {
  display: flex;
  justify-content: space-between;
  margin: 8px 0px 16px;
}

.downIcon {
  margin-left: 6px;
  width: 10px;
  height: 10px;
}

.downIcon--down {
  transform: rotate(90deg);
}

.checkbox {
  position: relative;
  /* 隐藏原生复选框 */
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  /* 设置自定义复选框样式 */
  width: 16px;
  height: 16px;
  border: 1px solid #ccc;
  border-radius: 50%;
  cursor: pointer;
}

/* 自定义复选框选中状态样式 */
.checkbox:checked {
  color: #fff;
  background-color: #007bff;
  border-color: #007bff;
}

.checkbox:checked::after {
  content: '\e728';
  font-family: vant-icon;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}</style>