为什么要重复开发,像这样的组件一大堆

  • 事务原因,api和的类型契合生成的请求方法,还有需求扩展一些功用
  • 技能栈,项目运用的是vue3、ts、antd4,契合这个条件的组件就比较少
  • 最最最重要的原因,活不多(●’◡’●)

功用运用

经过api获取数据、深层次的field-names(经过lodash-es实现)

在我的项目开发中api是经过工具(适用于openApi3)生成的恪守开发标准,所以在运用select组件时,只需求导入api ,就能够简略的加载数据


<script setup lang="ts">
import { ref } from 'vue'
import { testApi } from '@/api'  // 此处api为项目中的事务api
const value = ref(199)
</script>
<template>
  <h2>API简略用法</h2>
  <c-select
    v-model:value="value"
    :field-names="{ label: 'info.name', value: 'id', options: 'children' }"
    style="width: 200px"
    :api="testApi"
  />
</template>

自定义空值

像一些id的挑选,空值往往是0,但是在挑选器中,0并不是一个有用的空值。在当前的组件中仅需求传入:null-value=”0″,即可指定0为空值

根据antdv4 select组件二次封装,简化加载长途数据、只读扩展、fieldNames扩展
根据antdv4 select组件二次封装,简化加载长途数据、只读扩展、fieldNames扩展

只读功用插槽

在一些表单操作中往往需求只读的定制,所以组件不只提供了readOnly的特点操控,还提供了对应的插槽,能够经过{selected,selectNodes }做自定义的回显。

根据antdv4 select组件二次封装,简化加载长途数据、只读扩展、fieldNames扩展

  <template #readOnly="{ selectNodes }">
      总共挑选了: {{ selectNodes.length }}项
      <br>
      <a-tag v-for="node, i in selectNodes" :key="i" color="red">
        <component :is="node" />
      </a-tag>
 </template>

根据antdv4 select组件二次封装,简化加载长途数据、只读扩展、fieldNames扩展

话不多说直接上代码

组件核心代码

经过 useInjectFormConfig注入了特点,能够运用useAttrs代替

<script lang="ts" setup generic="T extends  DefaultOptionType, Api extends  DataApi<T>">
import { ref, useAttrs, watch } from 'vue'
import { Select } from 'ant-design-vue'
import type { ComponentSlots } from 'vue-component-type-helpers'
import { LoadingOutlined } from '@ant-design/icons-vue'
import type { DefaultOptionType, SelectValue } from 'ant-design-vue/lib/select'
import { useVModels } from '@vueuse/core'
import type { SelectProps } from '../types'
import { readOnlySlot, useInjectFormConfig } from '../../form/src/useFormConfig'
import { useSlotsHooks } from '../../hooks/useSlot'
import type { DataApi } from '../../global.types'
import { useFetchOptions, useReadComponents } from './useSelectTools'
defineOptions({
  inheritAttrs: false,
  group: 'form',
})
const props = withDefaults(defineProps<SelectProps<T, Api>>(), {
  bordered: true,
})
const emit = defineEmits<{
  'update:value': [value: SelectValue]
  'dropdownVisibleChange': [visible: boolean]
  'popupScroll': [e: UIEvent]
}>()
const slots = defineSlots<ComponentSlots<typeof Select> & { [readOnlySlot]: { selected: any } }>()
const attrs = useAttrs()
const { value } = useVModels(props, emit, { passive: true })
const { getBindValue } = useInjectFormConfig<SelectProps<T, Api>>({ name: 'select', attrs, props })
const { useExcludeSlots } = useSlotsHooks(slots, [readOnlySlot, 'suffixIcon'])
const { options, loading, onPopupScroll, onDropdownVisibleChange } = useFetchOptions(getBindValue, emit)
const { RenderReadNode, selectNodes } = useReadComponents(slots, getBindValue, options)
const modelValue = ref<SelectValue>()
watch(() => value?.value, () => {
  modelValue.value = value?.value
}, { immediate: true })
// #region 内部处理空值
watch(() => getBindValue.value.nullValue, () => {
  if (getBindValue.value.nullValue === value.value)
    modelValue.value = undefined
}, { immediate: true })
function updateValue(val: SelectValue) {
  if (value)
    value.value = val
  else
    modelValue.value = val
}
// #endregion
</script>
<template>
  <slot v-if="getBindValue?.readOnly" :name="readOnlySlot" :selected="getBindValue.value" :select-nodes="selectNodes">
    <RenderReadNode />
  </slot>
  <Select
    v-else v-bind="getBindValue" :value="modelValue" :options="options!" :field-names="undefined"
    @update:value="updateValue" @popup-scroll="onPopupScroll" @dropdown-visible-change="onDropdownVisibleChange"
  >
    <template #suffixIcon>
      <slot name="suffixIcon">
        <LoadingOutlined v-if="loading" spin />
      </slot>
    </template>
    <template v-for="_, key in useExcludeSlots" #[key]="data">
      <slot :name="key" v-bind="data || {}" />
    </template>
  </Select>
</template>

经过useFetchOptions 获取options

export function useReadComponents<T>(
  slots: any,
  props: Ref<Readonly<SelectProps<T, DataApi<T>>>>,
  options: Ref<Array<DefaultOptionType> | null>,
) {
  const selectNodes = shallowRef<Array<() => VNode>>([])
  /**
   * 检查给定的节点值是否在挑选值列表中
   * @param nodeValue 节点值,能够是字符串或数字类型
   * @returns 如果节点值在挑选值列表中则回来true,不然回来false
   */
  const isValueInSelectValue = (nodeValue: string | number) => {
    const { value: selectValue, labelInValue } = props.value
    const isItemSelected = (selectValue: SelectValue) => {
      if (labelInValue && typeof selectValue === 'object') {
        const _selectValue = selectValue as LabeledValue
        return _selectValue?.value === nodeValue
      }
      else {
        return selectValue === nodeValue
      }
    }
    if (Array.isArray(selectValue))
      return selectValue.some(v => isItemSelected(v))
    else
      return isItemSelected(selectValue)
  }
  const fetchOptionNodeByOptions = () => {
    selectNodes.value = []
    const deep = (options: Array<DefaultOptionType>) => {
      for (const item of options) {
        if (item.value && isValueInSelectValue(item.value))
          selectNodes.value.push(() => h('span', item.label))
        if (item.options)
          deep(item.options)
      }
    }
    deep(options.value || [])
    triggerRef(selectNodes)
  }
  const fetchOptionNodeBySlot = () => {
    selectNodes.value = []
    const deepNodeTree = (nodes: VNode[]) => {
      for (const node of nodes) {
        const children = node.children
        if (Array.isArray(children)) { deepNodeTree(children as VNode[]) }
        else if (typeof node.type === 'function') {
          if (node.type.name.includes('Option')) {
            const nodeProps = node.props as any
            const v = nodeProps?.value ?? nodeProps?.key
            if (isValueInSelectValue(v))
              selectNodes.value.push((node as any).children?.default || node)
          }
          else if (node.type.name.includes('OptGroup')) {
            const children = node.children as any
            if ('default' in children && typeof children.default === 'function')
              deepNodeTree(children.default())
          }
        }
      }
    }
    if ('default' in slots) {
      const slotsVNode = slots?.default()
      deepNodeTree(slotsVNode)
    }
    triggerRef(selectNodes)
  }
  const fetchOptionNode = () => {
    if (options.value)
      nextTick(fetchOptionNodeByOptions)
    else
      fetchOptionNodeBySlot()
  }
  watch([() => props.value.value, () => options.value, () => props.value.readOnly], () => {
    if (props.value.readOnly)
      fetchOptionNode()
  }, { immediate: true })
  const RenderReadNode = defineComponent(
    () => {
      return () => {
        return (
          <div style="display: inline-block;">
            {selectNodes.value.map(v => (<Tag>{v()}</Tag>))}
          </div>
        )
      }
    },
  )
  return { RenderReadNode, selectNodes }
}

经过useReadComponents 获取只读组件节点

export function useFetchOptions<T extends DefaultOptionType>(attrs: Ref<Readonly<SelectProps<T, DataApi<T>>>>, emit: ((evt: 'dropdownVisibleChange', visible: boolean) => void) & ((evt: 'popupScroll', e: UIEvent) => void)) {
  const options = ref<Array<DefaultOptionType> | null>(null)
  const loading = ref(false)
  const page = ref<PageParams>({ offset: 0, limit: 20 })
  const fetchCount = ref(0)
  /**
   * 将结果转换为默许选项类型数组
   * @param res - 可迭代目标或数组,包括要转换的数据
   * @returns 默许选项类型数组
   */
  const transformOptions = (res: IPagedEnumerable<T> | Array<T>): DefaultOptionType[] => {
    let data: Array<T> = []
    if (Array.isArray(res))
      data = res
    else if (res?.items)
      data = res.items
    return data.map((item) => {
      /**
       * 转换选项并获取 item 的 label、value 特点值
       */
      const options = transformOptions(get(item, attrs.value.fieldNames?.options || 'options'))
      const res: DefaultOptionType = {
        label: get(item, attrs.value.fieldNames?.label || 'label'),
        value: get(item, attrs.value.fieldNames?.value || 'value'),
        // option: item,
      }
      if (options.length)
        res.options = options
      if (item.disabled)
        res.disabled = item.disabled
      return res
    })
  }
  /**
   * 初始化options
   */
  const optionsInit = () => {
    options.value = attrs.value.options ? transformOptions(attrs.value.options as any) : null
    if (attrs.value.insertOptions)
      options.value = [...transformOptions(attrs.value.insertOptions), ...options.value || []]
  }
  const fetchApiData = async () => {
    if (!attrs.value.api)
      return
    try {
      fetchCount.value++
      loading.value = true
      let params = attrs.value.params
      if (attrs.value.page)
        params = { ...params, ...page.value }
      const res = await attrs.value.api(params, attrs.value.postParams)
      if (!options.value)
        options.value = []
      options.value.push(...transformOptions(res))
      loading.value = false
    }
    catch (e) {
      fetchCount.value--
      loading.value = false
      console.error(`select 调用api失败: ${e}`)
    }
  }
  const onDropdownVisibleChange = (visible: boolean) => {
    emit('dropdownVisibleChange', visible)
    if (visible && fetchCount.value === 0)
      fetchApiData()
  }
  const onPopupScroll = async (e: any) => {
    emit('popupScroll', e)
    if (!attrs.value.page)
      return
    const { scrollTop, offsetHeight, scrollHeight } = e.target
    if (Math.ceil(scrollTop + offsetHeight) >= scrollHeight) {
      const cur = options.value!.length / page.value.limit >= 1
        ? Math.ceil(options.value!.length / page.value.limit)
        : 1
      page.value.offset = cur * page.value.limit
      await fetchApiData()
    }
  }
  onMounted(() => {
    optionsInit()
    if (!attrs.value.options) {
      if (typeof attrs.value.page === 'object' && 'limit' in attrs.value.page && 'offset' in attrs.value.page)
        page.value = { ...attrs.value.page }
    }
  })
  watch([() => attrs.value.api, () => attrs.value.params, () => attrs.value.postParams, () => attrs.value.options], () => {
    if (attrs.value.api && !attrs.value.options) {
      fetchCount.value = 0
      optionsInit()
      fetchApiData()
    }
    else if (!attrs.value.api) {
      optionsInit()
    }
  }, { immediate: !!attrs.value.immediate })
  return {
    options,
    loading,
    onPopupScroll,
    onDropdownVisibleChange,
  }
}

总结

实现的功用大部分都是调库的,旨在扩展功用,还有增加对vue3的hook的理解。轻喷