使用elementUI的tree实现省市区街小区的选择

具体需求:

  • 支持按层级选择一个/多个小区
  • 层级懒加载
  • 选中非小区节点时默认选中该节点范围下的所有小区
  • 支持按列表选择一个/多个小区
  • 列表懒加载
  • “全部小区”选项和树/列表中的选项互斥
  • 按树结构和按列表结构选中的小区是同步更新的
  • 记忆节点选中状态
  • 支持搜索,可以在搜索出来的列表中进行选中/取消选中操作

效果如图:
在这里插入图片描述
在这里插入图片描述

设计思路:

  • 用一个变量a来存放真实选中的小区信息,树/列表只做展示用
  • 树的懒加载:展开节点时再加载下一层信息
  • 列表的懒加载:先加载50个小区,滚动到底部时再加载下一批小区,虚拟分页
  • 树的节点选中:如果选中非小区节点,则向接口请求该节点下的所有小区放到变量a中;如果选中小区节点,则直接将小区信息放到变量a中
  • 列表的节点选中:直接将选中信息同步到变量a中
  • 选择“全部小区”后,清空所有树/列表节点的选中状态,并且清空变量a;选择树/列表节点时,取消“全部小区”的选中状态,并且更新变量a
  • 切换tab时,同步更新树/列表的节点选中状态
  • 树节点选中状态的设置(重点):由于需要记忆节点选中状态,这就意味着树结构需要记忆全选/半选状态。如果树是全量加载的,那么不需要我们自己来判断,只需要告诉树选中的小区即可,但麻烦的是,现在树节点是懒加载的,在未打开某个节点的情况下,无法根据当前选中的小区来判断某个省市区街是全选还是半选,这个时间就需要依赖接口的判断,也就是说,需要在请求某一层的树节点信息时,把当前选中的小区传回去,由接口来告诉你某个节点需要全选还是半选
  • 列表选中状态的设置:直接更新就可以了

重点代码:
1.开发时遇到的比较麻烦的一个事情就是设置树的半选中状态,官方并没有提供半选的api,找了好久最后发现可以使用以下方法来直接修改节点状态:

let node = this.$refs.asyncTree.getNode(id);
node.indeterminate = true // 半选
node.checked = true // 全选
node.expand() // 展开
nodedata.loadData() // 加载数据

没错,就是这么简单粗暴,直接拿到节点,然后更改节点的属性或者调用节点的方法就好,需要哪个做哪个

2.想重新渲染树的话直接给树设置一个key,想渲染的时候更新下key就好了,简单方便

封装好的组件以及组件的使用:

PlotMultiSelectDialog.vue

<!-- 顶栏的选择小区的组件 -->
<!-- 只可用于全局,作为全局小区范围的控制 -->
<template>
  <div>
    <el-dialog
      :visible="visible"
      :append-to-body="true"
      :show-close="true"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      @close="clickCancle"
      @open="open"
      width="660px"
      v-dialogDrag
    >
      <div slot="title">
        <span class="dialog-title">更换小区</span>
      </div>
      <div class="plotSelectRoot">
        <el-input
          clearable
          placeholder="请输入小区名称"
          prefix-icon="el-icon-search"
          v-model="searchText"
        >
          <i slot="suffix" class="el-icon-loading" v-if="searchLoading"></i>
        </el-input>

        <div
          class="checkRect"
          v-show="searchText !== ''"
          style="margin-top: 20px"
        >
          <div style="height: 440px; overflow-y: auto">
            <div
              v-for="(item, index) in searchList"
              :key="index"
              class="plotItem"
            >
              <el-checkbox
                v-model="item.checked"
                @change="(val) => searchChange(item, val)"
                >{{ item.plotName }}
              </el-checkbox>
            </div>
          </div>
        </div>

        <el-tabs
          v-show="searchText === ''"
          v-model="activeTabName"
          @tab-click="handleTablick"
        >
          <el-tab-pane label="按地区选" name="byRegion">
            <div class="selectAllBox">
              <el-checkbox
                v-model="allChecked"
                @change="allCheckedChange"
                style="margin-left: 23px"
                >全部小区</el-checkbox
              >
              <div class="selectAllButton" @click="clickSelectNone">清空</div>
              <div class="selectCount">(已选择: {{ flatNum }}个小区)</div>
            </div>
            <div class="checkRect" :key="treeKey">
              <div style="height: 440px; overflow-y: auto" class="innerbox">
                <el-tree
                  :props="props"
                  :load="loadTreeNode"
                  node-key="id"
                  lazy
                  show-checkbox
                  ref="plotTree"
                  :expand-on-click-node="false"
                  :default-expanded-keys="defaultExpandedKeys"
                  @check="checkedTreeNodeChange"
                >
                </el-tree>
              </div>
            </div>
          </el-tab-pane>
          <el-tab-pane label="按小区选" name="byCommunity">
            <div class="selectAllBox">
              <el-checkbox
                v-model="allChecked"
                @change="allCheckedChange"
                style="margin-left: 23px"
                >全部小区</el-checkbox
              >
              <div class="selectAllButton" @click="clickSelectNone">清空</div>
              <div class="selectCount">(已选择: {{ flatNum }}个小区)</div>
            </div>

            <div class="checkRect">
              <div
                style="height: 440px; overflow-y: auto"
                class="innerbox"
                @scroll="scrollEvent"
              >
                <el-checkbox-group id="checkboxGroup" v-model="checkedPlotList">
                  <el-checkbox
                    v-for="plot in flatPlotList"
                    :key="plot.id"
                    :label="plot.id"
                    @change="checkedListNodeChange"
                    >{{ plot.plotName }}</el-checkbox
                  >
                </el-checkbox-group>
              </div>
            </div>
          </el-tab-pane>
        </el-tabs>
      </div>
      <span slot="footer">
        <div>
          <el-button @click="clickCancle">取消</el-button>
          <el-button type="primary" @click="clickConfirm">确定</el-button>
        </div>
      </span>
    </el-dialog>
  </div>
</template>
<script  lang="ts">
import { Component, Vue, Watch, Prop } from 'vue-property-decorator'
import { Mutation, Action, Getter } from 'vuex-class'
import { guid } from '@/utils/auth.js'
@Component({
  components: {},
})
export default class PlotMultiSelect extends Vue {
  @Action('elevator/getFlatPlotLists') private getFlatPlotLists: any //返回所有小区
  @Action('elevator/getHierachyPlots') private getHierachyPlots: any //按层级加载小区
  @Action('elevator/getPlotListHierachyUnder')
  private getPlotListHierachyUnder: any //返回某层级下的全部小区

  @Mutation('issue/updatePlotInfo') private updatePlotInfo: any // 更新当前小区信息
  @Mutation('issue/updatePlotListInfo') private updatePlotListInfo: any // 更新当前选中小区
  @Mutation('issue/updateAllCheckedPlot') private updateAllCheckedPlot: any // 更新当前选中小区-是否全选

  @Prop({ type: Boolean, default: false })
  private visible: any // 是否显示弹窗

  public $refs: any

  private activeTabName: string = 'byRegion' // 当前选中的tab name
  private allChecked: boolean = false // 是否选择全部小区

  private checkedPlotList: any = [] // 当前选中的小区
  private flatPlotList: any = [] // 按小区选的小区列表

  private defaultExpandedKeys: any = [] // 展开的节点

  private trueSelectedPlotsInfo: any = [] // 由于树和列表都是懒加载,所以需要一个变量来存放真正选中的小区信息(注意:不是小区id)

  private searchList: any = [] // 查询时展示的列表
  private searchLoading: boolean = false // 查询时loading
  private searchText: string = '' // 查询内容

  //树的prop
  private props: any = {
    children: 'children',
    label: 'name',
    isLeaf: 'isLeaf',
  }
  //滚动加载的参数
  private conditions: any = {
    index: 0,
    size: 50,
  }
  private totalCount: number = 0 // 全部小区的数量
  private treeKey: string = guid()

  /**
   * 搜索以后的小区点选
   */
  private searchChange(node: any, checked: boolean) {
    if (checked) {
      // 选中
      if (!this.hasSelected(node)) {
        this.trueSelectedPlotsInfo.push(node)
      }
      if (!this.checkedPlotList.includes(node.id)) {
        this.checkedPlotList.push(node.id)
      }
    } else {
      // 取消选中
      for (let i = this.trueSelectedPlotsInfo.length - 1; i > -1; i--) {
        const item = this.trueSelectedPlotsInfo[i]
        if (item.id == node.id) {
          this.trueSelectedPlotsInfo.splice(i, 1)
          break
        }
      }
      for (let i = this.checkedPlotList.length - 1; i > -1; i--) {
        const item = this.checkedPlotList[i]
        if (item == node.id) {
          this.checkedPlotList.splice(i, 1)
          break
        }
      }
    }
    this.setTreeChecked()
    // 选中列表节点就取消选中全部小区
    this.allChecked = false
  }

  @Watch('searchText')
  private async searchTextChange(value: string) {
    this.searchLoading = true
    let res = await this.getFlatPlotLists({
      searchText: this.searchText,
    })
    this.searchLoading = false
    if (res.list && Array.isArray(res.list)) {
      this.searchList = res.list.map((item: any) => {
        item.checked = this.hasSelected(item)
        return item
      })
    }
    // this.$forceUpdate()
    if (value == '') {
      this.treeKey = guid()
    }
  }

  // @Watch('visible', { immediate: true })
  private async visibleChange(newVal: any) {
    if (newVal === true) {
      this.conditions = {
        index: 0,
        size: 50,
      }
      this.checkedPlotList = []
      this.flatPlotList = []
      this.defaultExpandedKeys = []
      this.trueSelectedPlotsInfo = []
      this.totalCount = 0

      this.allChecked = localStorage.getItem('allChecked') == 'true'
      let selectedPlotList: any = localStorage.getItem('selectedPlotList')

      if (this.allChecked) {
        if (selectedPlotList) {
          selectedPlotList = ''
          localStorage.setItem('selectedPlotList', '')
        }
      } else {
        if (selectedPlotList) {
          this.trueSelectedPlotsInfo = JSON.parse(selectedPlotList)

          // 设置选中的小区
          this.checkedPlotList = this.trueSelectedPlotsInfo.map(
            (item: any) => item.id
          )
          if (this.$refs.plotTree) {
            this.setTreeChecked()
          }
        }
      }

      this.loadFlatPlotLists()
    }
  }

  private open() {
    this.treeKey = guid()
    this.visibleChange(true)
  }

  /**
   * 判断某个节点是否已选中
   */
  private hasSelected(node: any) {
    const plotInfo = this.trueSelectedPlotsInfo.find(
      (item: any) => item.id == node.id
    )
    return !!plotInfo
  }

  /**
   * 小区列表懒加载
   */
  private scrollEvent(e: any) {
    const target = e.target || e.currentTarget
    if (target.scrollTop == target.scrollHeight - target.offsetHeight) {
      this.loadFlatPlotLists()
    }
  }
  /**
   * 按小区选:分页加载所有小区选择项
   */
  private async loadFlatPlotLists() {
    let res = await this.getFlatPlotLists({
      index: ++this.conditions.index,
      size: this.conditions.size,
    })
    this.flatPlotList = this.flatPlotList.concat(res.list)
    this.totalCount = res.total
  }
  /**
   * 按小区选-列表节点选中状态变化
   */
  private checkedListNodeChange(checked: any, e: any) {
    const target = e.target || e.currentTarget
    const value = target.defaultValue
    const node = this.flatPlotList.find((item: any) => item.id == value)
    if (checked) {
      // 选中
      if (!this.hasSelected(node)) {
        this.trueSelectedPlotsInfo.push(node)
      }
    } else {
      // 取消选中
      for (let i = this.trueSelectedPlotsInfo.length - 1; i > -1; i--) {
        const item = this.trueSelectedPlotsInfo[i]
        if (item.id == node.id) {
          this.trueSelectedPlotsInfo.splice(i, 1)
          break
        }
      }
    }
    // 选中列表节点就取消选中全部小区
    this.allChecked = false
  }

  /**
   * 设置选中状态
   */
  private setTreeChecked() {
    this.$nextTick(() => {
      let provinceCodes = this.trueSelectedPlotsInfo.map(
        (item: any) => item.provinceCode
      )
      for (const item of [...new Set(provinceCodes)]) {
        let provinceNode = this.$refs.plotTree.getNode(item)
        if (provinceNode) {
          provinceNode.checked = provinceNode.checked
            ? provinceNode.checked
            : provinceNode.data.checkedType == 'ALL_CHECKED'
          provinceNode.indeterminate = provinceNode.indeterminate
            ? provinceNode.indeterminate
            : provinceNode.data.checkedType == 'PART_CHECKED'
        }
      }
      let cityCodes = this.trueSelectedPlotsInfo.map(
        (item: any) => item.cityCode
      )
      for (const item of [...new Set(cityCodes)]) {
        let cityNode = this.$refs.plotTree.getNode(item)
        if (cityNode) {
          cityNode.checked = cityNode.checked
            ? cityNode.checked
            : cityNode.data.checkedType == 'ALL_CHECKED'
          cityNode.indeterminate = cityNode.indeterminate
            ? cityNode.indeterminate
            : cityNode.data.checkedType == 'PART_CHECKED'
        }
      }
      let townCodes = this.trueSelectedPlotsInfo.map(
        (item: any) => item.townCode
      )
      for (const item of [...new Set(townCodes)]) {
        let townNode = this.$refs.plotTree.getNode(item)
        if (townNode) {
          townNode.checked = townNode.checked
            ? townNode.checked
            : townNode.data.checkedType == 'ALL_CHECKED'
          townNode.indeterminate = townNode.indeterminate
            ? townNode.indeterminate
            : townNode.data.checkedType == 'PART_CHECKED'
        }
      }
      let streetCodes = this.trueSelectedPlotsInfo.map(
        (item: any) => item.streetCode
      )
      for (const item of [...new Set(streetCodes)]) {
        let streetNode = this.$refs.plotTree.getNode(item)
        if (streetNode) {
          streetNode.checked = streetNode.checked
            ? streetNode.checked
            : streetNode.data.checkedType == 'ALL_CHECKED'
          streetNode.indeterminate = streetNode.indeterminate
            ? streetNode.indeterminate
            : streetNode.data.checkedType == 'PART_CHECKED'
        }
      }
      let plotIds = this.trueSelectedPlotsInfo.map((item: any) => item.id)
      for (const item of [...new Set(plotIds)]) {
        let plotNode = this.$refs.plotTree.getNode(item)
        if (plotNode) {
          plotNode.checked = true
        }
      }
    })
  }
  /**
   * 按地区选-树节点选中状态变化
   */
  private async checkedTreeNodeChange(node: any, checkedInfo: any) {
    // 选中树节点就取消选中全部小区
    this.allChecked = false

    if (checkedInfo.checkedKeys.includes(node.id)) {
      // 选中
      if (node.type == 'PLOT') {
        // 选中小区
        // 当前未被选中
        if (!this.hasSelected(node)) {
          this.trueSelectedPlotsInfo.push(node)
        }
      } else {
        // 选中省市区...
        const underPlotList = await this.getPlotListHierachyUnder(node)
        for (const item of underPlotList) {
          if (!this.hasSelected(item)) {
            this.trueSelectedPlotsInfo.push(item)
          }
        }
      }
    } else {
      // 取消选中
      if (node.type == 'PLOT') {
        // 选中小区
        for (let i = this.trueSelectedPlotsInfo.length - 1; i > -1; i--) {
          const item = this.trueSelectedPlotsInfo[i]
          if (item.id == node.id) {
            this.trueSelectedPlotsInfo.splice(i, 1)
            break
          }
        }
      } else {
        // 选中省市区...
        const underPlotList = await this.getPlotListHierachyUnder(node)
        const underPlotIds = underPlotList.map((item: any) => item.id)
        for (let i = this.trueSelectedPlotsInfo.length - 1; i > -1; i--) {
          const item = this.trueSelectedPlotsInfo[i]
          if (underPlotIds.includes(item.id)) {
            this.trueSelectedPlotsInfo.splice(i, 1)
          }
        }
      }
    }
  }
  /**
   * 按地区选-加载树节点
   */
  private async loadTreeNode(node: any, resolve: any) {
    // 根节点(省)
    if (node.level === 0) {
      const res = await this.getHierachyPlots({
        checkedPlots: this.trueSelectedPlotsInfo.map((item: any) => item.id),
      })
      resolve(res)
      this.setTreeChecked()
    } else {
      let codeType = node.data.type
      let res = []
      switch (codeType) {
        //省→市
        case 'PROVINCE':
          res = await this.getHierachyPlots({
            provinceCode: node.data.provinceCode,
            checkedPlots: this.trueSelectedPlotsInfo.map(
              (item: any) => item.id
            ),
          })
          break
        //市→区
        case 'CITY':
          res = await this.getHierachyPlots({
            cityCode: node.data.cityCode,
            checkedPlots: this.trueSelectedPlotsInfo.map(
              (item: any) => item.id
            ),
          })
          break
        //区→街道
        case 'TOWN':
          res = await this.getHierachyPlots({
            townCode: node.data.townCode,
            checkedPlots: this.trueSelectedPlotsInfo.map(
              (item: any) => item.id
            ),
          })
          break
        //街道→小区
        case 'STREET':
          res = await this.getHierachyPlots({
            streetCode: node.data.streetCode,
            checkedPlots: this.trueSelectedPlotsInfo.map(
              (item: any) => item.id
            ),
          })
          break
      }
      if (res && res.length && res[0].type == 'PLOT') {
        for (const item of res) {
          item.isLeaf = true
        }
      }
      resolve(res)
      this.setTreeChecked()
    }
  }

  /**
   * 切换tab
   */
  private handleTablick() {
    if (this.activeTabName == 'byRegion') {
      this.activeByRegion()
    } else if (this.activeTabName == 'byCommunity') {
      this.activeByCommunity()
    }
  }
  /**
   * 切换tab-按地区选
   */
  private activeByRegion() {
    this.treeKey = guid()
    this.setTreeChecked()
  }
  /**
   * 切换tab-按小区选
   */
  private activeByCommunity() {
    this.checkedPlotList = this.trueSelectedPlotsInfo.map(
      (item: any) => item.id
    )
  }

  // 已选小区的数量
  private get flatNum() {
    if (this.allChecked) {
      return this.totalCount
    }
    return this.trueSelectedPlotsInfo.length
  }

  /**
   * 清空
   */
  private clickSelectNone() {
    this.allChecked = false
    const plotTree: any = this.$refs.plotTree
    plotTree.setCheckedKeys([])
    this.checkedPlotList.splice(0, this.checkedPlotList.length)
    this.trueSelectedPlotsInfo.splice(0, this.trueSelectedPlotsInfo.length)
  }
  /**
   * 全部小区-选中/取消选中
   */
  private allCheckedChange() {
    const plotTree: any = this.$refs.plotTree
    plotTree.setCheckedKeys([])
    this.checkedPlotList.splice(0, this.checkedPlotList.length)
    this.trueSelectedPlotsInfo.splice(0, this.trueSelectedPlotsInfo.length)
  }

  /**
   * 取消
   */
  private clickCancle() {
    this.$emit('update:visible', false)
  }
  /**
   * 确定
   */
  private clickConfirm() {
    if (!this.trueSelectedPlotsInfo.length && !this.allChecked) {
      this.$message.warning('选择小区为空')
      return
    }

    if (this.allChecked) {
      // 选中全部小区
      localStorage.setItem('allChecked', 'true')
      this.updateAllCheckedPlot(true)
      localStorage.setItem('selectedPlotList', JSON.stringify([]))
      this.updatePlotListInfo([])
      localStorage.setItem('plotId', '')
      localStorage.setItem('plotName', '')
      this.updatePlotInfo({
        plotId: '',
        plotName: '',
      })
    } else {
      localStorage.setItem('allChecked', 'false')
      this.updateAllCheckedPlot(false)
      localStorage.setItem(
        'selectedPlotList',
        JSON.stringify([...this.trueSelectedPlotsInfo])
      )
      this.updatePlotListInfo([...this.trueSelectedPlotsInfo])
      localStorage.setItem('plotId', this.trueSelectedPlotsInfo[0].plotId)
      localStorage.setItem('plotName', this.trueSelectedPlotsInfo[0].plotName)
      this.updatePlotInfo({
        plotId: this.trueSelectedPlotsInfo[0].plotId,
        plotName: this.trueSelectedPlotsInfo[0].plotName,
      })
    }

    this.$emit('updatePlotSelectInfo')
    this.$emit('update:visible', false)
  }
}
</script>
<style lang="scss" scoped>
.selectAllBox {
  height: 46px;
  background: rgba(255, 255, 255, 1);
  border: 1px solid rgba(238, 238, 238, 1);
  border-bottom: none;
  display: flex;
  align-items: center;

  .selectAllButton {
    cursor: pointer;
    color: #0d86ff;
    font-weight: 500;
    margin-left: 24px;
  }

  .selectCount {
    color: #3f3f3f;
    font-size: 14px;
    margin-left: 10px;
  }
}

/deep/ .el-checkbox-group {
  display: flex;
  flex-direction: column;
  padding-left: 24px;
}
/deep/ .dialog-footer {
  padding-left: 24px;
}
/deep/ .el-checkbox {
  margin-top: 3px;
}
</style>

组件的使用

<PlotMultiSelectDialog
  :visible.sync="showPlogMultiSelectDialog"
  @updatePlotSelectInfo="updatePlotSelectInfo"
></PlotMultiSelectDialog>

private async updatePlotSelectInfo() {
  const allChecked = localStorage.getItem('allChecked') == 'true'
  this.allChecked = allChecked
  const selectedPlotList: any = localStorage.getItem('selectedPlotList')
  this.choosedPlotList = JSON.parse(selectedPlotList)
  const plotId = localStorage.getItem('plotId')
  if (plotId) {
    this.form.plotId = plotId
  } else {
    this.form.plotId = this.choosedPlotList.length
      ? this.choosedPlotList[0].plotId
      : ''
  }
  const plotName = localStorage.getItem('plotName')
  if (plotName) {
    this.form.plotName = plotName
  } else {
    this.form.plotName = this.choosedPlotList.length
      ? this.choosedPlotList[0].plotName
      : ''
  }

  this.streetCount = await this.getStreetCount({
    plotIdList: this.choosedPlotList.map((item: any) => item.plotId)
  })
}

贴几张接口返回数据:
在这里插入图片描述
在这里插入图片描述


版权声明:本文为guoyuanyuan6496原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。