小程序picker地区级联选择的问题及解决方案
发表时间:2021-4-30
发布人:葵宇科技
浏览次数:102
一、造轮子的原因
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为行政区域节点异步加载封装,可根自己数据自行封装。
<!--
* 行政区域选择器
*
* @alphaiar
* 20210408 created.
-->
<template>
<view class="region-picker">
<input v-if="!visibleInputer" placeholder-class="placeholder" :placeholder="placeholder" :value="https://www.wxapp-union.com/selectorPath"
disabled @tap="onPopupToggle" />
<view v-else @tap="onPopupToggle">
<slot name="inputer"></slot>
</view>
<view v-if="errorMessage" class="messager">{{errorMessage}}</view>
<tui-drawer :visible="visibled" mode="bottom" @close="onPopupToggle">
<view class="header">
<text class="cancel" @tap="onPopupToggle">取消</text>
<text class="confirm" @tap="onConfirm">确认</text>
</view>
<view class="tab-wrapper">
<template v-for="(lab,idx) in labels">
<label v-if="idx!==labelIndex" :key="idx" @tap="onLabelChange({index:idx})">
{{lab}}
</label>
<template v-else>
<label class="active">
{{lab}}
</label>
<iconfont class="indicator" name="arrow-down" />
</template>
</template>
</view>
<tui-loadmore v-if="loading" :index="3" type="primary" text="加载中..." />
<view v-else class="region-view">
<template v-for="(n,idx) in regions">
<label v-if="idx !== selectorIndexs[labelIndex]" @tap="onSelector(idx)" :key="idx">{{n}}</label>
<label v-else :key="idx">
<span class="selected">{{n}}</span>
</label>
</template>
</view>
<view v-if="errorTips" class="error-tips">
{{errorTips}}
</view>
</tui-drawer>
</view>
</template>
<script>
import utils from "../utils/utils.js";
import regionApi from "../apis/region.js";
export default {
name: 'regionPicker',
props: {
/**
* 选择器区级
* 0-省
* 1-地市
* 2-县区
* 3-乡镇
*/
selectorLevel: {
type: Number,
default: 1,
validator(val) {
return [0, 1, 2, 3].some(x => x === val);
}
},
/**
* 当前选择值
*/
value: {
type: Array,
default: null
},
/**
* 没有值时的占位符
*/
placeholder: {
type: String,
default: '请选择地区'
},
/**
* 表单验证错误提示消息
*/
errorMessage: {
type: String,
default: null
},
/**
* 启用自定义输入框
*/
visibleInputer: {
type: Boolean,
default: false
}
},
watch: {
selectorLevel(val) {
this.$emit('input', null);
this.initialize();
},
value(val) {
this.initialize();
}
},
data() {
return {
visibled: false,
loading: false,
labels: ['请选择'],
labelIndex: 0,
regions: [],
selectorIndexs: [],
selectorNodes: [],
errorTips: null
};
},
computed: {
selectorPath() {
let nodes = this.selectorNodes;
if (!nodes || nodes.length < 1)
return null;
let paths = nodes.map(x => x.name);
let path = paths.join(' / ');
return path;
}
},
mounted() {
const self = this;
regionApi.getNodes({
params: {
endCategory: 1
},
loading: false,
onLoading(ld) {
self.loading = ld;
},
showError: true,
callback(fkb) {
if (!fkb.success)
return;
let nodes = fkb.result;
self.__rawRegions = nodes;
if (!self.value || self.value.length < 1)
self.bindViews(nodes);
else
self.initialize();
}
});
},
methods: {
/**
* 初始化选择器
*/
initialize() {
//初始化数据没有执行完成
if (!this.__rawRegions)
return;
this.labels = ['请选择'];
this.labelIndex = 0;
this.selectorIndexs = [];
this.selectorNodes = [];
this.bindViews(this.__rawRegions);
//设定初始值
let values = this.value;
if (!values || values.length < 1)
return;
const self = this;
let prevs = this.__rawRegions;
let setValue = https://www.wxapp-union.com/function(idx) {
let nd = values[idx];
let about = false;
let exists = prevs.some((x, i) => {
if (nd.name !== x.name && nd.code !== x.code)
return false;
prevs = x.children || prevs;
//如果还有下级,但又未加载子节点,则先加载再来设定
if (!x.children && idx + 1 < values.length) {
self.getNextRegions(x, () => {
setValue(idx);
});
about = true;
return true;
}
self.selectorNodes.push({
category: x.category,
code: x.code,
name: x.name
});
self.onSelector(i);
return true;
});
if (about)
return;
if (exists && idx + 1 < values.length)
setValue(idx + 1);
};
setValue(0);
},
/**
* 将待选节点绑定至待选视图
*
* @param {Array} nodes 要绑定的原始节点
*/
bindViews(nodes) {
this.regions = nodes.map(x => x.name);
},
/**
* 获取下级节点
*
* @param {Object} prevNode 上级选中的节点
* @param {function} cb 加载完成后回调
*/
getNextRegions(prevNode, cb) {
const self = this;
regionApi.getChildren({
params: {
category: prevNode.category + 1,
prevCode: prevNode.code
},
loading: false,
onLoading(ld) {
self.loading = ld;
},
showError: true,
callback(fkb) {
if (!fkb.success)
return;
prevNode.children = fkb.result;
if (!cb)
self.bindViews(fkb.result);
else
cb();
}
});
},
/**
* 获取指定列选择的节点
*
* @param {Object} level 地区级别0-3
*/
getSelectorNode(level) {
let prevs = this.__rawRegions;
for (let i = 0; i < level; i++) {
let sidx = this.selectorIndexs[i];
if (!sidx)
return null;
prevs = prevs[sidx].children;
if (!prevs)
return null;
}
let cval = this.selectorIndexs[level];
let node = prevs[cval];
return node;
},
/**
* 切下至下一级区域选择
*
* @param {Object} current 当前选中级别0-3
*/
moveNextLevel(current) {
let node = this.getSelectorNode(current);
if (node == null)
return;
if (node.children)
this.bindViews(node.children);
else
this.getNextRegions(node);
},
onPopupToggle(e) {
this.visibled = !this.visibled;
},
onConfirm(e) {
if (this.selectorLevel + 1 > this.selectorIndexs.length) {
this.errorTips ='*请将地区选择完整。';
return;
}
let nodes = [];
for (let i = 0; i < this.selectorIndexs.length; i++) {
let node = this.getSelectorNode(i);
nodes.push({
category: node.category,
code: node.code,
name: node.name
});
}
this.selectorNodes = nodes;
this.onPopupToggle();
this.$emit('input', nodes);
this.$emit('change', nodes);
},
onLabelChange(e) {
//加载中,禁止切换
if (this.loading)
return;
let idx = e.index;
this.labelIndex = idx;
if (idx > 0)
this.moveNextLevel(idx - 1);
else
this.bindViews(this.__rawRegions);
},
onSelector(idx) {
this.errorTips = null;
let labIdx = this.labelIndex;
//由于uni 对于数组的值监听不完善,只有复制数组更新才生效
let labs = utils.clone(this.labels);
labs[labIdx] = this.regions[idx];
this.labels = labs;
//原因上同
let idexs = utils.clone(this.selectorIndexs);
if (idexs.length <= labIdx)
idexs.push(idx);
else
idexs[labIdx] = idx;
this.selectorIndexs = idexs;
//有下级,全清空
if (labIdx >= this.selectorLevel)
return;
this.selectorIndexs.splice(labIdx + 1, 4); //最大只有4级
this.labels.splice(labIdx + 1, 4); //最大只有4级
this.labels.push('请选择');
this.labelIndex = labIdx + 1;
this.moveNextLevel(labIdx);
}
}
}
</script>
<style lang="scss">
.region-picker {
.header {
width: 100%;
box-sizing: border-box;
margin: 7.2463rpx 0;
line-height: $uni-font-size-base+ 7.2463rpx;
.cancel {
padding: 0 18.1159rpx;
float: left;
//color: $uni-text-color-grey;
}
.confirm {
padding: 0 18.1159rpx;
float: right;
color: $uni-color-primary;
}
text:hover {
background-color: $uni-bg-color-hover;
}
}
.tab-wrapper {
width: 100%;
margin-bottom: 28.9855rpx;
display: flex;
justify-content: center;
box-sizing: border-box;
label {
margin: 7.2463rpx 28.9855rpx;
padding: 7.2463rpx 0;
color: $uni-text-color;
border-bottom: solid 3.6231rpx transparent;
}
.active {
color: $uni-color-primary;
border-color: $uni-color-primary;
}
.indicator {
margin-left: -10px;
margin-top: 6px;
color: $uni-color-primary;
}
}
.region-view {
width: 100%;
display: flex;
flex-wrap: wrap;
padding: 7.2463rpx 14.4927rpx 28.9855rpx 14.4927rpx;
box-sizing: border-box;
label {
margin: 7.2463rpx 0;
width: 33%;
text-align: center;
color: $uni-text-color-grey;
text-overflow: ellipsis;
overflow: hidden;
}
.selected {
padding: 3.6231rpx 14.4927rpx;
background-color: $uni-color-light-primary;
color: #FFF;
border-radius: 10.8695rpx;
}
}
.error-tips {
width: 100%;
height: auto;
padding-bottom: 21.7391rpx;
text-align: center;
color: $uni-color-error;
font-size: $uni-font-size-sm;
}
}
</style>
行政区化节点数据,来源国家统计局,到县区级。
https://files.cnblogs.com/fil...
最终效果