committed by
GitHub
12 changed files with 737 additions and 677 deletions
@ -0,0 +1,182 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import type { AnyPromiseFunction } from '@vben/types'; |
||||
|
|
||||
|
import { computed, ref, unref, useAttrs, type VNode, watch } from 'vue'; |
||||
|
|
||||
|
import { LoaderCircle } from '@vben/icons'; |
||||
|
import { get, isEqual, isFunction } from '@vben-core/shared/utils'; |
||||
|
|
||||
|
import { objectOmit } from '@vueuse/core'; |
||||
|
|
||||
|
type OptionsItem = { |
||||
|
[name: string]: any; |
||||
|
disabled?: boolean; |
||||
|
label?: string; |
||||
|
value?: string; |
||||
|
}; |
||||
|
|
||||
|
interface Props { |
||||
|
// 组件 |
||||
|
component: VNode; |
||||
|
numberToString?: boolean; |
||||
|
api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>; |
||||
|
params?: Record<string, any>; |
||||
|
resultField?: string; |
||||
|
labelField?: string; |
||||
|
valueField?: string; |
||||
|
immediate?: boolean; |
||||
|
alwaysLoad?: boolean; |
||||
|
beforeFetch?: AnyPromiseFunction<any, any>; |
||||
|
afterFetch?: AnyPromiseFunction<any, any>; |
||||
|
options?: OptionsItem[]; |
||||
|
// 尾部插槽 |
||||
|
loadingSlot?: string; |
||||
|
// 可见时触发的事件名 |
||||
|
visibleEvent?: string; |
||||
|
modelField?: string; |
||||
|
} |
||||
|
|
||||
|
defineOptions({ name: 'ApiSelect', inheritAttrs: false }); |
||||
|
|
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
labelField: 'label', |
||||
|
valueField: 'value', |
||||
|
resultField: '', |
||||
|
visibleEvent: '', |
||||
|
numberToString: false, |
||||
|
params: () => ({}), |
||||
|
immediate: true, |
||||
|
alwaysLoad: false, |
||||
|
loadingSlot: '', |
||||
|
beforeFetch: undefined, |
||||
|
afterFetch: undefined, |
||||
|
modelField: 'modelValue', |
||||
|
api: undefined, |
||||
|
options: () => [], |
||||
|
}); |
||||
|
|
||||
|
const emit = defineEmits<{ |
||||
|
optionsChange: [OptionsItem[]]; |
||||
|
}>(); |
||||
|
|
||||
|
const modelValue = defineModel({ default: '' }); |
||||
|
|
||||
|
const attrs = useAttrs(); |
||||
|
|
||||
|
const refOptions = ref<OptionsItem[]>([]); |
||||
|
const loading = ref(false); |
||||
|
// 首次是否加载过了 |
||||
|
const isFirstLoaded = ref(false); |
||||
|
|
||||
|
const getOptions = computed(() => { |
||||
|
const { labelField, valueField, numberToString } = props; |
||||
|
|
||||
|
const data: OptionsItem[] = []; |
||||
|
const refOptionsData = unref(refOptions); |
||||
|
|
||||
|
for (const next of refOptionsData) { |
||||
|
if (next) { |
||||
|
const value = get(next, valueField); |
||||
|
data.push({ |
||||
|
...objectOmit(next, [labelField, valueField]), |
||||
|
label: get(next, labelField), |
||||
|
value: numberToString ? `${value}` : value, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return data.length > 0 ? data : props.options; |
||||
|
}); |
||||
|
|
||||
|
const bindProps = computed(() => { |
||||
|
return { |
||||
|
[props.modelField]: unref(modelValue), |
||||
|
[`onUpdate:${props.modelField}`]: (val: string) => { |
||||
|
modelValue.value = val; |
||||
|
}, |
||||
|
...objectOmit(attrs, ['onUpdate:value']), |
||||
|
...(props.visibleEvent |
||||
|
? { |
||||
|
[props.visibleEvent]: handleFetchForVisible, |
||||
|
} |
||||
|
: {}), |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
async function fetchApi() { |
||||
|
let { api, beforeFetch, afterFetch, params, resultField } = props; |
||||
|
|
||||
|
if (!api || !isFunction(api) || loading.value) { |
||||
|
return; |
||||
|
} |
||||
|
refOptions.value = []; |
||||
|
try { |
||||
|
loading.value = true; |
||||
|
if (beforeFetch && isFunction(beforeFetch)) { |
||||
|
params = (await beforeFetch(params)) || params; |
||||
|
} |
||||
|
let res = await api(params); |
||||
|
if (afterFetch && isFunction(afterFetch)) { |
||||
|
res = (await afterFetch(res)) || res; |
||||
|
} |
||||
|
isFirstLoaded.value = true; |
||||
|
if (Array.isArray(res)) { |
||||
|
refOptions.value = res; |
||||
|
emitChange(); |
||||
|
return; |
||||
|
} |
||||
|
if (resultField) { |
||||
|
refOptions.value = get(res, resultField) || []; |
||||
|
} |
||||
|
emitChange(); |
||||
|
} catch (error) { |
||||
|
console.warn(error); |
||||
|
// reset status |
||||
|
isFirstLoaded.value = false; |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function handleFetchForVisible(visible: boolean) { |
||||
|
if (visible) { |
||||
|
if (props.alwaysLoad) { |
||||
|
await fetchApi(); |
||||
|
} else if (!props.immediate && !unref(isFirstLoaded)) { |
||||
|
await fetchApi(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
watch( |
||||
|
() => props.params, |
||||
|
(value, oldValue) => { |
||||
|
if (isEqual(value, oldValue)) { |
||||
|
return; |
||||
|
} |
||||
|
fetchApi(); |
||||
|
}, |
||||
|
{ deep: true, immediate: props.immediate }, |
||||
|
); |
||||
|
|
||||
|
function emitChange() { |
||||
|
emit('optionsChange', unref(getOptions)); |
||||
|
} |
||||
|
</script> |
||||
|
<template> |
||||
|
<div v-bind="{ ...$attrs }"> |
||||
|
<component |
||||
|
:is="component" |
||||
|
v-bind="bindProps" |
||||
|
:options="getOptions" |
||||
|
:placeholder="$attrs.placeholder" |
||||
|
> |
||||
|
<template v-for="item in Object.keys($slots)" #[item]="data"> |
||||
|
<slot :name="item" v-bind="data || {}"></slot> |
||||
|
</template> |
||||
|
<template v-if="loadingSlot && loading" #[loadingSlot]> |
||||
|
<LoaderCircle class="animate-spin" /> |
||||
|
</template> |
||||
|
</component> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1 @@ |
|||||
|
export { default as ApiSelect } from './api-select.vue'; |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue