咱们作的是后台类型的管理系统,所以相对应的表单就会不少。html
相信作过相似项目的老哥懂得都懂。vue
所以咱们但愿可以经过一些相对简单的配置方式生成表单,再也不须要写一大堆的组件。react
尽可能经过数据驱动。git
无论是哪一个平台,思路都是相通的。github
react咱们基于antd封装。api
vue咱们基于element封装。markdown
这两个框架下的表单,几乎都知足了咱们对表单的须要,只是须要写那么多标签代码,让人感到厌倦。网络
想要简化标签,首先就须要约定数据格式,什么样类型的数据渲染什么样的标签。antd
那么我能够暂定,须要一个type
,去作判断,渲染什么样的表单内容标签(是的,if
判断,没有那么多花里胡哨,最朴实无华的代码就能知足咱们的需求)app
业务中其实经常使用的表单标签就以下几类:
select
checkbox
radio
input
(包括各个类型的,password
,textarea
之类的)switch
等等,须要再加
须要把表单可能用到的属性传递下去。
由于咱们在vue和react上都有,因此我会给出两个框架的封装代码。
我使用的是vue3+element-plus
封装两个组件,Form和FormItem
代码以下:
<!-- Form/index.vue-->
<template>
<el-form :ref="setFormRef" :model="form" label-width="80px">
<el-form-item
v-for="(item, index) in needs"
:key="index"
:prop="item.prop"
:label="item.label"
:rules="item.rules"
>
<!-- 内容 -->
<FormItem
v-model="form[item.prop]"
:type="item.type"
placeholder="请输入内容"
:options="item.options || []"
:disabled="item.disabled"
v-bind="item"
/>
</el-form-item>
</el-form>
</template>
<script>
import { defineComponent, computed, watch } from 'vue';
import FormItem from '../FormItem/index.vue';
export default defineComponent({
components: {
FormItem,
},
props: {
// 须要写的表单内容
needs: {
type: Array,
default: () => [],
},
// 已知的表单内容
modelValue: {
type: Object,
default: () => {},
},
instance: {
type: Object,
default: () => {},
},
},
emits: ['update:modelValue', 'update:instance'],
setup(props, context) {
const form = computed({
get: () => props.modelValue,
set: (val) => {
console.log('变化');
context.emit('update:modelValue', val);
},
});
const setFormRef = (el) => {
context.emit('update:instance', el);
};
// 变化触发更新
watch(form, (newValue) => {
context.emit('update:modelValue', newValue);
});
return { form, setFormRef };
},
});
</script>
复制代码
<!-- FormItem/index.vue-->
<template>
<el-input v-if="type === 'input'" clearable v-model="value" v-bind="$attrs" :class="propsClass" />
<el-input
v-else-if="type === 'password'"
type="password"
clearable
v-model="value"
v-bind="$attrs"
:class="propsClass"
/>
<el-radio-group
v-else-if="type === 'radio'"
v-model="value"
v-bind="$attrs"
:disabled="disabled"
:class="propsClass"
>
<el-radio
v-for="(item, index) in options"
:key="index"
:label="item.value"
:disabled="item.disabled"
>
{{ item.label }}
</el-radio>
</el-radio-group>
<el-checkbox-group
v-else-if="type === 'checkbox'"
v-model="value"
v-bind="$attrs"
:disabled="disabled"
:class="propsClass"
>
<el-checkbox
v-for="(item, index) in options"
:key="index"
:label="item.value"
:disabled="item.disabled"
>
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
<el-input
v-else-if="type === 'textarea'"
type="textarea"
clearable
v-model="value"
v-bind="$attrs"
:class="propsClass"
/>
<el-select
v-else-if="type === 'select'"
clearable
v-model="value"
v-bind="$attrs"
:disabled="disabled"
:class="propsClass"
>
<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:disabled="item.disabled"
:value="item.value"
/>
</el-select>
<el-switch v-else-if="type === 'switch'" v-model="value" v-bind="$attrs" :class="propsClass" />
<el-time-select
v-else-if="type === 'timeSelect'"
v-model="value"
v-bind="$attrs"
:disabled="disabled"
:class="propsClass"
/>
</template>
<script>
import { defineComponent, computed, watchEffect } from 'vue';
export default defineComponent({
name: 'FormItem',
props: {
// 须要绑定的值
modelValue: {
type: [String, Boolean, Number, Array],
default: '',
},
// 传递下来的class
propsClass: {
type: String,
default: '',
},
/**
* 表单的类型 radio 单选 checkbox 多选 input 输入 select 选择 cascader 卡片 switch 切换 timeSelect 时间选择
* @values radio, checkbox, input, select, cascader, switch, timeSelect,
*/
type: {
type: String,
default: '',
require: true,
},
// {value,disabled,source}
options: {
type: Array,
default: () => [{}],
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup(props, context) {
const value = computed({
get: () => props.modelValue,
set: (val) => {
context.emit('update:modelValue', val);
},
});
watchEffect(
() => props.modelValue,
(newValue) => {
value.value = newValue;
},
);
return {
value,
};
},
});
</script>
<style lang="less" scoped>
:deep(.el-*) {
width: 100%;
}
.width100 {
width: 100%;
}
</style>
复制代码
这里要注意的点是v-bind="$attrs"
由于咱们不可能将全部组件可能用到的props
都写在这并导出没,并且也没有这个必要。
因此咱们能够用到vue提供的$attrs来帮助咱们透传下去
好比像这样一个表单
咱们只须要以下代码
Rules规则是咱们单独定义的符合async-validator
的规则,这里就不写引入了
<template>
<Form
v-model:instance="formRef"
v-model="formData"
:needs="needs"
/>
</template>
<script>
import {
defineComponent, reactive, computed, ref
} from 'vue';
export default defineComponent({
setup(){
const formRef = ref();
const options = reactive({
departments: [],
places: [],
roles: [],
});
const formData = reactive({
account: '',
department: [],
name: '',
password: '',
practicePlace: [],
rePassword: '',
roleId: '',
uniqueid: '',
});
const needs = computed(() => [
{
label: '用户名',
type: 'input',
prop: 'name',
propsClass: 'width100',
placeholder: '请输入2-20个汉字,字母或数字',
rules: [
Rules.required('用户名不得为空'),
Rules.dynamicLength(2, 20, '用户名长度为2-20位'),
Rules.cen,
],
},
{
label: '用户帐号',
type: 'input',
prop: 'account',
propsClass: 'width100',
placeholder: '请输入2-20个字母或数字',
rules: [
Rules.required('用户帐号不得为空'),
Rules.dynamicLength(2, 20, '用户帐号长度为2-20位'),
Rules.en,
],
},
{
label: '密码',
type: 'password',
prop: 'password',
propsClass: 'width100',
placeholder: '支持6-20个字母、数字、特殊字符',
rules: [
Rules.required('密码不得为空'),
Rules.dynamicLength(6, 20, '密码长度为6-20位'),
Rules.password,
],
},
{
label: '再输一次',
type: 'password',
prop: 'rePassword',
propsClass: 'width100',
placeholder: '支持6-20个字母、数字、特殊字符',
rules: [
Rules.required('请再输入一次密码'),
Rules.dynamicLength(6, 20, '密码长度为6-20位'),
Rules.password,
Rules.same(formData.password, formData.rePassword, '两次密码输入不一致'),
],
},
{
label: '角色',
type: 'select',
prop: 'roleId',
propsClass: 'width100',
placeholder: '请选择角色',
rules: [Rules.required('角色不得为空')],
options: options.roles,
},
{
label: '执业地点',
type: 'select',
prop: 'practicePlace',
propsClass: 'width100',
placeholder: '请选择执业地点',
multiple: true,
filterable: true,
options: [{ label: '所有', value: 'all' }].concat(options.places),
},
{
label: '科室',
type: 'select',
prop: 'department',
propsClass: 'width100',
placeholder: '请选择科室',
multiple: true,
filterable: true,
options: [{ label: '所有', value: 'all' }].concat(options.departments),
},
]);
// 网络请求获取options,这里就简写了
// *********************
return {
formData,
needs,
formRef,
}
}
})
</script>
复制代码
咱们只须要聚焦数据,就能够构造出一张表单。
也是类似的,并且较之Vue的更加灵活,除了咱们上述的这种经常使用表单,咱们能够把后台管理的搜索项也认为是表单
import React from 'react';
import { ColProps, Form, FormInstance } from 'antd';
import { FormLayout } from 'antd/lib/form/Form';
import FormItem, { IFormItem } from '../FormItem';
interface IForm {
form: FormInstance<any>;
itemLayout?: {
labelCol: ColProps;
wrapperCol: ColProps;
};
layout?: FormLayout;
options: IFormItem[];
initialValues?: { [key: string]: any };
onValuesChange?(changedValues: unknown, allValues: any): void;
}
// 这是个单独的表单校验模板
/* eslint-disable no-template-curly-in-string */
const validateMessages = {
required: '${label}是必填项',
};
/* eslint-enable no-template-curly-in-string */
const FormComponent = (props: IForm): JSX.Element => {
const {
form, onValuesChange, initialValues, options, layout, itemLayout,
} = props;
return (
<Form form={form} {...itemLayout} layout={layout} onValuesChange={onValuesChange} initialValues={initialValues} validateMessages={validateMessages} > {/* 内容 */} {options.map((item) => ( <FormItem key={item.value} {...item} /> ))} </Form>
);
};
FormComponent.defaultProps = {
layout: 'horizontal',
itemLayout: {
labelCol: {},
wrapperCol: {},
},
initialValues: {},
// 此处默认定义为空函数
onValuesChange() {},
};
export default FormComponent;
export type { IFormItem };
复制代码
须要注意的点
form
的引用实例由外部传入formInstance
作,由于和vue不同,react作父子双向绑定比较复杂(也多是我不太熟练的缘故),因此建议是不要作成受控组件
import React from 'react';
import {
Form, Radio, Select, Input, DatePicker, Switch,
} from 'antd';
import { Rule } from 'antd/lib/form';
const { Option } = Select;
const { RangePicker } = DatePicker;
export interface IFormItem {
type: 'input' | 'radio' | 'select' | 'rangePicker' | 'datePicker' | 'switch';
label: string;
// 须要绑定的key值
value: string;
// 可选项
placeholder?: string;
options?: { label: string; value: string | number }[];
otherConfig?: any;
itemConfig? : any;
rules?: Rule[];
itemClass?: string;
}
// Form.Item彷佛也不容许HOC
const FormItemComponent = (props: IFormItem): JSX.Element => {
const {
type, label, value, rules, placeholder, otherConfig, options, itemClass, itemConfig,
} = props;
// 判断类型
return (
<Form.Item label={label} name={value} rules={rules} className={itemClass} {...itemConfig}> {(() => { switch (type) { case 'input': return <Input placeholder={placeholder} {...otherConfig} />; case 'radio': return ( <Radio.Group {...otherConfig}> {options?.map((item) => ( <Radio key={item.value} value={item.value}> {item.label} </Radio> ))} </Radio.Group> ); case 'select': return ( <Select {...otherConfig} placeholder={placeholder}> {options?.map((item) => ( <Option key={item.value} value={item.value}> {item.label} </Option> ))} </Select> ); case 'rangePicker': return <RangePicker {...otherConfig} />; case 'datePicker': return <DatePicker {...otherConfig} />; case 'switch': return <Switch {...otherConfig} />; default: return <div />; } })()} </Form.Item>
);
};
export default FormItemComponent;
复制代码
这里要注意的点
例以下面两个例子
import React, { useEffect, useState } from 'react';
import {
Form
} from 'antd';
import FormComponent, { IFormItem } from '../../components/FormComponent';
const Welcome = (): JSX.Element => {
const [form] = Form.useForm();
const [saleList, setSaleList] = useState<Options[]>([]);
const [firmList, setFirmList] = useState<Options[]>([]);
const options: IFormItem[] = [{
type: 'select',
label: '厂商名称',
value: 'clientId',
options: firmList,
itemClass: 'width25',
otherConfig: {
onChange: () => {
// 选中触发搜索,具体的就不写了
search();
},
},
}, {
type: 'select',
label: '销售人员',
value: 'saleId',
options: saleList,
itemClass: 'width25',
otherConfig: {
onChange: () => {
// 选中触发搜索,具体的就不写了
search();
},
},
}];
useEffect(() => {
// 获取两个列表,具体的就不写了
getFirmList();
getSaleList();
}, []);
return (
<FormComponent form={form} layout="inline" options={options} initialValues={{ clientId: '', saleId: '', }} />
)
};
export default Welcome;
复制代码
import React, { useEffect, useState } from 'react';
import {
Form
} from 'antd';
import FormComponent, { IFormItem } from '../../components/FormComponent';
const UserList = (): JSX.Element => {
const initialValues = {
name: '',
email: '',
account: '',
password: '',
rePassword: '',
roleId: '',
};
const [userForm] = Form.useForm();
const userOptions: IFormItem[] = [{
type: 'input',
label: '名称',
value: 'name',
rules: [
{
required: true,
},
Rules.dynamicLength(2, 20),
Rules.chinese,
],
}, {
type: 'input',
label: '邮箱',
value: 'email',
}, {
type: 'input',
label: '帐号',
value: 'account',
rules: [
{
required: true,
},
Rules.dynamicLength(2, 20),
Rules.cen,
],
}, {
type: 'input',
label: '密码',
value: 'password',
rules: [
{
required: true,
},
Rules.minLength(6),
Rules.englishAndNumber,
],
}, {
type: 'input',
label: '再次确认密码',
value: 'rePassword',
itemConfig: {
dependencies: ['password'],
},
rules: [
{
required: true,
},
Rules.minLength(6),
Rules.englishAndNumber,
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次密码不一致'));
},
}),
],
}, {
type: 'select',
label: '用户角色',
value: 'roleId',
options,
rules: [
{
required: true,
},
],
}];
return (
<FormComponent form={userForm} options={userOptions} itemLayout={{ labelCol: { sm: { span: 5 }, }, wrapperCol: { sm: { span: 18 }, }, }} initialValues={initialValues} />
)
};
export default UserList;
复制代码