Browse Source

feat: improve `ApiSelect` component (#5075)

* feat: improve `ApiSelect` component

* chore: `ApiSelect` props name changed
Netfan 3 months ago
parent
commit
d085736bac

+ 18 - 1
apps/web-antd/src/adapter/component/index.ts

@@ -49,6 +49,7 @@ const withDefaultPlaceholder = <T extends Component>(
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
   | 'ApiSelect'
+  | 'ApiTreeSelect'
   | 'AutoComplete'
   | 'Checkbox'
   | 'CheckboxGroup'
@@ -88,7 +89,23 @@ async function initComponentAdapter() {
           component: Select,
           loadingSlot: 'suffixIcon',
           visibleEvent: 'onDropdownVisibleChange',
-          modelField: 'value',
+          modelPropName: 'value',
+        },
+        slots,
+      );
+    },
+    ApiTreeSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiSelect,
+        {
+          ...props,
+          ...attrs,
+          component: TreeSelect,
+          fieldNames: { label: 'label', value: 'value', children: 'children' },
+          loadingSlot: 'suffixIcon',
+          modelPropName: 'value',
+          optionsPropName: 'treeData',
+          visibleEvent: 'onVisibleChange',
         },
         slots,
       );

+ 18 - 1
apps/web-ele/src/adapter/component/index.ts

@@ -48,6 +48,7 @@ const withDefaultPlaceholder = <T extends Component>(
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
   | 'ApiSelect'
+  | 'ApiTreeSelect'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'DatePicker'
@@ -77,7 +78,23 @@ async function initComponentAdapter() {
           ...attrs,
           component: ElSelectV2,
           loadingSlot: 'loading',
-          visibleEvent: 'onDropdownVisibleChange',
+          visibleEvent: 'onVisibleChange',
+        },
+        slots,
+      );
+    },
+    ApiTreeSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiSelect,
+        {
+          ...props,
+          ...attrs,
+          component: ElTreeSelect,
+          props: { label: 'label', children: 'children' },
+          nodeKey: 'value',
+          loadingSlot: 'loading',
+          optionsPropName: 'data',
+          visibleEvent: 'onVisibleChange',
         },
         slots,
       );

+ 39 - 0
apps/web-ele/src/views/demos/form/basic.vue

@@ -6,6 +6,7 @@ import { Page } from '@vben/common-ui';
 import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
 
 import { useVbenForm } from '#/adapter/form';
+import { getAllMenusApi } from '#/api';
 
 const [Form, formApi] = useVbenForm({
   commonConfig: {
@@ -21,6 +22,44 @@ const [Form, formApi] = useVbenForm({
     ElMessage.success(`表单数据:${JSON.stringify(values)}`);
   },
   schema: [
+    {
+      // 组件需要在 #/adapter.ts内注册,并加上类型
+      component: 'ApiSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口转options格式
+        afterFetch: (data: { name: string; path: string }[]) => {
+          return data.map((item: any) => ({
+            label: item.name,
+            value: item.path,
+          }));
+        },
+        // 菜单接口
+        api: getAllMenusApi,
+        placeholder: '请选择',
+      },
+      // 字段名
+      fieldName: 'api',
+      // 界面显示的label
+      label: 'ApiSelect',
+    },
+    {
+      component: 'ApiTreeSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口
+        api: getAllMenusApi,
+        childrenField: 'children',
+        // 菜单接口转options格式
+        labelField: 'name',
+        placeholder: '请选择',
+        valueField: 'path',
+      },
+      // 字段名
+      fieldName: 'apiTree',
+      // 界面显示的label
+      label: 'ApiTreeSelect',
+    },
     {
       component: 'Input',
       fieldName: 'string',

+ 19 - 1
apps/web-naive/src/adapter/component/index.ts

@@ -45,6 +45,7 @@ const withDefaultPlaceholder = <T extends Component>(
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
   | 'ApiSelect'
+  | 'ApiTreeSelect'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'DatePicker'
@@ -74,7 +75,24 @@ async function initComponentAdapter() {
           ...props,
           ...attrs,
           component: NSelect,
-          modelField: 'value',
+          modelPropName: 'value',
+        },
+        slots,
+      );
+    },
+    ApiTreeSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiSelect,
+        {
+          ...props,
+          ...attrs,
+          component: NTreeSelect,
+          nodeKey: 'value',
+          loadingSlot: 'arrow',
+          keyField: 'value',
+          modelPropName: 'value',
+          optionsPropName: 'options',
+          visibleEvent: 'onVisibleChange',
         },
         slots,
       );

+ 39 - 0
apps/web-naive/src/views/demos/form/basic.vue

@@ -4,6 +4,7 @@ import { Page } from '@vben/common-ui';
 import { NButton, NCard, useMessage } from 'naive-ui';
 
 import { useVbenForm } from '#/adapter/form';
+import { getAllMenusApi } from '#/api';
 
 const message = useMessage();
 const [Form, formApi] = useVbenForm({
@@ -20,6 +21,44 @@ const [Form, formApi] = useVbenForm({
     message.success(`表单数据:${JSON.stringify(values)}`);
   },
   schema: [
+    {
+      // 组件需要在 #/adapter.ts内注册,并加上类型
+      component: 'ApiSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口转options格式
+        afterFetch: (data: { name: string; path: string }[]) => {
+          return data.map((item: any) => ({
+            label: item.name,
+            value: item.path,
+          }));
+        },
+        // 菜单接口
+        api: getAllMenusApi,
+        placeholder: '请选择',
+      },
+      // 字段名
+      fieldName: 'api',
+      // 界面显示的label
+      label: 'ApiSelect',
+    },
+    {
+      component: 'ApiTreeSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口
+        api: getAllMenusApi,
+        childrenField: 'children',
+        // 菜单接口转options格式
+        labelField: 'name',
+        placeholder: '请选择',
+        valueField: 'path',
+      },
+      // 字段名
+      fieldName: 'apiTree',
+      // 界面显示的label
+      label: 'ApiTreeSelect',
+    },
     {
       component: 'Input',
       fieldName: 'string',

+ 41 - 18
packages/effects/common-ui/src/components/api-select/api-select.vue

@@ -10,30 +10,47 @@ import { objectOmit } from '@vueuse/core';
 
 type OptionsItem = {
   [name: string]: any;
+  children?: OptionsItem[];
   disabled?: boolean;
   label?: string;
   value?: string;
 };
 
 interface Props {
-  // 组件
+  /** 组件 */
   component: VNode;
+  /** 是否将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;
-  modelField?: string;
+  /** 组件的v-model属性名,默认为modelValue。部分组件可能为value */
+  modelPropName?: string;
 }
 
 defineOptions({ name: 'ApiSelect', inheritAttrs: false });
@@ -41,6 +58,8 @@ defineOptions({ name: 'ApiSelect', inheritAttrs: false });
 const props = withDefaults(defineProps<Props>(), {
   labelField: 'label',
   valueField: 'value',
+  childrenField: '',
+  optionsPropName: 'options',
   resultField: '',
   visibleEvent: '',
   numberToString: false,
@@ -50,7 +69,7 @@ const props = withDefaults(defineProps<Props>(), {
   loadingSlot: '',
   beforeFetch: undefined,
   afterFetch: undefined,
-  modelField: 'modelValue',
+  modelPropName: 'modelValue',
   api: undefined,
   options: () => [],
 });
@@ -69,29 +88,34 @@ const loading = ref(false);
 const isFirstLoaded = ref(false);
 
 const getOptions = computed(() => {
-  const { labelField, valueField, numberToString } = props;
+  const { labelField, valueField, childrenField, 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),
+  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.modelField]: unref(modelValue),
-    [`onUpdate:${props.modelField}`]: (val: string) => {
+    [props.modelPropName]: unref(modelValue),
+    [props.optionsPropName]: unref(getOptions),
+    [`onUpdate:${props.modelPropName}`]: (val: string) => {
       modelValue.value = val;
     },
     ...objectOmit(attrs, ['onUpdate:value']),
@@ -168,7 +192,6 @@ function emitChange() {
     <component
       :is="component"
       v-bind="bindProps"
-      :options="getOptions"
       :placeholder="$attrs.placeholder"
     >
       <template v-for="item in Object.keys($slots)" #[item]="data">

+ 18 - 1
playground/src/adapter/component/index.ts

@@ -49,6 +49,7 @@ const withDefaultPlaceholder = <T extends Component>(
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
   | 'ApiSelect'
+  | 'ApiTreeSelect'
   | 'AutoComplete'
   | 'Checkbox'
   | 'CheckboxGroup'
@@ -88,7 +89,23 @@ async function initComponentAdapter() {
           ...attrs,
           component: Select,
           loadingSlot: 'suffixIcon',
-          modelField: 'value',
+          modelPropName: 'value',
+          visibleEvent: 'onVisibleChange',
+        },
+        slots,
+      );
+    },
+    ApiTreeSelect: (props, { attrs, slots }) => {
+      return h(
+        ApiSelect,
+        {
+          ...props,
+          ...attrs,
+          component: TreeSelect,
+          fieldNames: { label: 'label', value: 'value', children: 'children' },
+          loadingSlot: 'suffixIcon',
+          modelPropName: 'value',
+          optionsPropName: 'treeData',
           visibleEvent: 'onVisibleChange',
         },
         slots,

+ 17 - 0
playground/src/views/examples/form/basic.vue

@@ -62,6 +62,23 @@ const [BaseForm, baseFormApi] = useVbenForm({
       // 界面显示的label
       label: 'ApiSelect',
     },
+    {
+      component: 'ApiTreeSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口
+        api: getAllMenusApi,
+        childrenField: 'children',
+        // 菜单接口转options格式
+        labelField: 'name',
+        placeholder: '请选择',
+        valueField: 'path',
+      },
+      // 字段名
+      fieldName: 'apiTree',
+      // 界面显示的label
+      label: 'ApiTreeSelect',
+    },
     {
       component: 'InputPassword',
       componentProps: {