ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2021-04-19 10:03:45  阅读:460  来源: 互联网

标签:picker 级联 return color 解决方案 let rpx uni nodes


各种系统中行政区域选择的场景不少,我们也有不少这样的场景。本想使用第三方的组件,但是大多有些小问题,不能满足需要。后面使用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                 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,级联,return,color,解决方案,let,rpx,uni,nodes
来源: https://www.cnblogs.com/alphaair/p/14642589.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有