各种系统中行政区域选择的场景不少,我们也有不少这样的场景。本想使用第三方的组件,但是大多有些小问题,不能满足需要。后面使用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
最终效果