<!-- @format --> <template> <a-select v-bind="getBindValues" :options="getOptions" :getPopupContainer="(triggerNode) => triggerNode.parentNode" @dropdown-visible-change="dropdownHandle" @popupScroll="onPopupScroll" v-model:value="modelValue" @search="onSearch" @clear="onClear" :filterOption="onFilterOption"> <template #[item]="data" v-for="item in Object.keys($slots)" :key="item"> <slot :name="item" v-bind="data || {}"></slot> </template> </a-select> </template> <script lang="ts"> import { computed, defineComponent, nextTick, PropType, ref, unref, watch } from 'vue'; import { cloneDeep, get, isArray, isEqual, isFunction, isString, isUndefined } from 'lodash-es'; import { HttpRequestConfig, useApi } from '/nerv-lib/use/use-api'; type ChangeValue = string | number | undefined | string[] | number[]; export default defineComponent({ name: 'NsSelectApi', props: { api: { type: [String, Object, Function] as PropType<string | Function | HttpRequestConfig>, required: true, }, value: [String, Object, Array, Number], field: String, params: { type: Object, default: () => ({}), }, resultField: { type: String, default: 'data.data', }, firstOption: { type: Object, }, numberToString: { type: Boolean, default: false, }, immediate: { type: Boolean, default: false, }, labelField: { type: [String, Function], default: 'label', }, valueField: { type: String, default: 'value', }, autoSelectFirst: { // 与autoClearValue同时使用,只会执行autoSelectFirst type: Boolean, default: false, }, autoClearValue: { type: Boolean, default: false, }, //数据筛选函数 filterData: { type: Function, }, // 开启前端筛选默认label包含 autoSearch: { type: Boolean, default: false, }, //后端筛选字段 filterFiled: { type: String, }, //是否开启滚动加载 scrollLoad: { type: Boolean, default: false, }, //分页字段 pageField: { type: String, default: 'page', }, //第一页 defaultPage: { type: Number, default: 0, }, dropdownReload: { type: Boolean, default: false, }, }, emits: ['change', 'validateChange'], setup(props, { attrs, emit }) { const options = ref([]); const isLoad = ref(false); const changeValue = ref<any>(undefined); const filterFiledRef = ref<string | undefined>(undefined); let isFirstLoad = !!props.api; // 是否第一次加载 // eslint-disable-next-line vue/no-setup-props-destructure let page = props.defaultPage; // let isLoading = false; const getBindValues = computed(() => { const selectProps: Recordable = {}; if (props.filterFiled || props.autoSearch) { selectProps['showSearch'] = true; } return { ...attrs, ...selectProps, }; }); /** * 设置filterFiled时注册search事件 */ const onSearch = computed(() => { if (props.filterFiled) { return (input: string) => { if (filterFiledRef.value !== input) { filterFiledRef.value = input; page = props.defaultPage; fetch(); } }; } else { return undefined; } }); /** * 设置filterFiled时注册filterOption事件 */ const onFilterOption = computed(() => { if (props.autoSearch) { return (input: string, option: any) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0; }; } else { return false; } }); /** * 根据请求得到的data获取options */ const getOptions = computed(() => { const { firstOption, labelField, valueField, numberToString } = props; const sourceOptions = []; firstOption && sourceOptions.push(firstOption); return unref(options).reduce((acc, next: Recordable) => { if (isString(next)) { //值是字符串类型 const option = { label: next, value: next, }; acc.push(option); return acc; } else { const value = get(next, valueField); const option = { ...next, label: isFunction(labelField) ? labelField(next) : get(next, labelField as string), value: numberToString ? `${value}` : value, }; acc.push(option); return acc; } }, sourceOptions); }); /** * 获取数据 */ const fetch = () => { if (getBindValues.value.checkRequiredParams === false) { options.value = []; return; } isLoading = true; const requestConfig: HttpRequestConfig = { method: 'get' }; const { api, params: _params, resultField, filterData, filterFiled, pageField, scrollLoad, defaultPage, } = props; const params: Recordable = cloneDeep(_params); if (filterFiled && filterFiledRef.value) { params[filterFiled] = filterFiledRef.value; } if (scrollLoad) { params[pageField] = page; if (page === defaultPage) { options.value = []; } } const { httpRequest } = useApi(); httpRequest({ api, params, requestConfig }) .then((res: Recordable) => { emit('validateChange', { help: undefined }); if (resultField) { let data = get(res, resultField) || []; // data = data.splice(Math.floor(Math.random() * 3), Math.floor(Math.random() * 5 + 5)); if (isFunction(filterData)) { data = data.filter(filterData); } if (scrollLoad) { options.value = [...options.value, ...data]; } else { options.value = data; } } isLoad.value = true; isLoading = false; }) .catch((error: any) => { if (error?.response?.status === 403) { emit('validateChange', { help: '暂无权限', validateStatus: 'error' }); nextTick(() => { //清空编辑初始值 modelValue.value = undefined; }); } options.value = []; isLoading = false; }); }; let lastParams: any = undefined; /** ** 延迟获取数据 */ async function dropdownHandle(open: boolean) { if (!open) return; if ((!props.immediate && !isLoad.value) || props.dropdownReload) { await fetch(); } } /** ** 绑定value */ const modelValue = computed({ set(value: ChangeValue) { if (isEqual(value, changeValue.value)) return; changeValue.value = value; triggerChange(value); }, get() { return changeValue.value; }, }); /** * 传入值需要 * 前提option已获取到数据 */ watch( () => props.value, () => { if (isLoad.value) { modelValue.value = props.value; } }, { immediate: true, }, ); watch( () => getOptions.value, () => { const { value, autoSelectFirst, autoClearValue } = props; // 首次加载如果有值则选中值 if (isFirstLoad && !isUndefined(value)) { modelValue.value = value; } else if (!filterFiledRef.value) { if (autoSelectFirst) { modelValue.value = getOptions.value[0]?.value; } else if (autoClearValue) { modelValue.value = undefined; } } isFirstLoad = false; }, ); /** * 重写change ant 初始化数据、删除数据时不触发change * @param value */ function triggerChange(value: ChangeValue) { if (isUndefined(value)) { emit('change', value, undefined); } else if (isArray(value)) { const options: Record<string, any>[] = []; value.forEach((v) => { getOptions.value.forEach((option) => { if (option.value === v) { options.push(option); } }); }); emit('change', value, options); } else { let op = {}; getOptions.value.forEach((option) => { if (option.value === value) { op = option; } }); emit('change', value, op); } } /** ** 联动 immediate 为是否主动获取数据 */ watch( [() => props.params, () => props.api], async () => { const { params } = props; if (!isEqual(lastParams, params)) { if (props.immediate || true) { //todo 暂时全设为主动获取 lastParams = cloneDeep(params); await fetch(); } isLoad.value = false; // 设置成false 点击下拉才会触发 } }, { deep: true, immediate: props.immediate }, ); const onPopupScroll = computed(() => { if (props.scrollLoad) { return (e) => { const { scrollHeight, scrollTop, clientHeight } = e.target; if (scrollHeight - scrollTop - 80 < clientHeight && !isLoading) { page++; fetch(); } }; } else { return undefined; } }); const onClear = () => { filterFiledRef.value = undefined; }; return { onPopupScroll, dropdownHandle, getOptions, getBindValues, modelValue, onSearch, onFilterOption, onClear, }; }, }); </script> <style lang="less" scoped></style>