api-component.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <script lang="ts" setup>
  2. import type { Component } from 'vue';
  3. import type { AnyPromiseFunction } from '@vben/types';
  4. import { computed, nextTick, ref, unref, useAttrs, watch } from 'vue';
  5. import { LoaderCircle } from '@vben/icons';
  6. import { cloneDeep, get, isEqual, isFunction } from '@vben-core/shared/utils';
  7. import { objectOmit } from '@vueuse/core';
  8. type OptionsItem = {
  9. [name: string]: any;
  10. children?: OptionsItem[];
  11. disabled?: boolean;
  12. label?: string;
  13. value?: string;
  14. };
  15. interface Props {
  16. /** 组件 */
  17. component: Component;
  18. /** 是否将value从数字转为string */
  19. numberToString?: boolean;
  20. /** 获取options数据的函数 */
  21. api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>;
  22. /** 传递给api的参数 */
  23. params?: Record<string, any>;
  24. /** 从api返回的结果中提取options数组的字段名 */
  25. resultField?: string;
  26. /** label字段名 */
  27. labelField?: string;
  28. /** children字段名,需要层级数据的组件可用 */
  29. childrenField?: string;
  30. /** value字段名 */
  31. valueField?: string;
  32. /** 组件接收options数据的属性名 */
  33. optionsPropName?: string;
  34. /** 是否立即调用api */
  35. immediate?: boolean;
  36. /** 每次`visibleEvent`事件发生时都重新请求数据 */
  37. alwaysLoad?: boolean;
  38. /** 在api请求之前的回调函数 */
  39. beforeFetch?: AnyPromiseFunction<any, any>;
  40. /** 在api请求之后的回调函数 */
  41. afterFetch?: AnyPromiseFunction<any, any>;
  42. /** 直接传入选项数据,也作为api返回空数据时的后备数据 */
  43. options?: OptionsItem[];
  44. /** 组件的插槽名称,用来显示一个"加载中"的图标 */
  45. loadingSlot?: string;
  46. /** 触发api请求的事件名 */
  47. visibleEvent?: string;
  48. /** 组件的v-model属性名,默认为modelValue。部分组件可能为value */
  49. modelPropName?: string;
  50. /**
  51. * 自动选择
  52. * - `first`:自动选择第一个选项
  53. * - `last`:自动选择最后一个选项
  54. * - `one`: 当请求的结果只有一个选项时,自动选择该选项
  55. * - 函数:自定义选择逻辑,函数的参数为请求的结果数组,返回值为选择的选项
  56. * - false:不自动选择(默认)
  57. */
  58. autoSelect?:
  59. | 'first'
  60. | 'last'
  61. | 'one'
  62. | ((item: OptionsItem[]) => OptionsItem)
  63. | false;
  64. }
  65. defineOptions({ name: 'ApiComponent', inheritAttrs: false });
  66. const props = withDefaults(defineProps<Props>(), {
  67. labelField: 'label',
  68. valueField: 'value',
  69. childrenField: '',
  70. optionsPropName: 'options',
  71. resultField: '',
  72. visibleEvent: '',
  73. numberToString: false,
  74. params: () => ({}),
  75. immediate: true,
  76. alwaysLoad: false,
  77. loadingSlot: '',
  78. beforeFetch: undefined,
  79. afterFetch: undefined,
  80. modelPropName: 'modelValue',
  81. api: undefined,
  82. autoSelect: false,
  83. options: () => [],
  84. });
  85. const emit = defineEmits<{
  86. optionsChange: [OptionsItem[]];
  87. }>();
  88. const modelValue = defineModel<any>({ default: undefined });
  89. const attrs = useAttrs();
  90. const innerParams = ref({});
  91. const refOptions = ref<OptionsItem[]>([]);
  92. const loading = ref(false);
  93. // 首次是否加载过了
  94. const isFirstLoaded = ref(false);
  95. // 标记是否有待处理的请求
  96. const hasPendingRequest = ref(false);
  97. const getOptions = computed(() => {
  98. const { labelField, valueField, childrenField, numberToString } = props;
  99. const refOptionsData = unref(refOptions);
  100. function transformData(data: OptionsItem[]): OptionsItem[] {
  101. return data.map((item) => {
  102. const value = get(item, valueField);
  103. return {
  104. ...objectOmit(item, [labelField, valueField, childrenField]),
  105. label: get(item, labelField),
  106. value: numberToString ? `${value}` : value,
  107. ...(childrenField && item[childrenField]
  108. ? { children: transformData(item[childrenField]) }
  109. : {}),
  110. };
  111. });
  112. }
  113. const data: OptionsItem[] = transformData(refOptionsData);
  114. return data.length > 0 ? data : props.options;
  115. });
  116. const bindProps = computed(() => {
  117. return {
  118. [props.modelPropName]: unref(modelValue),
  119. [props.optionsPropName]: unref(getOptions),
  120. [`onUpdate:${props.modelPropName}`]: (val: string) => {
  121. modelValue.value = val;
  122. },
  123. ...objectOmit(attrs, [`onUpdate:${props.modelPropName}`]),
  124. ...(props.visibleEvent
  125. ? {
  126. [props.visibleEvent]: handleFetchForVisible,
  127. }
  128. : {}),
  129. };
  130. });
  131. async function fetchApi() {
  132. const { api, beforeFetch, afterFetch, resultField } = props;
  133. if (!api || !isFunction(api)) {
  134. return;
  135. }
  136. // 如果正在加载,标记有待处理的请求并返回
  137. if (loading.value) {
  138. hasPendingRequest.value = true;
  139. return;
  140. }
  141. refOptions.value = [];
  142. try {
  143. loading.value = true;
  144. let finalParams = unref(mergedParams);
  145. if (beforeFetch && isFunction(beforeFetch)) {
  146. finalParams = (await beforeFetch(cloneDeep(finalParams))) || finalParams;
  147. }
  148. let res = await api(finalParams);
  149. if (afterFetch && isFunction(afterFetch)) {
  150. res = (await afterFetch(res)) || res;
  151. }
  152. isFirstLoaded.value = true;
  153. if (Array.isArray(res)) {
  154. refOptions.value = res;
  155. emitChange();
  156. return;
  157. }
  158. if (resultField) {
  159. refOptions.value = get(res, resultField) || [];
  160. }
  161. emitChange();
  162. } catch (error) {
  163. console.warn(error);
  164. // reset status
  165. isFirstLoaded.value = false;
  166. } finally {
  167. loading.value = false;
  168. // 如果有待处理的请求,立即触发新的请求
  169. if (hasPendingRequest.value) {
  170. hasPendingRequest.value = false;
  171. // 使用 nextTick 确保状态更新完成后再触发新请求
  172. await nextTick();
  173. fetchApi();
  174. }
  175. }
  176. }
  177. async function handleFetchForVisible(visible: boolean) {
  178. if (visible) {
  179. if (props.alwaysLoad) {
  180. await fetchApi();
  181. } else if (!props.immediate && !unref(isFirstLoaded)) {
  182. await fetchApi();
  183. }
  184. }
  185. }
  186. const mergedParams = computed(() => {
  187. return {
  188. ...props.params,
  189. ...unref(innerParams),
  190. };
  191. });
  192. watch(
  193. mergedParams,
  194. (value, oldValue) => {
  195. if (isEqual(value, oldValue)) {
  196. return;
  197. }
  198. fetchApi();
  199. },
  200. { deep: true, immediate: props.immediate },
  201. );
  202. function emitChange() {
  203. if (
  204. modelValue.value === undefined &&
  205. props.autoSelect &&
  206. unref(getOptions).length > 0
  207. ) {
  208. let firstOption;
  209. if (isFunction(props.autoSelect)) {
  210. firstOption = props.autoSelect(unref(getOptions));
  211. } else {
  212. switch (props.autoSelect) {
  213. case 'first': {
  214. firstOption = unref(getOptions)[0];
  215. break;
  216. }
  217. case 'last': {
  218. firstOption = unref(getOptions)[unref(getOptions).length - 1];
  219. break;
  220. }
  221. case 'one': {
  222. if (unref(getOptions).length === 1) {
  223. firstOption = unref(getOptions)[0];
  224. }
  225. break;
  226. }
  227. }
  228. }
  229. if (firstOption) modelValue.value = firstOption.value;
  230. }
  231. emit('optionsChange', unref(getOptions));
  232. }
  233. const componentRef = ref();
  234. defineExpose({
  235. /** 获取options数据 */
  236. getOptions: () => unref(getOptions),
  237. /** 获取当前值 */
  238. getValue: () => unref(modelValue),
  239. /** 获取被包装的组件实例 */
  240. getComponentRef: <T = any,>() => componentRef.value as T,
  241. /** 更新Api参数 */
  242. updateParam(newParams: Record<string, any>) {
  243. innerParams.value = newParams;
  244. },
  245. });
  246. </script>
  247. <template>
  248. <component
  249. :is="component"
  250. v-bind="bindProps"
  251. :placeholder="$attrs.placeholder"
  252. ref="componentRef"
  253. >
  254. <template v-for="item in Object.keys($slots)" #[item]="data">
  255. <slot :name="item" v-bind="data || {}"></slot>
  256. </template>
  257. <template v-if="loadingSlot && loading" #[loadingSlot]>
  258. <LoaderCircle class="animate-spin" />
  259. </template>
  260. </component>
  261. </template>