各种系统中行政区域选择的场景不少,我们也有不少这样的场景。本想使用第三方的组件,但是大多有些小问题,不能满足需要。后面使用picker的mulitSelector模式写了一个,发现这种列模式的体验并好,最后仿京东模式自定义了一个。

一、造轮子的原因

1.1 数据要自定义

微信官方的picker的region模式使用的是标准的国家行政区域数据,而我们的场景有一些自设的区域要加入;也不可以自定久选择级数,只能选到县/区级。

1.2 picker的兼容性并不好。

uni-app的picker组件,在小程序模式是使用各自的picker,H5则是uni-app自的picker组件。所以在各平台中还是有差异的,在我们测试中微信的picker的mulitSelector模式,在列级联滑动中如果出现两次列数组值length不一致时,后绑定的选定索引时会无效,会自动致为0,且后续触发的change事件则仍是绑定索引,而在H5时不会。

1.3 picker是不适合异步加载数据

级联就是要简便的控制后续列的变化,如1.2所示,绑定索引bug。而如果数据是异步加载,则更难于控制加载状态,特别是滑动过快网络不佳时,很容易出现数据混乱。

1.4 picker作级联,不如京东级联模式的体验好效率高。

如图所示

 二、上代码

使用的了tui-drawer 、tui-loadmore等tui-xxx为uni-app第三方组件,具本使用参考官方文档,或使用别的组件替代。regionApi为行政区域节点异步加载封装,可根自己数据自行封装。

  1 <!--
2 * 行政区域选择器
3 *
4 * @alphaiar
5 * 20210408 created.
6 -->
7
8 <template>
9 <view class="region-picker">
10 <input placeholder-class="placeholder" :placeholder="placeholder" :value="selectorPath" disabled
11 @tap="onPopupToggle" />
12 <view v-if="errorMessage" class="messager">{{errorMessage}}</view>
13 <tui-drawer :visible="visibled" mode="bottom" @close="onPopupToggle">
14 <view class="header">
15 <text class="cancel" @tap="onPopupToggle">取消</text>
16 <text class="confirm" @tap="onConfirm">确认</text>
17 </view>
18 <view class="tab-wrapper">
19 <template v-for="(lab,idx) in labels">
20 <label v-if="idx!==labelIndex" :key="idx" @tap="onLabelChange({index:idx})">
21 {{lab}}
22 </label>
23 <template v-else>
24 <label class="active">
25 {{lab}}
26 </label>
27 <iconfont class="indicator" name="arrow-down" />
28 </template>
29 </template>
30 </view>
31 <tui-loadmore v-if="loading" :index="3" type="primary" text="加载中..." />
32 <view v-else class="region-view">
33 <template v-for="(n,idx) in regions">
34 <label v-if="idx !== selectorIndexs[labelIndex]" @tap="onSelector(idx)" :key="idx">{{n}}</label>
35 <label v-else :key="idx">
36 <span class="selected">{{n}}</span>
37 </label>
38 </template>
39 </view>
40 <view v-if="errorTips" class="error-tips">
41 {{errorTips}}
42 </view>
43 </tui-drawer>
44 </view>
45 </template>
46
47 <script>
48 import utils from "../utils/utils.js";
49 import regionApi from "../apis/region.js";
50
51 export default {
52 name: 'regionPicker',
53 props: {
54 /**
55 * 选择器区级
56 * 0-省
57 * 1-地市
58 * 2-县区
59 * 3-乡镇
60 */
61 selectorLevel: {
62 type: Number,
63 default: 1,
64 validator(val) {
65 return [0, 1, 2, 3].some(x => x === val);
66 }
67 },
68 /**
69 * 当前选择值
70 */
71 value: {
72 type: Array,
73 default: null
74 },
75 /**
76 * 没有值时的占位符
77 */
78 placeholder: {
79 type: String,
80 default: '请选择地区'
81 },
82 /**
83 * 表单验证错误提示消息
84 */
85 errorMessage: {
86 type: String,
87 default: null
88 }
89 },
90 watch: {
91 selectorLevel(val) {
92 this.$emit('input', null);
93 this.initialize();
94 },
95 value(val) {
96 this.initialize();
97 }
98 },
99 data() {
100
101 return {
102 visibled: false,
103 loading: false,
104 labels: ['请选择'],
105 labelIndex: 0,
106 regions: [],
107 selectorIndexs: [],
108 selectorNodes: [],
109 errorTips: null
110 };
111 },
112 computed: {
113 selectorPath() {
114 let nodes = this.selectorNodes;
115
116 if (!nodes || nodes.length < 1)
117 return null;
118
119 let paths = nodes.map(x => x.name);
120 let path = paths.join(' / ');
121
122 return path;
123 }
124 },
125 mounted() {
126 const self = this;
127 regionApi.getNodes({
128 params: {
129 endCategory: 1
130 },
131 loading: false,
132 onLoading(ld) {
133 self.loading = ld;
134 },
135 showError: true,
136 callback(fkb) {
137
138 if (!fkb.success)
139 return;
140
141 let nodes = fkb.result;
142 self.__rawRegions = nodes;
143
144 if (!self.value || self.value.length < 1)
145 self.bindViews(nodes);
146 else
147 self.initialize();
148 }
149 });
150
151 },
152 methods: {
153 /**
154 * 初始化选择器
155 */
156 initialize() {
157 this.labels = ['请选择'];
158 this.labelIndex = 0;
159 this.selectorIndexs = [];
160 this.selectorNodes = [];
161 this.bindViews(this.__rawRegions);
162
163 //设定初始值
164 let values = this.value;
165 if (!values || values.length < 1)
166 return;
167
168 const self = this;
169 let prevs = this.__rawRegions;
170 let setValue = function(idx) {
171 let nd = values[idx];
172 let about = false;
173 let exists = prevs.some((x, i) => {
174 if (nd.name !== x.name && nd.code !== x.code)
175 return false;
176
177 prevs = x.children || prevs;
178
179 //如果还有下级,但又未加载子节点,则先加载再来设定
180 if (!x.children && idx + 1 < values.length) {
181 self.getNextRegions(x, () => {
182 setValue(idx);
183 });
184 about = true;
185 return true;
186 }
187
188 self.selectorNodes.push({
189 category: x.category,
190 code: x.code,
191 name: x.name
192 });
193 self.onSelector(i);
194 return true;
195 });
196
197 if (about)
198 return;
199
200 if (exists && idx + 1 < values.length)
201 setValue(idx + 1);
202 };
203
204 setValue(0);
205 },
206 /**
207 * 将待选节点绑定至待选视图
208 *
209 * @param {Array} nodes 要绑定的原始节点
210 */
211 bindViews(nodes) {
212 this.regions = nodes.map(x => x.name);
213 },
214 /**
215 * 获取下级节点
216 *
217 * @param {Object} prevNode 上级选中的节点
218 * @param {function} cb 加载完成后回调
219 */
220 getNextRegions(prevNode, cb) {
221 const self = this;
222 regionApi.getChildren({
223 params: {
224 category: prevNode.category + 1,
225 prevCode: prevNode.code
226 },
227 loading: false,
228 onLoading(ld) {
229 self.loading = ld;
230 },
231 showError: true,
232 callback(fkb) {
233 if (!fkb.success)
234 return;
235
236 prevNode.children = fkb.result;
237 if (!cb)
238 self.bindViews(fkb.result);
239 else
240 cb();
241 }
242 });
243 },
244 /**
245 * 获取指定列选择的节点
246 *
247 * @param {Object} level 地区级别0-3
248 */
249 getSelectorNode(level) {
250 let prevs = this.__rawRegions;
251
252 for (let i = 0; i < level; i++) {
253
254 let sidx = this.selectorIndexs[i];
255 if (!sidx)
256 return null;
257
258 prevs = prevs[sidx].children;
259 if (!prevs)
260 return null;
261 }
262
263 let cval = this.selectorIndexs[level];
264 let node = prevs[cval];
265
266 return node;
267 },
268 /**
269 * 切下至下一级区域选择
270 *
271 * @param {Object} current 当前选中级别0-3
272 */
273 moveNextLevel(current) {
274 let node = this.getSelectorNode(current);
275 if (node == null)
276 return;
277
278 if (node.children)
279 this.bindViews(node.children);
280 else
281 this.getNextRegions(node);
282 },
283 onPopupToggle(e) {
284 this.visibled = !this.visibled;
285 },
286 onConfirm(e) {
287 if (this.selectorLevel + 1 > this.selectorIndexs.length) {
288 this.errorTips = '*请将地区选择完整。';
289 return;
290 }
291
292 let nodes = [];
293 for (let i = 0; i < this.selectorIndexs.length; i++) {
294 let node = this.getSelectorNode(i);
295 nodes.push({
296 category: node.category,
297 code: node.code,
298 name: node.name
299 });
300 }
301
302 this.selectorNodes = nodes;
303 this.onPopupToggle();
304
305 this.$emit('input', nodes);
306 this.$emit('change', nodes);
307 },
308 onLabelChange(e) {
309 //加载中,禁止切换
310 if (this.loading)
311 return;
312
313 let idx = e.index;
314 this.labelIndex = idx;
315 if (idx > 0)
316 this.moveNextLevel(idx - 1);
317 else
318 this.bindViews(this.__rawRegions);
319 },
320 onSelector(idx) {
321
322 this.errorTips = null;
323 let labIdx = this.labelIndex;
324
325 //由于uni 对于数组的值监听不完善,只有复制数组更新才生效
326 let labs = utils.clone(this.labels);
327 labs[labIdx] = this.regions[idx];
328 this.labels = labs;
329
330 //原因上同
331 let idexs = utils.clone(this.selectorIndexs);
332 if (idexs.length <= labIdx)
333 idexs.push(idx);
334 else
335 idexs[labIdx] = idx;
336 this.selectorIndexs = idexs;
337
338 //有下级,全清空
339 if (labIdx >= this.selectorLevel)
340 return;
341
342 this.selectorIndexs.splice(labIdx + 1, 4); //最大只有4级
343 this.labels.splice(labIdx + 1, 4); //最大只有4级
344
345 this.labels.push('请选择');
346 this.labelIndex = labIdx + 1;
347 this.moveNextLevel(labIdx);
348 }
349 }
350 }
351 </script>
352
353 <style lang="scss">
354 .region-picker {
355
356 .header {
357 width: 100%;
358 box-sizing: border-box;
359 margin: 7.2463rpx 0;
360 line-height: $uni-font-size-base+ 7.2463rpx;
361
362 .cancel {
363 padding: 0 18.1159rpx;
364 float: left;
365 //color: $uni-text-color-grey;
366 }
367
368 .confirm {
369 padding: 0 18.1159rpx;
370 float: right;
371 color: $uni-color-primary;
372 }
373
374 text:hover {
375 background-color: $uni-bg-color-hover;
376 }
377 }
378
379 .tab-wrapper {
380 width: 100%;
381 margin-bottom: 28.9855rpx;
382 display: flex;
383 justify-content: center;
384 box-sizing: border-box;
385
386 label {
387 margin: 7.2463rpx 28.9855rpx;
388 padding: 7.2463rpx 0;
389 color: $uni-text-color;
390 border-bottom: solid 3.6231rpx transparent;
391 }
392
393 .active {
394 color: $uni-color-primary;
395 border-color: $uni-color-primary;
396 }
397
398 .indicator {
399 margin-left: -10px;
400 margin-top: 6px;
401 color: $uni-color-primary;
402 }
403 }
404
405 .region-view {
406 width: 100%;
407 display: flex;
408 flex-wrap: wrap;
409 padding: 7.2463rpx 14.4927rpx 28.9855rpx 14.4927rpx;
410 box-sizing: border-box;
411
412 label {
413 margin: 7.2463rpx 0;
414 width: 33%;
415 text-align: center;
416 color: $uni-text-color-grey;
417 text-overflow: ellipsis;
418 overflow: hidden;
419 }
420
421 .selected {
422 padding: 3.6231rpx 14.4927rpx;
423 background-color: $uni-color-light-primary;
424 color: #FFF;
425 border-radius: 10.8695rpx;
426 }
427 }
428
429 .error-tips {
430 width: 100%;
431 height: auto;
432 padding-bottom: 21.7391rpx;
433 text-align: center;
434 color: $uni-color-error;
435 font-size: $uni-font-size-sm;
436 }
437 }
438 </style>

Region Picker

行政区化节点数据,来源国家统计局,到县区级。

https://files.cnblogs.com/files/blogs/677104/cn_regions.json

最终效果

小程序picker地区级联选择的问题及解决方案的更多相关文章

  1. 微信小程序picker组件两列关联使用方式

    在使用微信小程序picker组件时候,可以设置属性   mode = multiSelector   意为多列选择,关联选择,当第一列发生改变时侯,第二列甚至第三列发生相应的改变.但是官方文档上给的只 ...

  2. 微信小程序,创业新选择

    微信小程序,创业新选择 创业者们 总是站在时代的风口浪尖,他们踌躇满志无所畏惧,这大概就是梦想的力量.但是,如果没有把梦想拆解成没有可预期的目标和可执行的实现路径那么一切都只能叫做梦想. 小程序 张小 ...

  3. 微信小程序picker组件关于objectArray数据类型绑定

    一.前言: 我发现很多的同学都在抱怨说微信小程序的picker的mode = selector/mode = multiSelector 无法实现Object Array数据类型的绑定,其实很多人就想 ...

  4. 微信小程序—picker(滚动选择器)

    官方api:https://mp.weixin.qq.com/debug/wxadoc/dev/component/picker.html 上边是官网的api.小程序中,底部下拉滚动选择主要有这几种 ...

  5. mpvue + 微信小程序 picker 实现自定义多级联动 超简洁

    微信小程序官网只提供了省市区的三级联动,实际开发中更多的是自定义的多级联动: 依照微信小程序官网提供的自定义多级联动,需要使用到picker 的多列选择器,即设置 mode = multiSelect ...

  6. 关于小程序picker 的使用

    前言 以前做小程序的时候只会用那个picker mode = region的 3级选中, 现在需要自己根据后台给的编号省市区来用然后就研究了多列选择器:mode = multiSelector 的用法 ...

  7. 微信小程序开发语言的选择

    微信使用的开发语言和文件很「特殊」. 小程序所使用的程序文件类型大致分为以下几种: ①WXML(WeiXin Mark Language,微信标记语言) ②WXSS(WeiXin Style Shee ...

  8. 微信小程序——picker通过value返回你想获取的值

    关于微信小程序中的picker使用方法可以访问:picker-小程序 从它的官方文档中,可以看出它返回的value值是它range的下标: 在项目中,我们大多数时候传的值并不是需要这个下标,而是其他的 ...

  9. 微信小程序picker重写,精确到时分秒

    https://developers.weixin.qq.com/miniprogram/dev/component/picker.html 微信小程序提供的picker组件,只精确到分,项目中需要秒 ...

随机推荐

  1. python行与列显示不全

    在显示数据框时添加以下代码 #显示所有列 pd.set_option('display.max_columns', None) #显示所有行 pd.set_option('display.max_ro ...

  2. Vue项目的创建、路由、及生命周期钩子

    目录 一.Vue项目搭建 1.环境搭建 2.项目的创建 3.pycharm配置并启动vue项目 4.vue项目目录结构分析 5.Vue根据配置重新构建依赖 二.Vue项目创建时发生了什么 三.项目初始 ...

  3. 数据结构-PHP 线段树的实现

    转: 数据结构-PHP 线段树的实现 1.线段树介绍 线段树是基于区间的统计查询,线段树是一种 二叉搜索树,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点.使用线段树可以快速的查 ...

  4. 鸿蒙的js开发部模式18:鸿蒙的文件上传到python服务器端

    1.首先鸿蒙的js文件上传,设置目录路径为: 构建路径在工程主目录下: 该目录的说明见下面描述: 视图构建如下: 界面代码: <div class="container"&g ...

  5. [msys2]集成到右键菜单

    集成到右键菜单 在资源管理器中,空白处右键(right-clicking on folder backround in Windows Explorer)会弹出菜单,其中有如"在此处打开cm ...

  6. 链表算法题之中等级别,debug调试更简单

    文章简述 大家好,本篇是个人的第 5 篇文章 从本篇文章开始,分享关于链表的题目为中等难度,本次共有 3 道题目. 一,两数相加 1.1 题目分析 题中写到数字是按照逆序的方式存储,从进位的角度看,两 ...

  7. MyBatis中的Map

    接口 int addUserMap(Map<String, Object> map); Mapper.xml <!-- Map比较灵活 传递的值为Map的key,可以为任何(野路子, ...

  8. python3中post和get请求处理

    post 请求处理 def url(): url = "www.xxx.com.cn" data = { "csrfmiddlewaretoken":" ...

  9. EmEditor, 在正则使用()匹配后 使用$1 $2进行对括号内的值进行引用

    $1表示第一个括号,$2表示第二个括号,以此类推

  10. 局部莫兰指数的计算(运用ArcMap)

    做任务时需要运用到局部莫兰指数,卡在用Python计算的思路上好久,最后发现可以用ArcGIS进行处理,步骤简单易懂. 主要步骤为: 1.读入数据(一定要为shp文件),对于用ecognition直接 ...