feat(table): 实现五级模板协议与行级渲染上下文

This commit is contained in:
augushong
2026-09-11 00:46:22 +00:00
parent 2f4f29bcc1
commit a96137e290

View File

@@ -56,18 +56,113 @@
});
};
// ========== 多形态渲染(五级模板协议)==========
// 形态注册表: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;
};
// (b) 模板引擎优先级解析:tplFunction > tplLay > tplVue > tplLayId > tplVueId,
// 提供一种即生效;五种全无且非 table 条目 → console.warn 并回退 table。
// lay/vue/layId/vueId 的引擎挂载在任务 16(laytpl)/17(Vue),此处只做分发判定。
var resolveLayoutEngine = function (entry) {
if (entry.name === 'table') {
return {kind: 'table', entry: entry};
}
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);
};
// 形态可见性同步:引擎数据区(表头+表体+合计行)与模板容器互斥显隐,
// 引擎视图的工具栏(刷新/新增/批量删除等)两形态共用保持可见,搜索区
// 本就挂在表格元素之前不受影响。非 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') {
$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;
// 模板形态的加载态容器标记(容器内加载样式由任务 14 的内置形态接入)
var $layoutContainer = $('#ua-layout-' + tableId);
$layoutContainer.addClass('ua-layout-loading');
opts.done = function (res) {
originalTableReloadData.call(table, tableId, {
data: res.data,
totalRow: res.totalRow,
initSort: opts.initSort
});
// 受控渲染分发:table 形态走引擎纯渲染(现状),非 table 形态走
// 模板管线(渲染上下文 + 行级循环);模板管线不可用(引擎未挂载,
// 任务 16/17 接管)时回退引擎纯渲染兜底
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);
}
@@ -91,6 +186,163 @@
}
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 调用约定(任务 16/17 对接契约)】
// 单行 HTML = tplFunction.call(rowCtx, rowCtx, listCtx)
// - this/第一参 = 行上下文 {...row, _cells, _operatHtml, _checked, _disabled}
// - 第二参 = 列表上下文 {tableId, rows, page, count, checkedAll}
// - 返回值 = 该行的 HTML 字符串(框架直接拼接,不做转义)
// 【两条硬约束】
// 1. 模板容器恒带 class="layui-form"(form 控件渲染与事件受 .layui-form
// 祖先约束,缺失则模板里的 switch/checkbox 全部失效);
// 2. laytpl/Vue 模板节点(<script type="text/html"> / text/x-template)必须
// 位于渲染容器之外,防止被模板引擎编译时吞掉 {{ }} 插值(节点解析在
// 任务 16/17,节点由页面模板区提供)。
// 返回 false 表示管线不可渲染(table 形态或模板引擎未挂载),调用方回退
// 引擎纯渲染。
renderLayoutForm: function (tableId) {
if (controlledLayouts[tableId] === undefined) {
return false;
}
var resolved = resolveCurrentLayout(tableId);
if (resolved.kind === 'table') {
return false;
}
var $container = $('#ua-layout-' + tableId);
if ($container.length === 0) {
return false;
}
if (resolved.kind !== 'function') {
// lay/vue/layId/vueId 分发点:引擎挂载于任务 16(laytpl)/17(Vue)
console.warn('[ua.table] 模板引擎未挂载:' + resolved.kind + '(任务 16/17 实现),本次回退 table 形态');
return false;
}
var context = ua.table.buildLayoutContext(tableId);
var html = '';
if (context.rows.length === 0) {
html = '<div class="ua-layout-empty">无数据</div>';
} else {
$.each(context.rows, function (i, rowCtx) {
html += resolved.entry.tplFunction.call(rowCtx, rowCtx, context);
});
}
$container.html(html);
// 容器恒为 .layui-form(lay-filter 与容器 id 同名),scoped form.render
// 美化模板产出的 switch/checkbox 控件
form.render(null, 'ua-layout-' + tableId);
return true;
},
// 当前形态名(任务 15 的切换 UI 消费)
getLayoutForm: function (tableId) {
return controlledLayouts[tableId] ? controlledLayouts[tableId].current : 'table';
},
// 切换当前形态:仅改写运行期状态并重渲染,不发起网络请求(数据取自
// store 当页状态)。目标条目不存在返回 false;模板管线不可用(无钩子条目
// 回退 table)时走引擎纯渲染兜底。
setLayoutForm: function (tableId, name) {
var layout = controlledLayouts[tableId];
if (!layout || !layout.byName[name]) {
return false;
}
layout.current = name;
if (!ua.table.renderLayoutForm(tableId)) {
// table 形态(含无钩子条目回退):引擎纯渲染当页数据,直呼备份
// 原引用不走 shim(防路由回 store 造成 fetch)
var state = window.UA_TABLE_STORE.dataProvider.getState(tableId);
originalTableReloadData.call(table, tableId, {
data: state ? state.currentPageData : [],
totalRow: state && state.lastResponse ? state.lastResponse.totalRow : undefined
});
}
syncLayoutVisibility(tableId);
syncControlledPager(tableId);
return true;
},
render: function (options) {
options.init = options.init || ua.init;
options.modifyReload = ua.parame(options.modifyReload, true);
@@ -299,6 +551,34 @@
'<div id="' + pagerId + '" class="ua-store-pager"></div>'
);
// 模板形态容器:与 ua-store-pager 并列(分页条两形态共用)。
// 两条硬约束:容器恒带 class="layui-form"(form 控件渲染与事件受
// .layui-form 祖先约束);laytpl/Vue 模板节点必须位于本容器之外
// (由页面模板区提供,本层不生成任何模板节点)
var layoutId = 'ua-layout-' + options.id;
$('#' + layoutId).remove();
$('#' + pagerId).before(
'<div id="' + layoutId + '" class="ua-layout-container layui-form" lay-filter="ua-layout-' + options.id + '"></div>'
);
// 形态注册:layoutList 规范化 + 当前形态(layoutDefault / 'table';
// 形态切换 UI 在任务 15,运行期经 ua.table.setLayoutForm 切换)。
// options.cols 此时已过 formatCols/renderOperat/renderTrueHide,
// 渲染器族与 operat 缺省值已就绪,构建上下文直接消费
var layoutEntries = normalizeLayoutList(options.layoutList);
var layoutByName = {};
$.each(layoutEntries, function (i, entry) {
layoutByName[entry.name] = entry;
});
controlledLayouts[options.id] = {
elem: options.elem,
cols: options.cols,
init: options.init,
entries: layoutEntries,
byName: layoutByName,
current: options.layoutDefault || 'table'
};
window.UA_TABLE_STORE.register(options.id, {
indexUrl: options.init.indexUrl,
where: options.where,
@@ -325,6 +605,10 @@
window.UA_TABLE_STORE.selection.set(options.id, obj.data);
});
// 初始形态可见性:layoutDefault 非 table 时先隐引擎空视图(避免
// 空表头闪烁),首个 reload 完成后 wrapper 再次同步
syncLayoutVisibility(options.id);
// 首屏取数:经接线后的 store.reload 触发 fetch + 引擎纯渲染
window.UA_TABLE_STORE.reload(options.id);
}