mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-10-29 17:08:21 +08:00
570 lines
20 KiB
PHP
570 lines
20 KiB
PHP
(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
|
||
};
|
||
})();
|