From 6bb79180fc798b1a0b1a6c22f7c13ddb3a45a3b5 Mon Sep 17 00:00:00 2001
From: xk <1175047471@qq.com>
Date: 星期四, 21 十二月 2023 15:59:32 +0800
Subject: [PATCH] feat(BasicForm): Improve ts types for BasicForm (#3426)

---
 src/components/Form/src/components/ApiCascader.vue        |    7 +
 src/components/Form/src/types/index.ts                    |  134 +++++++++++++++++++++++----------
 src/components/Form/src/components/ApiRadioGroup.vue      |    9 +
 src/views/demo/form/DynamicForm.vue                       |    2 
 src/layouts/default/header/components/ChangeApi/index.vue |    2 
 src/components/Form/src/types/form.ts                     |   22 +++--
 src/views/demo/form/index.vue                             |   25 +++--
 src/components/Form/src/components/ApiTree.vue            |    2 
 src/components/Form/src/components/ApiTreeSelect.vue      |    2 
 src/views/demo/form/AppendForm.vue                        |    2 
 src/views/demo/form/UseForm.vue                           |   10 +-
 src/api/demo/cascader.ts                                  |    2 
 src/views/demo/table/tableData.tsx                        |    4 
 src/views/demo/form/CustomerForm.vue                      |    2 
 src/views/demo/page/form/basic/data.ts                    |    4 
 15 files changed, 148 insertions(+), 81 deletions(-)

diff --git a/src/api/demo/cascader.ts b/src/api/demo/cascader.ts
index 65a1f48..198853d 100644
--- a/src/api/demo/cascader.ts
+++ b/src/api/demo/cascader.ts
@@ -6,4 +6,4 @@
 }
 
 export const areaRecord = (data: AreaParams) =>
-  defHttp.post<AreaModel>({ url: Api.AREA_RECORD, data });
+  defHttp.post<AreaModel[]>({ url: Api.AREA_RECORD, data });
diff --git a/src/components/Form/src/components/ApiCascader.vue b/src/components/Form/src/components/ApiCascader.vue
index 1842e5a..04b423c 100644
--- a/src/components/Form/src/components/ApiCascader.vue
+++ b/src/components/Form/src/components/ApiCascader.vue
@@ -31,11 +31,12 @@
   import { useI18n } from '@/hooks/web/useI18n';
 
   interface Option {
-    value: string;
-    label: string;
+    value?: string;
+    label?: string;
     loading?: boolean;
     isLeaf?: boolean;
     children?: Option[];
+    [key: string]: any;
   }
 
   defineOptions({ name: 'ApiCascader' });
@@ -45,7 +46,7 @@
       type: Array,
     },
     api: {
-      type: Function as PropType<(arg?: Recordable<any>) => Promise<Option[]>>,
+      type: Function as PropType<(arg?: any) => Promise<Option[]>>,
       default: null,
     },
     numberToString: propTypes.bool,
diff --git a/src/components/Form/src/components/ApiRadioGroup.vue b/src/components/Form/src/components/ApiRadioGroup.vue
index 2775041..3cdbb8c 100644
--- a/src/components/Form/src/components/ApiRadioGroup.vue
+++ b/src/components/Form/src/components/ApiRadioGroup.vue
@@ -27,13 +27,18 @@
   import { propTypes } from '@/utils/propTypes';
   import { get, omit, isEqual } from 'lodash-es';
 
-  type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
+  type OptionsItem = {
+    label?: string;
+    value?: string | number | boolean;
+    disabled?: boolean;
+    [key: string]: any;
+  };
 
   defineOptions({ name: 'ApiRadioGroup' });
 
   const props = defineProps({
     api: {
-      type: Function as PropType<(arg?: any | string) => Promise<OptionsItem[]>>,
+      type: Function as PropType<(arg?: any) => Promise<OptionsItem[]>>,
       default: null,
     },
     params: {
diff --git a/src/components/Form/src/components/ApiTree.vue b/src/components/Form/src/components/ApiTree.vue
index 5a476b4..d2b9597 100644
--- a/src/components/Form/src/components/ApiTree.vue
+++ b/src/components/Form/src/components/ApiTree.vue
@@ -18,7 +18,7 @@
   defineOptions({ name: 'ApiTree' });
 
   const props = defineProps({
-    api: { type: Function as PropType<(arg?: Recordable<any>) => Promise<Recordable<any>>> },
+    api: { type: Function as PropType<(arg?: any) => Promise<Recordable<any>>> },
     params: { type: Object },
     immediate: { type: Boolean, default: true },
     resultField: { type: String, default: '' },
diff --git a/src/components/Form/src/components/ApiTreeSelect.vue b/src/components/Form/src/components/ApiTreeSelect.vue
index de45477..c426455 100644
--- a/src/components/Form/src/components/ApiTreeSelect.vue
+++ b/src/components/Form/src/components/ApiTreeSelect.vue
@@ -26,7 +26,7 @@
   defineOptions({ name: 'ApiTreeSelect' });
 
   const props = defineProps({
-    api: { type: Function as PropType<(arg?: Recordable<any>) => Promise<Recordable<any>>> },
+    api: { type: Function as PropType<(arg?: any) => Promise<Recordable<any>>> },
     params: { type: Object },
     immediate: { type: Boolean, default: true },
     async: { type: Boolean, default: false },
diff --git a/src/components/Form/src/types/form.ts b/src/components/Form/src/types/form.ts
index 0838de8..7beabe2 100644
--- a/src/components/Form/src/types/form.ts
+++ b/src/components/Form/src/types/form.ts
@@ -2,7 +2,7 @@
 import type { VNode, CSSProperties } from 'vue';
 import type { ButtonProps as AntdButtonProps } from '@/components/Button';
 import type { FormItem } from './formItem';
-import type { ColEx, ComponentType } from './';
+import type { ColEx, ComponentType, ComponentProps } from './';
 import type { TableActionType } from '@/components/Table/src/types/table';
 import type { RowProps } from 'ant-design-vue/lib/grid/Row';
 
@@ -130,7 +130,7 @@
   [key: string]: any;
 };
 
-interface BaseFormSchema {
+interface BaseFormSchema<T extends ComponentType = any> {
   // Field name
   field: string;
   // Extra Fields name[]
@@ -161,8 +161,8 @@
         tableAction: TableActionType;
         formActionType: FormActionType;
         formModel: Recordable;
-      }) => Recordable)
-    | object;
+      }) => ComponentProps[T])
+    | ComponentProps[T];
   // Required
   required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
 
@@ -224,17 +224,23 @@
 
   dynamicRules?: (renderCallbackParams: RenderCallbackParams) => Rule[];
 }
-export interface ComponentFormSchema extends BaseFormSchema {
+export interface ComponentFormSchema<T extends ComponentType = any> extends BaseFormSchema<T> {
   // render component
-  component: ComponentType;
+  component: T;
+  // fix: Object literal may only specify known properties, and 'slot' does not exist in type 'ComponentFormSchema'.
+  slot?: string;
 }
 
 export interface SlotFormSchema extends BaseFormSchema {
-  // Custom slot, in from-item
+  // Custom slot, in form-item
   slot: string;
 }
 
-export type FormSchema = ComponentFormSchema | SlotFormSchema;
+type ComponentFormSchemaType<T extends ComponentType = ComponentType> = T extends any
+  ? ComponentFormSchema<T>
+  : never;
+
+export type FormSchema = ComponentFormSchemaType | SlotFormSchema;
 
 export type FormSchemaInner = Partial<ComponentFormSchema> &
   Partial<SlotFormSchema> &
diff --git a/src/components/Form/src/types/index.ts b/src/components/Form/src/types/index.ts
index 5e19fb5..50d1400 100644
--- a/src/components/Form/src/types/index.ts
+++ b/src/components/Form/src/types/index.ts
@@ -1,3 +1,5 @@
+import type { Component, VNodeProps } from 'vue';
+
 type ColSpanType = number | string;
 export interface ColEx {
   style?: any;
@@ -80,43 +82,95 @@
   xxl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
 }
 
-export type ComponentType =
-  | 'Input'
-  | 'InputGroup'
-  | 'InputPassword'
-  | 'InputSearch'
-  | 'InputTextArea'
-  | 'InputNumber'
-  | 'InputCountDown'
-  | 'Select'
-  | 'ApiSelect'
-  | 'TreeSelect'
-  | 'ApiTree'
-  | 'ApiTreeSelect'
-  | 'ApiRadioGroup'
-  | 'RadioButtonGroup'
-  | 'RadioGroup'
-  | 'Checkbox'
-  | 'CheckboxGroup'
-  | 'AutoComplete'
-  | 'ApiCascader'
-  | 'Cascader'
-  | 'DatePicker'
-  | 'MonthPicker'
-  | 'RangePicker'
-  | 'WeekPicker'
-  | 'TimePicker'
-  | 'TimeRangePicker'
-  | 'Switch'
-  | 'StrengthMeter'
-  | 'Upload'
-  | 'ImageUpload'
-  | 'IconPicker'
-  | 'Render'
-  | 'Slider'
-  | 'Rate'
-  | 'Divider'
-  | 'ApiTransfer'
-  | 'Transfer'
-  | 'CropperAvatar'
-  | 'BasicTitle';
+export type ComponentType = keyof ComponentProps;
+
+type MethodsNameToCamelCase<
+  T extends string,
+  M extends string = '',
+> = T extends `${infer F}-${infer N}${infer Tail}`
+  ? MethodsNameToCamelCase<Tail, `${M}${F}${Uppercase<N>}`>
+  : `${M}${T}`;
+
+type MethodsNameTransform<T> = {
+  [K in keyof T as K extends `on${string}` ? MethodsNameToCamelCase<K> : never]: T[K];
+};
+
+type ExtractPropTypes<T extends Component> = T extends new (...args: any) => any
+  ? Omit<InstanceType<T>['$props'], keyof VNodeProps>
+  : never;
+
+interface _CustomComponents {
+  ApiSelect: ExtractPropTypes<(typeof import('../components/ApiSelect.vue'))['default']>;
+  ApiTree: ExtractPropTypes<(typeof import('../components/ApiTree.vue'))['default']>;
+  ApiTreeSelect: ExtractPropTypes<(typeof import('../components/ApiTreeSelect.vue'))['default']>;
+  ApiRadioGroup: ExtractPropTypes<(typeof import('../components/ApiRadioGroup.vue'))['default']>;
+  RadioButtonGroup: ExtractPropTypes<
+    (typeof import('../components/RadioButtonGroup.vue'))['default']
+  >;
+  ApiCascader: ExtractPropTypes<(typeof import('../components/ApiCascader.vue'))['default']>;
+  StrengthMeter: ExtractPropTypes<
+    (typeof import('@/components/StrengthMeter/src/StrengthMeter.vue'))['default']
+  >;
+  Upload: ExtractPropTypes<(typeof import('@/components/Upload/src/BasicUpload.vue'))['default']>;
+  ImageUpload: ExtractPropTypes<
+    (typeof import('@/components/Upload/src/components/ImageUpload.vue'))['default']
+  >;
+  IconPicker: ExtractPropTypes<(typeof import('@/components/Icon/src/IconPicker.vue'))['default']>;
+  ApiTransfer: ExtractPropTypes<(typeof import('../components/ApiTransfer.vue'))['default']>;
+  CropperAvatar: ExtractPropTypes<
+    (typeof import('@/components/Cropper/src/CropperAvatar.vue'))['default']
+  >;
+  BasicTitle: ExtractPropTypes<(typeof import('@/components/Basic/src/BasicTitle.vue'))['default']>;
+  InputCountDown: ExtractPropTypes<
+    (typeof import('@/components/CountDown/src/CountdownInput.vue'))['default']
+  >;
+}
+
+type CustomComponents<T = _CustomComponents> = {
+  [K in keyof T]: T[K] & MethodsNameTransform<T[K]>;
+};
+
+export interface ComponentProps {
+  Input: ExtractPropTypes<(typeof import('ant-design-vue/es/input'))['default']>;
+  InputGroup: ExtractPropTypes<(typeof import('ant-design-vue/es/input'))['InputGroup']>;
+  InputPassword: ExtractPropTypes<(typeof import('ant-design-vue/es/input'))['InputPassword']>;
+  InputSearch: ExtractPropTypes<(typeof import('ant-design-vue/es/input'))['InputSearch']>;
+  InputTextArea: ExtractPropTypes<(typeof import('ant-design-vue/es/input'))['Textarea']>;
+  InputNumber: ExtractPropTypes<(typeof import('ant-design-vue/es/input-number'))['default']>;
+  InputCountDown: CustomComponents['InputCountDown'] & ComponentProps['Input'];
+  Select: ExtractPropTypes<(typeof import('ant-design-vue/es/select'))['default']>;
+  ApiSelect: CustomComponents['ApiSelect'] & ComponentProps['Select'];
+  TreeSelect: ExtractPropTypes<(typeof import('ant-design-vue/es/tree-select'))['default']>;
+  ApiTree: CustomComponents['ApiTree'] &
+    ExtractPropTypes<(typeof import('ant-design-vue/es/tree'))['default']>;
+  ApiTreeSelect: CustomComponents['ApiTreeSelect'] & ComponentProps['TreeSelect'];
+  ApiRadioGroup: CustomComponents['ApiRadioGroup'] & ComponentProps['RadioGroup'];
+  RadioButtonGroup: CustomComponents['RadioButtonGroup'] & ComponentProps['RadioGroup'];
+  RadioGroup: ExtractPropTypes<(typeof import('ant-design-vue/es/radio'))['RadioGroup']>;
+  Checkbox: ExtractPropTypes<(typeof import('ant-design-vue/es/checkbox'))['default']>;
+  CheckboxGroup: ExtractPropTypes<(typeof import('ant-design-vue/es/checkbox'))['CheckboxGroup']>;
+  AutoComplete: ExtractPropTypes<(typeof import('ant-design-vue/es/auto-complete'))['default']>;
+  ApiCascader: CustomComponents['ApiCascader'] & ComponentProps['Cascader'];
+  Cascader: ExtractPropTypes<(typeof import('ant-design-vue/es/cascader'))['default']>;
+  DatePicker: ExtractPropTypes<(typeof import('ant-design-vue/es/date-picker'))['default']>;
+  MonthPicker: ExtractPropTypes<(typeof import('ant-design-vue/es/date-picker'))['MonthPicker']>;
+  RangePicker: ExtractPropTypes<(typeof import('ant-design-vue/es/date-picker'))['RangePicker']>;
+  WeekPicker: ExtractPropTypes<(typeof import('ant-design-vue/es/date-picker'))['WeekPicker']>;
+  TimePicker: ExtractPropTypes<(typeof import('ant-design-vue/es/time-picker'))['TimePicker']>;
+  TimeRangePicker: ExtractPropTypes<
+    (typeof import('ant-design-vue/es/time-picker'))['TimeRangePicker']
+  >;
+  Switch: ExtractPropTypes<(typeof import('ant-design-vue/es/switch'))['default']>;
+  StrengthMeter: CustomComponents['StrengthMeter'] & ComponentProps['InputPassword'];
+  Upload: CustomComponents['Upload'];
+  ImageUpload: CustomComponents['ImageUpload'];
+  IconPicker: CustomComponents['IconPicker'];
+  Render: Record<string, any>;
+  Slider: ExtractPropTypes<(typeof import('ant-design-vue/es/slider'))['default']>;
+  Rate: ExtractPropTypes<(typeof import('ant-design-vue/es/rate'))['default']>;
+  Divider: ExtractPropTypes<(typeof import('ant-design-vue/es/divider'))['default']>;
+  ApiTransfer: CustomComponents['ApiTransfer'] & ComponentProps['Transfer'];
+  Transfer: ExtractPropTypes<(typeof import('ant-design-vue/es/transfer'))['default']>;
+  CropperAvatar: CustomComponents['CropperAvatar'];
+  BasicTitle: CustomComponents['BasicTitle'];
+}
diff --git a/src/layouts/default/header/components/ChangeApi/index.vue b/src/layouts/default/header/components/ChangeApi/index.vue
index 8b5dfa4..5cbe07d 100644
--- a/src/layouts/default/header/components/ChangeApi/index.vue
+++ b/src/layouts/default/header/components/ChangeApi/index.vue
@@ -54,7 +54,7 @@
         },
         defaultValue: import.meta.env.MODE || 'development', // 褰撳墠鐜
         required: true,
-        component: 'Input',
+        // component: 'Input',
         slot: 'api',
       },
     ],
diff --git a/src/views/demo/form/AppendForm.vue b/src/views/demo/form/AppendForm.vue
index 7b0e47e..a7cd888 100644
--- a/src/views/demo/form/AppendForm.vue
+++ b/src/views/demo/form/AppendForm.vue
@@ -35,7 +35,7 @@
       },
       {
         field: '0',
-        component: 'Input',
+        // component: 'Input',
         label: ' ',
         slot: 'add',
       },
diff --git a/src/views/demo/form/CustomerForm.vue b/src/views/demo/form/CustomerForm.vue
index 797c91e..c7f766b 100644
--- a/src/views/demo/form/CustomerForm.vue
+++ b/src/views/demo/form/CustomerForm.vue
@@ -80,7 +80,7 @@
     },
     {
       field: 'field3',
-      component: 'Input',
+      // component: 'Input',
       label: '鑷畾涔塖lot',
       slot: 'f3',
       colProps: {
diff --git a/src/views/demo/form/DynamicForm.vue b/src/views/demo/form/DynamicForm.vue
index 2e9b952..1f3dbac 100644
--- a/src/views/demo/form/DynamicForm.vue
+++ b/src/views/demo/form/DynamicForm.vue
@@ -137,7 +137,7 @@
       componentProps: ({ formModel }) => {
         return {
           placeholder: '鍚屾f2鐨勫�间负f1',
-          onChange: (e: ChangeEvent) => {
+          onChange: (e) => {
             formModel.f2 = e.target.value;
           },
         };
diff --git a/src/views/demo/form/UseForm.vue b/src/views/demo/form/UseForm.vue
index 8f43426..2a57f35 100644
--- a/src/views/demo/form/UseForm.vue
+++ b/src/views/demo/form/UseForm.vue
@@ -37,7 +37,7 @@
 <script lang="ts" setup>
   import { ref } from 'vue';
   import { Drawer, Space } from 'ant-design-vue';
-  import { BasicForm, FormSchema, useForm, type FormProps } from '@/components/Form';
+  import { BasicForm, type FormSchema, useForm, type FormProps } from '@/components/Form';
   import { CollapseContainer } from '@/components/Container';
   import { PageWrapper } from '@/components/Page';
   import { areaRecord } from '@/api/demo/cascader';
@@ -86,7 +86,7 @@
       colProps: { span: 8 },
       componentProps: {
         getPopupContainer: () => {
-          return document.querySelector('.ant-form');
+          return document.querySelector('.ant-form')!;
         },
       },
     },
@@ -97,7 +97,7 @@
       colProps: { span: 8 },
       componentProps: {
         getPopupContainer: () => {
-          return document.querySelector('.ant-form');
+          return document.querySelector('.ant-form')!;
         },
       },
     },
@@ -147,7 +147,6 @@
       componentProps: {
         api: areaRecord,
         apiParamKey: 'parentCode',
-        dataField: 'data',
         labelField: 'name',
         valueField: 'code',
         initFetchParams: {
@@ -166,7 +165,6 @@
       componentProps: {
         api: areaRecord,
         apiParamKey: 'parentCode',
-        dataField: 'data',
         labelField: 'name',
         valueField: 'code',
         initFetchParams: {
@@ -360,7 +358,7 @@
       colProps: { span: 24 },
       componentProps: ({ formActionType }) => {
         return {
-          onChange: async (val: boolean) => {
+          onChange: (val) => {
             formActionType.updateSchema([
               { field: 'showResetButton', componentProps: { disabled: !val } },
               {
diff --git a/src/views/demo/form/index.vue b/src/views/demo/form/index.vue
index 42bf256..315c8db 100644
--- a/src/views/demo/form/index.vue
+++ b/src/views/demo/form/index.vue
@@ -58,7 +58,7 @@
 <script lang="ts" setup>
   import { type Recordable } from '@vben/types';
   import { computed, unref, ref } from 'vue';
-  import { BasicForm, FormSchema, ApiSelect } from '@/components/Form';
+  import { BasicForm, ApiSelect, FormSchema } from '@/components/Form';
   import { CollapseContainer } from '@/components/Container';
   import { useMessage } from '@/hooks/web/useMessage';
   import { PageWrapper } from '@/components/Page';
@@ -308,8 +308,8 @@
             value: '2',
           },
         ],
-        onChange: (e, v) => {
-          console.log('RadioButtonGroup====>:', e, v);
+        onChange: (e) => {
+          console.log(e);
         },
       },
     },
@@ -362,7 +362,7 @@
       component: 'BasicTitle',
       label: '鏍囬鍖哄垎',
       componentProps: {
-        line: true,
+        // line: true,
         span: true,
       },
       colProps: {
@@ -441,7 +441,7 @@
       componentProps: {
         api: areaRecord,
         apiParamKey: 'parentCode',
-        dataField: 'data',
+        // dataField: 'data',
         labelField: 'name',
         valueField: 'code',
         initFetchParams: {
@@ -457,7 +457,7 @@
     },
     {
       field: 'field31',
-      component: 'Input',
+      // component: 'Input',
       label: '涓嬫媺鏈湴鎼滅储',
       helpMessage: ['ApiSelect缁勪欢', '杩滅▼鏁版嵁婧愭湰鍦版悳绱�', '鍙彂璧蜂竴娆¤姹傝幏鍙栨墍鏈夐�夐」'],
       required: true,
@@ -466,10 +466,13 @@
         span: 8,
       },
       defaultValue: '0',
+      componentProps: {
+        onOptionsChange() {},
+      },
     },
     {
       field: 'field32',
-      component: 'Input',
+      // component: 'Input',
       label: '涓嬫媺杩滅▼鎼滅储',
       helpMessage: ['ApiSelect缁勪欢', '灏嗗叧閿瘝鍙戦�佸埌鎺ュ彛杩涜杩滅▼鎼滅储'],
       required: true,
@@ -578,8 +581,8 @@
         // use id as value
         valueField: 'id',
         isBtn: true,
-        onChange: (e, v) => {
-          console.log('ApiRadioGroup====>:', e, v);
+        onChange: (e) => {
+          console.log('ApiRadioGroup====>:', e);
         },
       },
       colProps: {
@@ -684,7 +687,7 @@
     },
     {
       field: 'selectA',
-      component: 'Select',
+      // component: 'Select',
       label: '浜掓枼SelectA',
       slot: 'selectA',
       defaultValue: [],
@@ -694,7 +697,7 @@
     },
     {
       field: 'selectB',
-      component: 'Select',
+      // component: 'Select',
       label: '浜掓枼SelectB',
       slot: 'selectB',
       defaultValue: [],
diff --git a/src/views/demo/page/form/basic/data.ts b/src/views/demo/page/form/basic/data.ts
index 9548bcd..98dcd2c 100644
--- a/src/views/demo/page/form/basic/data.ts
+++ b/src/views/demo/page/form/basic/data.ts
@@ -40,8 +40,8 @@
     colProps,
     subLabel: '( 閫夊~ )',
     componentProps: {
-      formatter: (value: string) => (value ? `${value}%` : ''),
-      parser: (value: string) => value.replace('%', ''),
+      formatter: (value: string | number) => (value ? `${value}%` : ''),
+      parser: (value: string) => Number(value.replace('%', '')),
       placeholder: '璇疯緭鍏�',
     },
   },
diff --git a/src/views/demo/table/tableData.tsx b/src/views/demo/table/tableData.tsx
index a4eb785..9d81766 100644
--- a/src/views/demo/table/tableData.tsx
+++ b/src/views/demo/table/tableData.tsx
@@ -230,7 +230,7 @@
   ];
 }
 export const getAdvanceSchema = (itemNumber = 6): FormSchema[] => {
-  const arr: any = [];
+  const arr: FormSchema[] = [];
   for (let index = 0; index < itemNumber; index++) {
     arr.push({
       field: `field${index}`,
@@ -252,7 +252,7 @@
       {
         field: `field11`,
         label: `Slot绀轰緥`,
-        component: 'Select',
+        // component: 'Select',
         slot: 'custom',
         colProps: {
           xl: 12,

--
Gitblit v1.8.0