用vue开发出魔法的级联选择器组件。

思路

目的通过包装器将 插槽内的元素都递归的遍历起来,从而达到监控想要监控的某种元素。
难点 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>

You May Also Like

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注