顾问要求展示多层级的地区选择,市面上有的不是结构不符合就是不能多选,没办法,只能自己写一个组件。
树形组件还是第一次做,好多地方用到了递归,参考了blog.csdn.net/bushanyanta… 可惜样式不符合,只能借鉴里面的思路
贴一个动图
效果如上,记录下代码和思路:
// CascaderCheckboxAddr.vue
<template>
<div class="bu">
<div class="bu-box">
<div class="bu-col left">
<!-- 国家 -->
<div v-for="item in addrData" :key="item.text" class="item"
:class="active1 === item.value ? 'active' : ''">
<div class="title" @click="changeTitle(item, 1)"> {{ item.text }}</div>
<!-- {{ active1 }} -->
<!-- {{ item.check }} -->
<!-- <van-checkbox icon-size="14px" class="checkbox" @click.native="changeCheckbox(item, 1)"
v-model="item.check" shape="square">
</van-checkbox> -->
<div class="van-checkbox__icon van-checkbox__icon--square"
:class="item.check || item.halfCheck ? 'van-checkbox__icon--checked' : ''"
style="font-size: 14px;" @click="changeCheckbox(item, 1)">
<i class="van-icon" :class="item.halfCheck ? 'van-icon-minus' : 'van-icon-success'"></i>
</div>
</div>
</div>
<div class="bu-col right grey1" v-show="active2">
<!-- 省份 -->
<div v-for="item in secondData" :key="item.value" class="item"
:class="active2 === item.value ? 'active' : ''">
<div class="title" @click="changeTitle(item, 2)"> {{ item.text }}</div>
<!-- <van-checkbox icon-size="14px" class="checkbox" @click.native="changeCheckbox(item, 2)"
v-model="item.check" shape="square">
</van-checkbox> -->
<div class="van-checkbox__icon van-checkbox__icon--square"
:class="item.check || item.halfCheck ? 'van-checkbox__icon--checked' : ''"
style="font-size: 14px;" @click="changeCheckbox(item, 2)">
<i class="van-icon" :class="item.halfCheck ? 'van-icon-minus' : 'van-icon-success'"></i>
</div>
</div>
</div>
<div class="bu-col right grey2" v-show="active3">
<!-- 城市 -->
<div v-for="item in thirdData" :key="item.value" class="item"
:class="active3 === item.value ? 'active' : ''">
<div class="title" @click="changeTitle(item, 3)"> {{ item.text }}</div>
<!-- <van-checkbox icon-size="14px" class="checkbox" @click.native="changeCheckbox(item, 3)"
v-model="item.check" shape="square">
</van-checkbox> -->
<div class="van-checkbox__icon van-checkbox__icon--square"
:class="item.check || item.halfCheck ? 'van-checkbox__icon--checked' : ''"
style="font-size: 14px;" @click="changeCheckbox(item, 3)">
<i class="van-icon" :class="item.halfCheck ? 'van-icon-minus' : 'van-icon-success'"></i>
</div>
</div>
</div>
<div class="bu-col right grey3" v-show="active4">
<!-- 区 -->
<div v-for="item in forthData" :key="item.value" class="item"
:class="active4 === item.value ? 'active' : ''">
<div class="title" @click="changeTitle(item, 4)"> {{ item.text }}</div>
<!-- <van-checkbox icon-size="14px" class="checkbox" @click.native="changeCheckbox(item, 4)"
v-model="item.check" shape="square">
</van-checkbox> -->
<div class="van-checkbox__icon van-checkbox__icon--square"
:class="item.check || item.halfCheck ? 'van-checkbox__icon--checked' : ''"
style="font-size: 14px;" @click="changeCheckbox(item, 4)">
<i class="van-icon" :class="item.halfCheck ? 'van-icon-minus' : 'van-icon-success'"></i>
</div>
</div>
</div>
</div>
<div class="footer-box">
<div @click="reset(addrData, false, true)">重置</div>
<div class="finally" @click="getData">完成</div>
</div>
</div>
</template>
<script>
import Vue from 'vue';
import { Tabbar, TabbarItem, Toast } from 'vant';
Vue.use(Toast);
Vue.use(Tabbar);
Vue.use(TabbarItem);
export default {
name: 'CascaderCheckboxAddr',
components: {},
mixins: [],
props: {
addrData: {
type: Array,
default: () => []
}
},
data() {
return {
secondData: [],
thirdData: [],
forthData: [],
active1: "",
active2: "",
active3: "",
active4: "",
activeItem1: "",
activeItem2: "",
activeItem3: "",
activeItem4: "",
}
},
computed: {
},
watch: {
},
mounted() {
this.addCheck(this.addrData)
this.addrData[0].check = true
},
methods: {
// 递归函数在每一项中添加check/halfCheck
addCheck(arr) {
for (var i = 0; i < arr.length; i++) {
this.$set(arr[i], 'check', false)
this.$set(arr[i], 'halfCheck', false)
this.$set(arr[i], 'children', [])
if (arr[i].children && arr[i].children.length > 0) {
this.addCheck(arr[i].children)
}
}
},
reset(arr, check, isInit) {
for (var i = 0; i < arr.length; i++) {
this.$set(arr[i], 'check', check)
this.$set(arr[i], 'halfCheck', false)
if (arr[i].children && arr[i].children.length > 0) {
this.reset(arr[i].children, check)
}
}
if (isInit) {
this.addrData[0].check = true
}
},
// 递归函数 修改子级状态
setChildrenChecked(arr, check, halfCheck) {
for (var i = 0; i < arr.length; i++) {
this.$set(arr[i], 'check', check)
this.$set(arr[i], 'halfCheck', halfCheck)
if (arr[i].children && arr[i].children.length > 0) {
this.setChildrenChecked(arr[i].children, check, halfCheck)
}
}
},
// 递归函数 修改父级状态
setFatherChecked(father, fatherId) {
// console.log(father, fatherId, "father,fatherId");
let flag = false;
let someFlag = false
someFlag = father.children.some(n => n.check || n.halfCheck)
flag = father.children.every(n => n.check)
someFlag = flag ? false : someFlag
this.$set(father, "halfCheck", someFlag)
this.$set(father, "check", flag)
if (fatherId - 1 > 0) {
this.setFatherChecked(this[`activeItem${fatherId - 1}`], fatherId - 1)
}
},
setFirstChange() {
},
changeCheckbox(item, col, type = 'box') {
console.log(item, col, type, "====");
if (type === 'box') {
item.check = !item.check
item.halfCheck = false
}
switch (col) {
case 1: // 省份
this.active1 = item.value
if (item.value !== 'CN') {
// 其他国家隐藏二三四列
this.active2 = ""
this.active3 = ""
this.active4 = ""
return
}
// activeItem1 只能为中国
this.activeItem1 = item
if (!this.activeItem1.hasLoad) {
this.queryDictionaryType('CUX_ADDRESS_PROVINCE', item.value).then(res => {
// res = res.slice(0, 3)
this.secondData = res.map(n => ({ ...n, check: item.check, type: "province" }))
this.$set(this.activeItem1, "children", this.secondData)
this.activeItem2 = this.secondData[0]
this.active2 = this.secondData[0].value
this.queryDictionaryType('CUX_ADDRESS_CITY', this.secondData[0].value).then(res1 => {
res1 = res1.slice(0, 3)
this.thirdData = res1.map(n => ({ ...n, check: item.check, type: "city" }))
this.$set(this.activeItem2, "children", this.thirdData)
this.activeItem3 = this.thirdData[0]
this.active3 = this.thirdData[0].value
this.queryDictionaryType('CUX_ADDRESS_AREA', this.thirdData[0].value).then(res2 => {
this.forthData = res2.map(n => ({ ...n, check: item.check, type: "district" }))
this.$set(this.activeItem3, "children", this.forthData)
this.activeItem4 = this.forthData[0]
this.active4 = this.forthData[0].value
}).catch(() => {
})
}).catch(() => {
})
}).catch(() => {
})
this.$set(item, "hasLoad", true)
} else {
this.setChildrenChecked(item.children, item.check, item.halfCheck)
this.secondData = item.children
this.active2 = this.secondData[0].value
this.activeItem2 = this.secondData[0]
this.thirdData = this.activeItem2.children
this.active3 = this.thirdData[0].value
this.activeItem3 = this.thirdData[0]
this.forthData = this.activeItem3.children
this.active4 = this.forthData[0].value
this.activeItem4 = this.forthData[0]
this.$set(this.activeItem1, "children", this.secondData)
}
break;
case 2: // 城市
this.active2 = item.value
this.activeItem2 = item
this.setFatherChecked(this.activeItem1, 1)
// console.log(this.activeItem1, "this.activeItem1");
if (!this.activeItem2.hasLoad) {
this.queryDictionaryType('CUX_ADDRESS_CITY', item.value).then(res => {
// res = res.slice(0, 3)
this.thirdData = res.map(n => ({ ...n, check: item.check, type: "city" }))
this.$set(this.activeItem2, "children", this.thirdData)
this.activeItem3 = this.thirdData[0]
this.active3 = this.thirdData[0].value
this.queryDictionaryType('CUX_ADDRESS_AREA', this.thirdData[0].value).then(res2 => {
this.forthData = res2.map(n => ({ ...n, check: item.check, type: "district" }))
this.$set(this.activeItem3, "children", this.forthData)
this.activeItem4 = this.forthData[0]
this.active4 = this.forthData[0].value
}).catch(() => {
})
}).catch(() => {
})
this.$set(item, "hasLoad", true)
} else {
this.thirdData = item.children
this.active3 = this.thirdData[0].value
this.activeItem3 = this.thirdData[0]
this.forthData = this.activeItem3.children
this.active4 = this.forthData[0].value
this.activeItem4 = this.forthData[0]
if (type === 'box') {
this.setChildrenChecked(item.children, item.check, item.halfCheck)
}
this.$set(this.activeItem2, "children", this.thirdData)
}
break;
case 3: // 县城区域
this.active3 = item.value
this.activeItem3 = item
this.setFatherChecked(this.activeItem2, 2)
if (!this.activeItem3.hasLoad) {
this.queryDictionaryType('CUX_ADDRESS_AREA', item.value).then(res => {
this.forthData = res.map(n => ({ ...n, check: item.check, type: "district" }))
this.$set(this.activeItem3, "children", this.forthData)
this.activeItem4 = this.forthData[0]
this.active4 = this.forthData[0].value
}).catch(() => {
})
this.$set(item, "hasLoad", true)
} else {
this.forthData = item.children
this.activeItem4 = this.forthData[0]
this.active4 = this.forthData[0].value
if (type === 'box') {
this.setChildrenChecked(item.children, item.check, item.halfCheck)
}
this.$set(this.activeItem3, "children", this.forthData)
}
break;
case 4: // 街道
this.active4 = item.value
this.activeItem4 = item
this.setFatherChecked(this.activeItem3, 3)
break;
default:
break;
}
},
changeTitle(item, col) {
this.changeCheckbox(item, col, "title")
},
getData() {
this.$emit("getData", this.addrData)
},
// 字典接口
queryDictionaryType(type, flag) {
return new Promise(async (resolve, reject) => {
const res = await this.baseService.queryDictionaryType({
language: "zh-CN",
dictionaryCode: type,
entryFlag: flag,
})
if (res.data.length > 0) {
res.data = res.data.map(n => {
return {
text: n.entryName, value: n.entryCode
}
})
resolve(res.data)
} else {
reject()
}
})
},
}
}
</script>
<style lang="less" scoped>
// @headerHeight: 50px;
// @avatarHeight: 42px;
// @avatarFontSize: 18px;
.bu {
position: relative;
height: 400px;
// overflow: hidden;
// border-radius: 0 10px 10px 0;
}
.footer-box {
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
border-top: 1px solid #eee;
>div {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 16px;
}
.finally {
background-color: var(--custom-primary-color);
color: #fff;
}
}
.grey1{
background-color: #fafafa;
}
.grey2{
background-color: #f5f5f5;
}
.grey3{
background-color: #f0f0f0;
}
.bu-box {
display: flex;
justify-content: space-between;
align-items: flex-start;
font-size: 14px;
height: calc(100% - 40px);
overflow: hidden;
overflow-x: scroll;
.bu-col {
flex: 1;
padding: 5px 0;
min-width: 40vw;
height: 100%;
// overflow: hidden;
overflow-y: scroll;
}
.left {
background-color: #fff;
// border-right: 1px solid #eee;
}
.right {
.item {
// background-color: #e8f0fd !important;
// color: var(--custom-primary-color) !important;
}
}
.item {
padding: 10px;
// margin-bottom: 5px;
display: flex;
justify-content: space-between;
align-items: center;
.title {
flex: 1;
}
.checkbox {
margin-left: 10px;
}
}
.active {
background-color: #e8f0fd;
color: var(--custom-primary-color);
}
}
</style>
父组件
<CascaderCheckboxAddr :addrData="addrData" @getData="getAddrsData"></CascaderCheckboxAddr>
async mounted() {
// 异步获取并处理国家字典项,区域 国家
this.queryDictionaryType('CUX_ADDRESS_COUNTRY').then(res => {
this.addrData = res.map(n => ({ ...n, type: "country" }))
this.addrData.unshift({
text: '全部国家', value: "全部国家"
});
});
},
methods: {
// 递归函数获取addrs
getAddrs(data) {
let countryList = []
let provinceList = []
let cityList = []
let districtList = []
function traverseData(item) {
// 判断当前组织是否为BU类型,如果是,则计数器加一
if ((item.check === true || item.halfCheck === true) && item.type === "country") {
countryList.push(item);
}
if ((item.check === true || item.halfCheck === true) && item.type === "province") {
provinceList.push(item);
}
if ((item.check === true || item.halfCheck === true) && item.type === "city") {
cityList.push(item);
}
if ((item.check === true || item.halfCheck === true) && item.type === "district") {
districtList.push(item);
}
if (item.children && Array.isArray(item.children)) {
for (let subItem of item.children) {
traverseData(subItem);
}
}
}
for (let i = 0; i < data.length; i++) {
traverseData(data[i]); // 从根节点开始遍历
}
// console.log("countryList: ", countryList);
// console.log("provinceList: ", provinceList);
// console.log("cityList : ", cityList);
// console.log("districtList : ", districtList);
return {
countryList,
provinceList,
cityList,
districtList,
}
},
// 切换组织
getAddrsData(data) {
// console.log(data, "getAddrs");
// countryCode: null,
// provinceCode: null,
// cityCode: null,
// countyCode: null,
let { countryList, provinceList, cityList, districtList } = this.getAddrs(data)
if (countryList.length == 0 && provinceList.length == 0 && cityList.length == 0 && districtList.length == 0) {
this.addrsTitle = "全部区域"
this.searchForm.countryCode = null
this.searchForm.provinceCode = null
this.searchForm.cityCode = null
this.searchForm.countyCode = null
this.getPageData('search')
this.$refs.addrs.toggle()
return;
}
this.searchForm.countryCode = countryList.filter(n => n.check).map(n => n.value)
this.searchForm.provinceCode = provinceList.filter(n => n.check).map(n => n.value)
this.searchForm.cityCode = cityList.filter(n => n.check).map(n => n.value)
this.searchForm.countyCode = districtList.filter(n => n.check).map(n => n.value)
// 显示名称------
if (countryList && countryList.length > 1) {
this.addrsTitle = "多个国家"
} else if (countryList && countryList.length === 1) {
if (countryList[0].value !== 'CN') {
this.addrsTitle = countryList[0].text
} else if (countryList && countryList.length === 1) {
if (countryList[0].check) {
this.addrsTitle = countryList[0].text
} else {
if (provinceList && provinceList.length > 1) {
this.addrsTitle = "多个省份"
} else if (provinceList && provinceList.length === 1) {
if (provinceList[0].check) {
this.addrsTitle = provinceList[0].text
} else {
if (cityList && cityList.length > 1) {
this.addrsTitle = "多个城市"
} else if (cityList && cityList.length === 1) {
if (cityList[0].check) {
this.addrsTitle = cityList[0].text
} else {
if (districtList && districtList.length > 1) {
this.addrsTitle = "多个区域"
} else if (districtList && districtList.length === 1) {
if (districtList[0].check) {
this.addrsTitle = districtList[0].text
}
}
}
}
}
}
}
}
}
this.getPageData('search')
this.$refs.addrs.toggle()
},
// 字典接口
queryDictionaryType(type, flag) {
return new Promise(async (resolve, reject) => {
const res = await this.baseService.queryDictionaryType({
language: "zh-CN",
dictionaryCode: type,
entryFlag: flag,
})
if (res.data.length > 0) {
res.data = res.data.map(n => {
return {
text: n.entryName, value: n.entryCode
}
})
resolve(res.data)
} else {
reject()
}
})
},
},
业务要求是:
- 国家-省份-城市-地区四个层级(都是用字典接口查询的,所以总的数据结构需要自己组装,后面要求除了中国别的国家不展开其他几个层级)
- 组件可以多选,展示效果是大于一个国家显示多个国家,只有一个国家展示那个国家名称,省份城市区域也是这样的显示逻辑
实现的思路主要是:
- 点击的是checkbox框还是只是点击title,如果是点击的title只作为展开操作,点击box,会更改check/halfCheck的状态;
- 第一次点击的时候从接口获取值,赋值之后将item的hasLoad设置为true,并且当前层级有下一层的时候需要继续调用下一层的接口,一直到最后一个层级结束,首次点击的不是第一层级国家的时候,也需要递归函数 修改父级状态
- 非首次点击的时候,相当于是切换子数据,需要递归函数 修改子级状态;
写这个组件的时候,顾问还要求写一个需求和这个类似,多选且树形结构,不同的是,那个多选组织的需求,只有两个层级,并且只有一个接口直接返回整个树形结构,比较简单,不用考虑点击item之后父级和子级的状态更改问题,也不用考虑数据接口的组装问题,算是比较顺利,到这个层级比较多的时候,思路已经不能使用了,本来开始的思路是点击第一层设置第二层数据,即打开上一层设置下一层数据,过程中发现上下层的数据一直对不上,比如说北京对上了天津的下一层数据,因为开始的时候没做树形的想法,控制页面展开那个变量做不了,后面才用的active1234做的绑定显示,到最后就成了一点击后面的几个层级全部展开,这算是代码自己的想法吧。