There are many scenarios in which administrative areas are selected in various systems, and we have many such scenarios. I wanted to use third-party components, but most of them had some small problems and couldn’t meet my needs. I wrote one using Picker’s MulitSelector pattern, and found that the experience of this column pattern was not so good. Finally, I created a custom one imitating the JD.com pattern.

First, the reason for the wheel

1.1 Data to be customized

WeChat official Picker's Region mode uses standard national administrative region data, while our scene has some self-created regions to be added; You can't choose the series for a long time, you can only choose the county/district level.

1.2 Picker compatibility is not good.

The Picker component of the Uni-App uses its own Picker component in the applet mode, and H5 is the Picker component of the Uni-App itself. Therefore, there are differences in different platforms. In our test, in the mulitSelector mode of picker of WeChat, if the column array value length is inconsistent twice in column cascading sliding, the selected index after binding will be invalid and will automatically be 0, and the change event triggered later will still be the bound index. Not at H5.

1.3 Picker is not suitable for asynchronously loading data

Cascading is a simple way to control subsequent column changes, as shown in 1.2, binding index bugs. And if the data is loaded asynchronously, it is more difficult to control the loading state, especially when the network is not good, it is easy to appear data chaos.

1.4 Picker makes cascading, which is not as good as JD’s cascading mode and has high efficiency.

As is shown in

Two, on the code

The TUI-XXX used are third party components of UNI-APP, such as TUI-drawer, TUI-loadmore, etc., with reference to official documents for use, or other components are used instead. RegionAPI is the asynchronously loaded and encapsulated administrative region node, which can self-encapsulate its own data.

<!--
* 行政区域选择器
*
* @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>

Region node data, from the National Bureau of Statistics, to the county level.

https://files.cnblogs.com/fil…

The final result