小程序picker地区级联选择的问题及解决方案

各种系统中行政区域选择的场景不少,我们也有不少这样的场景。本想使用第三方的组件,但是大多有些小问题,不能满足需要。后面使用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作级联,不如京东级联模式的体验好效率高。

如图所示

小程序picker地区级联选择的问题及解决方案

 二、上代码

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

小程序picker地区级联选择的问题及解决方案
  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                 onl oading(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                     onl oading(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地区级联选择的问题及解决方案

 

上一篇:threejs入门实例2--sprite演示下雨


下一篇:25. Reverse Nodes in k-Group