讲解在同名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>