Files
ulthon_admin/public/static/plugs/ulthon-admin/admin-table-store.js

570 lines
20 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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] 页码纠偏自动重取标记(防递归兜底,业务不传)
* @param {function} [fail] 失败回调(res.code != 0 与 HTTP error 两个失败通道各调用一次)
*/
var fetch = function (tableId, done, isCorrection, fail) {
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, fail);
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);
if (typeof fail === 'function') {
fail(res);
}
}
},
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 + ',请稍后再试!<br/>' + errorMsg);
if (typeof fail === 'function') {
fail(xhr, textstatus, thrown);
}
}
});
};
// ========== (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 的成功回调
* - opts.fail:透传给 fetch 的失败回调
* - opts.where 中仅 filter/op/sort 三键生效(normalizeWhere 白名单),其他自定义键会被丢弃——
* 需要额外请求参数的场景应在后端以 filter 解析或页面用 dataProvider.setWhere 全量构造
*/
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, undefined, opts.fail);
};
// ========== 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
};
})();