开发这样一个复杂的表单你需要用多久

36次阅读

共计 7610 个字符,预计需要花费 20 分钟才能阅读完成。

开发这样一个复杂的表单你需要用多久

表单在中后台开发的时,是最多也是最另人头疼的,多级联动,繁杂的验证,动态解析等可算是苦不堪言。所以出现了无数的表单解决方案,像 Uform, formily, NoForm 等等一大堆用来解决中后台开发表单,可想而知,解决复杂的表单开发是多么另人头大;有 XML 的,有 json-schema 格式的,无论哪一种都是想能够轻松的解决另人头脑的表单开发,提高生产力。

下面看一下商城后台添加商品常用的表单片段

当然,其中有部分不是,这只是为了做一个 DEMO, 解释一下这是个啥

  • 远程搜索(demo):通过输入文字动态去后台查询可选项
  • 搜索 & 创建(demo):通过输入文字动态去后台搜索,如果没有搜索到也可以创建,和上面的不同的是,上面查询不到是不可以选的
  • 商品名称:名称不用说,字符串,非空验证
  • 副标题: 不用说,一串字符串,简单描述一下商品
  • 分类:后台加载出分类,级联选择
  • 地址(demo): 多选项联动测试,选择 1 触发 2,选择 2 触发 3
  • 优惠方式:选择不同的优惠方式(无优惠,促销,会员特价,满减)
  • 无优惠:无特殊处理
  • 促销:表单,开始时间,结束时间,促销时的价格;验证:不能为空,结束时间不能小于开始时间,价格数值
  • 会员特价:黄金会员,白金会员输入不同的价格。验证:不为空,数值
  • 满减:当购买金额足够多少时,减少的金额,可以添加多个分段,验证:至少一条
  • 类型:选择商品的类型,联动不同的配置参数片段,此处模拟了两种:(服装,数码)用于选择;
  • 服装:

    • 规格:颜色,可以手工添加;尺寸,可以从后台查询预先配置好的参数。然后选择的颜色与尺寸生成笛卡尔表单,对每个枚举添加价格,库存预警值,SKU 等信息
    • 商品参数:表单,商品编号(必填),季节:单选,人群种类:多选,上市时间:日期
  • 数码:

    • 规格:容量,可以从后台查询预先配置好的参数。然后选择生成笛卡尔表单,对每个枚举添加价格,库存预警值,SKU 等信息
  • 填充数据:同编辑,根据数据来填充表单
  • 提交:验证通过,提交表单,不通过阻止提交并提示出错信息
  • 重置:清空表单,(当然可以分步表单,这里把所有都重置还是挺过分的)

根据一个复杂的需求,练手一个复杂程度还行的表单也是不错的选择

下面看一下实现吧

<fd-form   
    :data.sync="codeCompxPlus"
    @event="codeCompxPlusEvent"
    @submit="codeCompxPlusSubmit"
    :columns="[{type: 'select-remote', prop: 'selectRemote', label: '远程搜索', placeholder: '远程搜索选择', options({resolve, query}) {resolve(['选项 1', '选项 2', '选项 3'].map(e => query + e).toString())
        }},
        {type: 'input-remote', prop: 'inputRemote', label: '搜索 & 创建', placeholder: '远程搜索, 搜索不到创建: a/b/c', options({resolve, query}) {if (query && 'abc'.includes(query)) {resolve([{value: query + '选项 1'}, {value: query + '选项 2'}])
            } else {resolve([])
            }
        }, style: {width: '280px'}},
        {type: 'input', prop: 'name', label: '商品名称', placeholder: '请输入商品名称', rule: 'must'},
        {type: 'input', prop: 'title', label: '副标题', placeholder: '请输入副标题'},
        {type: 'cascader', prop: 'kind', label: '分类', placeholder: '请选择分类 / 模拟远程', options({resolve}) {
            // 可以在此访问 api  .then 函数中使用 resolve
            resolve([{label: '服装', value: 1, children: [{label: '外套', value: 11}, {label: '衬衫', value: 12}]}, 
                {label: '家用', value: 2}
            ])
        }}, 
        // 此处切换选择对应 options 已经变了,但如果已经选择过那么值不会清空,可以手动监听事件去清除 this.codeCompxPlus.city = '' 这样
        [{type: 'select', prop: 'province', label: '省', options({resolve}) {resolve({1: '江苏', 2: '河南', 3: '山东'})
            }},
            {type: 'select', prop: 'city', label: '市', options({resolve, data}) {
                // 老规矩,可以从后台取,可以是静态文件取,格式如何,自行设计
                resolve({1: {11: '苏州', 12: '南京'},
                    2: {21: '郑州'},
                    3: {31: '济南'}
                }[data.province])
            }},
            {type: 'select', prop: 'area', label: '区', options({resolve, data}) {
                resolve({
                    11: '苏州 AB, 苏州 DD',
                    12: '南京 CC, 南京 UU',
                    21: '郑州 VV, 郑州 KK',
                    31: '济南 MMM, 济南 LLL',
                }[data.city])
            }},
            // 如果多个 formitem 都有 rule,要添加个 prop(不重复就行)
            {type: 'formitem', label: '地址', prop: 'address', rule({resolve, data}) {resolve((!data.province || !data.city || !data.area) && '必须要选的')
            }}
        ],
        {type: 'radios-button', prop: 'yh', label: '优惠方式', value: 1, options: {1: '无优惠', 2: '促销', 3: '会员特价', 4: '满减'}},
        // 促销
        {type: 'render', load: ({data}) => data.yh == 2, prop: 'cx', render({createElement, value}) {
            return createElement('FdForm', {
                props: {
                    columns: [{type: 'date', prop: 'cxDateStart', label: '开始时间'},
                        {type: 'date', prop: 'cxDateEnd', label: '结束时间'},
                        {type: 'input', prop: 'cxPrice', label: '价格', style: {width: '220px'}}
                    ], 
                    data: value,
                    config: {labelWidth: '75px'}
                }
            })
        }, rule({resolve, value}) { 
            let message 
            if (!value.cxDateStart || !value.cxDateEnd || !value.cxPrice) {message = '别看了,都是必填项,可验证非空'}
            if (value.cxDateStart > value.cxDateEnd) {message =  '结束日期不能小于开始日期'}
            resolve(message)
        }},
        // 会员特价
        {type: 'render', load: ({data}) => data.yh == 3, prop: 'hytj', render({createElement, value}) {
            return createElement('FdForm', {
                props: {
                    columns: [{type: 'input', prop: 'hytjGlod', label: '黄金会员', style: {width: '220px'}},
                        {type: 'input', prop: 'hytjPlatinum', label: '白金会员', style: {width: '220px'}} 
                    ],
                    data: value,
                    config: {labelWidth: '75px'}
                }
            })
        }, rule({resolve, value}) {resolve((!value.hytjGlod || !value.hytjPlatinum) && '必须要选的, 数值验证,啥乱七八糟的验证自行写')
        }},
        // 满减
        {type: 'render', load: ({data}) => data.yh == 4, prop: 'mj', render({createElement, value}) {
            return createElement('FdTable', {
                props: {
                    columns: [{type: 'input', prop: 'mjEnough', label: '购买金额满'},
                        {type: 'input', prop: 'mjReduce', label: '减'},
                        {label: '操作', render() {
                            return [{type: 'button-text', value: '删除', prop: 'del'},
                                {type: 'button-text', value: '添加', prop: 'add'},
                            ]
                        }}
                    ],
                    data: value
                },
                on: {event(params) {if (params.prop == 'del') {value.splice(params.$index, 1)
                            if (value.length <= 0) {value.push({})
                            }
                        } else if (params.prop == 'add') {value.push({})
                        }
                    }
                }
            })
        }, rule({resolve, value}) {
            let message, len = value.length
            value.forEach(e => {if (!e.mjEnough || !e.mjReduce) {if (!e.mjEnough && !e.mjReduce)
                        len --
                    else message = '要填写的'
                }
            })
            if (len < 1)
                message = '至少填写一条'
            resolve(message)
        }},
        {type: 'span', load: ({data}) => data.yh == 4, value: '提示:至少写一行,如果两个属性都没写,那么这行不做记录', style: {fontWeight: '600'}},
        // 动态联动
        {type: 'select', prop: 'type', label: '类型', placeholder: '类型不同对应不同结构', options({resolve}) {
            // 同样,可以动态从 api 获取
            resolve({1: '服装', 2: '数码'})
        }, rule: 'must'},
        // 这里需要根据 data.type 改变来强制刷新 render 函数 
        {type: 'render', label: '规格', prop: 'gg', load: ({data}) => data.type, forceUpdate: true, render({createElement, data, value}) {let _columns = []
            // 这里模拟一下根据 type select 改变,改变为不同的属性
            if (data.type == 1) {
                _columns = [{type: 'span', value: '颜色:'},
                    {type: 'br'},
                    {type: 'tags-create', prop: 'ggColor'},
                    {type: 'span', value: '尺寸:'},
                    {type: 'br'},
                    {type: 'check-boxs', prop: 'ggSize', options: 'M,X,XL,L,2XL'},
                ]
            } else {
                _columns = [{type: 'span', value: '容量:'},
                    {type: 'br'},
                    {type: 'check-boxs', prop: 'ggSize', options: '1G,2G,3G'},
                ]
            }
            return createElement('FdRegion', {
                props: {
                    columns: _columns,
                    data: value
                },
                on: {event(params) {let _columns = []
                        for (let _value in Object.keys(data.gg)) {key = Object.keys(data.gg)[_value]
                            if (data.gg[key] && data.gg[key].length) {_columns.push(data.gg[key].map(el => {return {value: el, prop: key}
                                }))
                            }
                        } 
                        codeCompxPlus.propList = calcMultiplyData(_columns)
                    }
                }
            })
        }},
        {type: 'render', prop: 'propList', load: ({data}) => data.type, forceUpdate: true, render({createElement, data, value}) {let _columns = []
            // 这里也是根据 type 的切换改变不同的 columns
            if (data.type == 1) {
                _columns.push({label: '颜色', prop: 'ggColor'},
                    {label: '尺寸', prop: 'ggSize'}
                )
            } else {
                _columns.push({label: '容量', prop: 'ggSize'}
                )
            }
            _columns.push({label: '价格', prop: 'price', type: 'input'},
                {label: '库存', prop: 'store', type: 'input'},
                {label: '预警值', prop: 'val', type: 'input'},
                {label: 'SKU 编辑', prop: 'sku', type: 'input'},
                {label: '操作', prop: 'del', type: 'button-text', value: '删除'}
            )
            return createElement('FdTable', {
                props: {
                    columns: _columns,
                    data: value
                }
            })
        // 同样可以加 rule 进行对表格进行验证
        }},
        {type: 'render', prop: 'prop', label: '商品参数', load: ({data}) => data.type == 1, render({createElement, value}) {
            return createElement('FdForm', {
                props: {
                    columns: [{type: 'input', label: '商品编号', prop: 'propNo', rule: 'must', style: {width: '220px'}},
                        {type: 'select', label: '季节', prop: 'propSeason', options: '春季, 夏季, 秋季, 冬季'},
                        {type: 'select-multiple', label: '人群种类', prop: 'propCrowd', options: '儿童, 青少年, 中年, 老年'},
                        {type: 'date', label: '上市时间', prop: 'propUpTime'}
                    ],
                    config: {labelWidth: '85px'}, 
                    data: value
                }
            })
        }},
        [{type: 'button-info', prop: 'fullData', value: '填充数据'},
            {type: 'button-primary', prop: '$submit', value: '提交'},
            {type: 'button', prop: '$reset', value: '重置'},
            {type: 'formitem'}
        ]
    ]"
/>
methods: {calcMultiplyData(arr) {let res = [], cur = {}
        function search(deep = 0) {if (deep >= arr.length) {res.push(cur)
                cur = Object.assign({}, cur)
                return
            }
            for (let obj of arr[deep]) {cur[obj.prop] = obj.value
                search(deep + 1)
            }
        }
        search()
        return res
    },
    codeCompxPlusSubmit(data) {alert('提交成功,打开控制台查看提交的数据')
        console.log(data)
    },
    codeCompxPlusEvent(params) {if (params.prop == 'province') {this.codeCompxPlus.city = ''this.codeCompxPlus.area =''} else if (params.prop == 'city') {this.codeCompxPlus.area = ''} else if (params.prop =='type') {this.codeCompxPlus.propList = []
        } else if (params.prop == 'fullData') {
            this.codeCompxPlus = {
                selectRemote: '选项 1',
                inputRemote: '选项 2',
                name: '汪仔',
                title: '还是那个味道',
                kind: [1,12],
                province: '1',
                city: '11',
                area: '苏州 AB',
                yh: 4,
                mj: [{mjEnough: 200, mjReduce: 5}, {mjEnough: 400, mjReduce: 18}],
                type: 1,
                gg: {ggColor: 'blue', ggSize: ['XL', 'L']},
                propList: [{ggColor: 'blue', ggSize: 'XL', price: 280, store: 2299, val: 105, sku: 'wtf'},
                    {ggColor: 'blue', ggSize: 'L', price: 288, store: 2009, val: 100, sku: 'crete'},
                ],
                prop: {propNo: 'abc111', propSeason: '秋季', propCrowd: ['儿童','青少年'], propUpTime: new Date()}
            }
        }
    }
}

上面是实现这个表单的全部代码,使用的是基于 ElementUI 的 vue-elementui-freedomen 制作的 vue 语法表单,表单的展示在:http://115.159.65.195:8080/vefdoc 示例的最后一个

如果您有一些非常复杂的、奇葩的设计,希望您可以留言,可以让做成示例吧。

做这些的目的为了提供可供参考的解决思路,方法。来共同进步,使前端越来越好。

正文完
 0