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>
This commit is contained in:
xk
2023-12-21 15:59:32 +08:00
committed by GitHub
parent 65122ea1a5
commit 6bb79180fc
15 changed files with 148 additions and 81 deletions

View File

@@ -6,4 +6,4 @@ enum Api {
}
export const areaRecord = (data: AreaParams) =>
defHttp.post<AreaModel>({ url: Api.AREA_RECORD, data });
defHttp.post<AreaModel[]>({ url: Api.AREA_RECORD, data });

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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: '' },

View File

@@ -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 },

View File

@@ -2,7 +2,7 @@ import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface';
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 @@ export type RenderOpts = {
[key: string]: any;
};
interface BaseFormSchema {
interface BaseFormSchema<T extends ComponentType = any> {
// Field name
field: string;
// Extra Fields name[]
@@ -161,8 +161,8 @@ interface BaseFormSchema {
tableAction: TableActionType;
formActionType: FormActionType;
formModel: Recordable;
}) => Recordable)
| object;
}) => ComponentProps[T])
| ComponentProps[T];
// Required
required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
@@ -224,17 +224,23 @@ interface BaseFormSchema {
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> &

View File

@@ -1,3 +1,5 @@
import type { Component, VNodeProps } from 'vue';
type ColSpanType = number | string;
export interface ColEx {
style?: any;
@@ -80,43 +82,95 @@ export interface ColEx {
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'];
}

View File

@@ -54,7 +54,7 @@
},
defaultValue: import.meta.env.MODE || 'development', // 当前环境
required: true,
component: 'Input',
// component: 'Input',
slot: 'api',
},
],

View File

@@ -35,7 +35,7 @@
},
{
field: '0',
component: 'Input',
// component: 'Input',
label: ' ',
slot: 'add',
},

View File

@@ -80,7 +80,7 @@
},
{
field: 'field3',
component: 'Input',
// component: 'Input',
label: '自定义Slot',
slot: 'f3',
colProps: {

View File

@@ -137,7 +137,7 @@
componentProps: ({ formModel }) => {
return {
placeholder: '同步f2的值为f1',
onChange: (e: ChangeEvent) => {
onChange: (e) => {
formModel.f2 = e.target.value;
},
};

View File

@@ -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 } },
{

View File

@@ -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: [],

View File

@@ -40,8 +40,8 @@ export const schemas: FormSchema[] = [
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: '请输入',
},
},

View File

@@ -230,7 +230,7 @@ export function getMergeHeaderColumns(): BasicColumn[] {
];
}
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 @@ export function getFormConfig(): Partial<FormProps> {
{
field: `field11`,
label: `Slot示例`,
component: 'Select',
// component: 'Select',
slot: 'custom',
colProps: {
xl: 12,