共计 7066 个字符,预计需要花费 18 分钟才能阅读完成。
各种零碎中行政区域抉择的场景不少,咱们也有不少这样的场景。本想应用第三方的组件,然而大多有些小问题,不能满足需要。前面应用 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 为行政区域节点异步加载封装,可根本人数据自行封装。
<!--
* 行政区域选择器
*
* @alphaiar
* 20210408 created.
-->
<template>
<view class="region-picker">
<input v-if="!visibleInputer" placeholder-class="placeholder" :placeholder="placeholder" :value="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 = 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…
最终成果
正文完