通过json配置生成form表单,vue3+ts+elementPlus,form表单组件封
无论是后端返回的json数据还是前端自己写表单配置无疑都是非常方便的,让你体会到配置开发的美妙!!!
·
子组件
//src\components\form\index.vue
<script setup lang="ts">
import { PropType, ref, onMounted, watch, nextTick } from 'vue'
import { FormInstance, FormOptions } from './types/types'
import cloneDeep from 'lodash/cloneDeep'
import E from 'wangeditor'
const emits = defineEmits([
'on-preview',
'on-remove',
'on-success',
'on-error',
'on-progress',
'on-change',
'before-upload',
'before-remove',
'on-exceed'
])
const props = defineProps({
// 表单的配置项
formConfigList: {
type: Array as PropType<FormOptions[]>,
required: true
},
// 用户自定义上传方法
httpRequest: {
type: Function
}
})
const model = ref<any>(null)
const rules = ref<any>(null)
const formRef = ref<FormInstance | null>()
const edit = ref()
// 初始化表单
const initForm = () => {
if (props.formConfigList && props.formConfigList.length) {
let m: any = {}
let r: any = {}
props.formConfigList.map((item: FormOptions) => {
m[item.prop!] = item.value
r[item.prop!] = item.rules
if (item.type === 'editor') {
// 初始化富文本
nextTick(() => {
if (document.getElementById('editor')) {
const editor = new E('#editor')
editor.config.placeholder = item.placeholder!
editor.create()
// 初始化富文本的内容
editor.txt.html(item.value)
editor.config.onchange = (newHtml: string) => {
model.value[item.prop!] = newHtml
}
edit.value = editor
}
})
}
})
model.value = cloneDeep(m)
rules.value = cloneDeep(r)
}
}
// 重置表单
const resetFields = () => {
// 重置element-plus的表单
formRef.value!.resetFields()
// 重置富文本编辑器的内容,获取到富文本的配置项
if (props.formConfigList && props.formConfigList.length) {
const editorItem = props.formConfigList.find((item) => item.type === 'editor')!
if (editorItem) edit.value.txt.html(editorItem.value)
}
}
// 表单验证
const validate = () => {
return formRef.value!.validate
}
const getFormData = () => {
return model.value
}
// 分发方法
defineExpose({
resetFields,
validate,
getFormData
})
onMounted(() => {
initForm()
})
// 监听父组件传递进来的options
watch(
() => props.formConfigList,
() => {
initForm()
},
{ deep: true }
)
// 上传组件的所有方法
const onPreview = (file: File) => {
emits('on-preview', file)
}
const onRemove = (file: File, fileList: FileList) => {
emits('on-remove', { file, fileList })
}
const onSuccess = (response: any, file: File, fileList: FileList) => {
// 上传图片成功 给表单上传项赋值
const uploadItem = props.formConfigList.find((item) => item.type === 'upload')!
model.value[uploadItem.prop!] = { response, file, fileList }
emits('on-success', { response, file, fileList })
}
const onError = (err: any, file: File, fileList: FileList) => {
emits('on-error', { err, file, fileList })
}
const onProgress = (event: any, file: File, fileList: FileList) => {
emits('on-progress', { event, file, fileList })
}
const onChange = (file: File, fileList: FileList) => {
emits('on-change', { file, fileList })
}
const beforeUpload = (file: File) => {
emits('before-upload', file)
}
const beforeRemove = (file: File, fileList: FileList) => {
emits('before-remove', { file, fileList })
}
const onExceed = (files: File, fileList: FileList) => {
emits('on-exceed', { files, fileList })
}
</script>
<template>
<el-form
v-if="model"
ref="formRef"
:validate-on-rule-change="false"
:model="model"
:rules="rules"
label-width="auto"
v-bind="$attrs"
>
<template v-for="(item, index) in formConfigList" :key="index">
<!-- 单组件 -->
<el-form-item
v-if="!item.options || !item.options!.length"
:prop="item.prop"
:label="item.label"
>
<div id="editor" v-if="item.type === 'editor'"></div>
<el-upload
v-if="item.type === 'upload'"
v-bind="item.uploadAttrs"
:on-preview="onPreview"
:on-remove="onRemove"
:on-success="onSuccess"
:on-error="onError"
:on-progress="onProgress"
:on-change="onChange"
:before-upload="beforeUpload"
:before-remove="beforeRemove"
:http-request="httpRequest"
:on-exceed="onExceed"
>
<slot name="uploadArea"></slot>
<slot name="uploadTip"></slot>
</el-upload>
<component
v-else
:placeholder="item.placeholder"
v-bind="item.attrs"
:is="`el-${item.type}`"
v-model="model[item.prop!]"
></component>
</el-form-item>
<!-- 多组件 -->
<el-form-item
v-if="item.options && item.options.length"
:prop="item.prop"
:label="item.label"
>
<component
:placeholder="item.placeholder"
v-bind="item.attrs"
:is="`el-${item.type}`"
v-model="model[item.prop!]"
>
<component
v-for="(opt, i) in item.options"
:key="i"
:is="`el-${opt.type}`"
:label="opt.label"
:value="opt.value"
></component>
</component>
</el-form-item>
</template>
<el-form-item>
<slot name="action" :formRef="formRef" :model="model"></slot>
</el-form-item>
</el-form>
</template>
rules类型声明
//src\components\form\types\rule.ts
export type RuleType =
| 'string'
| 'number'
| 'boolean'
| 'method'
| 'regexp'
| 'integer'
| 'float'
| 'array'
| 'object'
| 'enum'
| 'date'
| 'url'
| 'hex'
| 'email'
| 'pattern'
| 'any';
export interface ValidateOption {
// whether to suppress internal warning
suppressWarning?: boolean;
// when the first validation rule generates an error stop processed
first?: boolean;
// when the first validation rule of the specified field generates an error stop the field processed, 'true' means all fields.
firstFields?: boolean | string[];
messages?: Partial<ValidateMessages>;
/** The name of rules need to be trigger. Will validate all rules if leave empty */
keys?: string[];
error?: (rule: InternalRuleItem, message: string) => ValidateError;
}
export type SyncErrorType = Error | string;
export type SyncValidateResult = boolean | SyncErrorType | SyncErrorType[];
export type ValidateResult = void | Promise<void> | SyncValidateResult;
export interface RuleItem {
type?: RuleType; // default type is 'string'
required?: boolean;
pattern?: RegExp | string;
min?: number; // Range of type 'string' and 'array'
max?: number; // Range of type 'string' and 'array'
len?: number; // Length of type 'string' and 'array'
enum?: Array<string | number | boolean | null | undefined>; // possible values of type 'enum'
whitespace?: boolean;
trigger?: string | string[];
fields?: Record<string, Rule>; // ignore when without required
options?: ValidateOption;
defaultField?: Rule; // 'object' or 'array' containing validation rules
transform?: (value: Value) => Value;
message?: string | ((a?: string) => string);
asyncValidator?: (
rule: InternalRuleItem,
value: Value,
callback: (error?: string | Error) => void,
source: Values,
options: ValidateOption,
) => void | Promise<void>;
validator?: (
rule: InternalRuleItem,
value: Value,
callback: (error?: string | Error) => void,
source: Values,
options: ValidateOption,
) => SyncValidateResult | void;
}
export type Rule = RuleItem | RuleItem[];
export type Rules = Record<string, Rule>;
/**
* Rule for validating a value exists in an enumerable list.
*
* @param rule The validation rule.
* @param value The value of the field on the source object.
* @param source The source object being validated.
* @param errors An array of errors that this rule may add
* validation errors to.
* @param options The validation options.
* @param options.messages The validation messages.
* @param type Rule type
*/
export type ExecuteRule = (
rule: InternalRuleItem,
value: Value,
source: Values,
errors: string[],
options: ValidateOption,
type?: string,
) => void;
/**
* Performs validation for any type.
*
* @param rule The validation rule.
* @param value The value of the field on the source object.
* @param callback The callback function.
* @param source The source object being validated.
* @param options The validation options.
* @param options.messages The validation messages.
*/
export type ExecuteValidator = (
rule: InternalRuleItem,
value: Value,
callback: (error?: string[]) => void,
source: Values,
options: ValidateOption,
) => void;
// >>>>> Message
type ValidateMessage<T extends any[] = unknown[]> =
| string
| ((...args: T) => string);
type FullField = string | undefined;
type EnumString = string | undefined;
type Pattern = string | RegExp | undefined;
type Range = number | undefined;
type Type = string | undefined;
export interface ValidateMessages {
default?: ValidateMessage;
required?: ValidateMessage<[FullField]>;
enum?: ValidateMessage<[FullField, EnumString]>;
whitespace?: ValidateMessage<[FullField]>;
date?: {
format?: ValidateMessage;
parse?: ValidateMessage;
invalid?: ValidateMessage;
};
types?: {
string?: ValidateMessage<[FullField, Type]>;
method?: ValidateMessage<[FullField, Type]>;
array?: ValidateMessage<[FullField, Type]>;
object?: ValidateMessage<[FullField, Type]>;
number?: ValidateMessage<[FullField, Type]>;
date?: ValidateMessage<[FullField, Type]>;
boolean?: ValidateMessage<[FullField, Type]>;
integer?: ValidateMessage<[FullField, Type]>;
float?: ValidateMessage<[FullField, Type]>;
regexp?: ValidateMessage<[FullField, Type]>;
email?: ValidateMessage<[FullField, Type]>;
url?: ValidateMessage<[FullField, Type]>;
hex?: ValidateMessage<[FullField, Type]>;
};
string?: {
len?: ValidateMessage<[FullField, Range]>;
min?: ValidateMessage<[FullField, Range]>;
max?: ValidateMessage<[FullField, Range]>;
range?: ValidateMessage<[FullField, Range, Range]>;
};
number?: {
len?: ValidateMessage<[FullField, Range]>;
min?: ValidateMessage<[FullField, Range]>;
max?: ValidateMessage<[FullField, Range]>;
range?: ValidateMessage<[FullField, Range, Range]>;
};
array?: {
len?: ValidateMessage<[FullField, Range]>;
min?: ValidateMessage<[FullField, Range]>;
max?: ValidateMessage<[FullField, Range]>;
range?: ValidateMessage<[FullField, Range, Range]>;
};
pattern?: {
mismatch?: ValidateMessage<[FullField, Value, Pattern]>;
};
}
export interface InternalValidateMessages extends ValidateMessages {
clone: () => InternalValidateMessages;
}
// >>>>> Values
export type Value = any;
export type Values = Record<string, Value>;
// >>>>> Validate
export interface ValidateError {
message?: string;
fieldValue?: Value;
field?: string;
}
export type ValidateFieldsError = Record<string, ValidateError[]>;
export type ValidateCallback = (
errors: ValidateError[] | null,
fields: ValidateFieldsError | Values,
) => void;
export interface RuleValuePackage {
rule: InternalRuleItem;
value: Value;
source: Values;
field: string;
}
export interface InternalRuleItem extends Omit<RuleItem, 'validator'> {
field?: string;
fullField?: string;
fullFields?: string[];
validator?: RuleItem['validator'] | ExecuteValidator;
}
form组件类型声明
//src\components\form\types\types.ts
// 可配置的表单
import { CSSProperties } from 'vue'
import { RuleItem } from "./rule"
import { ValidateFieldsError } from 'async-validator'
interface Callback {
(isValid?: boolean, invalidFields?: ValidateFieldsError): void,
}
// 表单每一项的配置选项
export interface FormOptions {
// 表单项显示的元素
type: 'cascader' | 'checkbox' | 'checkbox-group' | 'checkbox-button' | 'color-picker' |
'date-picker' | 'input' | 'input-number' | 'radio' | 'radio-group' | 'radio-button' | 'rate' |
'select' | 'option' | 'slider' | 'switch' | 'time-picker' | 'time-select' |
'transfer' | 'upload' | 'editor',
// 表单项的值
value?: any,
// 表单项label
label?: string,
// 表单项的标识
prop?: string,
// 表单项的验证规则
rules?: RuleItem[],
// 表单项的占位符
placeholder?: string,
// 表单元素特有的属性
attrs?: {
// css样式
style?: CSSProperties,
clearable?: boolean,
showPassword?: boolean,
disabled?: boolean,
},
// 表单项的子元素
options?: FormOptions[],
// 处理上传组件的属性和方法
uploadAttrs?: {
action: string,
headers?: object,
method?: 'post' | 'put' | 'patch',
multiple?: boolean,
data?: any,
name?: string,
withCredentials?: boolean,
showFileList?: boolean,
drag?: boolean,
accept?: string,
thumbnailMode?: boolean,
fileList?: any[],
listType?: 'text' | 'picture' | 'picture-card',
autoUpload?: boolean,
disabled?: boolean,
limit?: number,
}
}
export interface ValidateFieldCallback {
(message?: string, invalidFields?: ValidateFieldsError): void,
}
export interface FormInstance {
registerLabelWidth(width: number, oldWidth: number): void,
deregisterLabelWidth(width: number): void,
autoLabelWidth: string | undefined,
emit: (evt: string, ...args: any[]) => void,
labelSuffix: string,
inline?: boolean,
model?: Record<string, unknown>,
size?: string,
showMessage?: boolean,
labelPosition?: string,
labelWidth?: string,
rules?: Record<string, unknown>,
statusIcon?: boolean,
hideRequiredAsterisk?: boolean,
disabled?: boolean,
validate: (callback?: Callback) => Promise<boolean>,
resetFields: () => void,
clearValidate: (props?: string | string[]) => void,
validateField: (props: string | string[], cb: ValidateFieldCallback) => void,
}
父组件
// src\views\home.vue
<template>
<div class="homeMain">
<Form
ref="formRef"
:formConfigList="options"
label-width="auto"
@on-change="handleChange"
@before-upload="handleBeforeUpload"
@on-preview="handlePreview"
@on-remove="handleRemove"
@before-remove="beforeRemove"
@on-success="handleSuccess"
@on-exceed="handleExceed"
>
<template #uploadArea>
<el-button size="small" type="primary">Click to upload</el-button>
</template>
<template #uploadTip>
<div style="color: #ccc; font-size: 12px; margin-left: 10px">
jpg/png files with a size less than 500kb
</div>
</template>
<template #action="scope">
<el-button type="primary" @click="submitForm(scope)">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</template>
</Form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Form from '@/components/form/index.vue'
import { FormOptions, FormInstance } from '@/components/form/types/types'
import { ElMessage, ElMessageBox } from 'element-plus'
interface Scope {
formRef: FormInstance | null | undefined
model: any
}
const options: FormOptions[] = [
{
type: 'input',
value: '',
label: '用户名',
prop: 'username',
placeholder: '请输入用户名',
rules: [
{
required: true,
message: '用户名不能为空',
trigger: 'blur'
},
{
min: 2,
max: 6,
message: '用户名在2-6位之间',
trigger: 'blur'
}
],
attrs: {
clearable: true
}
},
{
type: 'input',
value: '',
label: '密码',
prop: 'password',
placeholder: '请输入密码',
rules: [
{
required: true,
message: '密码不能为空',
trigger: 'blur'
},
{
min: 6,
max: 15,
message: '密码在6-15位之间',
trigger: 'blur'
}
],
attrs: {
showPassword: true,
clearable: true
}
},
{
type: 'select',
value: '',
placeholder: '请选择职位',
prop: 'role',
label: '职位',
attrs: {
style: {
width: '100%'
}
},
rules: [
{
required: true,
message: '职位不能为空',
trigger: 'change'
}
],
options: [
{
type: 'option',
label: '经理',
value: '1'
},
{
type: 'option',
label: '主管',
value: '2'
},
{
type: 'option',
label: '员工',
value: '3'
}
]
},
{
type: 'checkbox-group',
value: [],
prop: 'like',
label: '爱好',
rules: [
{
required: true,
message: '爱好不能为空',
trigger: 'change'
}
],
options: [
{
type: 'checkbox',
label: '足球',
value: '1'
},
{
type: 'checkbox',
label: '篮球',
value: '2'
},
{
type: 'checkbox',
label: '排球',
value: '3'
}
]
},
{
type: 'radio-group',
value: '',
prop: 'gender',
label: '性别',
rules: [
{
required: true,
message: '性别不能为空',
trigger: 'change'
}
],
options: [
{
type: 'radio',
label: '男',
value: 'male'
},
{
type: 'radio',
label: '女',
value: 'female'
},
{
type: 'radio',
label: '保密',
value: 'not'
}
]
},
{
type: 'upload',
label: '上传',
prop: 'pic',
uploadAttrs: {
action: 'https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15',
multiple: true,
limit: 3
},
rules: [
{
required: true,
message: '图片不能为空',
trigger: 'blur'
}
]
},
{
type: 'editor',
value: '',
prop: 'desc',
label: '描述',
placeholder: '请输入描述',
rules: [
{
required: true,
message: '描述不能为空',
trigger: 'blur'
}
]
}
]
const formRef = ref()
const submitForm = (scope: Scope) => {
scope.formRef!.validate((valid: any) => {
if (valid) {
console.log(scope.model)
} else {
console.log(1212)
}
})
}
// 重置表单
const resetForm = () => {
formRef.value.resetFields()
}
// 上传
const handleRemove = (file: any, fileList: any) => {
console.log('handleRemove')
console.log(file, fileList)
}
const handlePreview = (file: any) => {
console.log('handlePreview')
console.log(file)
}
const beforeRemove = (val: any) => {
console.log('beforeRemove')
return ElMessageBox.confirm(`Cancel the transfert of ${val.file.name} ?`)
}
const handleExceed = (val: any) => {
console.log('handleExceed', val)
ElMessage.warning(
`The limit is 3, you selected ${val.files.length} files this time, add up to ${
val.files.length + val.fileList.length
} totally`
)
}
const handleSuccess = (val: any) => {
console.log('success')
console.log(val)
}
const handleChange = (val: any) => {
console.log('change')
console.log(val)
}
const handleBeforeUpload = (val: any) => {
console.log('handleBeforeUpload')
console.log(val)
}
</script>
<style scoped>
.homeMain {
padding: 50px 500px;
}
</style>
**记得安装wangeditor,lodash插件**
更多推荐
已为社区贡献2条内容
所有评论(0)