咱們作的是後臺類型的管理系統,所以相對應的表單就會不少。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;
複製代碼