api-component.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <script lang="ts" setup>
  2. import type { AnyPromiseFunction } from '@vben/types';
  3. import { type Component, computed, ref, unref, useAttrs, watch } from 'vue';
  4. import { LoaderCircle } from '@vben/icons';
  5. import { get, isEqual, isFunction } from '@vben-core/shared/utils';
  6. import { objectOmit } from '@vueuse/core';
  7. type OptionsItem = {
  8. [name: string]: any;
  9. children?: OptionsItem[];
  10. disabled?: boolean;
  11. label?: string;
  12. value?: string;
  13. };
  14. interface Props {
  15. /** 组件 */
  16. component: Component;
  17. /** 是否将value从数字转为string */
  18. numberToString?: boolean;
  19. /** 获取options数据的函数 */
  20. api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>;
  21. /** 传递给api的参数 */
  22. params?: Record<string, any>;
  23. /** 从api返回的结果中提取options数组的字段名 */
  24. resultField?: string;
  25. /** label字段名 */
  26. labelField?: string;
  27. /** children字段名,需要层级数据的组件可用 */
  28. childrenField?: string;
  29. /** value字段名 */
  30. valueField?: string;
  31. /** 组件接收options数据的属性名 */
  32. optionsPropName?: string;
  33. /** 是否立即调用api */
  34. immediate?: boolean;
  35. /** 每次`visibleEvent`事件发生时都重新请求数据 */
  36. alwaysLoad?: boolean;
  37. /** 在api请求之前的回调函数 */
  38. beforeFetch?: AnyPromiseFunction<any, any>;
  39. /** 在api请求之后的回调函数 */
  40. afterFetch?: AnyPromiseFunction<any, any>;
  41. /** 直接传入选项数据,也作为api返回空数据时的后备数据 */
  42. options?: OptionsItem[];
  43. /** 组件的插槽名称,用来显示一个"加载中"的图标 */
  44. loadingSlot?: string;
  45. /** 触发api请求的事件名 */
  46. visibleEvent?: string;
  47. /** 组件的v-model属性名,默认为modelValue。部分组件可能为value */
  48. modelPropName?: string;
  49. }
  50. defineOptions({ name: 'ApiComponent', inheritAttrs: false });
  51. const props = withDefaults(defineProps<Props>(), {
  52. labelField: 'label',
  53. valueField: 'value',
  54. childrenField: '',
  55. optionsPropName: 'options',
  56. resultField: '',
  57. visibleEvent: '',
  58. numberToString: false,
  59. params: () => ({}),
  60. immediate: true,
  61. alwaysLoad: false,
  62. loadingSlot: '',
  63. beforeFetch: undefined,
  64. afterFetch: undefined,
  65. modelPropName: 'modelValue',
  66. api: undefined,
  67. options: () => [],
  68. });
  69. const emit = defineEmits<{
  70. optionsChange: [OptionsItem[]];
  71. }>();
  72. const modelValue = defineModel({ default: '' });
  73. const attrs = useAttrs();
  74. const refOptions = ref<OptionsItem[]>([]);
  75. const loading = ref(false);
  76. // 首次是否加载过了
  77. const isFirstLoaded = ref(false);
  78. const getOptions = computed(() => {
  79. const { labelField, valueField, childrenField, numberToString } = props;
  80. const refOptionsData = unref(refOptions);
  81. function transformData(data: OptionsItem[]): OptionsItem[] {
  82. return data.map((item) => {
  83. const value = get(item, valueField);
  84. return {
  85. ...objectOmit(item, [labelField, valueField, childrenField]),
  86. label: get(item, labelField),
  87. value: numberToString ? `${value}` : value,
  88. ...(childrenField && item[childrenField]
  89. ? { children: transformData(item[childrenField]) }
  90. : {}),
  91. };
  92. });
  93. }
  94. const data: OptionsItem[] = transformData(refOptionsData);
  95. return data.length > 0 ? data : props.options;
  96. });
  97. const bindProps = computed(() => {
  98. return {
  99. [props.modelPropName]: unref(modelValue),
  100. [props.optionsPropName]: unref(getOptions),
  101. [`onUpdate:${props.modelPropName}`]: (val: string) => {
  102. modelValue.value = val;
  103. },
  104. ...objectOmit(attrs, ['onUpdate:value']),
  105. ...(props.visibleEvent
  106. ? {
  107. [props.visibleEvent]: handleFetchForVisible,
  108. }
  109. : {}),
  110. };
  111. });
  112. async function fetchApi() {
  113. let { api, beforeFetch, afterFetch, params, resultField } = props;
  114. if (!api || !isFunction(api) || loading.value) {
  115. return;
  116. }
  117. refOptions.value = [];
  118. try {
  119. loading.value = true;
  120. if (beforeFetch && isFunction(beforeFetch)) {
  121. params = (await beforeFetch(params)) || params;
  122. }
  123. let res = await api(params);
  124. if (afterFetch && isFunction(afterFetch)) {
  125. res = (await afterFetch(res)) || res;
  126. }
  127. isFirstLoaded.value = true;
  128. if (Array.isArray(res)) {
  129. refOptions.value = res;
  130. emitChange();
  131. return;
  132. }
  133. if (resultField) {
  134. refOptions.value = get(res, resultField) || [];
  135. }
  136. emitChange();
  137. } catch (error) {
  138. console.warn(error);
  139. // reset status
  140. isFirstLoaded.value = false;
  141. } finally {
  142. loading.value = false;
  143. }
  144. }
  145. async function handleFetchForVisible(visible: boolean) {
  146. if (visible) {
  147. if (props.alwaysLoad) {
  148. await fetchApi();
  149. } else if (!props.immediate && !unref(isFirstLoaded)) {
  150. await fetchApi();
  151. }
  152. }
  153. }
  154. watch(
  155. () => props.params,
  156. (value, oldValue) => {
  157. if (isEqual(value, oldValue)) {
  158. return;
  159. }
  160. fetchApi();
  161. },
  162. { deep: true, immediate: props.immediate },
  163. );
  164. function emitChange() {
  165. emit('optionsChange', unref(getOptions));
  166. }
  167. </script>
  168. <template>
  169. <div v-bind="{ ...$attrs }">
  170. <component
  171. :is="component"
  172. v-bind="bindProps"
  173. :placeholder="$attrs.placeholder"
  174. >
  175. <template v-for="item in Object.keys($slots)" #[item]="data">
  176. <slot :name="item" v-bind="data || {}"></slot>
  177. </template>
  178. <template v-if="loadingSlot && loading" #[loadingSlot]>
  179. <LoaderCircle class="animate-spin" />
  180. </template>
  181. </component>
  182. </div>
  183. </template>