【前端】活动表单

先看效果

【前端】活动表单

要求:

1、能够动态添加多个参数 因为不是每个设备都有对应的参数

2、同一个设备参数名不能重复 这个在后端很好整 前端需要限制用户的输入操作

3、保存之后能够随时修改

环境:

基于若依4.6.1开发的项目

前言:

因为没有现成的东西,值能把旧的代码翻出来找 找到了个类似的

在还不知道若伊这类管理平台的时候 生成表格、表单的代码都是自己写的

尤其是刚毕业后工作的两个单位 对前端框架仅仅是bootstrap这种单纯的前端框架

其实造轮子挺好玩的 就是速度慢一些

数据库部分:

按照一对多的模型创建的参数表

| uuid | 设备型号id | 参数键名 | 参数值 |

参数键存在数据字典中方便管理

参数键名为0x01 0x02这样的编号

参数键标签名为执行标准、公称直径这类业务数据 存在数据字典中

参数值为业务数据 GB/T 12345这类

后端略过:数据的增查删改会的都会,不会的学去,最基本的玩意

前端:

最麻烦的就是前端 要考虑用户的使用 要考虑到各种zz操作导致的各种bug

我也不会讲 代码写的不咋样 就是按ruoyi里面有个$.Table这个玩意 仿照着感觉写的

先是一个小工具 方便创建HTML元素

/**
 * 创建HTML元素
 * @param tagName html标签名
 * @param parent 父对象
 * @param callback 回调函数 参数为该元素
 * @returns {Element} 返回新建的HTML元素
 */
function createElement(tagName, parent, callback) {
    let element = document.createElement(tagName);
    if (parent) {
        $(parent).append(element);
    }
    if (callback) {
        callback(element);
    }
    return element;
}

能用代码整得就不要拼接字符串 除非我不会

这两大坨代码仍在工具类的js文件中

那个$.extend只是为了能够通过$.activeTable调用 没其他意思

/**
 * 用于管理活动表格的配置信息
 * @type {{config: {}, options: {}, set: activeTable.set, get: activeTable.get}}
 */
var activeTable = {
    // 配置信息的集合
    config: {},
    // 活动的配置信息
    options: {},
    // 选择活动的配置信息
    set: function (id) {
        if ($.common.getLength(activeTable.config) > 1) {
            let tableId = $.common.isEmpty(id) ?
                $(event.currentTarget).parents(".active-table").find("table.table").attr("id") : id;
            if ($.common.isNotEmpty(tableId)) {
                activeTable.options = activeTable.get(tableId);
            }
        }
    },
    // 获取配置信息
    get: function (id) {
        if ($.common.isNotEmpty(id)) {
            return activeTable.config[id];
        } else {
            return activeTable.options;
        }
    }
};

$.extend({
    /**
     * 活动表格
     */
    activeTable: {
        /**
         * 初始化活动表格 调用后会调用loadData方法填充数据
         * 回调函数接受三个参数{cell:单元格,
         * @param _options 配置信息 {id:唯一识别符,titleList:标题列,fieldList:字段列,dataList:数据列,callback:每个单元格的回调函数}
         *
         * option可以自行添加额外配置 以上内容为最小配置
         * 配置id目的是在同一页面使用多个active-table
         * 我也没试过 谁有兴趣试试看中不中
         */
        init: function (_options) {
            let defaults = {
                id: "active-table",  // 标识符
                titleList: [],       // 标题列
                fieldList: [],       // 字段列
                dataList: [],        // 数据列
                callback: null       // 处理每个单元格的回调函数
            };

            let options = $.extend(defaults, _options); // 合并配置项 覆盖默认配置
            activeTable.options = options;
            activeTable.config[options.id] = options;
        },
        /**
         * 活动表格填充数据
         * @param data 要填充的数据 为空则使用options中的数据
         */
        load: function (data) {
            let table = $('#' + activeTable.options['id']);
            let options = activeTable.options;
            if (data) {
                options.dataList = data;
            }
            let titleList = options.titleList;
            let dataList = options.dataList;
            let callback = options.callback;
            // 清空表格内容
            $(table).empty();
            // 创建表格结构
            let thead = createElement('thead', table);
            let tr_head = createElement('tr', thead);
            for (let title of titleList) {
                createElement('th', tr_head, function (e) {
                    $(e).text(title);
                    $(e).css('text-align', 'center');
                });
            }
            createElement('tbody', table);
            if (dataList) {
                // 追加数据
                $.activeTable.appendData(dataList, callback);
            }
            // 追加空行 做添加行的按钮
            $.activeTable.appendRow(null, callback);
        },
        /**
         * 创建一行
         * @param data 要插入的数据(该行的)
         * @param callback(options, cell, data, field) 回调函数处理每个单元格的数据
         * @returns {Element} 返回创建的行
         */
        createRow: function (data, callback) {
            let fieldList = activeTable.options.fieldList;
            let row = createElement('tr', null);
            for (let field of fieldList) {
                let cell = createElement('td', row);
                if (data) {
                    $(cell).text(data[field]);
                }
                if (callback) {
                    callback(activeTable.options, cell, data, field);
                }
            }
            return row;
        },
        /**
         * 追加行
         * @param data 该行的数据
         * @param callback(cell,data,field) 回调函数处理每个单元格的数据
         */
        appendRow: function (data, callback) {
            let tbody = $('#' + activeTable.options['id'] + ' tbody');
            let row = $.activeTable.createRow(data, callback);
            $(tbody).append(row);
        },
        /**
         * 插入行 在最后一行前插入一行数据
         * @param data 该行的数据
         * @param callback(cell,data,field) 回调函数处理每个单元格的数据
         */
        insertRow: function (data, callback) {
            let tbody = $('#' + activeTable.options['id'] + ' tbody');
            let row = $.activeTable.createRow(data, callback);
            $(tbody).find('tr:last').before(row);
        },
        /**
         * 追加数据
         * @param data 数据列
         * @param callback(cell,data,field) 回调函数处理每个单元格的数据
         */
        appendData: function (data, callback) {
            for (let item of data) {
                $.activeTable.appendRow(item, callback);
            }
        },
        /**
         * 插入数据 在最后一行前插入一行数据
         * @param data 该行的数据
         * @param callback(cell,data,field) 回调函数处理每个单元格的数据
         */
        insertData: function (data, callback) {
            for (let item of data) {
                $.activeTable.insertRow(item, callback);
            }
        }
    }
});

这只是个基本的框架 之前的动态表格就是这么整得 只不过那个只是数据展示 现在要做成表单填写提交数据

按照业务需要 在业务的公共js文件中编写如下配置 配置比框架代码都多 看着好头大

主要是处理联动部分写的太多了

var titleList = ['参数名', '参数值', '操作'];
var fieldList = ['paramKey', 'paramValue', 'options'];

// 自定义的配置选项 业务是配置设备参数 参数不一定都有 但是唯一 只能出现一次
// dictKeys 表示可能使用的参数键
// usedKeys 表示已经使用的参数键
// addParamKey 是添加行的时候在paramKey的时候调用的方法
// addParamValue 是添加行的时候在paramValue的时候调用的方法
var defaultOptions = {
    id: 'device_params',
    titleList: titleList,
    fieldList: fieldList,
    dictKeys: [],
    usedKeys: [],
    addParamKey: function (options, data) {
        // js的this好麻烦 不注意就成window了
        let dictKeys = this.dictKeys;
        let usedKeys = this.usedKeys;
        // 按照业务需求创建下拉框
        let paramKey = document.createElement('select');
        $(paramKey).attr('name', 'paramKeys');
        // 加上ruoyi提供的样式
        $(paramKey).addClass('form-control');
        // ruoyi框架提供了dict转下拉框的option的方法 直接调用了 自己实现也不难 遍历一遍键值创建一堆option填充数据就行
        paramKey.innerHTML = $.common.dictToSelect(dictKeys);
        // 先隐藏已经使用过的键
        for (let usedKey of usedKeys) {
            $(paramKey).find('option[value="' + usedKey + '"]').attr('hidden', 'true');
        }
        // 如果这行数据不空 将这行数据的key置成选中状态 并去掉隐藏属性
        // 建议直接移除hidden属性 而不是置为false
        if (data) {
            $(paramKey).find('option[value="' + data['paramKey'] + '"]').attr('selected', 'true');
            $(paramKey).find('option[value="' + data['paramKey'] + '"]').removeAttr('hidden');
        } else {
            // 如果这是个空行 将第一个未使用的key置为选中状态
            let optionList = $(paramKey).find('option');
            for (let usedKey of usedKeys) {
                optionList = $(optionList).not('[value="' + usedKey + '"]');
            }
            $(optionList).first().attr('selected', 'true');
            let initKey = $(paramKey).val();
            // 将该表格内其他的该键设置为隐藏 因为这个时候该select还没有添加进表格 所以该行不受影响
            $('#' + options.id).find('option[value=' + initKey + ']').each(function (idx, e) {
                $(e).attr('hidden', 'true');
            });
        }
        // 将该键放到使用的键集合中
        let selectKey = $(paramKey).val();
        if (usedKeys.indexOf(selectKey) == -1) {
            usedKeys.push(selectKey);
        }
        // 如果所有的键都使用完成 则隐藏添加按钮
        if (dictKeys.length == usedKeys.length) {
            $('#' + options.id).find('tr:last').attr('hidden', 'true');
        }
        // 以下为当某个下拉框发生变动 则联动其他下拉框隐藏和显示键
        let oldKey = null;
        // 当下拉框聚焦的时候获取值作为旧值
        $(paramKey).focus(function (e) {
            oldKey = $(e.target).val();
        }).change(function (e) {
            // 当发生改变的时候 获取新的值
            let newKey = $(e.target).val();
            let oldIdx = usedKeys.indexOf(oldKey);
            usedKeys.splice(oldIdx, 1);
            usedKeys.push(newKey);
            // 显示旧的值
            $('#' + options.id).find('option[value=' + oldKey + ']').each(function (idx, e) {
                $(e).removeAttr('hidden');
            });
            // 隐藏新的值
            $('#' + options.id).find('option[value=' + newKey + ']').each(function (idx, e) {
                $(e).attr('hidden', 'true');
            });
            // 旧的值取消选中状态
            $(paramKey).find('option[value=' + oldKey + ']').removeAttr('selected');
            // 该下拉框新的值取消隐藏状态
            $(paramKey).find('option[value=' + newKey + ']').removeAttr('hidden');
            // 该下拉框新的值设置选中状态
            $(paramKey).find('option[value=' + newKey + ']').attr('selected', 'true');
            oldKey = newKey;
        });

        return paramKey;
    },
    addParamValue: function (data) {
        // 参数值就是个input框 没啥说的
        let paramValue = document.createElement('input');
        $(paramValue).attr('name', 'paramValues');
        // 加上ruoyi提供的样式
        $(paramValue).addClass('form-control');
        if (data) {
            $(paramValue).val(data['paramValue']);
        }
        $(paramValue).attr('name', 'paramValues');
        return paramValue;
    },
    callback: function (options, element, data, field) {
        // 根据字段分别设置行为
        if (field == 'paramKey') {
            $(element).empty();
            if (data) {
                let paramKey = options.addParamKey(options, data);
                element.append(paramKey);
            }
        }
        if (field == 'paramValue') {
            $(element).empty();
            if (data) {
                let paramValue = options.addParamValue(data);
                element.append(paramValue);
            }
        }
        if (field == 'options') {
            // 如果有数据 则添加删除按钮
            if (data) {
                $(element).empty();
                $(element).css('text-align', 'center');
                let btn = createElement('a', element, function (e) {
                    $(e).addClass('btn');
                    $(e).addClass('btn-warning');
                    $(e).text('删除');
                });
                $(btn).click(function () {
                    let tr = $(element).parent();
                    tr.remove();
                    // 删除后需要联动其他下拉框 更新可用的键
                    let key = $(element).parent().find('select').val();
                    let index = options.usedKeys.indexOf(key);
                    options.usedKeys.splice(index, 1);
                    $('#' + options.id).find('option[value=' + key + ']').each(function (idx, e) {
                        $(e).removeAttr('hidden');
                    });
                    // 如果有可用的键 则显示添加按钮
                    if (options.dictKeys.length > options.usedKeys.length) {
                        $('#' + options.id).find('tr:last').attr('hidden', true);
                    }
                });
            } else {
                // 如果没有数据 则增加个添加参数的按钮 这一段只在初始化表格的时候使用
                let blankLine = $(element).parent();
                $(blankLine).empty();
                $(blankLine).append(element);
                $(element).attr('colspan', '3');

                let btn = createElement('a', element, function (e) {
                    $(e).addClass('btn');
                    $(e).addClass('btn-success');
                    $(e).css('width', '100%');
                    $(e).text('添加');
                });
                // 该按钮的行为和上面的回调函数应该相同
                $(btn).click(function () {
                    // 以下逻辑与上面大致相同 但要注意点击按钮新增的行数据肯定为空 而且只有删除功能
                    $.activeTable.insertRow(null, function (options, cellElement, data, field) {
                        let rowData = data == null ? null : data[field];
                        if (field == 'paramKey') {
                            $(cellElement).empty();
                            let paramKey = options.addParamKey(options, rowData);
                            cellElement.append(paramKey);
                        }
                        if (field == 'paramValue') {
                            $(cellElement).empty();
                            let paramValue = options.addParamValue(rowData);
                            cellElement.append(paramValue);
                        }
                        if (field == 'options') {
                            $(cellElement).empty();
                            $(cellElement).css('text-align', 'center');
                            let btn = createElement('a', cellElement, function (e) {
                                $(e).addClass('btn');
                                $(e).addClass('btn-warning');
                                $(e).text('删除');
                            });
                            $(btn).click(function () {
                                let tr = $(cellElement).parent();
                                tr.remove();
                                let key = $(cellElement).parent().find('select').val();
                                let index = options.usedKeys.indexOf(key);
                                options.usedKeys.splice(index, 1);
                                $('#' + options.id).find('option[value=' + key + ']').each(function (idx, e) {
                                    $(e).removeAttr('hidden');
                                });
                                if (options.dictKeys.length > options.usedKeys.length) {
                                    $('#' + options.id).find('tr:last').removeAttr('hidden');
                                }
                            });
                        }
                    });
                });
            }
        }
    }
};

里面一些class样式是直接用的bootstrap的 大概吧。。。 我对前端不是很熟 不对 是很不熟

代码也许很烂 但是功能实现了

用的时候html部分扔一个<table id='active-table'></table>

id的名称随便整 但是要和options中的一样 因为需要找到这个表格 然后

let options = objClone(defaultOptions);
options.id = 'device_params'; // 这个就是表格的id
options.dataList = deviceParams; // 从后台获取
options.dictKeys = paramKeys; // 从后台获取

for(let item of deviceParams) {
    options.usedKeys.push(item['paramKey']);
}
$.activeTable.init(options);
$.activeTable.load();

objClone是个深拷贝对象的方法 去百度能找到不少 反正就是最好不要修改业务默认配置 免得引起什么问题

到这就结束了