Sfoglia il codice sorgente

Merge branch 'v2' of github.com:kailong321200875/vue-element-plus-admin into v2

hongxinzz 2 anni fa
parent
commit
c72ac07de9

+ 302 - 0
src/components/Form/src/Form copy.vue

@@ -0,0 +1,302 @@
+<script lang="tsx">
+import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
+import { ElForm, ElFormItem, ElRow, ElCol, ElTooltip } from 'element-plus'
+import { componentMap } from './componentMap'
+import { propTypes } from '@/utils/propTypes'
+import { getSlot } from '@/utils/tsxHelper'
+import {
+  setTextPlaceholder,
+  setGridProp,
+  setComponentProps,
+  setItemComponentSlots,
+  initModel,
+  setFormItemSlots
+} from './helper'
+import { useRenderSelect } from './components/useRenderSelect'
+import { useRenderRadio } from './components/useRenderRadio'
+import { useRenderCheckbox } from './components/useRenderCheckbox'
+import { useDesign } from '@/hooks/web/useDesign'
+import { findIndex } from '@/utils'
+import { set } from 'lodash-es'
+import { FormProps } from './types'
+import { Icon } from '@/components/Icon'
+import { FormSchema, FormSetPropsType } from '@/types/form'
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('form')
+
+export default defineComponent({
+  name: 'Form',
+  props: {
+    // 生成Form的布局结构数组
+    schema: {
+      type: Array as PropType<FormSchema[]>,
+      default: () => []
+    },
+    // 是否需要栅格布局
+    isCol: propTypes.bool.def(true),
+    // 表单数据对象
+    model: {
+      type: Object as PropType<Recordable>,
+      default: () => ({})
+    },
+    // 是否自动设置placeholder
+    autoSetPlaceholder: propTypes.bool.def(true),
+    // 是否自定义内容
+    isCustom: propTypes.bool.def(false),
+    // 表单label宽度
+    labelWidth: propTypes.oneOfType([String, Number]).def('auto')
+  },
+  emits: ['register'],
+  setup(props, { slots, expose, emit }) {
+    // element form 实例
+    const elFormRef = ref<ComponentRef<typeof ElForm>>()
+
+    // useForm传入的props
+    const outsideProps = ref<FormProps>({})
+
+    const mergeProps = ref<FormProps>({})
+
+    const getProps = computed(() => {
+      const propsObj = { ...props }
+      Object.assign(propsObj, unref(mergeProps))
+      return propsObj
+    })
+
+    // 表单数据
+    const formModel = ref<Recordable>({})
+
+    onMounted(() => {
+      emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
+    })
+
+    // 对表单赋值
+    const setValues = (data: Recordable = {}) => {
+      formModel.value = Object.assign(unref(formModel), data)
+    }
+
+    const setProps = (props: FormProps = {}) => {
+      mergeProps.value = Object.assign(unref(mergeProps), props)
+      outsideProps.value = props
+    }
+
+    const delSchema = (field: string) => {
+      const { schema } = unref(getProps)
+
+      const index = findIndex(schema, (v: FormSchema) => v.field === field)
+      if (index > -1) {
+        schema.splice(index, 1)
+      }
+    }
+
+    const addSchema = (formSchema: FormSchema, index?: number) => {
+      const { schema } = unref(getProps)
+      if (index !== void 0) {
+        schema.splice(index, 0, formSchema)
+        return
+      }
+      schema.push(formSchema)
+    }
+
+    const setSchema = (schemaProps: FormSetPropsType[]) => {
+      const { schema } = unref(getProps)
+      for (const v of schema) {
+        for (const item of schemaProps) {
+          if (v.field === item.field) {
+            set(v, item.path, item.value)
+          }
+        }
+      }
+    }
+
+    const getElFormRef = (): ComponentRef<typeof ElForm> => {
+      return unref(elFormRef) as ComponentRef<typeof ElForm>
+    }
+
+    expose({
+      setValues,
+      formModel,
+      setProps,
+      delSchema,
+      addSchema,
+      setSchema,
+      getElFormRef
+    })
+
+    // 监听表单结构化数组,重新生成formModel
+    watch(
+      () => unref(getProps).schema,
+      (schema = []) => {
+        formModel.value = initModel(schema, unref(formModel))
+      },
+      {
+        immediate: true,
+        deep: true
+      }
+    )
+
+    // 渲染包裹标签,是否使用栅格布局
+    const renderWrap = () => {
+      const { isCol } = unref(getProps)
+      const content = isCol ? (
+        <ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
+      ) : (
+        renderFormItemWrap()
+      )
+      return content
+    }
+
+    // 是否要渲染el-col
+    const renderFormItemWrap = () => {
+      // hidden属性表示隐藏,不做渲染
+      const { schema = [], isCol } = unref(getProps)
+
+      return schema
+        .filter((v) => !v.hidden)
+        .map((item) => {
+          // 如果是 Divider 组件,需要自己占用一行
+          const isDivider = item.component === 'Divider'
+          const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
+          return isDivider ? (
+            <Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
+          ) : isCol ? (
+            // 如果需要栅格,需要包裹 ElCol
+            <ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
+          ) : (
+            renderFormItem(item)
+          )
+        })
+    }
+
+    // 渲染formItem
+    const renderFormItem = (item: FormSchema) => {
+      // 单独给只有options属性的组件做判断
+      const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
+      const componentSlots = (item?.componentProps as any)?.slots || {}
+      const slotsMap: Recordable = {
+        ...setItemComponentSlots(unref(formModel), componentSlots)
+      }
+      if (
+        item?.component !== 'SelectV2' &&
+        item?.component !== 'Cascader' &&
+        item?.componentProps?.options
+      ) {
+        slotsMap.default = () => renderOptions(item)
+      }
+
+      const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
+      // 如果有 labelMessage,自动使用插槽渲染
+      if (item?.labelMessage) {
+        formItemSlots.label = () => {
+          return (
+            <>
+              <span>{item.label}</span>
+              <ElTooltip placement="right" raw-content>
+                {{
+                  content: () => <span v-html={item.labelMessage}></span>,
+                  default: () => (
+                    <Icon
+                      icon="ep:warning"
+                      size={16}
+                      color="var(--el-color-primary)"
+                      class="ml-2px relative top-1px"
+                    ></Icon>
+                  )
+                }}
+              </ElTooltip>
+            </>
+          )
+        }
+      }
+      return (
+        <ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
+          {{
+            ...formItemSlots,
+            default: () => {
+              const Com = componentMap[item.component as string] as ReturnType<
+                typeof defineComponent
+              >
+
+              const { autoSetPlaceholder } = unref(getProps)
+
+              return slots[item.field] ? (
+                getSlot(slots, item.field, formModel.value)
+              ) : (
+                <Com
+                  vModel={formModel.value[item.field]}
+                  {...(autoSetPlaceholder && setTextPlaceholder(item))}
+                  {...setComponentProps(item)}
+                  style={item.componentProps?.style}
+                  {...(notRenderOptions.includes(item?.component as string) &&
+                  item?.componentProps?.options
+                    ? { options: item?.componentProps?.options || [] }
+                    : {})}
+                >
+                  {{ ...slotsMap }}
+                </Com>
+              )
+            }
+          }}
+        </ElFormItem>
+      )
+    }
+
+    // 渲染options
+    const renderOptions = (item: FormSchema) => {
+      switch (item.component) {
+        case 'Select':
+          const { renderSelectOptions } = useRenderSelect(slots)
+          return renderSelectOptions(item)
+        case 'Radio':
+        case 'RadioButton':
+          const { renderRadioOptions } = useRenderRadio()
+          return renderRadioOptions(item)
+        case 'Checkbox':
+        case 'CheckboxButton':
+          const { renderCheckboxOptions } = useRenderCheckbox()
+          return renderCheckboxOptions(item)
+        default:
+          break
+      }
+    }
+
+    // 过滤传入Form组件的属性
+    const getFormBindValue = () => {
+      // 避免在标签上出现多余的属性
+      const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
+      const props = { ...unref(getProps) }
+      for (const key in props) {
+        if (delKeys.indexOf(key) !== -1) {
+          delete props[key]
+        }
+      }
+      return props
+    }
+
+    return () => (
+      <ElForm
+        ref={elFormRef}
+        {...getFormBindValue()}
+        model={props.isCustom ? props.model : formModel}
+        class={prefixCls}
+      >
+        {{
+          // 如果需要自定义,就什么都不渲染,而是提供默认插槽
+          default: () => {
+            const { isCustom } = unref(getProps)
+            return isCustom ? getSlot(slots, 'default') : renderWrap()
+          }
+        }}
+      </ElForm>
+    )
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.@{elNamespace}-form.@{namespace}-form .@{elNamespace}-row {
+  margin-right: 0 !important;
+  margin-left: 0 !important;
+}
+</style>

+ 43 - 37
src/components/Form/src/Form.vue

@@ -21,6 +21,7 @@ import { set } from 'lodash-es'
 import { FormProps } from './types'
 import { Icon } from '@/components/Icon'
 import { FormSchema, FormSetPropsType } from '@/types/form'
+import { ComponentNameEnum } from '@/types/components.d'
 
 const { getPrefixCls } = useDesign()
 
@@ -172,46 +173,55 @@ export default defineComponent({
     // 渲染formItem
     const renderFormItem = (item: FormSchema) => {
       // 单独给只有options属性的组件做判断
-      const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
+      // const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
+      const componentSlots = (item?.componentProps as any)?.slots || {}
       const slotsMap: Recordable = {
-        ...setItemComponentSlots(unref(formModel), item?.componentProps?.slots)
+        ...setItemComponentSlots(unref(formModel), componentSlots)
       }
-      if (
-        item?.component !== 'SelectV2' &&
-        item?.component !== 'Cascader' &&
-        item?.componentProps?.options
-      ) {
-        slotsMap.default = () => renderOptions(item)
+      // 如果是select组件,并且没有自定义模板,自动渲染options
+      if (item.component === ComponentNameEnum.SELECT) {
+        slotsMap.default = !componentSlots.default
+          ? () => renderOptions(item)
+          : (option: any) => {
+              console.log(option)
+              return componentSlots.default(option)
+            }
       }
+      // if (
+      //   item?.component !== 'SelectV2' &&
+      //   item?.component !== 'Cascader' &&
+      //   item?.componentProps?.options
+      // ) {
+      //   slotsMap.default = () => renderOptions(item)
+      // }
 
-      const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
+      // const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
       // 如果有 labelMessage,自动使用插槽渲染
-      if (item?.labelMessage) {
-        formItemSlots.label = () => {
-          return (
-            <>
-              <span>{item.label}</span>
-              <ElTooltip placement="right" raw-content>
-                {{
-                  content: () => <span v-html={item.labelMessage}></span>,
-                  default: () => (
-                    <Icon
-                      icon="ep:warning"
-                      size={16}
-                      color="var(--el-color-primary)"
-                      class="ml-2px relative top-1px"
-                    ></Icon>
-                  )
-                }}
-              </ElTooltip>
-            </>
-          )
-        }
-      }
+      // if (item?.labelMessage) {
+      //   formItemSlots.label = () => {
+      //     return (
+      //       <>
+      //         <span>{item.label}</span>
+      //         <ElTooltip placement="right" raw-content>
+      //           {{
+      //             content: () => <span v-html={item.labelMessage}></span>,
+      //             default: () => (
+      //               <Icon
+      //                 icon="ep:warning"
+      //                 size={16}
+      //                 color="var(--el-color-primary)"
+      //                 class="ml-2px relative top-1px"
+      //               ></Icon>
+      //             )
+      //           }}
+      //         </ElTooltip>
+      //       </>
+      //     )
+      //   }
+      // }
       return (
         <ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
           {{
-            ...formItemSlots,
             default: () => {
               const Com = componentMap[item.component as string] as ReturnType<
                 typeof defineComponent
@@ -227,10 +237,6 @@ export default defineComponent({
                   {...(autoSetPlaceholder && setTextPlaceholder(item))}
                   {...setComponentProps(item)}
                   style={item.componentProps?.style}
-                  {...(notRenderOptions.includes(item?.component as string) &&
-                  item?.componentProps?.options
-                    ? { options: item?.componentProps?.options || [] }
-                    : {})}
                 >
                   {{ ...slotsMap }}
                 </Com>
@@ -244,7 +250,7 @@ export default defineComponent({
     // 渲染options
     const renderOptions = (item: FormSchema) => {
       switch (item.component) {
-        case 'Select':
+        case ComponentNameEnum.SELECT:
           const { renderSelectOptions } = useRenderSelect(slots)
           return renderSelectOptions(item)
         case 'Radio':

+ 9 - 7
src/components/Form/src/components/useRenderSelect.tsx

@@ -2,14 +2,15 @@ import { ElOption, ElOptionGroup } from 'element-plus'
 import { getSlot } from '@/utils/tsxHelper'
 import { Slots } from 'vue'
 import { FormSchema } from '@/types/form'
-import { ComponentOptions } from '@/types/components'
+import { SelectComponentProps, SelectOption } from '@/types/components'
 
 export const useRenderSelect = (slots: Slots) => {
   // 渲染 select options
   const renderSelectOptions = (item: FormSchema) => {
+    const componentsProps = item.componentProps as SelectComponentProps
     // 如果有别名,就取别名
-    const labelAlias = item?.componentProps?.optionsAlias?.labelField
-    return item?.componentProps?.options?.map((option) => {
+    const labelAlias = componentsProps?.labelAlias
+    return componentsProps?.options?.map((option) => {
       if (option?.options?.length) {
         return (
           <ElOptionGroup label={option[labelAlias || 'label']}>
@@ -27,10 +28,11 @@ export const useRenderSelect = (slots: Slots) => {
   }
 
   // 渲染 select option item
-  const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => {
+  const renderSelectOptionItem = (item: FormSchema, option: SelectOption) => {
     // 如果有别名,就取别名
-    const labelAlias = item?.componentProps?.optionsAlias?.labelField
-    const valueAlias = item?.componentProps?.optionsAlias?.valueField
+    const componentsProps = item.componentProps as SelectComponentProps
+    const labelAlias = componentsProps?.labelAlias
+    const valueAlias = componentsProps?.valueAlias
 
     const { label, value, ...other } = option
 
@@ -43,7 +45,7 @@ export const useRenderSelect = (slots: Slots) => {
         {{
           default: () =>
             // option 插槽名规则,{field}-option
-            item?.componentProps?.optionsSlot
+            componentsProps?.optionsSlot
               ? getSlot(slots, `${item.field}-option`, { item: option })
               : undefined
         }}

+ 7 - 5
src/components/Form/src/helper.ts

@@ -1,5 +1,5 @@
 import { useI18n } from '@/hooks/web/useI18n'
-import type { Slots } from 'vue'
+import { unref, type Slots } from 'vue'
 import { getSlot } from '@/utils/tsxHelper'
 import { PlaceholderMoel } from './types'
 import { FormSchema } from '@/types/form'
@@ -74,12 +74,14 @@ export const setGridProp = (col: ColProps = {}): ColProps => {
  */
 export const setComponentProps = (item: FormSchema): Recordable => {
   // const notNeedClearable = ['ColorPicker']
-  const componentProps = {
+  const componentProps: Recordable = {
     clearable: true,
     ...item.componentProps
   }
   // 需要删除额外的属性
-  delete componentProps?.slots
+  if (componentProps.slots) {
+    delete componentProps.slots
+  }
   return componentProps
 }
 
@@ -93,8 +95,8 @@ export const setItemComponentSlots = (formModel: any, slotsProps: Recordable = {
   for (const key in slotsProps) {
     if (slotsProps[key]) {
       if (isFunction(slotsProps[key])) {
-        slotObj[key] = () => {
-          return slotsProps[key]?.(formModel)
+        slotObj[key] = (item: any) => {
+          return slotsProps[key]?.(unref(item?.item) || undefined, formModel)
         }
       } else {
         slotObj[key] = () => {

+ 2 - 0
src/locales/en.ts

@@ -214,7 +214,9 @@ export default {
     default: 'Default',
     icon: 'Icon',
     mixed: 'Mixed',
+    password: 'Password',
     textarea: 'Textarea',
+    remoteSearch: 'Remote search',
     slot: 'Slot',
     position: 'Position',
     autocomplete: 'Autocomplete',

+ 2 - 0
src/locales/zh-CN.ts

@@ -214,7 +214,9 @@ export default {
     default: '默认',
     icon: '图标',
     mixed: '复合型',
+    password: '密码框',
     textarea: '多行文本',
+    remoteSearch: '远程搜索',
     slot: '插槽',
     position: '位置',
     autocomplete: '自动补全',

+ 171 - 16
src/types/components.d.ts

@@ -1,5 +1,5 @@
 import { CSSProperties } from 'vue'
-import { InputProps } from 'element-plus'
+import { InputProps, AutocompleteProps, InputNumberProps } from 'element-plus'
 
 export enum ComponentNameEnum {
   RADIO = 'Radio',
@@ -25,6 +25,16 @@ export enum ComponentNameEnum {
   EDITOR = 'Editor'
 }
 
+type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K
+  ? K extends string
+    ? K extends `${infer A}_${infer B}`
+      ? `${Capitalize<Lowercase<A>>}${Capitalize<Lowercase<B>>}`
+      : Capitalize<Lowercase<K>>
+    : never
+  : never
+
+export type ComponentName = CamelCaseComponentName
+
 export interface InputComponentProps {
   value?: string | number
   maxlength?: number | string
@@ -37,8 +47,8 @@ export interface InputComponentProps {
   showPassword?: boolean
   disabled?: boolean
   size?: InputProps['size']
-  prefixIcon?: string | JSX.Element | (<T>(data: T | any) => string | JSX.Element)
-  suffixIcon?: string | JSX.Element | (<T>(data: T | any) => string | JSX.Element)
+  prefixIcon?: string | JSX.Element | ((item: any, data: any) => string | JSX.Element)
+  suffixIcon?: string | JSX.Element | ((item: any, data: any) => string | JSX.Element)
   type?: InputProps['type']
   rows?: number
   autosize?: boolean | { Pows?: numer; maxRows?: number }
@@ -63,22 +73,167 @@ export interface InputComponentProps {
     input?: (value: string | number) => void
   }
   slots?: {
-    prefix?: JSX.Element | (<T>(data: T | any) => JSX.Element)
-    suffix?: JSX.Element | (<T>(data: T | any) => JSX.Element)
-    prepend?: JSX.Element | (<T>(data: T | any) => JSX.Element)
-    append?: JSX.Element | (<T>(data: T | any) => JSX.Element)
+    prefix?: JSX.Element | ((item: any, data: any) => JSX.Element)
+    suffix?: JSX.Element | ((item: any, data: any) => JSX.Element)
+    prepend?: JSX.Element | ((item: any, data: any) => JSX.Element)
+    append?: JSX.Element | ((item: any, data: any) => JSX.Element)
   }
+  style?: CSSProperties
 }
 
-type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K
-  ? K extends string
-    ? K extends `${infer A}_${infer B}`
-      ? `${Capitalize<Lowercase<A>>}${Capitalize<Lowercase<B>>}`
-      : Capitalize<Lowercase<K>>
-    : never
-  : never
+export interface AutocompleteComponentProps {
+  value?: string
+  placeholder?: string
+  clearable?: boolean
+  disabled?: boolean
+  valueKey?: string
+  debounce?: number
+  placement?: AutocompleteProps['placement']
+  fetchSuggestions?: (queryString: string, callback: (data: string[]) => void) => void
+  triggerOnFocus?: boolean
+  selectWhenUnmatched?: boolean
+  name?: string
+  label?: string
+  hideLoading?: boolean
+  popperClass?: string
+  popperAppendToBody?: boolean
+  teleported?: boolean
+  highlightFirstItem?: boolean
+  fitInputWidth?: boolean
+  on?: {
+    select?: (item: any) => void
+    change?: (value: string | number) => void
+  }
+  slots?: {
+    default?: JSX.Element | ((item: any, data: any) => JSX.Element)
+    prefix?: JSX.Element | ((item: any, data: any) => JSX.Element)
+    suffix?: JSX.Element | ((item: any, data: any) => JSX.Element)
+    prepend?: JSX.Element | ((item: any, data: any) => JSX.Element)
+    append?: JSX.Element | ((item: any, data: any) => JSX.Element)
+  }
+  style?: CSSProperties
+}
 
-export type ComponentName = CamelCaseComponentName
+export interface InputNumberComponentProps {
+  value?: number
+  min?: number
+  max?: number
+  step?: number
+  stepStrictly?: boolean
+  precision?: number
+  size?: InputNumberProps['size']
+  readonly?: boolean
+  disabled?: boolean
+  controls?: boolean
+  controlsPosition?: InputNumberProps['controlsPosition']
+  name?: string
+  label?: string
+  placeholder?: string
+  id?: string
+  valueOnClear?: number | null | 'min' | 'max'
+  validateEvent?: boolean
+  on?: {
+    change?: (currentValue: number | undefined, oldValue: number | undefined) => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+  }
+  style?: CSSProperties
+}
+
+interface SelectOption {
+  label?: string
+  disabled?: boolean
+  value?: any
+  key?: string | number
+  options?: SelectOption[]
+  [key: string]: any
+}
+
+export interface SelectComponentProps {
+  value?: Array | string | number | boolean | Object
+  multiple?: boolean
+  disabled?: boolean
+  valueKey?: string
+  size?: InputNumberProps['size']
+  clearable?: boolean
+  collapseTags?: boolean
+  collapseTagsTooltip?: number
+  multipleLimit?: number
+  name?: string
+  effect?: string
+  autocomplete?: string
+  placeholder?: string
+  filterable?: boolean
+  allowCreate?: boolean
+  filterMethod?: (query: string, item: any) => boolean
+  remote?: boolean
+  remoteMethod?: (query: string) => void
+  remoteShowSuffix?: boolean
+  loading?: boolean
+  loadingText?: string
+  noMatchText?: string
+  noDataText?: string
+  popperClass?: string
+  reserveKeyword?: boolean
+  defaultFirstOption?: boolean
+  popperAppendToBody?: boolean
+  teleported?: boolean
+  persistent?: boolean
+  automaticDropdown?: boolean
+  clearIcon?: string | JSX.Element | ((item: any, data: any) => string | JSX.Element)
+  fitInputWidth?: boolean
+  suffixIcon?: string | JSX.Element | ((item: any, data: any) => string | JSX.Element)
+  tagType?: 'success' | 'info' | 'warning' | 'danger'
+  validateEvent?: boolean
+  placement?:
+    | 'top'
+    | 'top-start'
+    | 'top-end'
+    | 'bottom'
+    | 'bottom-start'
+    | 'bottom-end'
+    | 'left'
+    | 'left-start'
+    | 'left-end'
+    | 'right'
+    | 'right-start'
+    | 'right-end'
+  maxCollapseTags?: number
+  /**
+   * label别名
+   */
+  labelAlias?: string
+
+  /**
+   * value别名
+   */
+  valueAlias?: string
+
+  /**
+   * key别名
+   */
+  keyAlias?: string
+
+  /**
+   * option是否禁用的统一拦截
+   */
+  optionDisabled?: (optin: any, data: any) => boolean
+  on?: {
+    change?: (value: string | number | boolean | Object) => void
+    visibleChange?: (visible: boolean) => void
+    removeTag?: (tag: any) => void
+    clear?: () => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+  }
+  slots?: {
+    default?: (item: any) => JSX.Element
+    prefix?: JSX.Element | ((item: any, data: any) => JSX.Element)
+    empty?: JSX.Element | ((item: any, data: any) => JSX.Element)
+  }
+  options?: SelectOption[]
+  style?: CSSProperties
+}
 
 export interface ColProps {
   span?: number
@@ -92,7 +247,7 @@ export interface ColProps {
 
 export interface ComponentOptions extends Recordable {
   label?: string
-  value?: FormValueType
+  value?: any
   disabled?: boolean
   key?: string | number
   children?: ComponentOptions[]

+ 45 - 15
src/types/form.d.ts

@@ -3,8 +3,10 @@ import {
   ColProps,
   ComponentProps,
   ComponentName,
-  ComponentNameEnum,
-  InputComponentProps
+  InputComponentProps,
+  AutocompleteComponentProps,
+  InputNumberComponentProps,
+  SelectComponentProps
 } from '@/types/components'
 import { FormValueType, FormValueType } from '@/types/form'
 import type { AxiosPromise } from 'axios'
@@ -28,29 +30,57 @@ export type FormItemProps = {
 }
 
 export interface FormSchema {
-  // 唯一值
+  /**
+   * 唯一标识
+   */
   field: string
-  // 标题
+
+  /**
+   * 标题
+   */
   label?: string
-  // 提示
+
+  /**
+   * 提示信息
+   */
   labelMessage?: string
-  // col组件属性
+
+  /**
+   * col组件属性
+   */
   colProps?: ColProps
-  // 表单组件属性,slots对应的是表单组件的插槽,规则:${field}-xxx,具体可以查看element-plus文档
-  // componentProps?: { slots?: Recordable } & ComponentProps
 
   /**
-   * 表单组件属性,slots对应的是表单组件的插槽,规则:${field}-xxx,具体可以查看element-plus文档
+   * 表单组件属性,具体可以查看element-plus文档
+   */
+  componentProps?:
+    | InputComponentProps
+    | AutocompleteComponentProps
+    | InputNumberComponentProps
+    | SelectComponentProps
+
+  /**
+   * formItem组件属性,具体可以查看element-plus文档
    */
-  componentProps?: InputComponentProps
-  // formItem组件属性
   formItemProps?: FormItemProps
-  // 渲染的组件
+
+  /**
+   * 渲染的组件名称
+   */
   component?: ComponentName
-  // 初始值
+
+  /**
+   * 初始值
+   */
   value?: FormValueType
-  // 是否隐藏
+
+  /**
+   * 是否隐藏
+   */
   hidden?: boolean
-  // 远程加载下拉项
+
+  /**
+   * @returns 远程加载下拉项
+   */
   api?: <T = any>() => AxiosPromise<T>
 }

+ 152 - 91
src/views/Components/Form/DefaultForm.vue

@@ -1,4 +1,4 @@
-<script setup lang="ts">
+<script setup lang="tsx">
 import { Form } from '@/components/Form'
 import { reactive, ref, onMounted, computed, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
@@ -25,6 +25,17 @@ const querySearch = (queryString: string, cb: Fn) => {
   // call callback function to return suggestions
   cb(results)
 }
+let timeout: NodeJS.Timeout
+const querySearchAsync = (queryString: string, cb: (arg: any) => void) => {
+  const results = queryString
+    ? restaurants.value.filter(createFilter(queryString))
+    : restaurants.value
+
+  clearTimeout(timeout)
+  timeout = setTimeout(() => {
+    cb(results)
+  }, 3000 * Math.random())
+}
 const createFilter = (queryString: string) => {
   return (restaurant: Recordable) => {
     return restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
@@ -359,7 +370,11 @@ const schema = reactive<FormSchema[]>([
   {
     field: 'field2',
     label: t('formDemo.default'),
-    component: 'Input'
+    component: 'Input',
+    componentProps: {
+      formatter: (value) => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ','),
+      parser: (value) => value.replace(/\$\s?|(,*)/g, '')
+    }
   },
   {
     field: 'field3',
@@ -378,7 +393,7 @@ const schema = reactive<FormSchema[]>([
     component: 'Input',
     componentProps: {
       slots: {
-        suffix: (data: any) => {
+        suffix: (_, data: any) => {
           return unref(toggle) && data.field4
             ? useIcon({ icon: 'ep:calendar' })
             : useIcon({ icon: 'ep:share' })
@@ -394,12 +409,20 @@ const schema = reactive<FormSchema[]>([
     componentProps: {
       slots: {
         prepend: useIcon({ icon: 'ep:calendar' }),
-        append: (data: any) => {
+        append: (_, data: any) => {
           return data.field5 ? useIcon({ icon: 'ep:calendar' }) : useIcon({ icon: 'ep:share' })
         }
       }
     }
   },
+  {
+    field: 'input-field7',
+    label: t('formDemo.password'),
+    component: 'Input',
+    componentProps: {
+      showPassword: true
+    }
+  },
   {
     field: 'field6',
     label: t('formDemo.textarea'),
@@ -408,95 +431,133 @@ const schema = reactive<FormSchema[]>([
       type: 'textarea',
       rows: 2
     }
+  },
+  {
+    field: 'field7',
+    label: t('formDemo.autocomplete'),
+    component: 'Divider'
+  },
+  {
+    field: 'field8',
+    label: t('formDemo.default'),
+    component: 'Autocomplete',
+    componentProps: {
+      fetchSuggestions: querySearch,
+      on: {
+        select: handleSelect
+      }
+    }
+  },
+  {
+    field: 'field9',
+    label: t('formDemo.slot'),
+    component: 'Autocomplete',
+    componentProps: {
+      fetchSuggestions: querySearch,
+      on: {
+        select: handleSelect
+      },
+      slots: {
+        default: (item: any) => {
+          return (
+            <>
+              <div class="value">{item.value}</div>
+              <span class="link">{item.link}</span>
+            </>
+          )
+        }
+      }
+    }
+  },
+  {
+    field: 'autocomplete-field10',
+    label: t('formDemo.remoteSearch'),
+    component: 'Autocomplete',
+    componentProps: {
+      fetchSuggestions: querySearchAsync,
+      on: {
+        select: handleSelect
+      }
+    }
+  },
+  {
+    field: 'field10',
+    component: 'Divider',
+    label: t('formDemo.inputNumber')
+  },
+  {
+    field: 'field11',
+    label: t('formDemo.default'),
+    component: 'InputNumber',
+    value: 0
+  },
+  {
+    field: 'field12',
+    label: t('formDemo.position'),
+    component: 'InputNumber',
+    componentProps: {
+      controlsPosition: 'right'
+    },
+    value: 10
+  },
+  {
+    field: 'field13',
+    label: t('formDemo.select'),
+    component: 'Divider'
+  },
+  {
+    field: 'field14',
+    label: t('formDemo.default'),
+    component: 'Select',
+    componentProps: {
+      optionDisabled: (item: any, data: any) => {
+        console.log(item, data)
+        return false
+      },
+      options: [
+        {
+          disabled: true,
+          label: 'option1',
+          value: '1'
+        },
+        {
+          label: 'option2',
+          value: '2'
+        }
+      ]
+    }
+  },
+  {
+    field: 'field15',
+    label: t('formDemo.slot'),
+    component: 'Select',
+    componentProps: {
+      options: [
+        {
+          label: 'option1',
+          value: '1'
+        },
+        {
+          label: 'option2',
+          value: '2'
+        }
+      ],
+      slots: {
+        default: (item) => {
+          console.log(item)
+          return (
+            <>
+              <span style="float: left">{item.label}</span>
+              <span style=" float: right; color: var(--el-text-color-secondary); font-size: 13px;">
+                {item.value}
+              </span>
+            </>
+          )
+        }
+      }
+    }
   }
   // {
-  //   field: 'field7',
-  //   label: t('formDemo.autocomplete'),
-  //   component: 'Divider'
-  // },
-  // {
-  //   field: 'field8',
-  //   label: t('formDemo.default'),
-  //   component: 'Autocomplete',
-  //   componentProps: {
-  //     fetchSuggestions: querySearch,
-  //     onSelect: handleSelect
-  //   }
-  // },
-  // {
-  //   field: 'field9',
-  //   label: t('formDemo.slot'),
-  //   component: 'Autocomplete',
-  //   componentProps: {
-  //     fetchSuggestions: querySearch,
-  //     onSelect: handleSelect,
-  //     slots: {
-  //       default: true
-  //     }
-  //   }
-  // },
-  // {
-  //   field: 'field10',
-  //   component: 'Divider',
-  //   label: t('formDemo.inputNumber')
-  // },
-  // {
-  //   field: 'field11',
-  //   label: t('formDemo.default'),
-  //   component: 'InputNumber',
-  //   value: 0
-  // },
-  // {
-  //   field: 'field12',
-  //   label: t('formDemo.position'),
-  //   component: 'InputNumber',
-  //   componentProps: {
-  //     controlsPosition: 'right'
-  //   },
-  //   value: 0
-  // },
-  // {
-  //   field: 'field13',
-  //   label: t('formDemo.select'),
-  //   component: 'Divider'
-  // },
-  // {
-  //   field: 'field14',
-  //   label: t('formDemo.default'),
-  //   component: 'Select',
-  //   componentProps: {
-  //     options: [
-  //       {
-  //         disabled: true,
-  //         label: 'option1',
-  //         value: '1'
-  //       },
-  //       {
-  //         label: 'option2',
-  //         value: '2'
-  //       }
-  //     ]
-  //   }
-  // },
-  // {
-  //   field: 'field15',
-  //   label: t('formDemo.slot'),
-  //   component: 'Select',
-  //   componentProps: {
-  //     options: [
-  //       {
-  //         label: 'option1',
-  //         value: '1'
-  //       },
-  //       {
-  //         label: 'option2',
-  //         value: '2'
-  //       }
-  //     ],
-  //     optionsSlot: true
-  //   }
-  // },
-  // {
   //   field: 'field16',
   //   label: t('formDemo.selectGroup'),
   //   component: 'Select',