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

设计思路:
- 用一个变量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版权协议,转载请附上原文出处链接和本声明。