在项目中很常见的交互:回显表单信息 + 验证表单 + 提交表单信息
,而表单的类型也有很多(复选框,单选框,下拉框,输入框,文本框等等等)如果多个页面都有表单验证的交互且表单的内容不一样,难道我们就要去写多个表单组件吗???那该怎么办呢????
根据element-ui 的Form表单组件,写了一个公共的组件,可以满足大多数的表单类型的验证,做的这个组件主要是以弹窗的形式在页面上展示
主要的功能:
-显示弹窗(根据传入的数据来决定来显示表单)
-验证表单信息
-提交表单信息
App.vue
想法:把弹窗组件挂载在app上,通过provide/inject实现跨级访问祖先组件的数据
app.vue(引入弹窗组件)
<template>
<div id="app">
<router-view :key="key"/>
<form-dialog ref="formDialog" />
</div>
</template>
<script>
import formDialog from '@/components/dialogFrom'
export default {
name: 'App',
components: {
formDialog
},
computed: {
key() {
return this.$route.path + Math.random()
}
},
provide() {
const me = this
return {
showForm(...args) {
me.$refs.formDialog.showDialog(...args)
}
}
}
}
</script>
dialogFrom.vue(对应的弹窗组件)
<template>
<el-dialog
:title="formTitle"
:visible.sync="formVisible"
:close-on-click-modal="false"
width="960px"
class="common-form">
<el-form ref="form" :model="formData" :rules="formRule" label-width="130px">
<el-form-item v-for="(item, i) in formColumns" :key="i" :prop="item.property" :label="item.title">
<!-- 文本框textarea -->
<el-input
v-if="item.type === 'textarea'"
v-model.number="formData[item.property]"
v-bind="item.bind"
:placeholder="item.placeholder||'请输入'"
rows="7"
type="textarea"
/>
<!-- radio -->
<div v-else-if="item.type=='radio'" class="radio-wrapper">
<el-radio v-for="(opt, j) in item.options" :key="j" :label="opt.id" v-model="formData[item.property]">{{ opt.name }}</el-radio>
</div>
<!-- 多选框checkbox -->
<div v-else-if="item.type=='checkbox'" class="checkbox-wrapper">
<el-checkbox v-if="item.isAllCheck" :indeterminate="item.isIndeterminate" v-model="item.checkAll" @change="handleCheckAllChange(item)">全选</el-checkbox>
<el-checkbox-group v-model="formData[item.property]" @change="handleCheckedUserGroupChange(item)">
<el-checkbox v-for="(opt, j) in item.options" :key="j" :label="opt.id">{{ opt.name }}</el-checkbox>
</el-checkbox-group>
</div>
<!-- 两个input + 加减按钮 -->
<div v-else-if="item.type === 'mutliInput'" class="mutli-input-wrapper">
<ul class="mutli-input" >
<li v-for="(element, index) in formData[item.property]" :key="index" class="input-list">
<div class="input-wrapper">
<el-input
v-model="element.key"
:placeholder="item.placeholder ||'请输入'" />
<el-input
v-model="element.value"
:placeholder="item.placeholder||'请输入'" />
</div>
<div class="handle-btn">
<el-button type="primary" icon="el-icon-plus" circle @click="addConfigInput(item, formData[item.property], index)"/>
<el-button :disabled="item.isDelete" type="danger" icon="el-icon-delete" circle @click="cutConfigInput(item, formData[item.property], index)" />
</div>
</li>
</ul>
</div>
<!-- 下拉框 + 是否可以多选 -->
<el-select
v-else-if="item.type === 'select'"
:multiple="item.multiple"
:disabled="item.disabled || false"
v-model="formData[item.property]"
v-bind="item.bind"
:placeholder="item.placeholder || '请选择'"
>
<el-option v-for="(opt, j) in item.options" :key="j" :label="opt.name" :value="opt.id" />
</el-select>
<div v-else-if="item.type === 'upload'" class="avatarUploader">
<el-upload
:show-file-list="false"
:before-upload="beforeUpload('image', item.property)"
accept="image/gif, image/jpeg"
drag
action="">
<img v-if="formData[item.property]" :src="formData[item.property]" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>
<div v-else-if="item.type === 'uploadAudio'" class="avatarUploader">
<el-upload
:show-file-list="false"
:before-upload="beforeUpload('audio', item.property)"
drag
action="">
<i class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
<span v-if="formData[item.property]">{{ formData[item.property] }}</span>
</div>
<!-- 密码 -->
<div v-else-if="item.type === 'password'" class="pass-box">
<el-input
v-model="formData[item.property]"
v-bind="item.bind"
:placeholder="item.placeholder||'请输入'"
:type="item.pwdType"
/>
<span class="show-pwd" @click="showPwd(item)">
<svg-icon :icon-class="item.pwdType === 'password' ? 'eye' : 'eye-open'" />
</span>
</div>
<el-input
v-else-if="item.type === 'englishText'"
v-model="formData[item.property]"
v-bind="item.bind"
:placeholder="item.placeholder||'请输入'"
/>
<el-input
v-else-if="item.type === 'preText'"
v-model="formData[item.property]"
v-bind="item.bind"
:placeholder="item.placeholder||'请输入'">
<template slot="prepend">/</template>
</el-input>
<el-input
v-else-if="item.type === 'authText'"
v-model="formData[item.property]"
v-bind="item.bind"
:placeholder="item.placeholder||'请输入'">
</el-input>
<el-input
v-else-if="item.type === 'text' || item.type === 'input'"
:disabled="item.disabled || false"
v-model="formData[item.property]"
v-bind="item.bind"
:placeholder="item.placeholder||'请输入'"
/>
</el-form-item>
<el-form-item>
<div class="dialog-btn">
<el-button type="primary" @click="onSubmit">确定</el-button>
<el-button @click="onHide">取消</el-button>
</div>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script>
var formCallback = () => {}
const qiniu = require('qiniu-js')
export default {
data() {
return {
formTitle: '编辑',
formVisible: false,
formColumns: [],
formRule: [],
formData: {},
currentName: '',
reEnglish: /^[a-zA-Z_]{5,}$/,
reMath: /^\d{6}$/,
rePassword: /^.{6,18}$/
}
},
watch: {
formData: {
handler(val, oldVal) {
this.setDataStatus(val)
},
deep: true,
immediate: true
}
},
methods: {
setDataStatus(val) {
// 多个input + 加减按钮
const mutliInputInfo = this.formColumns.find(item => item.property === 'config_content' && item.type === 'mutliInput')
// 复选框
const checkboxInfo = this.formColumns.find(item => item.type === 'checkbox')
if (mutliInputInfo) {
mutliInputInfo.isDelete = val[mutliInputInfo.property].length === 1
}
if (checkboxInfo) {
this.handleCheckedUserGroupChange(checkboxInfo)
}
},
showDialog({ formColumns, formData, formTitle }, callback) {
formCallback = callback
this.formTitle = formTitle || this.formTitle
this.formVisible = true
this.formColumns = formColumns
this.createForm(formColumns, formData)
this.$nextTick(() => {
this.$refs.form.clearValidate()
})
},
onSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
Promise.resolve(formCallback(Object.assign({}, this.formData))).then((result) => {
if(result === true){
this.formVisible = false
}
})
} else {
console.log('validate fail')
}
})
},
onHide() {
this.formVisible = false
this.$refs.form.resetFields()
formCallback = () => {}
},
createForm(formColumns, formData) {
var data = {}
var rule = {}
formColumns.forEach(col => {
if (col.type === 'select' && col.multiple) {
data[col.property] = formData[col.property] === 'undefined' ? [] : formData[col.property]
rule[col.property] = this.genRule(col)
} else if(col.hasOwnProperty('property')){
data[col.property] = formData[col.property]
rule[col.property] = this.genRule(col)
} else if(typeof formData[col.property] === 'undefined'){
data[col.name] = typeof formData[col.name] === 'undefined' ? '' : formData[col.name]
rule[col.name] = this.genRule(col)
}
})
this.formData = data
this.formRule = rule
},
addConfigInput(item, element, index) {
if (item.isLimit && element.length >= item.isLimit) {
this.$message({
message: `最多只能添加${item.isLimit}条`,
type: 'warning'
})
return
}
element.splice(index + 1, 0, { key: '', value: '' })
},
cutConfigInput(item, element, index) {
if (item.isDelete) return
element.splice(index, 1)
},
// 复选框勾选
handleCheckedUserGroupChange(item) {
const checkedCount = this.formData[item.property].length
item.checkAll = checkedCount === item.options.length
item.isIndeterminate = checkedCount > 0 && checkedCount < item.options.length
},
// 复选框全选
handleCheckAllChange(item) {
this.formData[item.property] = item.checkAll ? item.options.map(item => item.id) : []
item.isIndeterminate = false
},
// 得到当前上传图片的name
getCurrentName(ev) {
this.currentName = ev.currentTarget.getAttribute('data-name')
},
// 上传图片
beforeUpload(mime, property) {
// console.log('beforeUpload', mime, property)
const me = this
return (file)=>{
this.$request.get('file/token', { params: { dir: 'adminas', num: 1, mime: "" }}).then(res => {
// console.log(res, 'res')
res = res[0]
const observable = qiniu.upload(file, res.key, res.token)
observable.subscribe({
complete(imageInfo) {
me.formData[property] = imageInfo.data.url
},
error(err) {
console.log("error:", err)
}
})
return false
}).catch(error => {
console.error(error)
})
}
},
// 是否显示密码
showPwd(it) {
if (it.pwdType === 'password') {
it.pwdType = ''
} else {
it.pwdType = 'password'
}
},
genRule(column) {
const r = {
message: `请输入${column.title || column.name}`,
trigger: 'blur'
}
// 验证规则
this.verifyInput(r, column)
r.required = !!column.required
return r
},
verifyInput(r, column) {
switch (column.type) {
case 'englishText':
r.validator = (rule, value, callback) => {
if (!value) {
return callback(new Error('英文名称不能为空!'))
}
if (this.reEnglish.test(value)) {
callback()
}else{
callback(new Error('该项必须为英文字母,并且长度不能少于5个字符!'))
}
}
break
case 'authText':
r.validator = (rule, value, callback) => {
if (!value) {
return callback(new Error('验证码不能为空!'))
}
if (this.reMath.test(value)) {
callback()
}else{
callback(new Error('验证码必须为6个字符的数字!'))
}
}
break
case 'password':
r.validator = (rule, value, callback) => {
if (!value) {
return callback(new Error('密码不能为空!'))
}
if (this.rePassword.test(value)) {
callback()
} else {
callback(new Error('密码长度应该在6-18个字符之间!'))
}
}
break
case 'mutliInput':
r.validator = (rule, value, callback) => {
const isRequiredConfig = value.every(item => item.key !== '' && item.value !== '')
// 判断key值是否重复
const keyList = []
value.forEach(item => {
if (keyList.indexOf(item.key) !== -1) {
return callback(`${column.title || column.name}的key值不得重复`)
}
keyList.push(item.key)
})
if (!isRequiredConfig) {
return callback(r.message)
}
callback()
}
break
}
r.message = ''
}
}
}
</script>
<style>
.common-form .el-upload-dragger {
width: 100px !important;
height: 100px !important;
}
</style>
<style lang="scss">
.common-form {
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
line-height: 100px;
text-align: center;
}
.avatar {
width: 100px;
height: 100px;
display: block;
}
.pass-box{
position: relative;
.show-pwd{
position: absolute;
right: 335px;
top: 2px;
cursor: pointer;
user-select: none;
}
}
.mutli-input-wrapper {
.input-list {
font-size: 0;
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
.input-wrapper, .handle-btn {
display: inline-block;
vertical-align: middle;
font-size: 14px;
}
.el-input {
width: 300px !important;
margin-right: 15px;
&:last-child {
margin-right: 0;
}
}
.handle-btn {
margin-left: 15px;
}
}
}
</style>
如何去使用上面的这个表单组件呢?只需要在把下面的代码放在点击事件里就可以了
例如a.vue
关于表单组件的各个字段的说明
formTitle string 弹窗的标题
formColumns Array 表单的类型
eg:
[{ property: 'name', title: '配置名:', type: 'text', required: true, placeholder: '请输入配置名' },
{ property: 'config_content', title: '配置内容:', type: 'mutliInput', required: true, placeholder: '配置内容', isLimit: 5, isDelete: false },
{ property: 'type', title: '类型:', type: 'select', required: true, disabled: false, placeholder: '请选择类型', multiple: false, options: [{ id: 'custom', name: 'custom' }, { id: 'service', name: 'service' }] },
{ property: 'checkbox_name', type: 'checkbox', required: true, isAllCheck: true, checkAll: false, isIndeterminate: true, options: [] } ]
property 表示存储表单某类型数据的key值
title 表示表单某类型对应的标题
type 表示表单对应的类型
required Boolean 是否为必填
formData Object 初始化表单的数据
eg: { checkbox_name: [] }
上面只是进行了一个简单的表单类型的使用说明,大家有需要可以看一下dialogFrom.vue
就知道怎么用啦