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