/** * 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; }, resize: table.resize, getOptions: table.getOptions, hideCol: table.hideCol, renderData: table.renderData }; // 操作当前实例 var thisTreeTable = function () { var that = this; var options = that.config; var 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); } }; }; /** * 获取当前实例 * @param {string} id 表格id * @returns {Class} */ 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 MOD_NAME = 'treeTable'; var MOD_ID = 'lay-table-id'; var HIDE = 'layui-hide'; var ELEM_VIEW = '.layui-table-view'; // var ELEM_TREE = '.layui-table-tree'; // var ELEM_TOOL = '.layui-table-tool'; // var ELEM_BOX = '.layui-table-box'; // var ELEM_HEADER = '.layui-table-header'; var ELEM_BODY = '.layui-table-body'; var ELEM_MAIN = '.layui-table-main'; // var ELEM_FIXED = '.layui-table-fixed'; var ELEM_FIXL = '.layui-table-fixed-l'; var ELEM_FIXR = '.layui-table-fixed-r'; var ELEM_CHECKED = 'layui-table-checked'; var TABLE_TREE = 'layui-table-tree'; var LAY_DATA_INDEX = 'LAY_DATA_INDEX'; var LAY_DATA_INDEX_HISTORY = 'LAY_DATA_INDEX_HISTORY'; var LAY_PARENT_INDEX = 'LAY_PARENT_INDEX'; var LAY_CHECKBOX_HALF = 'LAY_CHECKBOX_HALF'; var LAY_EXPAND = 'LAY_EXPAND'; var LAY_HAS_EXPANDED = 'LAY_HAS_EXPANDED'; var LAY_ASYNC_STATUS = 'LAY_ASYNC_STATUS'; var LAY_CASCADE = ['all', 'parent', 'children', 'none']; var HTML_TAG_RE = /<[^>]+?>/; var ICON_PROPS = [ 'flexIconClose', 'flexIconOpen', 'iconClose', 'iconOpen', 'iconLeaf', 'icon' ]; /** * 构造器 * @class */ var Class = function (options) { var that = this; that.index = ++treeTable.index; that.config = $.extend(true, {}, that.config, treeTable.config, options); // 处理一些属性 that.init(); that.render(); }; var updateCache = function (id, childrenKey, data) { var tableCache = table.cache[id]; layui.each(data || tableCache, function (index, item) { var itemDataIndex = item[LAY_DATA_INDEX] || ''; if (itemDataIndex.indexOf('-') !== -1) { tableCache[itemDataIndex] = item; } item[childrenKey] && updateCache(id, childrenKey, item[childrenKey]); }); }; var updateOptions = function (id, options, reload) { var that = getThisTable(id); reload === 'reloadData' || (that.status = { // 用于记录一些状态信息 expand: {} // 折叠状态 }); var thatOptionsTemp = $.extend(true, {}, that.getOptions(), options); var treeOptions = thatOptionsTemp.tree; var childrenKey = treeOptions.customName.children; var idKey = treeOptions.customName.id; // 处理属性 delete options.hasNumberCol; delete options.hasChecboxCol; delete options.hasRadioCol; table.eachCols( null, function (i1, item1) { if (item1.type === 'numbers') { options.hasNumberCol = true; } else if (item1.type === 'checkbox') { options.hasChecboxCol = true; } else if (item1.type === 'radio') { options.hasRadioCol = true; } }, thatOptionsTemp.cols ); var parseData = options.parseData; var done = options.done; // treeTable重载数据时,会先加载显示顶层节点,然后根据重载数据前的子节点展开状态,展开相应的子节点, // 那么如果重载数据前有滚动条滚动在某个位子,重新加载时顶层节点如果比较少,只显示顶层节点时没有滚动条的情况下, // 自动展开子节点后,滚动条就会显示在顶部,无法保持在重载数据之前的位置。 // 处理保持滚动条的问题,重载数据前记录滚动条的位置 if (reload === 'reloadData' && thatOptionsTemp.scrollPos === 'fixed') { that.scrollTopCache = that.config.elem.next().find(ELEM_BODY).scrollTop(); } if (thatOptionsTemp.url) { // 异步加载的时候需要处理parseData进行转换 if (!reload || (reload && parseData && !parseData.mod)) { 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; // 处理 isSimpleData if (treeOptions.data.isSimpleData && !treeOptions.async.enable) { // 异步加载和 isSimpleData 不应该一起使用 retData[dataName] = that.flatToTree(retData[dataName]); } // 处理节点状态 updateStatus( retData[dataName], function (item) { item[LAY_EXPAND] = LAY_EXPAND in item ? item[LAY_EXPAND] : item[idKey] !== undefined && that.status.expand[item[idKey]]; }, childrenKey ); if ( parseDataThat.autoSort && parseDataThat.initSort && parseDataThat.initSort.type ) { layui.sort( retData[dataName], parseDataThat.initSort.field, parseDataThat.initSort.type === 'desc', true ); } that.initData(retData[dataName]); return retData; }; options.parseData.mod = true; } } else { if (options.data !== undefined) { options.data = options.data || []; // 处理 isSimpleData if (treeOptions.data.isSimpleData) { options.data = that.flatToTree(options.data); } that.initData(options.data); } } if (!reload || (reload && done && !done.mod)) { options.done = function () { var args = arguments; var doneThat = this; // undefined: 初始 render 或 reload,两者本质没有区别可以不做区分 // 'reloadData': 重载数据 // 'renderData': 重新渲染数据 var renderType = args[3]; var isRenderData = renderType === 'renderData'; if (!isRenderData) { delete that.isExpandAll; } var tableView = this.elem.next(); that.updateStatus(null, { LAY_HAS_EXPANDED: false // 去除已经打开过的状态 }); // 更新cache中的内容 将子节点也存到cache中 updateCache(id, childrenKey); // 更新全选框的状态 var layTableAllChooseElem = tableView.find( '[name="layTableCheckbox"][lay-filter="layTableAllChoose"]' ); if (layTableAllChooseElem.length) { var checkStatus = treeTable.checkStatus(id); layTableAllChooseElem.prop({ checked: checkStatus.isAll && checkStatus.data.length, indeterminate: !checkStatus.isAll && checkStatus.data.length }); } if ( !isRenderData && thatOptionsTemp.autoSort && thatOptionsTemp.initSort && thatOptionsTemp.initSort.type ) { treeTable.sort(id); } that.renderTreeTable(tableView); // 恢复滚动条位置 if (renderType === 'reloadData' && doneThat.scrollPos === 'fixed') { tableView.find(ELEM_BODY).scrollTop(that.scrollTopCache); } if (layui.type(done) === 'function') { return done.apply(doneThat, args); } }; options.done.mod = true; } // 处理图标 if (options && options.tree && options.tree.view) { layui.each(ICON_PROPS, function (i, iconProp) { if (options.tree.view[iconProp] !== undefined) { options.tree.view[iconProp] = that.normalizedIcon( options.tree.view[iconProp] ); } }); } }; Class.prototype.init = function () { var that = this; var options = that.config; var cascade = options.tree.data.cascade; if (LAY_CASCADE.indexOf(cascade) === -1) { options.tree.data.cascade = 'all'; // 超出范围的都重置为全联动 } // 先初始一个空的表格以便拿到对应的表格实例信息 var tableIns = table.render( $.extend({}, options, { data: [], url: '', done: null }) ); var id = tableIns.config.id; thisTreeTable.that[id] = that; // 记录当前实例对象 that.tableIns = tableIns; updateOptions(id, options); }; // 初始默认配置 Class.prototype.config = { tree: { customName: { children: 'children', // 节点数据中保存子节点数据的属性名称 isParent: 'isParent', // 节点数据保存节点是否为父节点的属性名称 name: 'name', // 节点数据保存节点名称的属性名称 id: 'id', // 唯一标识的属性名称 pid: 'parentId', // 父节点唯一标识的属性名称 icon: 'icon' // 图标的属性名称 }, view: { indent: 14, // 层级缩进量 flexIconClose: '', // 关闭时候的折叠图标 flexIconOpen: '', // 打开时候的折叠图标 showIcon: true, // 是否显示图标(节点类型图标) icon: '', // 节点图标,如果设置了这个属性或者数据中有这个字段信息,不管打开还是关闭都以这个图标的值为准 iconClose: '', // 关闭时候的图标 iconOpen: '', // 打开时候的图标 iconLeaf: '', // 叶子节点的图标 showFlexIconIfNotParent: false, // 当节点不是父节点的时候是否显示折叠图标 dblClickExpand: true, // 双击节点时,是否自动展开父节点的标识 expandAllDefault: false // 默认展开所有节点 }, data: { isSimpleData: false, // 是否简单数据模式 rootPid: null, // 根节点的父 ID 值 cascade: 'all' // 级联方式 默认全部级联:all 可选 级联父 parent 级联子 children }, async: { enable: false, // 是否开启异步加载模式,只有开启的时候其他参数才起作用 url: '', // 异步加载的接口,可以根据需要设置与顶层接口不同的接口,如果相同可以不设置该参数 type: null, // 请求的接口类型,设置可缺省同上 contentType: null, // 提交参数的数据类型,设置可缺省同上 headers: null, // 设置可缺省同上 where: null, // 设置可缺省同上 autoParam: [] // 自动参数 }, callback: { beforeExpand: null, // 展开前的回调 return false 可以阻止展开的动作 onExpand: null // 展开之后的回调 } } }; Class.prototype.normalizedIcon = function (iconStr) { return iconStr ? HTML_TAG_RE.test(iconStr) ? iconStr : '' : ''; }; 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, rootPid) { idKey = idKey || 'id'; pIdKey = pIdKey || 'parentId'; childrenKey = childrenKey || 'children'; // 创建一个空的 map 对象,用于保存所有的节点 var map = {}; var rootNodes = []; var idTemp = ''; var pidTemp = ''; layui.each(flatArr, function (index, item) { idTemp = idKey + item[idKey]; pidTemp = idKey + item[pIdKey]; // 将节点存入 map 对象 if (!map[idTemp]) { map[idTemp] = {}; map[idTemp][childrenKey] = []; } // 合并节点 var tempObj = {}; tempObj[childrenKey] = map[idTemp][childrenKey]; map[idTemp] = $.extend({}, item, tempObj); var isRootNode = rootPid ? map[idTemp][pIdKey] === rootPid : !map[idTemp][pIdKey]; if (isRootNode) { rootNodes.push(map[idTemp]); } else { if (!map[pidTemp]) { map[pidTemp] = {}; map[pidTemp][childrenKey] = []; } map[pidTemp][childrenKey].push(map[idTemp]); } }); return rootNodes; } Class.prototype.flatToTree = function (tableData) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var customName = treeOptions.customName; var tableId = options.id; tableData = tableData || table.cache[tableId]; return flatToTree( tableData, customName.id, customName.pid, customName.children, treeOptions.data.rootPid ); }; Class.prototype.treeToFlat = function (tableData, parentId, parentIndex) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var customName = treeOptions.customName; var childrenKey = customName.children; var pIdKey = customName.pid; var flat = []; layui.each(tableData, function (i1, item1) { var dataIndex = (parentIndex ? parentIndex + '-' : '') + i1; var dataNew = $.extend({}, item1); dataNew[pIdKey] = typeof item1[pIdKey] !== 'undefined' ? item1[pIdKey] : parentId; flat.push(dataNew); flat = flat.concat( that.treeToFlat(item1[childrenKey], item1[customName.id], dataIndex) ); }); return flat; }; // 通过当前行数据返回 treeNode 信息 Class.prototype.getTreeNode = function (data) { var that = this; if (!data) { return hint.error('Node data not found'); } // var options = that.getOptions(); // var treeOptions = options.tree; // var tableId = options.id; // var customName = treeOptions.customName; // 带上一些常用的方法 return { data: data, dataIndex: data[LAY_DATA_INDEX], getParentNode: function () { return that.getNodeByIndex(data[LAY_PARENT_INDEX]); } }; }; // 通过 index 返回节点信息 Class.prototype.getNodeByIndex = function (index) { var that = this; var treeNodeData = that.getNodeDataByIndex(index); if (!treeNodeData) { return hint.error('Node data not found by index: ' + index); } var options = that.getOptions(); // var treeOptions = options.tree; // var customName = treeOptions.customName; // var parentKey = customName.parent; var tableId = options.id; var treeNode = { data: treeNodeData, dataIndex: treeNodeData[LAY_DATA_INDEX], getParentNode: function () { return that.getNodeByIndex(treeNodeData[LAY_PARENT_INDEX]); }, update: function (data) { return treeTable.updateNode(tableId, index, data); }, remove: function () { return treeTable.removeNode(tableId, index); }, expand: function (opts) { return treeTable.expandNode( tableId, $.extend({}, opts, { index: index }) ); }, setChecked: function (opts) { return treeTable.setRowChecked( tableId, $.extend({}, opts, { index: index }) ); } }; treeNode.dataIndex = index; return treeNode; }; // 通过 id 获取节点信息 Class.prototype.getNodeById = function (id) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var customName = treeOptions.customName; var idKey = customName.id; // 通过 id 拿到数据的 dataIndex var dataIndex = ''; var tableDataFlat = treeTable.getData(options.id, true); layui.each(tableDataFlat, function (i1, item1) { if (item1[idKey] === id) { dataIndex = item1[LAY_DATA_INDEX]; return true; } }); if (!dataIndex) { return; } // 用 index return that.getNodeByIndex(dataIndex); }; // 通过 index 获取节点数据 Class.prototype.getNodeDataByIndex = function (index, clone, newValue) { var that = this; var options = that.getOptions(); var treeOptions = options.tree; var tableId = options.id; var tableCache = table.cache[tableId]; // 获取当前行中的数据 var dataCache = tableCache[index]; // 若非删除操作,则返回合并后的数据 if (newValue !== 'delete' && dataCache) { $.extend(dataCache, newValue); return clone ? $.extend({}, dataCache) : dataCache; } // 删除操作 var dataRet = tableCache; var indexArr = String(index).split('-'); // if (options.url || indexArr.length > 1) tableCache = null // 只有在删除根节点的时候才需要处理 // 根据 index 进行数据处理 for ( var i = 0, childrenKey = treeOptions.customName.children; i < indexArr.length; i++ ) { if (newValue && i === indexArr.length - 1) { if (newValue === 'delete') { // 删除并返回当前数据 // 同步 cache --- 此段代码注释缘由:data 属性模式造成数据重复执行 splice (@Gitee: #I7Z0A/I82E2S) /*if (tableCache) { layui.each(tableCache, function (i1, item1) { if (item1[LAY_DATA_INDEX] === index) { tableCache.splice(i1, 1); return true; } }) }*/ 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); if (!that) return; 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 customName = treeOptions.customName; var isParentKey = customName.isParent; var childrenKey = customName.children; var update = function (data, parentIndex) { 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); update(item1[childrenKey] || [], dataIndex); }); }; update(data, parentIndex); updateCache(tableId, childrenKey, data); return data; }; // 与 tableId 有关带防抖的方法 var debounceFn = (function () { var fn = {}; return function (tableId, func, wait) { if (!fn[tableId]) { fn[tableId] = layui.debounce(func, wait); } return fn[tableId]; }; })(); // 优化参数,添加一个 getNodeByIndex 方法 只传 表格id 和行 dataIndex 分几步优化 todo var expandNode = function ( treeNode, expandFlag, sonSign, focus, callbackFlag, done ) { // treeNode // 需要展开的节点 var trElem = treeNode.trElem; var tableViewElem = treeNode.tableViewElem || trElem.closest(ELEM_VIEW); var tableId = treeNode.tableId || tableViewElem.attr(MOD_ID); var options = treeNode.options || table.getOptions(tableId); var dataIndex = treeNode.dataIndex || trElem.attr('lay-data-index'); // 可能出现多层 var treeTableThat = getThisTable(tableId); var treeOptions = options.tree || {}; var customName = treeOptions.customName || {}; var isParentKey = customName.isParent; var trData = treeTableThat.getNodeDataByIndex(dataIndex); // 后续调优:对已经展开的节点进行展开和已经关闭的节点进行关闭应该做优化减少不必要的代码执行 todo var isToggle = layui.type(expandFlag) !== 'boolean'; var trExpand = isToggle ? !trData[LAY_EXPAND] : expandFlag; var retValue = trData[isParentKey] ? trExpand : null; if ( callbackFlag && trExpand != 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 trExpanded = trData[LAY_HAS_EXPANDED]; // 展开过,包括异步加载 // 找到表格中的同类节点(需要找到lay-data-index一致的所有行) var trsElem = tableViewElem.find('tr[lay-data-index="' + dataIndex + '"]'); var flexIconElem = trsElem.find('.layui-table-tree-flexIcon'); treeTableThat.updateNodeIcon({ scopeEl: trsElem, isExpand: trExpand, isParent: trData[isParentKey] }); trData[LAY_EXPAND] = trExpand; var trDataId = trData[customName.id]; trDataId !== undefined && (treeTableThat.status.expand[trDataId] = trExpand); if (retValue === null) { return retValue; } var childNodes = trData[customName.children] || []; // 处理子节点展示与否 if (trExpand) { // 展开 if (trExpanded) { // 已经展开过 if (!childNodes.length) return; //异步如果子节点没有数据情况下双点行展开所有已展开的节点问题解决 trsElem .nextAll( childNodes .map(function (value, index, array) { return 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"]'; }) .join(',') ) .removeClass(HIDE); layui.each(childNodes, function (i1, item1) { if (!item1[isParentKey]) { return; } if (sonSign && !isToggle && !item1[LAY_EXPAND]) { // 非状态切换的情况下 // 级联展开子节点 expandNode( { dataIndex: item1[LAY_DATA_INDEX], trElem: tableViewElem .find('tr[lay-data-index="' + item1[LAY_DATA_INDEX] + '"]') .first(), tableViewElem: tableViewElem, tableId: tableId, options: options }, expandFlag, sonSign, focus, callbackFlag, done ); } else if (item1[LAY_EXPAND]) { // 初始化级联展开 expandNode( { dataIndex: item1[LAY_DATA_INDEX], trElem: tableViewElem .find('tr[lay-data-index="' + item1[LAY_DATA_INDEX] + '"]') .first(), tableViewElem: tableViewElem, tableId: tableId, options: options }, true, undefined, undefined, undefined, done ); } }); } else { var asyncSetting = treeOptions.async || {}; var asyncUrl = asyncSetting.url || options.url; if ( asyncSetting.enable && trData[isParentKey] && (!trData[LAY_ASYNC_STATUS] || trData[LAY_ASYNC_STATUS] === 'error') ) { trData[LAY_ASYNC_STATUS] = 'loading'; flexIconElem.html( '' ); // 异步获取子节点数据成功之后处理方法 var asyncSuccessFn = function (data) { trData[LAY_ASYNC_STATUS] = 'success'; trData[customName.children] = data; treeTableThat.initData( trData[customName.children], trData[LAY_DATA_INDEX] ); expandNode( treeNode, true, isToggle ? false : sonSign, focus, callbackFlag, done ); }; var format = asyncSetting.format; // 自定义数据返回方法 if (layui.type(format) === 'function') { format(trData, options, asyncSuccessFn); return retValue; } 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; var ajaxOptions = { type: asyncType || 'get', url: asyncUrl, contentType: asyncContentType, data: data, dataType: asyncDataType || 'json', jsonpCallback: asyncJsonpCallback, headers: asyncHeaders || {}, success: function (res) { // 若有数据解析的回调,则获得其返回的数据 if (typeof asyncParseData === 'function') { res = asyncParseData.call(options, res) || res; } // 检查数据格式是否符合规范 if (res[asyncResponse.statusName] != asyncResponse.statusCode) { trData[LAY_ASYNC_STATUS] = 'error'; trData[LAY_EXPAND] = false; // 异常处理 todo flexIconElem.html( '' ); // 事件 } else { // 正常返回 asyncSuccessFn(res[asyncResponse.dataName]); } }, error: function (e, msg) { trData[LAY_ASYNC_STATUS] = 'error'; trData[LAY_EXPAND] = false; // 异常处理 todo typeof options.error === 'function' && options.error(e, msg); } }; if (options.ajax) { options.ajax(ajaxOptions, 'treeNodes'); } else { $.ajax(ajaxOptions); } return retValue; } trExpanded = trData[LAY_HAS_EXPANDED] = true; if (childNodes.length) { // 判断是否需要排序 if (options.initSort && (!options.url || options.autoSort)) { var initSort = options.initSort; if (initSort.type) { layui.sort( childNodes, initSort.field, initSort.type === 'desc', true ); } else { // 恢复默认 layui.sort(childNodes, table.config.indexName, null, true); } } treeTableThat.initData( trData[customName.children], trData[LAY_DATA_INDEX] ); // 将数据通过模板得出节点的html代码 var str2 = table.getTrHtml( tableId, childNodes, null, null, dataIndex ); var str2Obj = { trs: $(str2.trs.join('')), trs_fixed: $(str2.trs_fixed.join('')), trs_fixed_r: $(str2.trs_fixed_r.join('')) }; var dataLevel = dataIndex.split('-').length - 1; var dataLevelNew = (dataLevel || 0) + 1; layui.each(childNodes, function (childIndex, childItem) { str2Obj.trs .eq(childIndex) .attr({ 'data-index': childItem[LAY_DATA_INDEX], 'lay-data-index': childItem[LAY_DATA_INDEX], 'data-level': dataLevelNew }) .data('index', childItem[LAY_DATA_INDEX]); str2Obj.trs_fixed .eq(childIndex) .attr({ 'data-index': childItem[LAY_DATA_INDEX], 'lay-data-index': childItem[LAY_DATA_INDEX], 'data-level': dataLevelNew }) .data('index', childItem[LAY_DATA_INDEX]); str2Obj.trs_fixed_r .eq(childIndex) .attr({ 'data-index': childItem[LAY_DATA_INDEX], 'lay-data-index': childItem[LAY_DATA_INDEX], 'data-level': dataLevelNew }) .data('index', childItem[LAY_DATA_INDEX]); }); tableViewElem .find(ELEM_MAIN) .find('tbody tr[lay-data-index="' + dataIndex + '"]') .after(str2Obj.trs); tableViewElem .find(ELEM_FIXL) .find('tbody tr[lay-data-index="' + dataIndex + '"]') .after(str2Obj.trs_fixed); tableViewElem .find(ELEM_FIXR) .find('tbody tr[lay-data-index="' + dataIndex + '"]') .after(str2Obj.trs_fixed_r); // 初始化新增的节点中的内容 treeTableThat.renderTreeTable(str2Obj.trs, dataLevelNew); if (sonSign && !isToggle) { // 非状态切换的情况下 // 级联展开/关闭子节点 layui.each(childNodes, function (i1, item1) { expandNode( { dataIndex: item1[LAY_DATA_INDEX], trElem: tableViewElem .find('tr[lay-data-index="' + item1[LAY_DATA_INDEX] + '"]') .first(), tableViewElem: tableViewElem, tableId: tableId, options: options }, expandFlag, sonSign, focus, callbackFlag, done ); }); } } } } else { treeTableThat.isExpandAll = false; // 关闭 if (sonSign && !isToggle) { // 非状态切换的情况下 layui.each(childNodes, function (i1, item1) { expandNode( { dataIndex: item1[LAY_DATA_INDEX], trElem: tableViewElem .find('tr[lay-data-index="' + item1[LAY_DATA_INDEX] + '"]') .first(), tableViewElem: tableViewElem, tableId: tableId, options: options }, expandFlag, sonSign, focus, callbackFlag, done ); }); tableViewElem .find( childNodes .map(function (value, index, array) { // 只隐藏直接子节点,其他由递归的处理 return 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"]'; }) .join(',') ) .addClass(HIDE); } else { var childNodesFlat = treeTableThat.treeToFlat( childNodes, trData[customName.id], dataIndex ); tableViewElem .find( childNodesFlat .map(function (value, index, array) { return 'tr[lay-data-index="' + value[LAY_DATA_INDEX] + '"]'; }) .join(',') ) .addClass(HIDE); } } debounceFn( 'resize-' + tableId, function () { treeTable.resize(tableId); }, 0 )(); if (callbackFlag && trData[LAY_ASYNC_STATUS] !== 'loading') { var onExpand = treeOptions.callback.onExpand; layui.type(onExpand) === 'function' && onExpand(tableId, trData, trExpand); } if ( layui.type(done) === 'function' && trData[LAY_ASYNC_STATUS] !== 'loading' ) { done(tableId, trData, trExpand); } return retValue; }; /** * 展开或关闭一个节点 * @param {String} id 树表id * @param {Object} opts * @param {Number|String} opts.index 展开行的数据下标 * @param {Boolean} [opts.expandFlag] 展开、关闭、切换 * @param {Boolean} [opts.inherit] 是否级联子节点 * @param {Boolean} [opts.callbackFlag] 是否触发 tree.callback 事件 * @param {Boolean} [opts.done] 节点操作完成后的回调函数 * @return [{Boolean}] 状态结果 * */ treeTable.expandNode = function (id, opts) { var that = getThisTable(id); if (!that) return; opts = opts || {}; var index = opts.index; var expandFlag = opts.expandFlag; var sonSign = opts.inherit; var callbackFlag = opts.callbackFlag; var options = that.getOptions(); var tableViewElem = options.elem.next(); return expandNode( { trElem: tableViewElem.find('tr[lay-data-index="' + index + '"]').first() }, expandFlag, sonSign, null, callbackFlag, opts.done ); }; /** * 展开或关闭全部节点 * @param {String} id 树表id * @param {Boolean} expandFlag 展开或关闭 * */ treeTable.expandAll = function (id, expandFlag) { if (layui.type(expandFlag) !== 'boolean') { return hint.error( 'treeTable.expandAll param "expandFlag" must be a boolean value.' ); } var that = getThisTable(id); if (!that) return; that.isExpandAll = expandFlag; var options = that.getOptions(); var treeOptions = options.tree; var tableView = options.elem.next(); var isParentKey = treeOptions.customName.isParent; var idKey = treeOptions.customName.id; var showFlexIconIfNotParent = treeOptions.view.showFlexIconIfNotParent; if (!expandFlag) { // 关闭所有 // 将所有已经打开的节点的状态设置为关闭, that.updateStatus(null, function (d) { if (d[isParentKey] || showFlexIconIfNotParent) { d[LAY_EXPAND] = false; d[idKey] !== undefined && (that.status.expand[d[idKey]] = false); } }); // 只处理当前页,如果需要处理全部表格,需要用treeTable.updateStatus // 隐藏所有非顶层的节点 tableView .find('.layui-table-box tbody tr[data-level!="0"]') .addClass(HIDE); tableView .find('.layui-table-tree-flexIcon') .html(treeOptions.view.flexIconClose); treeOptions.view.showIcon && tableView .find( '.layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom,.layui-table-tree-iconLeaf)' ) .html(treeOptions.view.iconClose); } else { var tableDataFlat = treeTable.getData(id, true); // 展开所有 // 存在异步加载 if (treeOptions.async.enable) { // 判断是否有未加载过的节点 var isAllAsyncDone = true; layui.each(tableDataFlat, function (i1, item1) { if (item1[isParentKey] && !item1[LAY_ASYNC_STATUS]) { isAllAsyncDone = false; return true; } }); // 有未加载过的节点 if (!isAllAsyncDone) { // 逐个展开 layui.each(treeTable.getData(id), function (i1, item1) { treeTable.expandNode(id, { index: item1[LAY_DATA_INDEX], expandFlag: true, inherit: true }); }); return; } } // 先判断是否全部打开过了 var isAllExpanded = true; layui.each(tableDataFlat, function (i1, item1) { if (item1[isParentKey] && !item1[LAY_HAS_EXPANDED]) { isAllExpanded = false; return true; } }); // 如果全部节点已经都打开过,就可以简单处理跟隐藏所有节点反操作 if (isAllExpanded) { that.updateStatus(null, function (d) { if (d[isParentKey] || showFlexIconIfNotParent) { d[LAY_EXPAND] = true; d[idKey] !== undefined && (that.status.expand[d[idKey]] = true); } }); // 显示所有子节点 tableView.find('tbody tr[data-level!="0"]').removeClass(HIDE); // 处理节点的图标 tableView .find('.layui-table-tree-flexIcon') .html(treeOptions.view.flexIconOpen); treeOptions.view.showIcon && tableView .find( '.layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom,.layui-table-tree-iconLeaf)' ) .html(treeOptions.view.iconOpen); } else { // 如果有未打开过的父节点,将 tr 内容全部重新生成 that.updateStatus(null, function (d) { if (d[isParentKey] || showFlexIconIfNotParent) { d[LAY_EXPAND] = true; d[LAY_HAS_EXPANDED] = true; d[idKey] !== undefined && (that.status.expand[d[idKey]] = true); } }); if (options.initSort && options.initSort.type && options.autoSort) { return treeTable.sort(id); } var trAll = table.getTrHtml(id, tableDataFlat); var trAllObj = { trs: $(trAll.trs.join('')), trs_fixed: $(trAll.trs_fixed.join('')), trs_fixed_r: $(trAll.trs_fixed_r.join('')) }; var props; layui.each(tableDataFlat, function (dataIndex, dataItem) { var dataLevel = dataItem[LAY_DATA_INDEX].split('-').length - 1; props = { 'data-index': dataItem[LAY_DATA_INDEX], 'lay-data-index': dataItem[LAY_DATA_INDEX], 'data-level': dataLevel }; trAllObj.trs .eq(dataIndex) .attr(props) .data('index', dataItem[LAY_DATA_INDEX]); trAllObj.trs_fixed .eq(dataIndex) .attr(props) .data('index', dataItem[LAY_DATA_INDEX]); trAllObj.trs_fixed_r .eq(dataIndex) .attr(props) .data('index', dataItem[LAY_DATA_INDEX]); }); layui.each(['main', 'fixed-l', 'fixed-r'], function (i, item) { tableView .find('.layui-table-' + item + ' tbody') .html(trAllObj[['trs', 'trs_fixed', 'trs_fixed_r'][i]]); }); that.renderTreeTable(tableView, 0, false); } } treeTable.resize(id); }; /** * @typedef updateNodeIconOptions * @prop {JQuery} scopeEl - tr 元素 * @prop {boolean} isExpand - 是否是展开图标 * @prop {boolean} isParent - 是否是父节点图标 */ /** * 更新节点图标 * @param {updateNodeIconOptions} opts */ Class.prototype.updateNodeIcon = function (opts) { var that = this; var options = that.getOptions(); var treeOptions = options.tree || {}; var scopeEl = opts.scopeEl; var isExpand = opts.isExpand; var isParent = opts.isParent; // 处理折叠按钮图标 var flexIconElem = scopeEl.find('.layui-table-tree-flexIcon'); flexIconElem .css( 'visibility', isParent || treeOptions.view.showFlexIconIfNotParent ? 'visible' : 'hidden' ) .html( isExpand ? treeOptions.view.flexIconOpen : treeOptions.view.flexIconClose ); // 处理节点图标 if (treeOptions.view.showIcon) { var nodeIconElem = scopeEl.find( '.layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom)' ); var nodeIcon = isParent ? isExpand ? treeOptions.view.iconOpen : treeOptions.view.iconClose : treeOptions.view.iconLeaf; nodeIconElem .toggleClass('layui-table-tree-iconLeaf', !isParent) .html(nodeIcon); } }; Class.prototype.renderTreeTable = function (tableView, level, sonSign) { var that = this; var options = that.getOptions(); var tableViewElem = options.elem.next(); !tableViewElem.hasClass(TABLE_TREE) && tableViewElem.addClass(TABLE_TREE); var tableId = options.id; var treeOptions = options.tree || {}; // var treeOptionsData = treeOptions.data || {}; var treeOptionsView = treeOptions.view || {}; var customName = treeOptions.customName || {}; var isParentKey = customName.isParent; // var tableFilterId = tableViewElem.attr('lay-filter'); var treeTableThat = that; var existsData = options.data.length; // 是否直接赋值 data // 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) { // fix: 修正直接赋值 data 时顶层节点 LAY_DATA_INDEX 值的异常问题 if (existsData) { dataItem[LAY_DATA_INDEX] = String(dataIndex); } var layDataIndex = dataItem[LAY_DATA_INDEX]; tableViewElem .find( '.layui-table-main tbody tr[data-level="0"]:eq(' + dataIndex + ')' ) .attr('lay-data-index', layDataIndex); tableViewElem .find( '.layui-table-fixed-l tbody tr[data-level="0"]:eq(' + dataIndex + ')' ) .attr('lay-data-index', layDataIndex); tableViewElem .find( '.layui-table-fixed-r tbody tr[data-level="0"]:eq(' + dataIndex + ')' ) .attr('lay-data-index', layDataIndex); }); } var dataExpand = null; // 记录需要展开的数据 var nameKey = customName.name; var indent = treeOptionsView.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; } var trIndex = trElem.attr('lay-data-index'); if (!trIndex) { // 排除在统计行中的节点 return; } trElem = tableViewElem.find('tr[lay-data-index="' + trIndex + '"]'); var trData = treeTableThat.getNodeDataByIndex(trIndex); if (trData[LAY_EXPAND] && trData[isParentKey]) { // 需要展开 dataExpand = dataExpand || {}; dataExpand[trIndex] = true; } if (trData[LAY_CHECKBOX_HALF]) { trElem .find('input[type="checkbox"][name="layTableCheckbox"]') .prop('indeterminate', true); } var htmlTemp = itemCell.html(); itemCell = trElem.find( 'td[data-field="' + nameKey + '"]>div.layui-table-cell' ); itemCell.addClass('layui-table-tree-item'); var flexIconElem = itemCell .html( [ '