(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 += '
';
$.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 是页面