(function () { var ua = window.UA_ADMIN; var form = window.UA_CORE.form, layer = window.UA_CORE.layer, table = window.UA_CORE.table, laydate = window.UA_CORE.laydate, laytpl = window.UA_CORE.laytpl, util = window.UA_CORE.util; var lastTableWhere = {}; var selectMode, selectConfirmCallback; // reloadData 引擎接线(加载期执行一次):受控表(UA_TABLE_STORE 已注册)的数据 // 重载分流到 store,非受控表透传原引擎调用。原引用只备份一次,防止重复加载时 // 把包装层当原引用再包一层;透传用 arguments 保真,reloadData(id, opts, type) // 三参形态的 type 语义不变。 if (table.__uaOriginalReloadData === undefined) { table.__uaOriginalReloadData = table.reloadData; } var originalTableReloadData = table.__uaOriginalReloadData; table.reloadData = function (tableId, opts) { if (window.UA_TABLE_STORE.has(tableId)) { return window.UA_TABLE_STORE.reload(tableId, opts); } return originalTableReloadData.apply(this, arguments); }; // 受控表渲染接线(加载期执行一次):store 的 fetch/reload 只负责取数与状态、 // 不触发渲染,这里在唯一的数据到达点(store.reload 的成功回调)注入引擎 // 纯渲染——首屏/搜索/排序/switch 回滚/删除刷新全部经 shim 汇入 reload。 // 渲染必须走备份的原引擎引用:走 table.reloadData 会被上方 shim 路由回 // store 造成请求循环。initSort 透传保持表头排序箭头(与引擎 url 模式的 // listenTableSort 行为一致)。受控表分页条由 render() 内的 // syncControlledPager 自管:store 的 pager.jump 在内部直呼 fetch,外部 // 注入不了渲染回调,故 register 时不传 pagerElem 使 store 分页静默跳过, // laypage 的 jump 经 shim(table.reloadData)回 store.reload 形成闭环。 var syncControlledPager = function (tableId) { var state = window.UA_TABLE_STORE.dataProvider.getState(tableId); if (!state) { return; } layui.laypage.render({ elem: 'ua-store-pager-' + tableId, count: state.count, curr: state.page.curr, limit: state.page.limit, limits: state.limits, layout: ['count', 'prev', 'page', 'next', 'limit', 'skip'], jump: function (obj, first) { if (!first) { table.reloadData(tableId, { page: {curr: obj.curr, limit: obj.limit} }); } } }); }; // ========== 多形态渲染(五级模板协议)========== // 形态注册表:tableId → {elem, cols, init, entries, byName, current}。 // render() 受控分支写入;受控渲染 wrapper 与 ua.table.renderLayoutForm 消费。 var controlledLayouts = {}; // 已回退告警去重表(同一形态条目只 warn 一次) var warnedLayouts = {}; // (a) layoutList 规范化:字符串 'table'/'card' 转对象 {name, title};自定义条目 // {name, title, icon?, tplFunction?, tplLay?, tplVue?, tplLayId?, tplVueId?} 原样保留 var normalizeLayoutList = function (layoutList) { var stringTitles = {table: '表格', card: '卡片'}; var result = []; $.each(layoutList || [], function (i, item) { if (typeof item === 'string') { result.push({name: item, title: stringTitles[item] || item}); } else if (item && typeof item === 'object' && item.name) { result.push(item); } }); return result; }; // ========== 内置 card 形态(name='card' 且无自定义钩子时启用)========== // 结构参照旧寄生实现的 header/body/footer 骨架(代码全新):header(标题 + // 勾位图标)、body(键值对字段列表)、footer(create_time + 操作按钮)。 // 数据全部来自渲染上下文(_cells/_operatHtml/_checked/_disabled),列元信息读 // controlledLayouts 的 cols(本闭包内),无任何业务字段硬编码。 // // 【与 buildLayoutContext 同规则的单元格列筛选】操作列无 field,须先于 // field 判定识别;checkbox/radio 等无字段列跳过(勾选语义由勾位图标承接) var builtinCardCellCols = function (tableId) { var cols = (controlledLayouts[tableId] && controlledLayouts[tableId].cols[0]) || []; var cellCols = []; $.each(cols, function (i, col) { if (!col) { return; } if (col.templet === ua.table.tool) { return; } if (!col.field) { return; } cellCols.push(col); }); return cellCols; }; // 内置 card 单行模板(tplFunction 调用约定:this/第一参 = 行上下文,第二参 = 列表上下文) // - 标题列 = 首个字段列(含 checkbox 列的表格即"第二列",无 checkbox 表格即"首字段") // - 标题列与 create_time 列不重复进 body(分别在 header/footer 展示) // - create_time 无对应列时 footer 时间留空,不回退原始字段值(避免裸时间戳) var builtinCardTemplate = function (row, list) { var cellCols = builtinCardCellCols(list.tableId); var titleCol = cellCols.length > 0 ? cellCols[0] : null; var html = '
'; html += '
'; html += '
' + (titleCol ? (row._cells[titleCol.field] || '') : '') + '
'; html += '
'; html += '
'; html += '
'; $.each(cellCols, function (i, col) { if (titleCol && col.field === titleCol.field) { return; } if (col.field === 'create_time') { return; } html += '
' + '' + (col.title || col.field) + '' + '' + (row._cells[col.field] || '') + '' + '
'; }); html += '
'; html += ''; html += '
'; return html; }; // 统一行选中委托(任务 18):模板渲染容器内凡带 data-table-id + data-id 属性 // 的行元素——内置 card 的 .ua-data-card 与自定义 laytpl/Vue 模板的卡片/勾位 // 元素——点击即切换选中,全部走本委托,无任何形态专用事件。模板作者契约: // 给行根元素(或勾位元素)加这两属性即获得点选交互,无需页面级监听。 // 交互元素(按钮/表单控件/layui 美化层)豁免不触发勾选;禁用行 // (ua-data-card-disabled 标记或行 LAY_DISABLED)整元素不可勾选。选择态落 // store(与引擎勾选同一事实源,批量删除经 ua.table.checkStatus 分发即取到), // 随后重渲模板管线回显勾位标记。 $(document).on('click', '[data-table-id][data-id]', function (e) { var $card = $(this); var tableId = $card.attr('data-table-id'); // 仅模板渲染容器内的行元素生效(页面其他区域的 data-id 元素不受影响) if ($card.closest('#ua-layout-' + tableId).length === 0) { return; } if ($card.hasClass('ua-data-card-disabled')) { return; } if ($(e.target).closest('a,button,input,select,textarea,label,.layui-form-switch,.layui-form-checkbox,.layui-form-radio').length) { return; } var state = window.UA_TABLE_STORE && window.UA_TABLE_STORE.dataProvider.getState(tableId); if (!state) { return; } var row = null; var rowId = $card.attr('data-id'); $.each(state.currentPageData || [], function (i, item) { if (String(item.id) === String(rowId)) { row = item; return false; } }); if (!row || row.LAY_DISABLED) { return; } window.UA_TABLE_STORE.selection.set(tableId, row); ua.table.renderLayoutForm(tableId); }); // (b) 模板引擎优先级解析:tplFunction > tplLay > tplVue > tplLayId > tplVueId, // 提供一种即生效;五种全无且非 table 条目 → console.warn 并回退 table。 // lay/layId(任务 16,layui.laytpl)与 vue/vueId(任务 17,全局 Vue 2.6.10 // 运行时编译)均挂载于 renderLayoutForm。 var resolveLayoutEngine = function (entry) { if (entry.name === 'table') { return {kind: 'table', entry: entry}; } // 内置 card 形态:name='card' 且未提供任何模板钩子时挂内置模板;提供了 // 钩子(tplFunction/tplLay/tplVue/tplLayId/tplVueId 任一)则照常走五级 // 优先级,自定义实现完全覆盖内置形态。builtinCard 标记供模板管线给容器 // 挂网格布局类(.ua-layout-cards) if (entry.name === 'card') { var hasHook = typeof entry.tplFunction === 'function' || (typeof entry.tplLay === 'string' && entry.tplLay !== '') || (typeof entry.tplVue === 'string' && entry.tplVue !== '') || (typeof entry.tplLayId === 'string' && entry.tplLayId !== '') || (typeof entry.tplVueId === 'string' && entry.tplVueId !== ''); if (!hasHook) { var builtinEntry = $.extend({}, entry, {tplFunction: builtinCardTemplate, builtinCard: true}); return {kind: 'function', entry: builtinEntry}; } } if (typeof entry.tplFunction === 'function') { return {kind: 'function', entry: entry}; } if (typeof entry.tplLay === 'string' && entry.tplLay !== '') { return {kind: 'lay', entry: entry}; } if (typeof entry.tplVue === 'string' && entry.tplVue !== '') { return {kind: 'vue', entry: entry}; } if (typeof entry.tplLayId === 'string' && entry.tplLayId !== '') { return {kind: 'layId', entry: entry}; } if (typeof entry.tplVueId === 'string' && entry.tplVueId !== '') { return {kind: 'vueId', entry: entry}; } if (!warnedLayouts[entry.name]) { warnedLayouts[entry.name] = true; console.warn('[ua.table] layoutList 条目 "' + entry.name + '" 未提供任何模板钩子(tplFunction/tplLay/tplVue/tplLayId/tplVueId),已回退 table 形态'); } return {kind: 'table', entry: entry}; }; // 当前形态解析:先取 options.layoutDefault(缺省 'table');运行期切形态 // 经 ua.table.setLayoutForm 改写 current。条目缺失/未登记时恒 table。 var resolveCurrentLayout = function (tableId) { var layout = controlledLayouts[tableId]; var entry = (layout && layout.byName[layout.current]) || {name: 'table', title: '表格'}; return resolveLayoutEngine(entry); }; // 模板管线失败回退(tplLayId/tplVueId 节点缺失 / laytpl 渲染错误 / Vue 未引入 // 或挂载渲染失败):当前形态归位 'table'(与"无钩子条目回退"同语义,幂等、 // 不写 localStorage 记忆,用户重新切换可再次触发),容器销毁 Vue 实例后清空 // 隐藏。调用方据 false 返回值做引擎纯渲染,随后的 syncLayoutVisibility 按 // table kind 复显引擎数据区,避免"容器残留上一形态旧内容 + 引擎数据被隐藏" // 的视觉错位。 var fallbackLayoutToTable = function (tableId, $container) { var layout = controlledLayouts[tableId]; if (layout) { layout.current = 'table'; } destroyLayoutVueInstances(tableId); if ($container && $container.length) { $container.addClass('layui-hide').empty(); } return false; }; // ========== vue/vueId 引擎(任务 17,全局 Vue 2.6.10 运行时编译)========== // 渲染模式为列表级单实例:模板内 v-for="d in rows" 循环行,一次渲染只挂载 // 一个实例(逐行 new Vue 的成本随行数线性膨胀,销毁纪律也难以收口)。 // 实例存活表:tableId → 当前实例数组。 var layoutVueInstances = {}; // 销毁本表全部存活实例(渲染入口 / fallbackLayoutToTable / // syncLayoutVisibility 三处统一调用,幂等)。$destroy 只拆 watcher 与事件, // 不动 DOM,容器清空由调用方完成。任何重渲路径(store done 管线/搜索/ // 切换/回退)都必须先销毁旧实例再挂新实例——销毁重建式是"实例存活期禁止 // 响应式数据变更"约束(约束 2)的落地方式:form.render 美化插入的 DOM // (div.layui-form-switch 等)不受 Vue 虚拟 DOM 管理,存活期 re-patch 会 // 清除它们。 var destroyLayoutVueInstances = function (tableId) { var list = layoutVueInstances[tableId] || []; $.each(list, function (i, vm) { try { vm.$destroy(); } catch (e) {} }); layoutVueInstances[tableId] = []; }; // Vue 生产构建中模板编译错误不抛异常也不打日志(dev 告警被裁剪),坏模板 // 只会在挂载渲染时经全局 errorHandler 暴露——链式接管 Vue.config.errorHandler: // 本框架实例(vm.__uaLayout 标记)按形态名 console.error 并记录 // lastVueLayoutError,由 renderLayoutForm 探测后回退;其余实例 // (propertyInput/tagInput 等页面自有 Vue)透传既有处理器或复刻默认行为。 var lastVueLayoutError = null; var vueErrorHandlerInstalled = false; var installVueErrorHandler = function () { if (vueErrorHandlerInstalled) { return; } vueErrorHandlerInstalled = true; var previous = window.Vue.config.errorHandler; window.Vue.config.errorHandler = function (err, vm, info) { if (vm && vm.__uaLayout) { var meta = vm.__uaLayout; lastVueLayoutError = {tableId: meta.tableId, name: meta.name}; console.error('[ua.table] Vue 模板渲染失败(形态 "' + meta.name + '"):' + ((err && err.message) || err) + ',本次回退 table 形态'); return; } if (typeof previous === 'function') { previous(err, vm, info); return; } console.error(err); }; }; // 形态可见性同步:引擎数据区(表头+表体+合计行)与模板容器互斥显隐, // 引擎视图的工具栏(刷新/新增/批量删除等)两形态共用保持可见,搜索区 // 本就挂在表格元素之前不受影响。非 table 形态下引擎根本不渲染行数据 // (wrapper 非 table 分支不走引擎渲染),并非旧寄生实现"引擎恒渲+CSS 隐藏"。 var syncLayoutVisibility = function (tableId) { var layout = controlledLayouts[tableId]; if (!layout) { return; } var $view = $(layout.elem).siblings('.layui-table-view').first(); var $box = $view.find('.layui-table-box').first(); var $total = $view.find('.layui-table-total').first(); var $container = $('#ua-layout-' + tableId); if (resolveCurrentLayout(tableId).kind === 'table') { destroyLayoutVueInstances(tableId); $box.add($total).removeClass('layui-hide'); $container.addClass('layui-hide').empty(); } else { $box.add($total).addClass('layui-hide'); $container.removeClass('layui-hide'); } }; if (window.UA_TABLE_STORE) { var storeReload = window.UA_TABLE_STORE.reload; window.UA_TABLE_STORE.reload = function (tableId, opts) { opts = opts || {}; var userDone = opts.done; var userFail = opts.fail; // 模板形态的加载态容器标记(容器内加载样式由任务 14 的内置形态接入) var $layoutContainer = $('#ua-layout-' + tableId); $layoutContainer.addClass('ua-layout-loading'); // 失败通道(res.code != 0 / HTTP error)不进 done,需经 fail 摘除加载态, // 避免请求失败后容器残留半透明禁点状态 opts.fail = function () { $layoutContainer.removeClass('ua-layout-loading'); if (typeof userFail === 'function') { userFail.apply(null, arguments); } }; opts.done = function (res) { // 受控渲染分发:table 形态走引擎纯渲染(现状),非 table 形态走 // 模板管线(渲染上下文 + 行级循环);模板管线不可用(Vue 未引入/ // 挂载渲染失败;tplLayId/tplVueId 节点缺失/laytpl 渲染错误)时 // 回退引擎纯渲染兜底 var engineRender = function () { originalTableReloadData.call(table, tableId, { data: res.data, totalRow: res.totalRow, initSort: opts.initSort }); }; if (resolveCurrentLayout(tableId).kind === 'table' || !ua.table.renderLayoutForm(tableId)) { engineRender(); } $layoutContainer.removeClass('ua-layout-loading'); syncControlledPager(tableId); syncLayoutVisibility(tableId); if (typeof userDone === 'function') { userDone(res); } }; return storeReload(tableId, opts); }; } ua.table = { // 受控/非受控分发入口(后续任务直呼改造的定义点):非受控路径走备份的 // 原引擎引用,不经过上方已包装的 table.reloadData,避免递归 reloadData: function (tableId, opts) { if (window.UA_TABLE_STORE.has(tableId)) { return window.UA_TABLE_STORE.reload(tableId, opts); } return originalTableReloadData.apply(this, arguments); }, checkStatus: function (tableId) { if (window.UA_TABLE_STORE.has(tableId)) { return window.UA_TABLE_STORE.checkStatus(tableId); } return table.checkStatus(tableId); }, // ========== 多形态渲染管线(五级模板协议)========== // (c) 渲染上下文构建器:对 store 当页每行产出 // {...row, _cells: {field: 列 templet 渲染后的 HTML}, _operatHtml: 操作按钮 HTML, // _checked: 是否选中, _disabled: 是否禁选} // _cells 复用 formatCols 之后的列渲染器族(image/switch/list/自定义 templet 与 // 引擎同行为),逐列注入 LAY_COL(与引擎 templet 调用同构:渲染器经 data.LAY_COL // 读列配置);_operatHtml 走既有 ua.table.tool(按钮权限 data-* 属性原样输出, // 事件靠页面级委托天然覆盖,无旧的"暂时隐藏按钮"逻辑);_checked/_disabled // 数据源为 store 选中集合(selectedIds)与行 LAY_DISABLED 标记。 buildLayoutContext: function (tableId) { var layout = controlledLayouts[tableId]; var state = window.UA_TABLE_STORE.dataProvider.getState(tableId); var rows = (state && state.currentPageData) || []; var selectedIds = (state && state.selectedIds) || []; var cols = (layout && layout.cols[0]) || []; var operatCol = null; var cellCols = []; $.each(cols, function (i, col) { if (!col) { return; } // 操作列无 field,须先于 field 判定识别 if (col.templet === ua.table.tool) { operatCol = col; return; } if (!col.field) { // checkbox/radio 等无字段列:勾选语义由 _checked/_disabled 与模板作者承接 return; } cellCols.push(col); }); var context = { tableId: tableId, rows: [], page: state ? state.page : {curr: 1, limit: 0}, count: state ? state.count : 0, checkedAll: state ? window.UA_TABLE_STORE.selection.isAll(tableId) : false }; $.each(rows, function (i, row) { var rowCtx = $.extend({}, row); rowCtx._cells = {}; $.each(cellCols, function (j, col) { var cellData = $.extend({}, row); cellData.LAY_COL = col; try { if (typeof col.templet === 'function') { rowCtx._cells[col.field] = col.templet(cellData) || ''; } else if (typeof col.templet === 'string') { rowCtx._cells[col.field] = laytpl(col.templet).render(cellData) || ''; } else { rowCtx._cells[col.field] = ''; } } catch (e) { rowCtx._cells[col.field] = ''; } }); if (operatCol) { var toolData = $.extend({}, row); toolData.LAY_COL = operatCol; rowCtx._operatHtml = ua.table.tool(toolData); } else { rowCtx._operatHtml = ''; } rowCtx._checked = selectedIds.indexOf(String(row.id)) > -1; rowCtx._disabled = !!row.LAY_DISABLED; context.rows.push(rowCtx); }); return context; }, // (d) 行级循环渲染框架:模板只定义单行 HTML 生成函数,框架循环拼接并维护 // 空态容器;加载态由 store.fetch 的全局 loading 与容器的 ua-layout-loading // 标记承接(内置形态的加载态样式在任务 14 接入)。 // // 【tplFunction 调用约定】 // 单行 HTML = tplFunction.call(rowCtx, rowCtx, listCtx) // - this/第一参 = 行上下文 {...row, _cells, _operatHtml, _checked, _disabled} // - 第二参 = 列表上下文 {tableId, rows, page, count, checkedAll} // - 返回值 = 该行的 HTML 字符串(框架直接拼接,不做转义) // 【laytpl 调用约定(tplLay / tplLayId)】 // 单行 HTML = laytpl(tplLay | $(tplLayId).html()).render(rowCtx) // - d = 行上下文,与 tplFunction 第一参同构,另挂 d._list = 列表上下文 // (与 tplFunction 第二参对齐;laytpl 仅支持单数据对象) // - 模板语法(layui 内置 laytpl v2.0.0 实测):{{ d.x }} 与 {{= d.x }} // 均经 laytpl.escape 转义;{{- d.x }} 原样输出(_cells/_operatHtml 等 // HTML 内容必须用此形态);{{# }} 为 JS 语句(if/each 等) // - tplLayId 是页面