/** * layui.treeTable * 树表组件 */ layui.define(['table'], function (exports) { "use strict"; var $ = layui.$; var form = layui.form; var table = layui.table; var hint = layui.hint(); // api var treeTable = { config: {}, // 事件 on: table.on, // 遍历字段 eachCols: table.eachCols, index: table.index, set: function (options) { var that = this; that.config = $.extend({}, that.config, options); return that; }, }; // 操作当前实例 var thisTreeTable = function () { var that = this , options = that.config , id = options.id || options.index; return { config: options, reload: function (options, deep) { that.reload.call(that, options, deep); }, reloadData: function (options, deep) { treeTable.reloadData(id, options, deep); } } } // 获取当前实例 var getThisTable = function (id) { var that = thisTreeTable.that[id]; if (!that) hint.error(id ? ('The treeTable instance with ID \'' + id + '\' not found') : 'ID argument required'); return that || null; } // 获取当前实例配置项 var getThisTableConfig = function (id) { return getThisTable(id).config; } // 字符 var MOD_NAME = 'treeTable' , ELEMTREE = '.layui-table-tree' , THIS = 'layui-this' , SHOW = 'layui-show' , HIDE = 'layui-hide' , HIDE_V = 'layui-hide-v' , DISABLED = 'layui-disabled' , NONE = 'layui-none' , ELEM_VIEW = '.layui-table-view' , ELEM_TOOL = '.layui-table-tool' , ELEM_BOX = '.layui-table-box' , ELEM_INIT = '.layui-table-init' , ELEM_HEADER = '.layui-table-header' , ELEM_BODY = '.layui-table-body' , ELEM_MAIN = '.layui-table-main' , ELEM_FIXED = '.layui-table-fixed' , ELEM_FIXL = '.layui-table-fixed-l' , ELEM_FIXR = '.layui-table-fixed-r' , ELEM_TOTAL = '.layui-table-total' , ELEM_PAGE = '.layui-table-page' , ELEM_SORT = '.layui-table-sort' , ELEM_EDIT = 'layui-table-edit' , ELEM_HOVER = 'layui-table-hover' , ELEM_GROUP = 'laytable-cell-group' var LAY_DATA_INDEX = 'LAY_DATA_INDEX'; var LAY_DATA_INDEX_HISTORY = 'LAY_DATA_INDEX_HISTORY'; var LAY_PARENT_INDEX = 'LAY_PARENT_INDEX'; // 构造器 var Class = function (options) { var that = this; that.index = ++treeTable.index; that.config = $.extend(true, {}, that.config, treeTable.config, options); // 处理一些属性 that.init(); that.render(); }; Class.prototype.init = function () { var that = this; var options = that.config; // 先初始一个空的表格以便拿到对应的表格实例信息 var tableIns = table.render($.extend({}, options, { data: [], url: '', done: null })) var id = tableIns.config.id; thisTreeTable.that[id] = that; // 记录当前实例对象 that.tableIns = tableIns; var treeOptions = options.tree; var isParentKey = treeOptions.data.key.isParent; var childrenKey = treeOptions.data.key.children; // 处理属性 var parseData = options.parseData; var done = options.done; if (options.url) { // 异步加载的时候需要处理parseData进行转换 options.parseData = function () { var parseDataThat = this; var args = arguments; var retData = args[0]; if (layui.type(parseData) === 'function') { retData = parseData.apply(parseDataThat, args) || args[0]; } var dataName = parseDataThat.response.dataName; // 处理simpleData if (treeOptions.data.simpleData.enable && !treeOptions.async.enable) { // 异步加载和simpleData不应该一起使用 retData[dataName] = that.flatToTree(retData[dataName]); } that.initData(retData[dataName]); return retData; } } else { options.data = options.data || []; // 处理simpleData if (treeOptions.data.simpleData.enable) { options.data = that.flatToTree(options.data); } if (options.initSort && options.initSort.type) { options.data = layui.sort(options.data, options.initSort.field, options.initSort.type === 'desc') } that.initData(options.data); } options.done = function () { var args = arguments; var doneThat = this; var tableView = this.elem.next(); that.updateStatus(null, { LAY_HAS_EXPANDED: false // 去除已经打开过的状态 }) that.renderTreeTable(tableView); if (layui.type(done) === 'function') { return done.apply(doneThat, args); } } } // 初始默认配置 Class.prototype.config = { tree: { view: { indent: 14, // 层级缩进量 flexIconClose: '', // 关闭时候的折叠图标 flexIconOpen: '', // 打开时候的折叠图标 showIcon: true, // 是否显示图标(节点类型图标) icon: '', // 节点图标,如果设置了这个属性或者数据中有这个字段信息,不管打开还是关闭都以这个图标的值为准 iconClose: '', // 打开时候的图标 iconOpen: '', // 关闭时候的图标 iconLeaf: '', // 叶子节点的图标 showFlexIconIfNotParent: false, // 当节点不是父节点的时候是否显示折叠图标 dblClickExpand: true, // 双击节点时,是否自动展开父节点的标识 }, data: { key: { checked: 'LAY_CHECKED', // 节点数据中保存 check 状态的属性名称 children: "children", // 节点数据中保存子节点数据的属性名称 isParent: "isParent", // 节点数据保存节点是否为父节点的属性名称 isHidden: "hidden", // 节点数据保存节点是否隐藏的属性名称 name: "name", // 节点数据保存节点名称的属性名称 title: "", // 节点数据保存节点提示信息的属性名称 url: "url", // 节点数据保存节点链接的目标 URL 的属性名称 }, simpleData: { enable: false, // 是否简单数据模式 idKey: "id", // 唯一标识的属性名称 pIdKey: "parentId", rootPId: null } }, async: { enable: false, // 是否开启异步加载模式,只有开启的时候其他参数才起作用 url: '', // 异步加载的接口,可以根据需要设置与顶层接口不同的接口,如果相同可以不设置该参数 type: null, // 请求的接口类型,设置可缺省同上 contentType: null, // 提交参数的数据类型,设置可缺省同上 headers: null, // 设置可缺省同上 where: null, // 设置可缺省同上 autoParam: [], // 自动参数 }, callback: { beforeExpand: null, // 展开前的回调 return false 可以阻止展开的动作 onExpand: null, // 展开之后的回调 } }, autoSort: false }; Class.prototype.getOptions = function () { var that = this; if (that.tableIns) { return table.getOptions(that.tableIns.config.id); // 获取表格的实时配置信息 } else { return that.config; } }; function flatToTree(flatArr, idKey, pIdKey, childrenKey) { idKey = idKey || 'id'; pIdKey = pIdKey || 'parentId'; childrenKey = childrenKey || 'children'; // 创建一个空的 nodes 对象,用于保存所有的节点 var nodes = {}; // 遍历所有节点,将其加入 nodes 对象中 layui.each(flatArr, function (index, item) { nodes[item[idKey]] = $.extend({}, item); nodes[item[idKey]][childrenKey] = []; }) // 遍历所有节点,将其父子关系加入 nodes 对象 layui.each(nodes, function (index, item) { if (item[pIdKey] && nodes[item[pIdKey]]) { nodes[item[pIdKey]][childrenKey].push(item); } }) // 返回顶层节点,即所有节点中 parentId 为 null 的节点 return Object.values(nodes).filter(function (item) { return !item[pIdKey]; }) } Class.prototype.flatToTree = function (tableData) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var tableId = options.id; tableData = tableData || table.cache[tableId]; return flatToTree(tableData, treeOptions.data.simpleData.idKey, treeOptions.data.simpleData.pIdKey, treeOptions.data.key.children) } Class.prototype.treeToFlat = function (tableData, parentId, parentIndex) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var childrenKey = treeOptions.data.key.children; var pIdKey = treeOptions.data.simpleData.pIdKey; var flat = []; layui.each(tableData, function (i1, item1) { var dataIndex = (parentIndex ? parentIndex + '-' : '') + i1; var dataNew = $.extend({}, item1); dataNew[childrenKey] = null; dataNew[pIdKey] = item1[pIdKey] || parentId; flat.push(dataNew); flat = flat.concat(that.treeToFlat(item1[childrenKey], item1[treeOptions.data.simpleData.idKey], dataIndex)); }); return flat; } // 通过index获取节点数据 Class.prototype.getNodeDataByIndex = function (index, clone, newValue) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var tableId = options.id; var dataCache = table.cache[tableId][index]; if (newValue !== 'delete' && dataCache) { return clone ? $.extend({}, dataCache) : dataCache; } var tableData = that.getTableData(); index += ''; var indexArr = index.split('-'); var dataRet = tableData; var tableCache = (options.url || indexArr.length > 1) ? null : table.cache[tableId]; // 只有在删除根节点的时候才需要处理 for (var i = 0, childrenKey = treeOptions.data.key.children; i < indexArr.length; i++) { if (newValue && i === indexArr.length - 1) { if (newValue === 'delete') { // 删除 if (tableCache) { // 同步cache tableCache.splice(tableCache.findIndex(function (value) { return value[LAY_DATA_INDEX] === index; }), 1) } return (i ? dataRet[childrenKey] : dataRet).splice(indexArr[i], 1)[0]; } else { // 更新值 $.extend((i ? dataRet[childrenKey] : dataRet)[indexArr[i]], newValue); } } dataRet = i ? dataRet[childrenKey][indexArr[i]] : dataRet[indexArr[i]]; } return clone ? $.extend({}, dataRet) : dataRet; } treeTable.getNodeDataByIndex = function (id, index) { var that = getThisTable(id); return that.getNodeDataByIndex(index, true); } // 判断是否是父节点 var checkIsParent = function (data, isParentKey, childrenKey) { isParentKey = isParentKey || 'isParent'; childrenKey = childrenKey || 'children'; layui.each(data, function (i1, item1) { if (!(isParentKey in item1)) { item1[isParentKey] = !!(item1[childrenKey] && item1[childrenKey].length); checkIsParent(item1[childrenKey]); } }) } Class.prototype.initData = function (data, parentIndex) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var tableId = options.id; data = data || that.getTableData(); var isParentKey = treeOptions.data.key.isParent; var childrenKey = treeOptions.data.key.children; layui.each(data, function (i1, item1) { if (!(isParentKey in item1)) { item1[isParentKey] = !!(item1[childrenKey] && item1[childrenKey].length); } item1[LAY_DATA_INDEX_HISTORY] = item1[LAY_DATA_INDEX]; item1[LAY_PARENT_INDEX] = parentIndex = parentIndex || ''; var dataIndex = item1[LAY_DATA_INDEX] = (parentIndex ? parentIndex + '-' : '') + i1; dataIndex.indexOf('-') !== -1 && (table.cache[tableId][dataIndex] = item1); that.initData(item1[childrenKey] || [], dataIndex); }); return data; } var expandNode = function (treeNode, expandFlag, sonSign, focus, callbackFlag) { // treeNode // 需要展开的节点 var trElem = treeNode.trElem; var tableViewElem = trElem.closest(ELEM_VIEW); var tableViewFilterId = tableViewElem.attr('lay-filter'); var tableId = tableViewElem.attr('lay-id'); var options = table.getOptions(tableId); var treeOptions = options.tree || {}; var isParentKey = treeOptions.data.key.isParent; var trIndex = trElem.attr('data-index'); // 可能出现多层 var treeTableThat = getThisTable(tableId); var tableData = treeTableThat.getTableData(); var trData = treeTableThat.getNodeDataByIndex(trIndex); var dataLevel = trElem.data('level'); var dataLevelNew = (dataLevel || 0) + 1; // 后续调优:对已经展开的节点进行展开和已经关闭的节点进行关闭应该做优化减少不必要的代码执行 todo var isToggle = layui.type(expandFlag) !== 'boolean'; // var LAY_EXPAND = (layui.type(expandFlag) === 'boolean' ? expandFlag : !trData['LAY_EXPAND']); var LAY_EXPAND = isToggle ? !trData['LAY_EXPAND'] : expandFlag; var retValue = trData[isParentKey] ? LAY_EXPAND : null; if (callbackFlag && LAY_EXPAND != trData['LAY_EXPAND'] && (!trData['LAY_ASYNC_STATUS'] || trData['LAY_ASYNC_STATUS'] === 'local')) { var beforeExpand = treeOptions.callback.beforeExpand; if (layui.type(beforeExpand) === 'function') { if (beforeExpand(tableId, trData, expandFlag) === false) { return retValue; } } } var LAY_HAS_EXPANDED = trData['LAY_HAS_EXPANDED']; // 展开过,包括异步加载 // 找到表格中的同类节点(需要找到data-index一致的所有行) var trsElem = tableViewElem.find('tr[data-index="' + trIndex + '"]'); // 处理折叠按钮图标 var flexIconElem = trsElem.find('.layui-table-tree-flexIcon'); flexIconElem.html(LAY_EXPAND ? treeOptions.view.flexIconOpen : treeOptions.view.flexIconClose) trData[isParentKey] && flexIconElem.css('visibility', 'visible'); // 处理节点图标 if (treeOptions.view.showIcon && trData[isParentKey] && !trData.icon && !treeOptions.view.icon) { var nodeIconElem = trsElem.find('.layui-table-tree-nodeIcon'); nodeIconElem.html(LAY_EXPAND ? treeOptions.view.iconOpen : treeOptions.view.iconClose); } var childNodes = trData[treeOptions.data.key.children] || []; // 测试用后续需要改成子节点的字段名称 // 处理子节点展示与否 if (LAY_EXPAND) { // 展开 if (LAY_HAS_EXPANDED) { // 已经展开过 if (LAY_EXPAND == trData['LAY_EXPAND']) { // 已经和当前的状态一样,是否有优化的空间,要注意直接方法调用级联展开和触发和不是级联的情况下的区别 } trData['LAY_EXPAND'] = LAY_EXPAND; tableViewElem.find(childNodes.map(function (value, index, array) { return 'tr[data-index="' + value[LAY_DATA_INDEX] + '"]' }).join(',')).removeClass('layui-hide'); layui.each(childNodes, function (i1, item1) { if (sonSign && !isToggle) { // 非状态切换的情况下 // 级联展开子节点 expandNode({trElem: tableViewElem.find('tr[data-index="' + item1.LAY_DATA_INDEX + '"]').first()}, expandFlag, sonSign, focus, callbackFlag); } else if (item1.LAY_EXPAND) { // 级联展开 expandNode({trElem: tableViewElem.find('tr[data-index="' + item1.LAY_DATA_INDEX + '"]').first()}, true); } }) } else { var asyncSetting = treeOptions.async || {}; var asyncUrl = asyncSetting.url || options.url; // 提供一个能支持用户在获取子数据转换调用的回调,这样让子节点数据获取更加灵活 todo if (asyncSetting.enable && asyncUrl && !trData['LAY_ASYNC_STATUS']) { trData['LAY_ASYNC_STATUS'] = 'loading'; var params = {}; // 参数 var data = $.extend(params, asyncSetting.where || options.where); var asyncAutoParam = asyncSetting.autoParam; layui.each(asyncAutoParam, function (index, item) { var itemStr = item; var itemArr = item.split('='); data[itemArr[0].trim()] = trData[(itemArr[1] || itemArr[0]).trim()] }) var asyncContentType = asyncSetting.contentType || options.contentType; if (asyncContentType && asyncContentType.indexOf("application/json") == 0) { // 提交 json 格式 data = JSON.stringify(data); } var asyncType = asyncSetting.method || options.method; var asyncDataType = asyncSetting.dataType || options.dataType; var asyncJsonpCallback = asyncSetting.jsonpCallback || options.jsonpCallback; var asyncHeaders = asyncSetting.headers || options.headers; var asyncParseData = asyncSetting.parseData || options.parseData; var asyncResponse = asyncSetting.response || options.response; // that.loading(); flexIconElem.html('') $.ajax({ type: asyncType || 'get' , url: asyncUrl , contentType: asyncContentType , data: data , dataType: asyncDataType || 'json' , jsonpCallback: asyncJsonpCallback , headers: asyncHeaders || {} , success: function (res) { trData['LAY_ASYNC_STATUS'] = 'success'; // 若有数据解析的回调,则获得其返回的数据 if (typeof asyncParseData === 'function') { res = asyncParseData.call(options, res) || res; } // 检查数据格式是否符合规范 if (res[asyncResponse.statusName] != asyncResponse.statusCode) { trData['LAY_ASYNC_STATUS'] = 'error'; // 异常处理 todo flexIconElem.html(''); // 事件 } else { trData[treeOptions.data.key.children] = res[asyncResponse.dataName]; treeTableThat.initData(trData[treeOptions.data.key.children], trData[LAY_DATA_INDEX]) // 正常返回 expandNode(treeNode, true, isToggle ? false : sonSign, focus, callbackFlag); } } , error: function (e, msg) { trData['LAY_ASYNC_STATUS'] = 'error'; // 异常处理 todo typeof options.error === 'function' && options.error(e, msg); } }); return retValue; } trData['LAY_EXPAND'] = LAY_EXPAND; LAY_HAS_EXPANDED = trData['LAY_HAS_EXPANDED'] = true; if (childNodes.length) { // 判断是否需要排序 if (options.initSort && !options.url) { var initSort = options.initSort; if (initSort.type) { childNodes = trData[treeOptions.data.key.children] = layui.sort(childNodes, initSort.field, initSort.type === 'desc'); } else { // 恢复默认 childNodes = trData[treeOptions.data.key.children] = layui.sort(childNodes, table.config.indexName); } } treeTableThat.initData(trData[treeOptions.data.key.children], trData[LAY_DATA_INDEX]); // 将数据通过模板得出节点的html代码 var str2 = table.getTrHtml(tableId, childNodes, null, null, trIndex); var str2Obj = { trs: $(str2.trs.join('')), trs_fixed: $(str2.trs_fixed.join('')), trs_fixed_r: $(str2.trs_fixed_r.join('')) } layui.each(childNodes, function (childIndex, childItem) { str2Obj.trs.eq(childIndex).attr({'data-index': childItem[LAY_DATA_INDEX], 'data-level': dataLevelNew}) str2Obj.trs_fixed.eq(childIndex).attr({ 'data-index': childItem[LAY_DATA_INDEX], 'data-level': dataLevelNew }) str2Obj.trs_fixed_r.eq(childIndex).attr({ 'data-index': childItem[LAY_DATA_INDEX], 'data-level': dataLevelNew }) }) tableViewElem.find(ELEM_MAIN).find('tbody tr[data-index="' + trIndex + '"]').after(str2Obj.trs); tableViewElem.find(ELEM_FIXL).find('tbody tr[data-index="' + trIndex + '"]').after(str2Obj.trs_fixed); tableViewElem.find(ELEM_FIXR).find('tbody tr[data-index="' + trIndex + '"]').after(str2Obj.trs_fixed_r); // 初始化新增的节点中的内容 layui.each(str2Obj, function (key, item) { treeTableThat.renderTreeTable(item, dataLevelNew); }) if (sonSign && !isToggle) { // 非状态切换的情况下 // 级联展开/关闭子节点 layui.each(childNodes, function (i1, item1) { expandNode({trElem: tableViewElem.find('tr[data-index="' + item1.LAY_DATA_INDEX + '"]').first()}, expandFlag, sonSign, focus, callbackFlag); }) } } } } else { if (LAY_EXPAND == trData['LAY_EXPAND']) { } trData['LAY_EXPAND'] = LAY_EXPAND; // 折叠 if (sonSign && !isToggle) { // 非状态切换的情况下 layui.each(childNodes, function (i1, item1) { expandNode({trElem: tableViewElem.find('tr[data-index="' + item1.LAY_DATA_INDEX + '"]').first()}, expandFlag, sonSign, focus, callbackFlag); }); tableViewElem.find(childNodes.map(function (value, index, array) { // 只隐藏直接子节点,其他由递归的处理 return 'tr[data-index="' + value[LAY_DATA_INDEX] + '"]' }).join(',')).addClass('layui-hide'); } else { var childNodesFlat = treeTableThat.treeToFlat(childNodes, trData[treeOptions.data.simpleData.idKey], trIndex); tableViewElem.find(childNodesFlat.map(function (value, index, array) { return 'tr[data-index="' + value[LAY_DATA_INDEX] + '"]' }).join(',')).addClass('layui-hide'); } } table.resize(tableId); if (callbackFlag && trData['LAY_ASYNC_STATUS'] !== 'loading') { var onExpand = treeOptions.callback.onExpand; layui.type(onExpand) === 'function' && onExpand(tableId, trData, expandFlag); } return retValue; } treeTable.expandNode = function (id, index, expandFlag, sonSign, callbackFlag) { var that = getThisTable(id); var options = that.getOptions(); var tableViewElem = options.elem.next(); return expandNode({trElem: tableViewElem.find('tr[data-index="' + index + '"]').first()}, expandFlag, sonSign, null, callbackFlag) } // 目前还有性能问题特别是在data模式需要优化暂时不能使用 todo treeTable.expandAll = function (id, expandFlag) { if (layui.type(expandFlag) !== 'boolean') { return hint.error('expandAll的展开状态参数只接收true/false') } layui.each(table.cache[id], function (i1, item1) { treeTable.expandNode(id, item1['LAY_DATA_INDEX'], expandFlag, true); }) } Class.prototype.renderTreeTable = function (tableView, level, sonSign) { var that = this; var options = that.getOptions(); var tableViewElem = options.elem.next(); tableViewElem.addClass('layui-table-tree'); var tableId = options.id; var treeOptions = options.tree || {}; var treeOptionsData = treeOptions.data || {}; var treeOptionsView = treeOptions.view || {}; var isParentKey = treeOptionsData.key.isParent; var tableFilterId = tableViewElem.attr('lay-filter'); var treeTableThat = that; // var tableData = treeTableThat.getTableData(); level = level || 0; if (!level) { // 初始化的表格里面没有level信息,可以作为顶层节点的判断 tableViewElem.find('.layui-table-body tr:not([data-level])').attr('data-level', level); layui.each(table.cache[tableId], function (dataIndex, dataItem) { tableViewElem.find('.layui-table-main tbody tr[data-level="0"]:eq(' + dataIndex + ')').attr('data-index', dataItem[LAY_DATA_INDEX]); tableViewElem.find('.layui-table-fixed-l tbody tr[data-level="0"]:eq(' + dataIndex + ')').attr('data-index', dataItem[LAY_DATA_INDEX]); tableViewElem.find('.layui-table-fixed-r tbody tr[data-level="0"]:eq(' + dataIndex + ')').attr('data-index', dataItem[LAY_DATA_INDEX]); }) } var dataExpand = {}; // 记录需要展开的数据 var nameKey = treeOptions.data.key.name; var indent = treeOptions.view.indent || 14; // 后期需要添加一个配置来决定展开的图标放在哪个字段 layui.each(tableView.find('td[data-field="' + nameKey + '"]'), function (index, item) { item = $(item); var trElem = item.closest('tr'); var itemCell = item.children('.layui-table-cell'); if (itemCell.hasClass('layui-table-tree-item')) { return; } itemCell.addClass('layui-table-tree-item'); var trIndex = trElem.attr('data-index'); if (!trIndex) { // 排除在统计行中的节点 return; } var trData = treeTableThat.getNodeDataByIndex(trIndex); if (trData.LAY_EXPAND) { // 需要展开 dataExpand[trIndex] = true; } var tableCellElem = item.find('div.layui-table-cell'); var htmlTemp = tableCellElem.html(); var flexIconElem = item.find('div.layui-table-cell') .html(['