From 824b79102f4ae6e6bc97e025377833aa93e60ce8 Mon Sep 17 00:00:00 2001 From: augushong Date: Thu, 10 Sep 2026 18:06:06 +0000 Subject: [PATCH] =?UTF-8?q?feat(table):=20=E6=96=B0=E5=A2=9E=E5=8F=97?= =?UTF-8?q?=E6=8E=A7=E6=95=B0=E6=8D=AE=E5=B1=82=20store=EF=BC=88dataProvid?= =?UTF-8?q?er/pager/selection/reload=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugs/ulthon-admin/admin-table-store.js | 557 ++++++++++++++++++ 1 file changed, 557 insertions(+) create mode 100644 public/static/plugs/ulthon-admin/admin-table-store.js diff --git a/public/static/plugs/ulthon-admin/admin-table-store.js b/public/static/plugs/ulthon-admin/admin-table-store.js new file mode 100644 index 0000000..4db3a93 --- /dev/null +++ b/public/static/plugs/ulthon-admin/admin-table-store.js @@ -0,0 +1,557 @@ +(function () { + // ulthon-admin.js 会把 UA_ADMIN 挂到 window.ua 并 delete window.UA_ADMIN, + // 本文件位于 admin-core.js 之后(UA_ADMIN 尚存),此处回退 window.ua 兼容独立加载 + var ua = window.UA_ADMIN || window.ua; + var laypage = layui.laypage; + + /** + * 表格数据层(数据源 / 分页 / 选择 / 重载编排) + * + * 与 admin-table.js 的 url 模式解耦:由本层负责取数并维护表格运行状态, + * 渲染层(table / 模板 / 卡片形态)只消费本层产出的响应数据。 + * + * 语义平移来源(行为保持一致): + * - LAY_DISABLED 注入:admin-table.js render 内 parseData 注入段 + * (select_mode 页面 URL selectedIds 回显禁选 + rowDisableCallback) + * - LAY_CHECKED 注入:store 选中集合命中行(引擎 data 模式每次 reloadData 重建 + * cache,不注入则翻页回来勾选全丢) + * - where 单一事实源:admin-table.js lastTableWhere(renderSearch 产物 + listenTableSort 合入) + * - 失败双通道提示:admin-core.js ua.request.ajax(res.code != 0 与 HTTP error) + * - radio 选择语义:tableData 组件 selectType:'radio'(新选中替换旧选中) + */ + + // 分页条数默认档位(与 admin-table.js render 默认 limits 一致) + var DEFAULT_LIMITS = [10, 15, 20, 25, 50, 100]; + + // 注册表:tableId → 表格运行状态 + var states = {}; + + // where 单一事实源(语义平移自 admin-table.js 的 lastTableWhere) + var lastTableWhere = {}; + + // ========== 内部工具 ========== + + var getState = function (tableId) { + return states[tableId] || null; + }; + + // filter/op 统一为 JSON 字符串(renderSearch/listenTableSearch 产出的 wire 格式, + // 后端 buildTableParames 按此解析),sort 保持对象(layui where.sort 同构) + var normalizeWhere = function (where) { + where = where || {}; + var normalized = {}; + + if (where.filter !== undefined) { + normalized.filter = typeof where.filter === 'string' ? where.filter : JSON.stringify(where.filter); + } + if (where.op !== undefined) { + normalized.op = typeof where.op === 'string' ? where.op : JSON.stringify(where.op); + } + if (where.sort !== undefined) { + normalized.sort = where.sort; + } + + return normalized; + }; + + // 剥离 LAY_ 开头的临时字段(与 table.checkStatus.data 的"不含临时字段"语义同构) + var stripTempFields = function (rows) { + var cleanRows = []; + + for (var i = 0; i < rows.length; i++) { + var clean = {}; + for (var key in rows[i]) { + if (Object.prototype.hasOwnProperty.call(rows[i], key) && String(key).indexOf('LAY_') !== 0) { + clean[key] = rows[i][key]; + } + } + cleanRows.push(clean); + } + + return cleanRows; + }; + + // ========== (a) dataProvider ========== + + // 请求参数:page/limit + where(filter/op 为 JSON 字符串、sort 为对象) + var buildRequestData = function (state) { + var data = { + page: state.page.curr, + limit: state.page.limit + }; + + var where = state.where || {}; + + var keys = ['filter', 'op', 'sort']; + for (var i = 0; i < keys.length; i++) { + if (where[keys[i]] !== undefined) { + data[keys[i]] = where[keys[i]]; + } + } + + return data; + }; + + // 选择态标记注入 + // LAY_DISABLED 段:语义平移自 admin-table.js parseData 注入段(仅选择器页面生效) + // LAY_CHECKED 段:store 选中集合命中行,跨页勾选回显的关键 + var injectSelectionMarks = function (state, res) { + var rows = res.data || []; + + if (state.selectMode == 'checkbox' || state.selectMode == 'radio') { + if (typeof state.rowDisableCallback == 'function') { + for (var i = 0; i < rows.length; i++) { + var dataItem = rows[i]; + + var rowDisableResult = state.rowDisableCallback(dataItem); + if (rowDisableResult === false || rowDisableResult === true) { + rows[i].LAY_DISABLED = rowDisableResult; + } + } + } + + var urlSelectedIds = ua.getQueryVariable('selectedIds', ''); + + if (urlSelectedIds.length > 0) { + var selectedIdArr = urlSelectedIds.split(','); + + for (var j = 0; j < rows.length; j++) { + if (selectedIdArr.indexOf(rows[j].id.toString()) > -1) { + rows[j].LAY_DISABLED = true; + } + } + } + } + + // LAY_CHECKED:选中集合命中行(含 URL selectedIds 预置的选择器回显行), + // 同时刷新 selectedRows 行引用,保持 dataCache 与最新一次数据一致 + for (var k = 0; k < rows.length; k++) { + var selectedIdx = state.selectedIds.indexOf(String(rows[k].id)); + if (selectedIdx > -1) { + rows[k].LAY_CHECKED = true; + state.selectedRows[selectedIdx] = rows[k]; + } + } + }; + + /** + * 拉取列表数据 + * + * @param {string} tableId 已注册的表格 ID + * @param {function} [done] 成功回调,入参为后端响应 {code:0, msg, count, data} + * @param {boolean} [isCorrection] 页码纠偏自动重取标记(防递归兜底,业务不传) + */ + var fetch = function (tableId, done, isCorrection) { + var state = getState(tableId); + + if (!state) { + ua.msg.error('表格未注册:' + tableId); + return; + } + + // 语义同 ua.request.ajax 的空地址拦截 + if (state.indexUrl === '' || state.indexUrl === undefined) { + ua.msg.error('请求地址不能为空'); + return; + } + + // 弥补 data 模式没有引擎 loading 的差异:fetch 前后显隐全局 loading + loading.show(); + + $.ajax({ + url: ua.url(state.indexUrl), + type: 'get', + dataType: 'json', + headers: ua.headers(), + data: buildRequestData(state), + timeout: 60000, + success: function (res) { + loading.hide(); + + if (res.code === 0) { + // 页码纠偏:curr 超出末页时回落到末页并自动重取 + var count = parseInt(res.count) || 0; + var lastPage = Math.max(1, Math.ceil(count / state.page.limit)); + + if (state.page.curr > lastPage) { + state.page.curr = lastPage; + if (!isCorrection) { + fetch(tableId, done, true); + return; + } + } + + injectSelectionMarks(state, res); + + state.count = count; + state.currentPageData = res.data || []; + state.lastResponse = res; + lastTableWhere[tableId] = state.where; + + // 刷新分页条(count 变化后重渲染;jump 首次回调只同步状态不请求) + pager.render(tableId); + + if (typeof done === 'function') { + done(res); + } + } else { + // res.code != 0:错误提示并保持当前视图(不改动已渲染数据) + var msg = res.msg == undefined ? '返回数据格式有误' : res.msg; + ua.msg.error(msg); + } + }, + error: function (xhr, textstatus, thrown) { + loading.hide(); + + // HTTP error 通道:语义平移 ua.request.ajax,守卫 responseJSON + // (404 等非 JSON 响应时 responseJSON 为 undefined,直接取 message 会抛异常) + var errorMsg = ''; + if (xhr.responseJSON && xhr.responseJSON.message) { + errorMsg = xhr.responseJSON.message; + } + ua.msg.error('Status:' + xhr.status + ',' + xhr.statusText + ',请稍后再试!
' + errorMsg); + } + }); + }; + + // ========== (b) pager ========== + + /** + * layui.laypage.render 封装:分页条驱动 fetch + * DOM 容器由调用方传入(register 的 pagerElem 或 render 的第二参) + */ + var pager = { + render: function (tableId, elem) { + var state = getState(tableId); + + if (!state) { + return; + } + + if (elem !== undefined) { + state.pagerElem = elem; + } + + if (!state.pagerElem) { + return; + } + + laypage.render({ + elem: state.pagerElem, + 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) { + state.page.curr = obj.curr; + state.page.limit = obj.limit; + if (!first) { + fetch(tableId); + } + } + }); + } + }; + + // ========== (c) selection ========== + + // radio:checkStatus 返回单行数组、isAll 恒 false(tableData selectType:'radio' 语义) + var selection = { + /** + * 单行选中 + * radio:新选中替换旧选中 + * checkbox:再次点击为取消(layui checkbox 交互语义) + */ + set: function (tableId, row) { + var state = getState(tableId); + + if (!state || row === undefined || row === null) { + return; + } + + var id = String(row.id); + + if (state.selectMode === 'radio') { + state.selectedIds = [id]; + state.selectedRows = [row]; + return; + } + + var index = state.selectedIds.indexOf(id); + if (index > -1) { + state.selectedIds.splice(index, 1); + state.selectedRows.splice(index, 1); + } else { + state.selectedIds.push(id); + state.selectedRows.push(row); + } + }, + + /** + * 全选/取消全选当页:禁用行不参与 + * rows 缺省取最近一次成功返回的当页数据;checked 为 false 时取消全选 + * radio 模式无全选语义(layui radio 列不渲染表头全选),保持 no-op + */ + checkAll: function (tableId, rows, checked) { + var state = getState(tableId); + + if (!state || state.selectMode === 'radio') { + return; + } + + rows = rows === undefined ? state.currentPageData : (rows || []); + checked = checked !== false; + + for (var i = 0; i < rows.length; i++) { + if (rows[i].LAY_DISABLED) { + continue; + } + + var id = String(rows[i].id); + var index = state.selectedIds.indexOf(id); + + if (checked) { + if (index === -1) { + state.selectedIds.push(id); + state.selectedRows.push(rows[i]); + } else { + // 刷新行数据引用 + state.selectedRows[index] = rows[i]; + } + } else if (index > -1) { + state.selectedIds.splice(index, 1); + state.selectedRows.splice(index, 1); + } + } + }, + + // 清空选中集合 + clear: function (tableId) { + var state = getState(tableId); + + if (!state) { + return; + } + + state.selectedIds = []; + state.selectedRows = []; + }, + + /** + * 当页是否全选:isAll = 选中数 === 当页数 - 禁用数(当页口径,与 layui isAll 语义一致) + * radio 恒 false + */ + isAll: function (tableId) { + var state = getState(tableId); + + if (!state || state.selectMode === 'radio') { + return false; + } + + var rows = state.currentPageData || []; + var selectable = 0; + var selected = 0; + + for (var i = 0; i < rows.length; i++) { + if (rows[i].LAY_DISABLED) { + continue; + } + selectable++; + if (state.selectedIds.indexOf(String(rows[i].id)) > -1) { + selected++; + } + } + + return selectable > 0 && selected === selectable; + }, + + /** + * 导出与 table.checkStatus 同构的结果 {data, dataCache, isAll} + * data:选中行数据(不含 LAY_ 临时字段);dataCache:原始缓存(含临时字段) + * radio 语义:selectedRows 至多一行(单行数组),isAll 恒 false + */ + checkStatus: function (tableId) { + var state = getState(tableId); + + if (!state) { + return { data: [], dataCache: [], isAll: false }; + } + + return { + data: stripTempFields(state.selectedRows), + dataCache: state.selectedRows, + isAll: selection.isAll(tableId) + }; + } + }; + + // ========== (d) reload 编排 ========== + + /** + * 更新 where:整体替换 + * 语义平移 renderSearch 末尾对 lastTableWhere[tableId] 的无条件覆盖 + * (页面级 options.where 为死参数的现状如实平移:不单独合并) + */ + var setWhere = function (tableId, where) { + var state = getState(tableId); + + if (!state) { + return; + } + + state.where = normalizeWhere(where); + lastTableWhere[tableId] = state.where; + }; + + /** + * 排序合入 where + * 语义平移 listenTableSort:lastWhere.sort = {field: type} + */ + var setSort = function (tableId, field, type) { + var state = getState(tableId); + + if (!state) { + return; + } + + state.where.sort = {}; + state.where.sort[field] = type; + lastTableWhere[tableId] = state.where; + }; + + /** + * 重载编排:opts.where / opts.page 合入状态后 fetch + * + * - opts.where:传入时经 setWhere 整体替换 lastTableWhere(单一事实源), + * 不传则沿用当前 where(刷新语义保持筛选与排序) + * - opts.page:{curr, limit} 部分合入;搜索场景由调用方传 {curr: 1} + * (语义平移 listenTableSearch 的 reloadData page.curr=1) + * - opts.done:透传给 fetch 的成功回调 + */ + var reload = function (tableId, opts) { + var state = getState(tableId); + + if (!state) { + ua.msg.error('表格未注册:' + tableId); + return; + } + + opts = opts || {}; + + if (opts.where !== undefined) { + setWhere(tableId, opts.where); + } + + if (opts.page) { + if (opts.page.curr !== undefined) { + state.page.curr = opts.page.curr; + } + if (opts.page.limit !== undefined) { + state.page.limit = opts.page.limit; + } + } + + fetch(tableId, opts.done); + }; + + // ========== register / has 注册表 ========== + + /** + * 注册表格运行状态 + * + * config 支持项: + * - indexUrl:列表数据地址(必填,可为 'system.quick/index' 或以 / 开头的完整路径) + * - init.indexUrl:等价入口(与页面 _common.js 的 init 对象形态一致) + * - where:初始查询条件(缺省空条件,请求不携带 filter/op 参数) + * - page:{curr, limit} 初始页码与条数(limit 亦可直接传 config.limit) + * - limits:分页档位(缺省 [10,15,20,25,50,100]) + * - selectMode:'checkbox' | 'radio'(缺省读 URL select_mode,与选择器页面约定一致) + * - rowDisableCallback:选择器页面行禁用回调(语义平移 admin-table.js) + * - pagerElem:分页条容器(也可后续经 pager.render 传入) + */ + var register = function (tableId, config) { + config = config || {}; + + var selectMode = config.selectMode; + if (selectMode !== 'checkbox' && selectMode !== 'radio') { + // 语义平移:选择器页面 URL 携带 select_mode=checkbox|radio(admin-table.js 同源读取) + selectMode = ua.getQueryVariable('select_mode', ''); + if (selectMode !== 'checkbox' && selectMode !== 'radio') { + selectMode = null; + } + } + + var state = { + id: tableId, + indexUrl: config.indexUrl !== undefined ? config.indexUrl : (config.init ? config.init.indexUrl : ''), + where: normalizeWhere(config.where), + page: { + curr: (config.page && config.page.curr) || 1, + limit: config.limit || (config.page && config.page.limit) || 15 + }, + limits: config.limits || DEFAULT_LIMITS, + selectMode: selectMode, + rowDisableCallback: config.rowDisableCallback || null, + selectedIds: [], + selectedRows: [], + count: 0, + currentPageData: [], + pagerElem: config.pagerElem || null, + lastResponse: null + }; + + // 选择器页面回显:URL selectedIds 预置进选中集合(与 admin-table.js 同源读取), + // 使回显行获得 LAY_CHECKED(选中且禁改) + if (selectMode) { + var urlSelectedIds = ua.getQueryVariable('selectedIds', ''); + if (urlSelectedIds.length > 0) { + var selectedIdArr = urlSelectedIds.split(','); + for (var i = 0; i < selectedIdArr.length; i++) { + if (selectedIdArr[i] !== '' && state.selectedIds.indexOf(selectedIdArr[i]) === -1) { + state.selectedIds.push(selectedIdArr[i]); + } + } + } + } + + states[tableId] = state; + lastTableWhere[tableId] = state.where; + + return state; + }; + + // 该 tableId 是否已注册(任务 5 接线 shim 依赖此判定分流受控/非受控表) + var has = function (tableId) { + return states[tableId] !== undefined; + }; + + // ========== 挂载 ========== + + // 五个主入口:register / has / reload / fetch / checkStatus + // 子模块暴露给任务 5 接线(search/sort 监听 → dataProvider.setWhere/setSort) + // 与后续渲染形态(模板/卡片)复用(selection/pager) + window.UA_TABLE_STORE = { + register: register, + has: has, + reload: reload, + fetch: fetch, + checkStatus: function (tableId) { + return selection.checkStatus(tableId); + }, + dataProvider: { + fetch: fetch, + setWhere: setWhere, + setSort: setSort, + getState: getState, + getLastResponse: function (tableId) { + var state = getState(tableId); + return state ? state.lastResponse : null; + } + }, + pager: { + render: pager.render + }, + selection: selection + }; +})();