feat(BasicForm): Improve ts types for BasicForm (#3426)
* fix(ApiCascader): Resolve api type conflict with labelField/valueField
* chore: Improve ts types for BasicForm
* fix(ApiCascader): Resolve API type error
* chore: Resolve type:check error
* chore: fix form type error
* fix(ApiRadioGroup): Resolve api type conflict with labelField/valueField
* fix(ApiTree): api type error
* chore(demo): form basic page schemas use FormSchemaAll
* chore: FormSchemaAll to FormSchema
* fix: ComponentFormSchemaType
* fix: Object literal may only specify known properties
---------
Co-authored-by: invalid w <wangjuesix@gmail.com>
Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>
| | |
| | | } |
| | | |
| | | export const areaRecord = (data: AreaParams) => |
| | | defHttp.post<AreaModel>({ url: Api.AREA_RECORD, data }); |
| | | defHttp.post<AreaModel[]>({ url: Api.AREA_RECORD, data }); |
| | |
| | | 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' }); |
| | |
| | | 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, |
| | |
| | | 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: { |
| | |
| | | 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: '' }, |
| | |
| | | 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 }, |
| | |
| | | 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'; |
| | | |
| | |
| | | [key: string]: any; |
| | | }; |
| | | |
| | | interface BaseFormSchema { |
| | | interface BaseFormSchema<T extends ComponentType = any> { |
| | | // Field name |
| | | field: string; |
| | | // Extra Fields name[] |
| | |
| | | tableAction: TableActionType; |
| | | formActionType: FormActionType; |
| | | formModel: Recordable; |
| | | }) => Recordable) |
| | | | object; |
| | | }) => ComponentProps[T]) |
| | | | ComponentProps[T]; |
| | | // Required |
| | | required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); |
| | | |
| | |
| | | |
| | | 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> & |
| | |
| | | import type { Component, VNodeProps } from 'vue'; |
| | | |
| | | type ColSpanType = number | string; |
| | | export interface ColEx { |
| | | style?: any; |
| | |
| | | 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']; |
| | | } |
| | |
| | | }, |
| | | defaultValue: import.meta.env.MODE || 'development', // 当前环境 |
| | | required: true, |
| | | component: 'Input', |
| | | // component: 'Input', |
| | | slot: 'api', |
| | | }, |
| | | ], |
| | |
| | | }, |
| | | { |
| | | field: '0', |
| | | component: 'Input', |
| | | // component: 'Input', |
| | | label: ' ', |
| | | slot: 'add', |
| | | }, |
| | |
| | | }, |
| | | { |
| | | field: 'field3', |
| | | component: 'Input', |
| | | // component: 'Input', |
| | | label: '自定义Slot', |
| | | slot: 'f3', |
| | | colProps: { |
| | |
| | | componentProps: ({ formModel }) => { |
| | | return { |
| | | placeholder: '同步f2的值为f1', |
| | | onChange: (e: ChangeEvent) => { |
| | | onChange: (e) => { |
| | | formModel.f2 = e.target.value; |
| | | }, |
| | | }; |
| | |
| | | <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'; |
| | |
| | | colProps: { span: 8 }, |
| | | componentProps: { |
| | | getPopupContainer: () => { |
| | | return document.querySelector('.ant-form'); |
| | | return document.querySelector('.ant-form')!; |
| | | }, |
| | | }, |
| | | }, |
| | |
| | | colProps: { span: 8 }, |
| | | componentProps: { |
| | | getPopupContainer: () => { |
| | | return document.querySelector('.ant-form'); |
| | | return document.querySelector('.ant-form')!; |
| | | }, |
| | | }, |
| | | }, |
| | |
| | | componentProps: { |
| | | api: areaRecord, |
| | | apiParamKey: 'parentCode', |
| | | dataField: 'data', |
| | | labelField: 'name', |
| | | valueField: 'code', |
| | | initFetchParams: { |
| | |
| | | componentProps: { |
| | | api: areaRecord, |
| | | apiParamKey: 'parentCode', |
| | | dataField: 'data', |
| | | labelField: 'name', |
| | | valueField: 'code', |
| | | initFetchParams: { |
| | |
| | | colProps: { span: 24 }, |
| | | componentProps: ({ formActionType }) => { |
| | | return { |
| | | onChange: async (val: boolean) => { |
| | | onChange: (val) => { |
| | | formActionType.updateSchema([ |
| | | { field: 'showResetButton', componentProps: { disabled: !val } }, |
| | | { |
| | |
| | | <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'; |
| | |
| | | value: '2', |
| | | }, |
| | | ], |
| | | onChange: (e, v) => { |
| | | console.log('RadioButtonGroup====>:', e, v); |
| | | onChange: (e) => { |
| | | console.log(e); |
| | | }, |
| | | }, |
| | | }, |
| | |
| | | component: 'BasicTitle', |
| | | label: '标题区分', |
| | | componentProps: { |
| | | line: true, |
| | | // line: true, |
| | | span: true, |
| | | }, |
| | | colProps: { |
| | |
| | | componentProps: { |
| | | api: areaRecord, |
| | | apiParamKey: 'parentCode', |
| | | dataField: 'data', |
| | | // dataField: 'data', |
| | | labelField: 'name', |
| | | valueField: 'code', |
| | | initFetchParams: { |
| | |
| | | }, |
| | | { |
| | | field: 'field31', |
| | | component: 'Input', |
| | | // component: 'Input', |
| | | label: '下拉本地搜索', |
| | | helpMessage: ['ApiSelect组件', '远程数据源本地搜索', '只发起一次请求获取所有选项'], |
| | | required: true, |
| | |
| | | span: 8, |
| | | }, |
| | | defaultValue: '0', |
| | | componentProps: { |
| | | onOptionsChange() {}, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'field32', |
| | | component: 'Input', |
| | | // component: 'Input', |
| | | label: '下拉远程搜索', |
| | | helpMessage: ['ApiSelect组件', '将关键词发送到接口进行远程搜索'], |
| | | required: true, |
| | |
| | | // use id as value |
| | | valueField: 'id', |
| | | isBtn: true, |
| | | onChange: (e, v) => { |
| | | console.log('ApiRadioGroup====>:', e, v); |
| | | onChange: (e) => { |
| | | console.log('ApiRadioGroup====>:', e); |
| | | }, |
| | | }, |
| | | colProps: { |
| | |
| | | }, |
| | | { |
| | | field: 'selectA', |
| | | component: 'Select', |
| | | // component: 'Select', |
| | | label: '互斥SelectA', |
| | | slot: 'selectA', |
| | | defaultValue: [], |
| | |
| | | }, |
| | | { |
| | | field: 'selectB', |
| | | component: 'Select', |
| | | // component: 'Select', |
| | | label: '互斥SelectB', |
| | | slot: 'selectB', |
| | | defaultValue: [], |
| | |
| | | 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: '请输入', |
| | | }, |
| | | }, |
| | |
| | | ]; |
| | | } |
| | | export const getAdvanceSchema = (itemNumber = 6): FormSchema[] => { |
| | | const arr: any = []; |
| | | const arr: FormSchema[] = []; |
| | | for (let index = 0; index < itemNumber; index++) { |
| | | arr.push({ |
| | | field: `field${index}`, |
| | |
| | | { |
| | | field: `field11`, |
| | | label: `Slot示例`, |
| | | component: 'Select', |
| | | // component: 'Select', |
| | | slot: 'custom', |
| | | colProps: { |
| | | xl: 12, |