vue 封装Form表单组件

- 前景

在项目中很常见的交互:回显表单信息 + 验证表单 + 提交表单信息,而表单的类型也有很多(复选框,单选框,下拉框,输入框,文本框等等等)如果多个页面都有表单验证的交互且表单的内容不一样,难道我们就要去写多个表单组件吗???那该怎么办呢????

- 作用

根据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就知道怎么用啦

你可能感兴趣的:(vue,vue,vue.js)