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