浏览代码

refactor: `ApiComponent` with docs (#5099)

* refactor:  `ApiComponent` with docs

* docs: remove invalid docs

* docs: remove duplicate prop docs

* docs: update `ApiComponent` docs
Netfan 5 月之前
父节点
当前提交
eec6f41f6a

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

@@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
 import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
-import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import {
@@ -82,7 +82,7 @@ async function initComponentAdapter() {
     // import('xxx').then((res) => res.Button),
     ApiSelect: (props, { attrs, slots }) => {
       return h(
-        ApiSelect,
+        ApiComponent,
         {
           placeholder: $t('ui.placeholder.select'),
           ...props,
@@ -97,7 +97,7 @@ async function initComponentAdapter() {
     },
     ApiTreeSelect: (props, { attrs, slots }) => {
       return h(
-        ApiSelect,
+        ApiComponent,
         {
           placeholder: $t('ui.placeholder.select'),
           ...props,

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

@@ -9,7 +9,7 @@ import type { Recordable } from '@vben/types';
 import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
-import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import {
@@ -70,7 +70,7 @@ async function initComponentAdapter() {
     // import('xxx').then((res) => res.Button),
     ApiSelect: (props, { attrs, slots }) => {
       return h(
-        ApiSelect,
+        ApiComponent,
         {
           placeholder: $t('ui.placeholder.select'),
           ...props,
@@ -84,7 +84,7 @@ async function initComponentAdapter() {
     },
     ApiTreeSelect: (props, { attrs, slots }) => {
       return h(
-        ApiSelect,
+        ApiComponent,
         {
           placeholder: $t('ui.placeholder.select'),
           ...props,

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

@@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
 import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
-import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import {
@@ -70,7 +70,7 @@ async function initComponentAdapter() {
 
     ApiSelect: (props, { attrs, slots }) => {
       return h(
-        ApiSelect,
+        ApiComponent,
         {
           placeholder: $t('ui.placeholder.select'),
           ...props,
@@ -83,7 +83,7 @@ async function initComponentAdapter() {
     },
     ApiTreeSelect: (props, { attrs, slots }) => {
       return h(
-        ApiSelect,
+        ApiComponent,
         {
           placeholder: $t('ui.placeholder.select'),
           ...props,

+ 4 - 0
docs/.vitepress/config/zh.mts

@@ -162,6 +162,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
       collapsed: false,
       text: '通用组件',
       items: [
+        {
+          link: 'common-ui/vben-api-component',
+          text: 'ApiComponent Api组件包装器',
+        },
         {
           link: 'common-ui/vben-modal',
           text: 'Modal 模态框',

+ 150 - 0
docs/src/components/common-ui/vben-api-component.md

@@ -0,0 +1,150 @@
+---
+outline: deep
+---
+
+# Vben ApiComponent Api组件包装器
+
+框架提供的API“包装器”,它一般不独立使用,主要用于包装其它组件,为目标组件提供自动获取远程数据的能力,但仍然保持了目标组件的原始用法。
+
+::: info 写在前面
+
+我们在各个应用的组件适配器中,使用ApiComponent包装了Select、TreeSelect组件,使得这些组件可以自动获取远程数据并生成选项。其它类似的组件(比如Cascader)如有需要也可以参考示例代码自行进行包装。
+
+:::
+
+## 基础用法
+
+通过 `component` 传入其它组件的定义,并配置相关的其它属性(主要是一些名称映射)。包装组件将通过`api`获取数据(`beforerFetch`、`afterFetch`将分别在`api`运行前、运行后被调用),使用`resultField`从中提取数组,使用`valueField`、`labelField`等来从数据中提取value和label(如果提供了`childrenField`,会将其作为树形结构递归处理每一级数据),之后将处理好的数据通过`optionsPropName`指定的属性传递给目标组件。
+
+::: details 包装级联选择器,点击下拉时开始加载远程数据
+
+```vue
+<script lang="ts" setup>
+import { ApiComponent } from '@vben/common-ui';
+
+import { Cascader } from 'ant-design-vue';
+
+const treeData: Record<string, any> = [
+  {
+    label: '浙江',
+    value: 'zhejiang',
+    children: [
+      {
+        value: 'hangzhou',
+        label: '杭州',
+        children: [
+          {
+            value: 'xihu',
+            label: '西湖',
+          },
+          {
+            value: 'sudi',
+            label: '苏堤',
+          },
+        ],
+      },
+      {
+        value: 'jiaxing',
+        label: '嘉兴',
+        children: [
+          {
+            value: 'wuzhen',
+            label: '乌镇',
+          },
+          {
+            value: 'meihuazhou',
+            label: '梅花洲',
+          },
+        ],
+      },
+      {
+        value: 'zhoushan',
+        label: '舟山',
+        children: [
+          {
+            value: 'putuoshan',
+            label: '普陀山',
+          },
+          {
+            value: 'taohuadao',
+            label: '桃花岛',
+          },
+        ],
+      },
+    ],
+  },
+  {
+    label: '江苏',
+    value: 'jiangsu',
+    children: [
+      {
+        value: 'nanjing',
+        label: '南京',
+        children: [
+          {
+            value: 'zhonghuamen',
+            label: '中华门',
+          },
+          {
+            value: 'zijinshan',
+            label: '紫金山',
+          },
+          {
+            value: 'yuhuatai',
+            label: '雨花台',
+          },
+        ],
+      },
+    ],
+  },
+];
+/**
+ * 模拟请求接口
+ */
+function fetchApi(): Promise<Record<string, any>> {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(treeData);
+    }, 1000);
+  });
+}
+</script>
+<template>
+  <ApiComponent
+    :api="fetchApi"
+    :component="Cascader"
+    :immediate="false"
+    children-field="children"
+    loading-slot="suffixIcon"
+    visible-event="onDropdownVisibleChange"
+  />
+</template>
+```
+
+:::
+
+### Props
+
+| 属性名 | 描述 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| component | 欲包装的组件 | `Component` | - |
+| numberToString | 是否将value从数字转为string | `boolean` | `false` |
+| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - |
+| params | 传递给api的参数 | `Record<string, any>` | - |
+| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - |
+| labelField | label字段名 | `string` | `label` |
+| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` |
+| valueField | value字段名 | `string` | `value` |
+| optionsPropName | 组件接收options数据的属性名称 | `string` | `options` |
+| modelPropName | 组件的双向绑定属性名,默认为modelValue。部分组件可能为value | `string` | `modelValue` |
+| immediate | 是否立即调用api | `boolean` | `true` |
+| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` |
+| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - |
+| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - |
+| options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - |
+| visibleEvent | 触发重新请求数据的事件名 | `string` | - |
+| loadingSlot | 组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |
+
+```
+
+```

+ 100 - 0
docs/src/demos/vben-api-component/cascader/index.vue

@@ -0,0 +1,100 @@
+<script lang="ts" setup>
+import { ApiComponent } from '@vben/common-ui';
+
+import { Cascader } from 'ant-design-vue';
+
+const treeData: Record<string, any> = [
+  {
+    label: '浙江',
+    value: 'zhejiang',
+    children: [
+      {
+        value: 'hangzhou',
+        label: '杭州',
+        children: [
+          {
+            value: 'xihu',
+            label: '西湖',
+          },
+          {
+            value: 'sudi',
+            label: '苏堤',
+          },
+        ],
+      },
+      {
+        value: 'jiaxing',
+        label: '嘉兴',
+        children: [
+          {
+            value: 'wuzhen',
+            label: '乌镇',
+          },
+          {
+            value: 'meihuazhou',
+            label: '梅花洲',
+          },
+        ],
+      },
+      {
+        value: 'zhoushan',
+        label: '舟山',
+        children: [
+          {
+            value: 'putuoshan',
+            label: '普陀山',
+          },
+          {
+            value: 'taohuadao',
+            label: '桃花岛',
+          },
+        ],
+      },
+    ],
+  },
+  {
+    label: '江苏',
+    value: 'jiangsu',
+    children: [
+      {
+        value: 'nanjing',
+        label: '南京',
+        children: [
+          {
+            value: 'zhonghuamen',
+            label: '中华门',
+          },
+          {
+            value: 'zijinshan',
+            label: '紫金山',
+          },
+          {
+            value: 'yuhuatai',
+            label: '雨花台',
+          },
+        ],
+      },
+    ],
+  },
+];
+/**
+ * 模拟请求接口
+ */
+function fetchApi(): Promise<Record<string, any>> {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(treeData);
+    }, 1000);
+  });
+}
+</script>
+<template>
+  <ApiComponent
+    :api="fetchApi"
+    :component="Cascader"
+    :immediate="false"
+    children-field="children"
+    loading-slot="suffixIcon"
+    visible-event="onDropdownVisibleChange"
+  />
+</template>

+ 3 - 3
packages/effects/common-ui/src/components/api-select/api-select.vue → packages/effects/common-ui/src/components/api-component/api-component.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { AnyPromiseFunction } from '@vben/types';
 
-import { computed, ref, unref, useAttrs, type VNode, watch } from 'vue';
+import { type Component, computed, ref, unref, useAttrs, watch } from 'vue';
 
 import { LoaderCircle } from '@vben/icons';
 import { get, isEqual, isFunction } from '@vben-core/shared/utils';
@@ -18,7 +18,7 @@ type OptionsItem = {
 
 interface Props {
   /** 组件 */
-  component: VNode;
+  component: Component;
   /** 是否将value从数字转为string */
   numberToString?: boolean;
   /** 获取options数据的函数 */
@@ -53,7 +53,7 @@ interface Props {
   modelPropName?: string;
 }
 
-defineOptions({ name: 'ApiSelect', inheritAttrs: false });
+defineOptions({ name: 'ApiComponent', inheritAttrs: false });
 
 const props = withDefaults(defineProps<Props>(), {
   labelField: 'label',

+ 1 - 0
packages/effects/common-ui/src/components/api-component/index.ts

@@ -0,0 +1 @@
+export { default as ApiComponent } from './api-component.vue';

+ 0 - 1
packages/effects/common-ui/src/components/api-select/index.ts

@@ -1 +0,0 @@
-export { default as ApiSelect } from './api-select.vue';

+ 1 - 1
packages/effects/common-ui/src/components/index.ts

@@ -1,4 +1,4 @@
-export * from './api-select';
+export * from './api-component';
 export * from './captcha';
 export * from './ellipsis-text';
 export * from './icon-picker';

+ 3 - 3
playground/src/adapter/component/index.ts

@@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
 import type { Component, SetupContext } from 'vue';
 import { h } from 'vue';
 
-import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import {
@@ -83,7 +83,7 @@ async function initComponentAdapter() {
 
     ApiSelect: (props, { attrs, slots }) => {
       return h(
-        ApiSelect,
+        ApiComponent,
         {
           placeholder: $t('ui.placeholder.select'),
           ...props,
@@ -98,7 +98,7 @@ async function initComponentAdapter() {
     },
     ApiTreeSelect: (props, { attrs, slots }) => {
       return h(
-        ApiSelect,
+        ApiComponent,
         {
           placeholder: $t('ui.placeholder.select'),
           ...props,