f
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { AnyPromiseFunction } from '@vben/types';
|
||||
|
||||
import { computed, ref, unref, useAttrs, 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;
|
||||
children?: OptionsItem[];
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
/** 组件 */
|
||||
component: Component;
|
||||
/** 是否将value从数字转为string */
|
||||
numberToString?: boolean;
|
||||
/** 获取options数据的函数 */
|
||||
api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>;
|
||||
/** 传递给api的参数 */
|
||||
params?: Record<string, any>;
|
||||
/** 从api返回的结果中提取options数组的字段名 */
|
||||
resultField?: string;
|
||||
/** label字段名 */
|
||||
labelField?: string;
|
||||
/** children字段名,需要层级数据的组件可用 */
|
||||
childrenField?: string;
|
||||
/** value字段名 */
|
||||
valueField?: string;
|
||||
/** 组件接收options数据的属性名 */
|
||||
optionsPropName?: string;
|
||||
/** 是否立即调用api */
|
||||
immediate?: boolean;
|
||||
/** 每次`visibleEvent`事件发生时都重新请求数据 */
|
||||
alwaysLoad?: boolean;
|
||||
/** 在api请求之前的回调函数 */
|
||||
beforeFetch?: AnyPromiseFunction<any, any>;
|
||||
/** 在api请求之后的回调函数 */
|
||||
afterFetch?: AnyPromiseFunction<any, any>;
|
||||
/** 直接传入选项数据,也作为api返回空数据时的后备数据 */
|
||||
options?: OptionsItem[];
|
||||
/** 组件的插槽名称,用来显示一个"加载中"的图标 */
|
||||
loadingSlot?: string;
|
||||
/** 触发api请求的事件名 */
|
||||
visibleEvent?: string;
|
||||
/** 组件的v-model属性名,默认为modelValue。部分组件可能为value */
|
||||
modelPropName?: string;
|
||||
/**
|
||||
* 自动选择
|
||||
* - `first`:自动选择第一个选项
|
||||
* - `last`:自动选择最后一个选项
|
||||
* - `one`: 当请求的结果只有一个选项时,自动选择该选项
|
||||
* - 函数:自定义选择逻辑,函数的参数为请求的结果数组,返回值为选择的选项
|
||||
* - false:不自动选择(默认)
|
||||
*/
|
||||
autoSelect?:
|
||||
| 'first'
|
||||
| 'last'
|
||||
| 'one'
|
||||
| ((item: OptionsItem[]) => OptionsItem)
|
||||
| false;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'ApiComponent', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
labelField: 'label',
|
||||
valueField: 'value',
|
||||
childrenField: '',
|
||||
optionsPropName: 'options',
|
||||
resultField: '',
|
||||
visibleEvent: '',
|
||||
numberToString: false,
|
||||
params: () => ({}),
|
||||
immediate: true,
|
||||
alwaysLoad: false,
|
||||
loadingSlot: '',
|
||||
beforeFetch: undefined,
|
||||
afterFetch: undefined,
|
||||
modelPropName: 'modelValue',
|
||||
api: undefined,
|
||||
autoSelect: false,
|
||||
options: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
optionsChange: [OptionsItem[]];
|
||||
}>();
|
||||
|
||||
const modelValue = defineModel<any>({ default: undefined });
|
||||
|
||||
const attrs = useAttrs();
|
||||
const innerParams = ref({});
|
||||
const refOptions = ref<OptionsItem[]>([]);
|
||||
const loading = ref(false);
|
||||
// 首次是否加载过了
|
||||
const isFirstLoaded = ref(false);
|
||||
|
||||
const getOptions = computed(() => {
|
||||
const { labelField, valueField, childrenField, numberToString } = props;
|
||||
|
||||
const refOptionsData = unref(refOptions);
|
||||
|
||||
function transformData(data: OptionsItem[]): OptionsItem[] {
|
||||
return data.map((item) => {
|
||||
const value = get(item, valueField);
|
||||
return {
|
||||
...objectOmit(item, [labelField, valueField, childrenField]),
|
||||
label: get(item, labelField),
|
||||
value: numberToString ? `${value}` : value,
|
||||
...(childrenField && item[childrenField]
|
||||
? { children: transformData(item[childrenField]) }
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const data: OptionsItem[] = transformData(refOptionsData);
|
||||
|
||||
return data.length > 0 ? data : props.options;
|
||||
});
|
||||
|
||||
const bindProps = computed(() => {
|
||||
return {
|
||||
[props.modelPropName]: unref(modelValue),
|
||||
[props.optionsPropName]: unref(getOptions),
|
||||
[`onUpdate:${props.modelPropName}`]: (val: string) => {
|
||||
modelValue.value = val;
|
||||
},
|
||||
...objectOmit(attrs, [`onUpdate:${props.modelPropName}`]),
|
||||
...(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const params = computed(() => {
|
||||
return {
|
||||
...props.params,
|
||||
...unref(innerParams),
|
||||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
params,
|
||||
(value, oldValue) => {
|
||||
if (isEqual(value, oldValue)) {
|
||||
return;
|
||||
}
|
||||
fetchApi();
|
||||
},
|
||||
{ deep: true, immediate: props.immediate },
|
||||
);
|
||||
|
||||
function emitChange() {
|
||||
if (
|
||||
modelValue.value === undefined &&
|
||||
props.autoSelect &&
|
||||
unref(getOptions).length > 0
|
||||
) {
|
||||
let firstOption;
|
||||
if (isFunction(props.autoSelect)) {
|
||||
firstOption = props.autoSelect(unref(getOptions));
|
||||
} else {
|
||||
switch (props.autoSelect) {
|
||||
case 'first': {
|
||||
firstOption = unref(getOptions)[0];
|
||||
break;
|
||||
}
|
||||
case 'last': {
|
||||
firstOption = unref(getOptions)[unref(getOptions).length - 1];
|
||||
break;
|
||||
}
|
||||
case 'one': {
|
||||
if (unref(getOptions).length === 1) {
|
||||
firstOption = unref(getOptions)[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstOption) modelValue.value = firstOption.value;
|
||||
}
|
||||
emit('optionsChange', unref(getOptions));
|
||||
}
|
||||
const componentRef = ref();
|
||||
defineExpose({
|
||||
/** 获取options数据 */
|
||||
getOptions: () => unref(getOptions),
|
||||
/** 获取当前值 */
|
||||
getValue: () => unref(modelValue),
|
||||
/** 获取被包装的组件实例 */
|
||||
getComponentRef: <T = any,>() => componentRef.value as T,
|
||||
/** 更新Api参数 */
|
||||
updateParam(newParams: Record<string, any>) {
|
||||
innerParams.value = newParams;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<component
|
||||
:is="component"
|
||||
v-bind="bindProps"
|
||||
:placeholder="$attrs.placeholder"
|
||||
ref="componentRef"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
Reference in New Issue
Block a user