diff --git a/public/static/plugs/ulthon-admin/admin-table.js b/public/static/plugs/ulthon-admin/admin-table.js index 347a286..2e22d4e 100644 --- a/public/static/plugs/ulthon-admin/admin-table.js +++ b/public/static/plugs/ulthon-admin/admin-table.js @@ -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 模板节点(