Skip to content

AdvancedForm

基于FForm封装的表单组件。

  • 关注点分离,可将更多精力投入到业务逻辑梳理而尽量避免关注样式及交互
  • 统一UI交互行为,统一编码风格,便于长期维护,若将来UI变更只需修改此组件,无需侵入各业务
  • 配置化支持,内置常用数据录入组件,以及支持自定义Field

业务场景

  • 符合UI规范的表单场景

如何使用

vue
<script setup lang="ts">
import { ref } from 'vue'
import { AdvancedForm } from '@fs/fui'

const model = ref({})

// 配置
const config = {
  column: 2,
  fields: [
    {
      type: IFieldType.Input,
      label: '用户名',
      name: 'uname',
      required: true,
    },
    {
      type: IFieldType.InputNumber,
      label: '年龄',
      name: 'uage',
    },
  ],
}

// 重置
const handleReset = () => {
  advForm.value?.reset()
}

// 提交
const handleSubmit = () => {
  advForm.value
    ?.validate()
    .then(() => {
      console.log('validate=>success', model.value)
    })
    .catch((e) => {
      console.log('validate=>error', e)
    })
}
</script>

<template>
  <AdvancedForm v-model="model" :config="config" />

  <f-space>
    <f-button @click="handleReset">重置</f-button>
    <f-button type="primary" @click="handleSubmit">提交</f-button>
  </f-space>
</template>

代码演示

基本使用

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { AdvancedFormInstance, IFieldType } from '@fs/lib'

const advForm = ref<AdvancedFormInstance>()

const model = ref({})

// 模拟接口请求
const fetchOptions = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { label: 'AAA', value: 'a' },
        { label: 'BBB', value: 'b' },
        { label: 'CCC', value: 'c' },
      ])
    }, 500)
  })
}

// 配置
const config = {
  column: 2,
  fields: [
    {
      type: IFieldType.Input,
      label: '用户名',
      name: 'uname',
      required: true,
    },
    {
      type: IFieldType.Select,
      label: '性别',
      name: 'usex',
      required: true,
    },
    {
      type: IFieldType.RemoteSelect,
      label: '部门',
      name: 'department',
      required: true,
      props: {
        fetch: fetchOptions,
      },
    },
    {
      type: IFieldType.InputNumber,
      label: '年龄',
      name: 'uage',
      props: {
        min: 1,
        precision: 0,
      },
    },
  ],
}

onMounted(() => {
  advForm.value?.setOptions('usex', [
    { label: '男', value: 1 },
    { label: '女', value: 2 },
  ])
})

// 监听v-model
const onChange = (k: string, v: any, ...rest: any) => {
  console.log(k, v, '=>', ...rest)
  if (k === 'usex') {
    advForm.value?.setFieldProperty('uage', 'disabled', v === 2)
  }
}

const handleReset = () => {
  advForm.value?.reset()
}
const handleSubmit = () => {
  console.log('model=>', model.value)
  advForm.value
    ?.validate()
    .then((res) => {
      console.log('validate=>success', res)
      console.log('validate=>success=>model', model.value)
    })
    .catch((e) => {
      console.log('validate=>error', e)
    })
}
</script>

<template>
  <AdvancedForm ref="advForm" v-model="model" :config="config" @change="onChange" />

  <f-space style="padding: 0 24px">
    <f-button type="primary" @click="handleSubmit">提交</f-button>
    <f-button @click="handleReset">重置</f-button>
  </f-space>
</template>

分组使用

<script setup lang="ts">
import { ref } from 'vue'
import { AdvancedFormInstance, IFieldType } from '@fs/lib'

const advForm = ref<AdvancedFormInstance>()

const model = ref({})

// 配置
const config = {
  column: 2,
  groups: [
    {
      label: '学生信息',
      fields: [
        {
          type: IFieldType.Input,
          label: '用户名',
          name: 'uname',
          required: true,
        },
        {
          type: IFieldType.InputNumber,
          label: '年龄',
          name: 'uage',
        },
      ],
    },
    {
      label: '学校信息',
      fields: [
        {
          type: IFieldType.Input,
          label: '学校名称',
          name: 'school',
          required: true,
        },
        {
          type: IFieldType.DatePicker,
          label: '创办年月',
          name: 'date',
          required: true,
        },
        {
          type: IFieldType.TextArea,
          label: '学校地址',
          name: 'address',
          placeholder: '请输入学校地址',
          span: 24,
          props: {
            maxlength: 100,
            showCount: true,
            autosize: {
              minRows: 2,
              maxRows: 6,
            },
          },
        },
      ],
    },
  ],
}

// 监听v-model
const onChange = (k: string, v: any, ...rest: any) => {
  console.log(k, v, '=>', ...rest)
}

const handleReset = () => {
  advForm.value?.reset()
}
const handleSubmit = () => {
  console.log('model=>', model.value)
  advForm.value
    ?.validate()
    .then((res) => {
      console.log('validate=>success', res)
      console.log('validate=>success=>model', model.value)
    })
    .catch((e) => {
      console.log('validate=>error', e)
    })
}
</script>

<template>
  <AdvancedForm ref="advForm" v-model="model" :config="config" @change="onChange" />

  <f-space style="padding: 0 24px">
    <f-button type="primary" @click="handleSubmit">提交</f-button>
    <f-button @click="handleReset">重置</f-button>
  </f-space>
</template>

自定义Field
常见数据录入组件已经内置,但是必然有一些特殊的field场景(如wms中入库提交模块,dcs中关联文件模块,业务开发时可参考如何扩展自定义Field)

提示

如下两个要点需要在自定义组件中实现,才可与此form组件契合,

  • 实现v-model:value,为何不是v-model?
  • onFieldChange时,需要useInjectFormItemContext(),如Antd示例
<script setup lang="ts">
import { IFieldType, useForm, CusFormItem } from '@fs/lib'

const { advForm, model } = useForm()

// 配置
const config = {
  column: 2,
  fields: [
    {
      type: IFieldType.Input,
      label: '用户名',
      name: 'uname',
      required: true,
    },
    {
      type: IFieldType.InputNumber,
      label: '年龄',
      name: 'uage',
    },
    {
      type: CusFormItem,
      label: '自定义',
      name: 'cus',
      required: true,
      options: [
        { label: 'AAA', value: 'a' },
        { label: 'BBB', value: 'b' },
        { label: 'CCC', value: 'c' },
      ],
    },
  ],
}

// 监听v-model
const onChange = (k: string, v: any, ...rest: any) => {
  console.log(k, v, '=>', ...rest)
}

const handleReset = () => {
  advForm.value?.reset()
}
const handleSubmit = () => {
  console.log('model=>', model.value)
  advForm.value
    ?.validate()
    .then((res) => {
      console.log('validate=>success', res)
      console.log('validate=>success=>model', model.value)
    })
    .catch((e) => {
      console.log('validate=>error', e)
    })
}
</script>

<template>
  <AdvancedForm ref="advForm" v-model="model" :config="config" @change="onChange" />

  <f-space style="padding: 0 24px">
    <f-button type="primary" @click="handleSubmit">提交</f-button>
    <f-button @click="handleReset">重置</f-button>
  </f-space>
</template>

响应式配置

<script setup lang="ts">
import { computed } from 'vue'
import { IFieldType, useForm } from '@fs/lib'

const { advForm, model } = useForm()

// 配置
const config = computed(() => {
  return {
    column: 2,
    fields: [
      {
        type: IFieldType.Select,
        label: '候选人来源',
        name: 'usource',
        required: true,
        options: [
          { label: '社招', value: 1 },
          { label: '校招', value: 2 },
        ],
      },
      {
        type: IFieldType.Input,
        label: '候选人姓名',
        name: 'uname',
        required: true,
      },
      model.value.usource
        ? model.value.usource === 1 // 社招
          ? {
              type: IFieldType.InputNumber,
              label: '社会工龄',
              name: 'uage',
              required: true,
            }
          : {
              type: IFieldType.Input,
              label: '毕业院校',
              name: 'uschool',
              required: true,
            }
        : null,
    ].filter(Boolean),
  }
})

const handleReset = () => {
  advForm.value?.reset()
}
const handleSubmit = () => {
  console.log('model=>', model.value)
  advForm.value
    ?.validate()
    .then((res) => {
      console.log('validate=>success', res)
      console.log('validate=>success=>model', model.value)
    })
    .catch((e) => {
      console.log('validate=>error', e)
    })
}
</script>

<template>
  <AdvancedForm ref="advForm" v-model="model" :config="config" />

  <f-space style="padding: 0 24px">
    <f-button type="primary" @click="handleSubmit">提交</f-button>
    <f-button @click="handleReset">重置</f-button>
  </f-space>
</template>

也可以使用setFieldProperty方法控制

<script setup lang="ts">
import { IFieldType, useForm } from '@fs/lib'

const { advForm, model } = useForm()

// 配置
const config = {
  column: 2,
  fields: [
    {
      type: IFieldType.Input,
      label: '候选人姓名',
      name: 'uname',
      required: true,
    },
    {
      type: IFieldType.Select,
      label: '候选人来源',
      name: 'usource',
      required: true,
      options: [
        { label: '社招', value: 1 },
        { label: '校招', value: 2 },
      ],
    },
    {
      type: IFieldType.InputNumber,
      label: '社会工龄',
      name: 'uage',
    },
    {
      type: IFieldType.Input,
      label: '毕业院校',
      name: 'uschool',
    },
  ],
}

const onChange = (k: string, v: any) => {
  if (k === 'usource') {
    const required = v === 1
    advForm.value?.setFieldProperty('uage', 'required', required)
    advForm.value?.setFieldProperty('uschool', 'required', !required)
  }
}

const handleReset = () => {
  advForm.value?.reset()
}

const handleSubmit = () => {
  console.log('model=>', model.value)
  advForm.value
    ?.validate()
    .then((res) => {
      console.log('validate=>success', res)
      console.log('validate=>success=>model', model.value)
    })
    .catch((e) => {
      console.log('validate=>error', e)
    })
}
</script>

<template>
  <AdvancedForm ref="advForm" v-model="model" :config="config" @change="onChange" />

  <f-space style="padding: 0 24px">
    <f-button type="primary" @click="handleSubmit">提交</f-button>
    <f-button @click="handleReset">重置</f-button>
  </f-space>
</template>

综合示例

<script setup lang="ts">
import { h } from 'vue'
import { FIcon, FTooltip } from '@fs/smart-design'
import { IFieldType, useForm, CusFormItem as Cus } from '@fs/lib'

const { advForm, model } = useForm()

const options = [
  { label: '男', value: 1 },
  { label: '女', value: 2 },
  { label: '未知', value: 0 },
]

// 模拟接口请求
const fetchOptions = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        {
          label: 'AAA',
          value: 'a',
          parcelSizeName: 'ABC',
          parcelCode: 'abc',
        },
        {
          label: 'BBB',
          value: 'b',
          parcelSizeName: 'DEF',
          parcelCode: 'def',
        },
      ])
    }, 500)
  })
}

const treeData = [
  {
    label: '中国',
    value: '1',
    children: [
      {
        label: '湖北',
        value: '1-0',
        children: [
          {
            label: '武汉',
            value: '1-0-0',
          },
          {
            label: '宜昌',
            value: '1-0-1',
          },
        ],
      },
      {
        label: '湖南',
        value: '1-1',
      },
    ],
  },
  {
    label: 'X国',
    value: 'x',
    children: [
      {
        label: 'X湖北',
        value: 'x-0',
        children: [
          {
            label: 'X武汉',
            value: 'x-0-0',
          },
          {
            label: 'X宜昌',
            value: 'x-0-1',
          },
        ],
      },
      {
        label: 'X湖南',
        value: 'x-1',
      },
    ],
  },
]
// 自定义校验
function checkAge(_: any, value: number) {
  if (value >= 18) return Promise.resolve()

  return Promise.reject(new Error('年龄不满18岁!'))
}

const fields = [
  {
    type: IFieldType.Input,
    label: h('span', null, [
      '用户名',
      h(
        FTooltip,
        { title: '提示文字' },
        {
          default: () =>
            h(FIcon, {
              type: 'icon-tishi1',
              style: {
                fontSize: '14px',
                color: '#bbbbbb',
                marginLeft: '4px',
              },
            }),
        },
      ),
    ]),
    name: 'uname',
    rules: [
      {
        type: 'string',
        required: true,
      },
    ],
  },
  {
    type: IFieldType.Empty,
  },
  {
    type: IFieldType.InputNumber,
    label: '年龄',
    desc: '要大于18岁',
    name: 'uage',
    rules: [
      {
        type: 'number',
        required: true,
        validator: checkAge,
      },
    ],
  },
  {
    type: IFieldType.InputNumber,
    label: '班级',
    desc: h('img', {
      src: 'https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png',
    }),
    name: 'glass',
  },
  {
    type: IFieldType.Select,
    label: '下拉组件',
    name: 'usex',
    width: '120px',
    required: true,
    defaultValue: 1,
    options,
  },
  {
    type: IFieldType.RemoteSelect,
    label: '远程下拉',
    name: 'remote',
    width: '120px',
    required: true,
    options: [],
    fetch: fetchOptions,
  },
  {
    type: IFieldType.RemoteSelect,
    label: '整箱材积(mm)',
    name: 'parcelCode',
    required: true,
    props: {
      fetch: fetchOptions,
      fieldNames: { label: 'parcelSizeName', value: 'parcelCode' },
    },
  },
  {
    type: IFieldType.Cascader,
    label: '级联选择',
    name: 'cascader',
    options: treeData,
    required: true,
  },
  {
    type: IFieldType.TreeSelect,
    label: '树形下拉',
    name: 'tree',
    options: treeData,
    required: true,
  },
  {
    type: IFieldType.DatePicker,
    label: '日期选择',
    name: 'date',
    required: true,
  },
  {
    type: IFieldType.TimePicker,
    label: '时间选择',
    name: 'time',
    required: true,
  },
  {
    type: Cus,
    label: '自定义',
    name: 'cus',
    required: true,
    options,
  },
  {
    type: IFieldType.Switch,
    label: '开关',
    name: 'switch',
    required: true,
  },
  {
    type: IFieldType.CheckboxGroup,
    label: '复选框',
    name: 'checkbox',
    required: true,
    options,
  },
  {
    type: IFieldType.RadioGroup,
    label: '单选框',
    name: 'radio',
    required: true,
    options,
  },
  {
    type: IFieldType.TextArea,
    label: '备注',
    name: 'remark',
    span: 24,
    props: {
      autosize: { minRows: 2, maxRows: 6 },
      showCount: true,
      maxlength: 200,
    },
  },
  {
    type: IFieldType.FileInput,
    label: '文件',
    name: 'sn',
    span: 24,
    props: {
      maxSize: 1024,
    },
  },
]

const config = {
  column: 2,
  groups: [
    {
      label: '基础信息A',
      fields: fields.slice(0, 4),
    },
    {
      label: '基础信息B',
      fields: fields.slice(4),
    },
  ],
}

function onChange(k: string, v: any, ...rest: any) {
  console.log(k, v, '=>', ...rest)
}
function onReset(v: any) {
  console.log('onReset=>', v)
}

function handleReset() {
  advForm.value?.reset()
}

function handleSubmit() {
  console.log('model=>', model.value)
  advForm.value
    ?.validate()
    .then((res) => {
      console.log('validate=>success', res)
      console.log('validate=>success=>model', model.value)
    })
    .catch((e) => {
      console.log('validate=>error', e)
    })
}
</script>

<template>
  <Page>
    <AdvancedForm ref="advForm" v-model="model" :config="config" @reset="onReset" @change="onChange" />

    <template #footer>
      <f-space>
        <f-button @click="handleReset">重置</f-button>
        <f-button type="primary" @click="handleSubmit">提交</f-button>
      </f-space>
    </template>
  </Page>
</template>

业务场景举例

局部范围自定义组件

业务系统内全局自定义组件

API

参数说明类型默认值版本
value(v-model)表单内容object--
config表单配置IAdvancedFormConfig--

说明,
config类型约束如下。
其中width适用于SearchForm,常见240px | 120px,可通过此参数指定其他宽度。
关于span适用于AdvancedForm默认根据column自动计算,也可以单独指定,规则见Antd定义的栅格系统

ts
// 内置表单类型
export enum IFieldType {
  Empty = 'Empty', // 占位
  Input = 'FInput', // input 输入
  InputNumber = 'FInputNumber', // input 输入
  Select = 'FSelect', // 下拉选择
  Cascader = 'FCascader', // 级联选择
  TreeSelect = 'FTreeSelect', // 树形下拉选择
  DatePicker = 'FDatePicker', // 日期选择器
  RangePicker = 'FRangePicker', // 日期起始
  TimePicker = 'FTimePicker', // 时间选择

  RemoteSelect = 'RemoteSelect', // 远程搜索下拉

  // 以上支持SearchForm UI比较友好
  TextArea = 'FTextarea', // FTextarea 输入
  Switch = 'FSwitch', // 开关
  // Checkbox+Radio 仅支持group,原因是统一API v-model:value
  CheckboxGroup = 'FCheckboxGroup', // 复选Checkbox
  RadioGroup = 'FRadioGroup', // 单选Radio

  FileInput = 'FileInput', // 文件组件

  UserSelect = 'UserSelect', // 用户选择
  DeptSelect = 'DeptSelect', // 部门选择
  DicSelect = 'DicSelect', // 字典选择
}

export interface IField {
  type?: IFieldType | Component // 内置field | 自定义Component
  label: string | Slot | VNode // 见 Antd Form.Item label 组件
  name: string // 字段key formModel
  defaultValue?: any // 默认值
  placeholder?: string | [string, string] // 默认 下拉类=>请选择,其他类=>请输入
  readonly?: boolean
  disabled?: boolean
  required?: boolean
  rules?: object | [] // 校验规则 https://github.com/yiminghe/async-validator
  options?: Array<{ [key: string]: any }> // 针对下拉类组件
  desc?: string | Slot | VNode // 描述 如文案说明或者图示
  span?: number | [number, number] // 栅格占位数 24栅格系统
  width?: number | string // 散列排版,如SearchForm

  props?: Record<string, any> // 其他扩展属性(透传)
}

export interface IAdvancedFormConfig {
  column?: number // 列占位数
  gutter?: number // field 间距
  fields?: Array<IField> // field项配置
  groups?: Array<IFieldGroup> // 分组field项配置
}

事件

事件名说明回调参数版本
change表单内容变更时回调function(name: string, value: any, ...rest: any)-
reset表单重置时回调function(modelValue: any)-
init表单初始化时回调function(modelValue: any)-
click表单域点击时function(e: Event, name: string)-

说明,
一般可通过change监听表单数据变更,进行后续业务操作,如监听到某field达到一定条件,操作另外的field,(如禁用可通过setFieldProperty方法调用);
其余三个事件使用场景较少,click在处理多个field点击事件时可能有用,一般field数量少可直接通过指定的props透传。

方法

方法名说明参数版本
validate统一校验表单--
reset统一重置表单--
setFieldProperty设置表单属性function(name: string, property: string, value: any)-
setOptions设置表单下拉项function(name: string, options: Array<{ [key: string]: any }>)-
getInstance获取FForm实例--

说明,
validatereset统一走整个表单的验证和重置,如需更细粒度的操作可通过getInstance获取底层依赖实例实现,见AntForm文档
setFieldProperty可用于指定属性变更,setOptions只是一个快捷方法,本质也是调用setFieldProperty

源文档