Content Table

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
<!--
名字:
地址选择器 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, [...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 的下级地区,下级地区会放入 parent.children 数组中
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 } parent 上级地区
* @param {Array } regionIds 组成地址的所有地区 id 的数组,前一个地区是后一个地区的上一级
* @param {Function} finishCallback 地址加载完时的回调函数
*/
loadHierarchyRegions(parent, regionIds, finishCallback) {
// 1. 加载 parent 下的地区
// 2. 找到 parent 下的地区中 value 为 regionIds[0] 的地区 topRegion,并且删除 regionIds[0],如果找不到,结束递归
// 3. 递归加载 topRegion 下的地区

this.loadRegions(parent, () => {
let topId = regionIds.shift();
let topRegion = parent.children.find(child => child.value === topId);

if (topRegion) {
this.loadHierarchyRegions(topRegion, regionIds, finishCallback);
} else {
finishCallback && 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>

思考

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