mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-10-29 08:58:22 +08:00
feat(table): 实现五级模板协议与行级渲染上下文
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user