下拉联动的二次封装

100 阅读1分钟

讲解在同名B/D上都有,主要介绍一些跟业务无关的代码技巧

注: 部分内容主观性较大,一家之言姑且听之

本文主要介绍下拉-联动的二次封装

上一期 兴趣爱好者的下拉和专业的下拉也是有区别的

基础实现

  • 使用

下拉联动只是数据源的修改,我们不需要维护template,只需要修改data即可实现

<template>
    <div id="app">
        <div>省{{ data.form.provinceId }}:
            <xxSelect v-model="data.form.provinceId" :api="data.store1">
            </xxSelect>
        </div>
        <div>市{{ data.form.cityId }}:
            <xxSelect v-model="data.form.cityId" :api="data.store2">
            </xxSelect>
        </div>
    </div>
</template>
<script>
import { getProvinceList, getCityList } from './api'
import xxSelect from './xxSeelct.vue'
import Vue from 'vue'

export default {
    components: {
        xxSelect
    },
    beforeCreate() {
        /**
         * 关联属性的定义
         */
        const data = Vue.observable({
            form: {
                provinceId: '',
                cityId: '',
            },
            store1: [],
            store2: []
        })
        this.$watch(() => data.form.provinceId, async (id) => {
            // 切换后,直接修改
            data.form.cityId = "";
            const ret = await getCityList(id)
            data.store2 = ret
        })
        this.data = data;
        /**
         * 需要封装,执行
         * 语义是不一样的
         */
        (async () => {
            const ret = await getProvinceList()
            data.store1 = ret
        })();
    }
}
</script>
  • 组件实现

对组件而言,只需要让api支持数组即可

<template>
    <el-select v-bind="$attrs" v-on="$listeners" @visible-change="visibleChangeHanlder">
        <el-option v-for="item in data" :key="item[props.key]" :label="item[props.label]" :value="item[props.value]">
            <slot name="option" :item="item"></slot>
        </el-option>
        <template slot="empty">
            <div v-if="status === 1">
                loading...
            </div>
            <div v-else-if="status === 2">
                没有数据
            </div>
            <div v-else-if="status === 3">
                error
                <el-button @click="loadData">
                    reTry
                </el-button>
            </div>
        </template>
    </el-select>
</template>
<script>
/**
 * 静态组件
 * ui复用: 每一帧的状态
 * 业务组件: 产品逻辑
 * - 下拉没有数据 -- 无数据ui
 * - 下拉对应的url正在通讯 - loadingui
 * - 下拉接口挂了 - 错误ui
 * 
 * 预加载: 组件生命周期 === 接口生命周期
 * 惰性加载: 组件展示是进行请求
 * 
 * 
 * 级联,静态,api层重新维护
 * 
 */
export default {
    name: 'xxSelect',
    props: {
        props: {
            type: Object,
            default: () => ({
                key: "key",
                value: "value",
                label: "label"
            })
        },
        autoLoad: {
            type: [Boolean, String],
            default: true
        },
        api: {
            type: [Function, Array],
            required: true
        }
    },
    created() {
        if (this.autoLoad === true && this.api instanceof Function) {
            this.loadData()
        }
    },
    computed: {
        data() {
            if (this.api instanceof Array) {
                // 可能有问题
                return this.api
            }
            return this.localData
        }
    },
    data() {
        return {
            /**
             * 0:未初始化
             * 1: 加载中
             * 2: 加载成功
             * 3: 加载失败
             */
            status: 0,
            localData: []
        }
    },
    methods: {
        visibleChangeHanlder() {
            if (this.status === 0 && this.api instanceof Function) {
                this.loadData()
            }
        },
        abort() {
            // axios/fetch 取消请求
        },
        async loadData() {
            /**
             * 默认abort:prefetch
             * 请求参数一直:使用上一次请求  preload
             * 
             */
            if (this.status === 1) {
                this.abort()
            }
            try {
                this.status = 1
                const data = await this.api()
                this.localData = data
                this.status = 2
            } catch (error) {
                this.status = 3
            }
        }
    }
}
</script>

最终实现

我们期望将接口的生命周期,交给组件维护

使用

<template>
    <div id="app">
        <div>省{{ form.provinceId }}:
            <xxSelect v-model="form.provinceId" api="/api/province">
            </xxSelect>
        </div>
        <div>市{{ form.cityId }}:
            <xxSelect v-model="form.cityId" :api="form.provinceId?'/api/province/'+form.provinceId:''" >
            </xxSelect>
        </div>
    </div>
</template>
<script>
import xxSelect from './xxSeelct.vue'


export default {
    components: {
        xxSelect
    },
    data(){
        return {
            form: {
                provinceId: '',
                cityId: '',
            }
        }
    }
}
</script>

组件

这是配合api为String的方式进行实现,但显然也有一些场景无法处理

  • 接口请求方法不一致
  • 几个下拉使用一个接口
  • 接口使用全量请求或静态数据,由前端过滤
<template>
    <el-select v-bind="$attrs" v-on="$listeners" @visible-change="visibleChangeHanlder">
        <el-option v-for="item in data" :key="item[props.key]" :label="item[props.label]" :value="item[props.value]">
            <slot name="option" :item="item"></slot>
        </el-option>
        <template slot="empty">
            <div v-if="status === 1">
                loading...
            </div>
            <div v-else-if="status === 2">
                没有数据
            </div>
            <div v-else-if="status === 3">
                error
                <el-button @click="loadData">
                    reTry
                </el-button>
            </div>
        </template>
    </el-select>
</template>
<script>
/**
 * 静态组件
 * ui复用: 每一帧的状态
 * 业务组件: 产品逻辑
 * - 下拉没有数据 -- 无数据ui
 * - 下拉对应的url正在通讯 - loadingui
 * - 下拉接口挂了 - 错误ui
 * 
 * 预加载: 组件生命周期 === 接口生命周期
 * 惰性加载: 组件展示是进行请求
 * 
 * 联动:
 *   - 根据url进行联动
 *   - 问题1: 接口请求不一致
 *   - 问题2:全量请求/静态数据
 * 静态,api层重新维护
 * 
 */
import { getProvinceList, getCityList } from './api'

export default {
    name: 'xxSelect',
    props: {
        props: {
            type: Object,
            default: () => ({
                key: "key",
                value: "value",
                label: "label"
            })
        },
        autoLoad: {
            type: [Boolean, String],
            default: true
        },
        api: {
            type: [Function, Array, String],
            required: true
        }
    },
    created() {
        if (this.autoLoad === true &&  this.localApi) {
            this.loadData()
        }
        if(typeof this.api === 'string' ){
            /**
             * 如果请求接口修改,则复用以下逻辑
             * - 当前选中信息取消
             * - 数据请求重新刷新
             */
            this.$watch(()=>this.api,()=>{
                this.value = "";
                this.loadData()
            })
        }
    },
    computed: {
        data() {
            if (this.api instanceof Array) {
                // 可能有问题
                return this.api
            }else if(this.status !== 2){
                return []
            }
            return this.localData
        },
        localApi(){
            if(this.api instanceof Function){
                return this.api
            }else if(typeof this.api === 'string'  ){
                if(this.api === ""){
                    return null
                }
                return this.loadDataByUrl
            }
            return null
        }
    },
    data() {
        return {
            /**
             * 0:未初始化
             * 1: 加载中
             * 2: 加载成功
             * 3: 加载失败
             */
            status: 0,
            localData: []
        }
    },
    methods: {
        visibleChangeHanlder() {
            if (this.status === 0 && this.localApi) {
                this.loadData()
            }
        },
        abort() {
            // axios/fetch 取消请求
        },
        /**
         * 模拟接口请求
         */
        async loadDataByUrl() {
            if (this.api === "/api/province") {
                return getProvinceList()
            } else {
                return getCityList(this.api.slice(this.api.lastIndexOf("/") + 1))
            }
        },
        async loadData() {
            /**
             * 默认abort:prefetch
             * 请求参数一直:使用上一次请求  preload
             * 
             */
            if (this.status === 1) {
                this.abort()
            }
            try {
                this.status = 1
                const data = await this.localApi()
                this.localData = data
                this.status = 2
            } catch (error) {
                this.status = 3
            }
        }
    }
}
</script>