思路
目的通过包装器将 插槽内的元素都递归的遍历起来,从而达到监控想要监控的某种元素。
难点 VNode中的VNodeComponentOptions转为render所需要的对象。并且对于指定组件素可以进行组件内方法的调用。测试发现ref可以进行指定组件内的方法调用。
用法
通过以下的调用。可以级联的进行选择结果如图所示。选择框之间可以联动。

<template>
<div style="background: #2f3d8a;">
one: {{ one }}
two: {{ two }}
three: {{ three }}
<!-- 级联选择器的包装器-->
<CascadeSelectWrapper :refList="['level1', 'level2', 'level3']">
<a-form-model layout="inline" class="ul-form-search">
<a-form-model-item label="一级指标">
<!-- 级联选择器 ref必须为 refList中指定的-->
<CascadeSelect v-model="one" :showAll="false" ref="level1" :api-config="oneConfig" option-value="code"
option-name="value"/>
</a-form-model-item>
<a-form-model-item label="二级指标">
<!-- ref必须为 refList中指定的-->
<CascadeSelect v-model="two" :showAll="false" ref="level2" :api-config="twoConfig"
:before-query="twoBefore" option-value="code" option-name="value"/>
</a-form-model-item>
<a-form-model-item label="三级指标">
<!-- ref必须为 refList中指定的-->
<CascadeSelect v-model="three" :showAll="false" ref="level3" :api-config="threeConfig"
:before-query="threeBefore" option-value="code" option-name="value"/>
</a-form-model-item>
</a-form-model>
</CascadeSelectWrapper>
</div>
</template>
<script lang="ts">
import {defineComponent, onMounted, ref, unref} from "@vue/composition-api";
import CascadeSelect from './CascadeSelect.vue'
import CascadeSelectWrapper from './CascadeSelectWrapper.vue'
import {orderlyPowerBaseUrl} from "@/api/orderlyPower/common";
import {useRouterQuery} from "@/hooks/route/useRouter";
export default defineComponent({
name: "Index",
components: {
CascadeSelect,
CascadeSelectWrapper,
},
setup() {
const one = ref('')
const two = ref('')
const three = ref('')
const routerQuery = useRouterQuery();
onMounted(() => {
one.value = routerQuery.one
two.value = routerQuery.two
three.value = routerQuery.three
})
const twoBefore = () => {
return {
params: {
type: 'business_type_level_one',
code: unref(one)
}
}
}
const threeBefore = () => {
return {
params: {
type: 'business_type_level_two',
code: unref(two)
}
}
}
return {
oneConfig: {
url: `${orderlyPowerBaseUrl}/org/public/dict/type/business_type_level_one`,
method: 'GET'
},
twoConfig: {
url: `${orderlyPowerBaseUrl}/org/public/dict/code`,
method: 'GET'
},
threeConfig: {
url: `${orderlyPowerBaseUrl}/org/public/dict/code`,
method: 'GET'
},
one,
two,
three,
twoBefore,
threeBefore
}
}
})
</script>
<style scoped>
</style>
包装器
<script lang="js">
import {filterEmpty, getEvents} from "ant-design-vue/lib/_util/props-util.js";
import {cloneElement} from 'ant-design-vue/lib/_util/vnode.js';
export default {
name: "BaseSelectWrapper",
props: {
refList: {
type: Array,
required: true
}
},
render(h) {
const $scopedSlots = this.$scopedSlots;
const $slots = this.$slots;
const children = filterEmpty($scopedSlots['default'] ? $scopedSlots['default']() : $slots['default']);
const level = this.refList;
if(!level || !level.length) {
throw new Error('请在CascadeSelectWrapper组件中指定级联等级!')
}
let $refs;
function findLevel(child) {
if(child && child.length) {
return child.map(item => {
if (item.data && item.data.ref && level.indexOf(item.data.ref) > -1) {
if(!$refs) {
$refs = item.context?.$refs
}
const originalEvents = getEvents(item);
const originalChange = originalEvents.change;
return cloneElement(item, {
on: {
change: function change() {
if (Array.isArray(originalChange)) {
for (let i = 0, l = originalChange.length; i < l; i++) {
// eslint-disable-next-line prefer-rest-params
originalChange[i]?.apply(originalChange, arguments);
}
} else if (originalChange) {
// eslint-disable-next-line prefer-rest-params
originalChange?.apply(undefined, arguments);
}
const currentIndex = level.indexOf(item.data.ref);
const $ref = $refs[level[currentIndex + 1]];
if($ref) {
$ref.reClear && $ref.reClear();
$ref.findOptions && $ref.findOptions();
}
}
}
})
} else {
if (item.componentOptions && item.componentOptions.children && item.componentOptions.children.length) {
item.componentOptions.children = findLevel(item.componentOptions.children)
}
return cloneElement(item)
}
})
}else {
return '';
}
}
const cloneChildren = findLevel(children);
return h('div', {}, cloneChildren)
}
}
</script>
select
<template>
<a-select v-model="reValue" :placeholder="placeholder" @change="change" :mode="mode" :allowClear="allowClear">
<a-select-option v-if="showAll" value="">全部</a-select-option>
<a-select-option v-for="(_) in options" :key="_[OptionValue]" :value="_[OptionValue]">
{{_[OptionName]}}
</a-select-option>
</a-select>
</template>
<script lang="ts">
import {defineComponent, ref, PropType, onMounted, watch} from "@vue/composition-api";
//@ts-ignore
import PropTypes from "ant-design-vue/lib/_util/vue-types";
import {AxiosRequestConfig} from "axios";
import {GetApiService} from "@/hooks/page/usePage2";
export default defineComponent({
name: "ProvinceSelect",
model: {
event: 'change',
prop: 'value'
},
props: {
placeholder: String,
mode: PropTypes.oneOf(['default', 'multiple']).def('default'),
ApiConfig: {
type: Object as PropType<AxiosRequestConfig>,
required: true
},
OptionValue: {
type: String,
default: 'code'
},
OptionName: {
type: String,
default: 'name'
},
beforeQuery: {
type: Function as PropType<() => AxiosRequestConfig>,
},
value: [String , Array],
allowClear: {
type: Boolean,
default: false
},
showAll: {
type: Boolean,
default: false
}
},
setup(props, {emit}) {
const options = ref([]);
const reValue = ref(props.value)
watch(() => props.value, (newValue) => {
reValue.value = newValue;
})
const getApiService = new GetApiService(props.ApiConfig, data => options.value = data);
const change = (value: any) => {
emit('change', value)
}
const findOptions = () => {
if(props.beforeQuery) {
getApiService.execute(props.beforeQuery())
}else {
getApiService.execute()
}
}
const reClear = () => {
emit('change', props.mode === 'default' ? '' : [])
options.value = [];
}
onMounted(() => setTimeout(() => findOptions(), 200))
return {
options,
change,
findOptions,
reClear,
reValue
}
},
methods: {
test() {
console.log('test')
}
},
})
</script>
<style scoped>
</style>