显示目录

Vue 的地址选择器组件

有时候会用到地址选择的功能,已经有了很多使用 Vue 实现的地址选择组件,下面使用 Vue + iView 的 Cascader 来介绍怎么使用一个简单的地址选择组件 AddressPicker

AddressPicker.vue

地址选择组件 AddressPicker 的实现比较直观,关键是使用 Cascader 的动态加载特性,点击一个地区的时候使用 Ajax 加载它的下一级地区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<!--
名字:
地址选择器 AddressPicker

说明:
地址是按地区的层级从上到下构成的,例如 '北京市 / 西城区 / 新街口街道',可以使用地区的 id 的数组 [1, 38, 580] 表示一个地址,数组中前一个地区是后一个地区的上一级。

案例:
1. <AddressPicker @on-change="change"/>
2. 传入初始化地址,地址选择器会高亮选中该地址:
<AddressPicker :address="address" @on-change="change"/>

data() {
return {
address: {
path: '', // 地址的路径,便于阅读,可选
regionIds: [1, 40, 593], // 地址的地区 id 数组,用于表示一个唯一的地址
}
};
},

参数:
address [地址,可选参数]: 必须有属性 regionIds,其类型为数组,例如 { regionIds: [1, 38], path: '' }

事件:
on-change: 选择地区的时候触发,参数为地址对象,例如 {"path":"北京市宣武区大栅栏街道","regionIds":[1,40,591]},
同时绑定的 address 的属性 path 和 regionIds 也会进行更新
-->
<template>
<Cascader :data="country.children" :load-data="loadData" v-model="selection" change-on-select @on-change="onChange"/>
</template>

<script>
export default {
props: {
address: { type: Object, default: () => { return { regionIds: [] }; } },
},
data() {
return {
selection: [],
country : { value: 0, children: [] },
};
},
mounted() {
this.loadHierarchyRegions([this.country], [0, ...this.address.regionIds], () => {
this.selection.push(...this.address.regionIds);
});
},
methods: {
onChange(value, selectedData) {
this.address.path = '';
this.address.regionIds = [];

for (const region of selectedData) {
this.address.regionIds.push(region.value);
this.address.path += region.label;
}

this.$emit('on-change', this.address);
},
loadData(item, callback) {
item.loading = true;

this.loadRegions(item, () => {
item.loading = false;
callback();
});
},
// 加载地区 parent 下的地区
loadRegions(parent, callback) {
// 注意: Rest 是基于 axios 封装的功能,访问 https://qtdebug.com/fe-axios-rest/
// Rest.get 只是个 Ajax 请求,可以替换为自己熟悉的方式
Rest.get({ url: '/api/regions', data: { parentId: parent.value } }).then(regions => {
for (const region of regions) {
// 提示: 有属性 loading 则表明需要加载 children,界面中 item 上有箭头,否则就是最后一级,没有箭头
if (region.childrenCount === 0) {
parent.children.push({ label: region.name, value: region.id, children: [] });
} else {
parent.children.push({ label: region.name, value: region.id, children: [], loading: false });
}
}

callback && callback();
});
},
/**
* 加载地区的 id 的数组 regionIds 指定路径上的所有地区。
* 地址是按地区的层级从上到下构成的,例如 '北京市 / 西城区 / 新街口街道'
* 可以使用地区的 id 的数组 [1, 38, 580] 表示一个地址,数组中前一个地区是后一个地区的上一级。
*
* @param {Array } siblingRegions 同 regionIds[0] 的地区在同一层级的所有地区
* @param {Array } regionIds 组成地址的所有地区 id 的数组,前一个地区是后一个地区的上一级
* @param {Function } finishCallback 地址加载完时的回调函数
*/
loadHierarchyRegions(siblingRegions, regionIds, finishCallback) {
// 1. 如果数组 regionIds 为空,则说明加载完成,执行完成的回调函数
// 2. 找到最顶级的地区 topRegion
// 3. 加载 topRegion 的下一级地区
// 4. 加载完后从 regionIds 中删除第一个元素 (对应上面的 topRegion),第二个地区变成了最顶级的地区,递归继续加载

// [1] 如果数组 regionIds 为空,则说明加载完成,执行完成的回调函数
if (regionIds.length === 0) {
finishCallback && finishCallback();
return;
}

// [2] 找到最顶级的地区 topRegion
let topRegion = siblingRegions.find(sibling => sibling.value === regionIds[0]);

if (topRegion) {
// [3] 加载 topRegion 的下一级地区
this.loadRegions(topRegion, () => {
// [4] 加载完后从 regionIds 中删除第一个元素 (对应上面的 topRegion),第二个地区变成了最顶级的地区,递归继续加载
regionIds.shift();
this.loadHierarchyRegions(topRegion.children, regionIds, finishCallback);
});
}
}
},
};
</script>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div class="demo">
<AddressPicker @on-change="change1"/><br>
<AddressPicker :address="address" @on-change="change2"/>
</div>
</template>

<script>
import AddressPicker from '@/components/AddressPicker.vue';

export default {
components: { AddressPicker },
data() {
return {
address: {
path: '',
regionIds: [1, 40, 593],
}
};
},
methods: {
change1(address) {
console.log(JSON.stringify(address));
},
change2(address) {
console.log(JSON.stringify(address), JSON.stringify(this.address));
}
}
};
</script>

<style lang="scss">
.demo {
width: 200px;
}
</style>

地址格式

服务器响应的地址格式使用 Json 数组,需要属性 id, name, childrenCount (有多少个下一级地区):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[{
"name": "大栅栏街道",
"id": 591,
"parentId": 40,
"childrenCount": 0
}, {
"name": "天桥街道",
"id": 592,
"parentId": 40,
"childrenCount": 0
}, {
"name": "广安门内街道",
"id": 593,
"parentId": 40,
"childrenCount": 0
}, {
"name": "陶然亭街道",
"id": 598,
"parentId": 40,
"childrenCount": 0
}]

地区数据

全国的地区数据 region.sql,直接导入 MySQL 即可使用。

服务器端

服务器端可以根据自己的情况实现,使用 SpringMVC + MyBatis 的代码如下:

RegionController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class RegionContrller {
/**
* 获取指定地区的下一级地区,省为第一级地区,它的 parentId 为 0
* 地址: http://localhost:8080/api/regions?parentId=1
*
* @param parentId
* @return 返回地区的数组
*/
@GetMapping("/api/regions")
@ResponseBody
public List<Region> findRegionsByParentId(@RequestParam(defaultValue="0") int parentId) {
return regionMapper.findRegionsByParentId(parentId);
}
}

RegionMapper.java

1
2
3
4
5
6
7
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface RegionMapper {
List<Region> findRegionsByParentId(int parentId);
}

RegionMapper.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.xtuer.mapper.RegionMapper">
<!--查找 parentId 为传入的 parentId 的地区-->
<select id="findRegionsByParentId" resultType="Region">
SELECT id, name, parent_id as parentId, children_count as childrenCount FROM region WHERE parent_id=#{parentId}
</select>
</mapper>

思考

上面提供的地区数据只有省、市、县、乡等,数据不全,很多地方没有到街道、小区等,如果需要,可以找一个更全的地区数据库来使用,或者缺失的部分让用户进行输入,我们的核心是介绍怎么实现一个地址选择工具。