開發這樣一個複雜的表單你需要用多久

> 表單在中後臺開發的時,是最多也是最另人頭疼的,多級聯動,繁雜的驗證,動態解析等可算是苦不堪言。所以出現了無數的表單解決方案,像Uform, formily, NoForm等等一大堆用來解決中後臺開發表單,可想而知,解決複雜的表單開發是多麼另人頭大;有XML的,有json-schema格式的,無論哪一種都是想能夠輕鬆的解決另人頭腦的表單開發,提高生產力。

#### 下面看一下商城後臺添加商品常用的表單片段

![](https://demo-mall-1256372626.cos.ap-chengdu.myqcloud.com/form-plus.gif)

> 當然,其中有部分不是,這只是爲了做一個 DEMO, 解釋一下這是個啥

* 遠程搜索(demo):通過輸入文字動態去後臺查詢可選項 
* 搜索&創建(demo):通過輸入文字動態去後臺搜索,如果沒有搜索到也可以創建,和上面的不同的是,上面查詢不到是不可以選的
* 商品名稱:名稱不用說,字符串,非空驗證
* 副標題: 不用說,一串字符串,簡單描述一下商品
* 分類:後臺加載出分類,級聯選擇
* 地址(demo): 多選項聯動測試,選擇1觸發2,選擇2觸發3
* 優惠方式:選擇不同的優惠方式 (無優惠,促銷,會員特價,滿減)
* 無優惠:無特殊處理
* 促銷:表單,開始時間,結束時間,促銷時的價格;驗證:不能爲空,結束時間不能小於開始時間,價格數值
* 會員特價:黃金會員,白金會員輸入不同的價格。驗證:不爲空,數值
* 滿減:當購買金額足夠多少時,減少的金額,可以添加多個分段,驗證:至少一條
* 類型:選擇商品的類型,聯動不同的配置參數片段,此處模擬了兩種:(服裝,數碼)用於選擇;
* 服裝:
  * 規格:顏色,可以手工添加;尺寸,可以從後臺查詢預先配置好的參數。然後選擇的顏色與尺寸生成笛卡爾表單,對每個枚舉添加價格,庫存預警值,SKU等信息
  * 商品參數:表單,商品編號(必填),季節:單選,人羣種類:多選,上市時間:日期

* 數碼:
  * 規格:容量,可以從後臺查詢預先配置好的參數。然後選擇生成笛卡爾表單,對每個枚舉添加價格,庫存預警值,SKU等信息

* 填充數據:同編輯,根據數據來填充表單
* 提交:驗證通過,提交表單,不通過阻止提交併提示出錯信息
* 重置:清空表單,(當然可以分步表單,這裏把所有都重置還是挺過分的)

> 根據一個複雜的需求,練手一個複雜程度還行的表單也是不錯的選擇

#### 下面看一下實現吧

```js
<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 示例的最後一個

#### 如果您有一些非常複雜的、奇葩的設計,希望您可以留言,可以讓做成示例吧。

>做這些的目的爲了提供可供參考的解決思路,方法。來共同進步,使前端越來越好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章