HTML 一行代码,可实现表单输入框/日期选择/下拉选择/复选框选中等及规则校验功能
<t-form
v-model="formOpts.ref"
:formOpts="formOpts"
:widthSize="2"
@handleEvent="handleEvent"
/>
//注意formOpts.ref(t-form组件实例相当于vue2 ref)需要要v-model接收
参数 | 说明 | 类型 | 是否必须 |
---|---|---|---|
className | 自定义类名 | String | 否 |
labelPosition | 改变表单项 label 与输入框的布局方式(默认:right) /top | String | 否 |
widthSize | 每行显示几个输入项(默认两项) 最大值 4 | Number | 否 |
isTrim | 全局是否开启清除前后空格(comp 为 el-input 且 type 不等于’password’) | Boolean | true |
formOpts | 表单配置项 | Object | 否 |
—listTypeInfo | 下拉选择数据源(type:'select’有效) | Object | 否 |
—fieldList | form 表单每项 list | Array | 是 |
------slotName | 自定义表单某一项输入框 | slot | 否 |
------childSlotName | 自定义表单某一下拉选择项子组件插槽(el-option) | slot | 否 |
------comp | form 表单每一项组件是输入框还是下拉选择等(可使用第三方 UI 如 el-select/el-input 也可以使用自定义组件) | String | 是 |
------bind | 表单每一项属性(继承第三方 UI 的 Attributes,如 el-input 中的 clearable 清空功能)默认清空及下拉过滤 | Object/function | 否 |
------type | form 表单每一项类型 | String | 是 |
------isTrim | 是否不清除前后空格(comp 为 el-input 且 type 不等于’password’) | Boolean | false |
------eventHandle | 继承 comp 组件的事件 | Object | - |
------widthSize | form 表单某一项所占比例(如果占一整行则设置 1) | Number | 否 |
------width | form 表单某一项所占实际宽度 | String | 否 |
------arrLabel | type=select-arr 时,每个下拉显示的中文 | String | 否 |
------arrKey | type=select-arr 时,每个下拉显示的中文传后台的数字 | String | 否 |
------label | form 表单每一项 title | String | 是 |
------labelRender | 自定义某一项 title | function | 是 |
------value | form 表单每一项传给后台的参数 | String | 是 |
------rules | 每一项输入框的表单校验规则 | Object/Array | 否 |
------list | 下拉选择数据源(仅仅对 type:'select’有效) | String | 否 |
------event | 表单每一项事件标志(handleEvent 事件) | String | 否 |
—formData | 表单提交数据(对应 fieldList 每一项的 value 值) | Object | 是 |
—labelWidth | label 宽度 | String | 否 |
—rules | 规则(可依据 elementUI el-form 配置————对应 formData 的值) | Object/Array | 否 |
—operatorList | 操作按钮 list | Array | 否 |
事件名 | 说明 | 返回值 |
---|---|---|
handleEvent | 单个查询条件触发事件 | fieldList 中 type/查询条件输入的值/fieldList 中 event 值 |
事件名 | 说明 | 返回值 |
---|---|---|
validate | 校验表单数据 | valid:boolean; formData:最终表单数据 |
resetFields | 重置表单 | - |
clearValidate | 清空校验 | - |
<template>
<el-form class="t-form" ref="tform" :class="className" :model="formOpts.formData" :rules="formOpts.rules"
:label-width="formOpts.labelWidth || '100px'" :label-position="formOpts.labelPosition || 'right'" v-bind="$attrs">
<template v-for="(item, index) in formOpts.fieldList">
<el-form-item v-if="!item.isHideItem" :key="index" :prop="item.value" :label="item.label" :class="[
item.className,
{ render_label: item.labelRender },
{ slot_label: item.slotName },
{ render_laber_position_left: formOpts.labelPosition === 'left' },
]" :rules="item.rules" :style="getChildWidth(item)" v-bind="$attrs">
<template #label v-if="item.labelRender">
<render-comp :render="item.labelRender" :item="item" />
template>
<template v-if="item.slotName">
<slot :name="item.slotName">slot>
template>
<template v-if="item.textShow">
<span class="text_show">{{
item.textValue || formOpts.formData[item.value]
}}span>
template>
<component v-if="!item.slotName && !item.textShow && item.comp.includes('date')" :is="item.comp"
v-model="formOpts.formData[item.value]" :type="item.type"
:placeholder="item.placeholder || getPlaceholder(item)"
@change="handleEvent(item.event, formOpts.formData[item.value], item)" v-bind="typeof item.bind == 'function'
? item.bind(item)
: { clearable: true, filterable: true, ...item.bind }
" :style="{ width: item.width || '100%' }" v-on="cEvent(item)" />
<component v-if="!item.slotName &&
!item.textShow &&
item.comp.includes('tree-select')
" :is="item.comp" v-model="formOpts.formData[item.value]" :type="item.type"
:placeholder="item.placeholder || getPlaceholder(item)"
@change="handleEvent(item.event, formOpts.formData[item.value], item)" v-bind="typeof item.bind == 'function'
? item.bind(item)
: { clearable: true, filterable: true, ...item.bind }
" :style="{ width: item.width || '100%' }" v-on="cEvent(item)" />
<component v-if="!item.slotName &&
!item.textShow &&
!item.comp.includes('date') &&
!item.comp.includes('tree-select')
" :is="item.comp" v-model="formOpts.formData[item.value]" :type="item.type"
:placeholder="item.placeholder || getPlaceholder(item)"
@change="handleEvent(item.event, formOpts.formData[item.value], item)" v-bind="typeof item.bind == 'function'
? item.bind(item)
: { clearable: true, filterable: true, ...item.bind }
" :style="{ width: item.width || '100%' }" v-on="cEvent(item)">
<template #prepend v-if="item.prepend">{{ item.prepend }}template>
<template #append v-if="item.append">{{ item.append }}template>
<template v-if="item.childSlotName">
<slot :name="item.childSlotName">slot>
template>
<component :is="compChildName(item)" v-for="(value, key, index) in selectListType(item)" :key="index"
:disabled="value.disabled" :label="compChildLabel(item, value)" :value="compChildValue(item, value, key)">
{{ compChildShowLabel(item, value) }}
component>
component>
el-form-item>
template>
<div class="footer_btn flex-box flex-ver t-margin-top-5">
<template v-if="formOpts.btnSlotName">
<slot :name="formOpts.btnSlotName">slot>
template>
<template v-if="!formOpts.btnSlotName &&
formOpts.operatorList &&
formOpts.operatorList.length > 0
">
<el-button v-for="(val, index) in formOpts.operatorList" :key="index" @click="val.fun(val)"
:type="val.type || 'primary'" :icon="val.icon" :size="val.size || 'small'" :disabled="val.disabled">
{{ val.label }}
el-button>
template>
div>
el-form>
template>
<script lang="ts">
export default {
name: 'TForm',
}
script>
<script setup lang="ts">
import RenderComp from './renderComp.vue'
import { ElMessage } from 'element-plus'
import { computed, ref, watch, onMounted, getCurrentInstance } from 'vue'
const props = defineProps({
// 自定义类名
className: {
type: String,
},
/** 表单配置项说明
* formData object 表单提交数据
* rules object 验证规则
* fieldList Array 表单渲染数据
* operatorList Array 操作按钮list
* listTypeInfo object 下拉选项数据
* labelWidth String label宽度
*/
formOpts: {
type: Object,
default: () => ({}),
},
// 一行显示几个输入项;最大值4
widthSize: {
type: Number,
default: 2,
validator: (value: any) => {
return value <= 4
},
},
// 全局是否开启清除前后空格
isTrim: {
type: Boolean,
default: true,
},
})
const cEvent = computed(() => {
return ({ eventHandle }) => {
return { ...eventHandle }
}
})
const selectListType = computed(() => {
return ({ list }) => {
if (props.formOpts.listTypeInfo) {
return props.formOpts.listTypeInfo[list]
} else {
return []
}
}
})
// 子组件名称
const compChildName = computed(() => {
return (opt: any) => {
switch (opt.type) {
case 'checkbox':
return 'el-checkbox'
case 'radio':
return 'el-radio'
case 'select-arr':
case 'select-obj':
return 'el-option'
}
}
})
// 子子组件label
const compChildLabel = computed(() => {
return (opt: any, value) => {
switch (opt.type) {
case 'radio':
case 'checkbox':
return value.value
case 'el-select-multiple':
case 'select-arr':
return value[opt.arrLabel || 'label']
case 'select-obj':
return value
}
}
})
// 子子组件value
const compChildValue = computed(() => {
return (opt: any, value, key) => {
switch (opt.type) {
case 'radio':
case 'checkbox':
return value.value
case 'el-select-multiple':
case 'select-arr':
return value[opt.arrKey || 'key']
case 'select-obj':
return key
}
}
})
// 子子组件文字展示
const compChildShowLabel = computed(() => {
return (opt: any, value) => {
switch (opt.type) {
case 'radio':
case 'checkbox':
return value.label
case 'el-select-multiple':
case 'select-arr':
return value[opt.arrLabel || 'label']
case 'select-obj':
return value
}
}
})
const colSize = ref(props.widthSize)
// 获取ref
const tform: any = ref<HTMLElement | null>(null)
// 获取实例方法
const instance: any = getCurrentInstance()
// 抛出事件
const emits = defineEmits(['update:modelValue', 'handleEvent'])
watch(
() => props.formOpts.formData,
(val) => {
// state.form = initForm(opts, true)
// 将form实例返回到父级
emits('update:modelValue', tform.value)
},
{ deep: true }
)
watch(
() => props.widthSize,
(val) => {
if (val > 4) {
ElMessage.warning('widthSize值不能大于4!')
colSize.value = 4
} else {
colSize.value = val
}
},
{ deep: true }
)
onMounted(() => {
const entries = Object.entries(tform.value.$.exposed)
// console.log('111', entries)
for (const [key, value] of entries) {
instance.exposed[key] = value
}
// console.log(789, instance)
// 将form实例返回到父级
emits('update:modelValue', tform.value)
})
// label与输入框的布局方式
const getChildWidth = (item) => {
if (props.formOpts.labelPosition === 'top') {
return `flex: 0 1 calc((${100 / (item.widthSize || colSize.value)
}% - 10px));margin-right:10px;`
} else {
return `flex: 0 1 ${100 / (item.widthSize || colSize.value)}%;`
}
}
// placeholder的显示
const getPlaceholder = (row: any) => {
// console.log(77, row.date)
let placeholder
if (row.comp && typeof row.comp == 'string') {
if (row.comp.includes('input')) {
placeholder = '请输入' + row.label
} else if (row.comp.includes('select') || row.comp.includes('date')) {
placeholder = '请选择' + row.label
} else {
placeholder = row.label
}
}
return placeholder
}
// 查询条件change事件
const handleEvent = (type, val, item) => {
// 去除前后空格
if (
props.isTrim &&
!item.isTrim &&
item.comp.includes('el-input') &&
item.type !== 'password' &&
item.type !== 'inputNumber'
) {
props.formOpts.formData[item.value] =
props.formOpts.formData[item.value].trim()
}
emits('handleEvent', type, val)
}
// 自定义校验
const selfValidate = () => {
return new Promise((resolve: any, reject: any) => {
tform.value.validate((valid: boolean) => {
if (valid) {
resolve({
valid,
formData: props.formOpts.formData,
})
} else {
// eslint-disable-next-line prefer-promise-reject-errors
reject({
valid,
formData: null,
})
}
})
})
}
// 暴露方法出去
defineExpose({ ...instance.exposed, selfValidate })
script>
<style lang="scss">
.t-form {
display: flex;
flex-wrap: wrap;
.el-form-item {
align-items: center;
.el-form-item__content {
.el-input,
.el-select,
.el-date-editor,
.el-textarea {
width: 100%;
}
.el-input-number {
.el-input {
width: inherit;
}
}
}
}
// 左对齐
.asterisk-left {
.el-form-item__label {
margin-left: 5px;
}
}
.t-margin-top-5 {
margin-top: calc(5px);
}
.el-input-number {
.el-input {
.el-input__inner {
text-align: left;
}
}
}
.render_label {
.el-form-item__label {
display: flex;
align-items: center;
justify-content: flex-end;
&::before {
margin-top: 1px;
}
}
}
// 左对齐
.render_laber_position_left {
.el-form-item__label {
justify-content: flex-start;
}
}
// 顶部对齐
&.el-form--label-top {
.render_label {
.el-form-item__label {
justify-content: flex-start;
}
}
}
.label_render {
display: flex;
align-items: center;
justify-content: flex-end;
}
.text_show {
color: var(--el-text-color-primary);
}
.slot_label {
// margin-bottom: 0 !important;
.el-form-item__content {
// margin-left: 0 !important;
label {
min-width: 108px;
color: var(--el-text-color-primary);
text-align: right;
margin-right: 12px;
}
}
}
.flex-box {
display: -webkit-box;
display: -webkit-flex;
display: flex;
}
.flex-ver {
align-items: center;
justify-content: center;
}
.footer_btn {
width: 100%;
}
}
style>
gitHub组件地址
gitee码云组件地址
vue3+ts基于Element-plus再次封装基础组件文档
基于ElementUi再次封装基础组件文档
vue+element-ui的table组件二次封装