接到过一个城市级联选择的需求,要求将中国大陆内的所有城市以列表的形式返回给前端,但只定义一个cityLevel的key用于标记该城市为几线城市,且需要实现根据任意城市选择或根据城市等级选择,还要包含全选和反选功能。初看其实需求并不复杂,但其实坑点非常多,尤其在使用了Element Plus中的el-cascader这个组件来实现的时候,遇到了包括勾选的城市及省的联动关系、数据结构转换、全选反选部分情况下视图不更新等问题。所以大致做一下记录,这个问题解决后的收获还是挺多的。
template的模板如下
<template>
<el-cascader-panel @change="handleCitiesChange" v-model="selectedCities" :props="{
multiple: true, value: 'geoId',
label: 'geoName',
children: 'children',
emitPath: true
}" :options="options" />
<el-button @click="handleGetSelectedData">获取已选择的市</el-button>
<el-button @click="reverseSelection">反选</el-button>
<div class="selected-provinces">
<el-tag v-for="item in selectedData" :key="item.geoId" closable @close="handleTagClose(item)">
{{ item.geoName }}
</el-tag>
</div>
</template>
填充的初始数据结构如下,也就是后端初始返回的数据结构(原本是希望后端根据前端需求返回正确且方便的数据结构的,但该需求新来的菜b后端同事似乎并不是很清楚)
const options = [
{
geoId: '100000',
geoName: '云南省',
children: [
{
geoId: '100100',
geoName: '昆明市'
},
{
geoId: '100200',
geoName: '玉溪市'
},
// 其他城市...
],
},
{
geoId: '440000',
geoName: '广东省',
children: [
{
geoId: '440100',
geoName: '广州市'
},
{
geoId: '440200',
geoName: '韶关市'
},
{
geoId: '440300',
geoName: '东莞市'
},
// 其他城市...
],
},
// 其他省市...
]
首先要实现在勾选某个省下所有城市的时候,右侧展示区域只展示这个省的信息;反之则展示已经勾选的所有城市。这里其实挺简单,只需要考虑数组去重及组件的v-model数据同步问题,简单实现如下:
const handleGetSelectedData = () => {
const flatSelectedCities = selectedCities.value.flat();
const newData = [];
for (const province of options) {
const provinceCityIds = province.children ? province.children.map(city => city.geoId) : [];
const allSelected = provinceCityIds.every(cityId => flatSelectedCities.includes(cityId));
if (allSelected) {
newData.push(province);
} else {
const selectedCityIds = flatSelectedCities.filter(cityId => provinceCityIds.includes(cityId));
const selectedCitiesData = province.children.filter(city => selectedCityIds.includes(city.geoId));
newData.push(...selectedCitiesData);
}
}
selectedData.value = newData;
};
大致思路其实是先把某个省下的所有事的id筛选出来,然后定义一个allSelected布尔值,标记这个省下的所有市是否全选了,如果已经全选,则加这个省加入到newData数组;反之则加入这些所有市的id,再将newData赋值给selectedData进行响应式视图更新。ok,第一个小点完成,相比之后遇到的坑,这个点算是一个很顺利的实现。接下来遇到了第一个棘手的问题。
观察如下代码:
const reverseSelection = () => {
const selectedCityIds = selectedCities.value.flat();
selectedCities.value = options
.filter(province => province.children)
.flatMap(province => {
const provinceCityIds = province.children.map(city => city.geoId);
return provinceCityIds.filter(cityId => !selectedCityIds.includes(cityId));
});
}
上述代码其实是勾选的城市进行反选的功能,首先将整个数组进行展平(selectedCityIds为二维数组),然后通过filter求差集的操作,看似已经实现了功能,但多在视图上点击几次就发现有时勾选状态会有一个短暂的freezing状态,也就是控制台有数据变化,但已勾选的城市并未变化。为什么视图会不更新呢?这个问题我想后面单独写一篇文章来描述,先说临时的解决办法(使用了条件渲染指令v-if来销毁dom后重新渲染),这个方法显然并不是一个好方法,一点也不优雅。
。。。