xk
2023-12-21 6bb79180fc798b1a0b1a6c22f7c13ddb3a45a3b5
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>
15个文件已修改
229 ■■■■■ 已修改文件
src/api/demo/cascader.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Form/src/components/ApiCascader.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Form/src/components/ApiRadioGroup.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Form/src/components/ApiTree.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Form/src/components/ApiTreeSelect.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Form/src/types/form.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Form/src/types/index.ts 134 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/components/ChangeApi/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/form/AppendForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/form/CustomerForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/form/DynamicForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/form/UseForm.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/form/index.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/page/form/basic/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/table/tableData.tsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 });
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,
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: {
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: '' },
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 },
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> &
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'];
}
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',
      },
    ],
src/views/demo/form/AppendForm.vue
@@ -35,7 +35,7 @@
      },
      {
        field: '0',
        component: 'Input',
        // component: 'Input',
        label: ' ',
        slot: 'add',
      },
src/views/demo/form/CustomerForm.vue
@@ -80,7 +80,7 @@
    },
    {
      field: 'field3',
      component: 'Input',
      // component: 'Input',
      label: '自定义Slot',
      slot: 'f3',
      colProps: {
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;
          },
        };
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 } },
              {
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: [],
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: '请输入',
    },
  },
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,