mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 12:45:32 +08:00
控制器逻辑移入 extend/base/admin/controller/system/TimerConfigBase.php,app 层改为空壳继承。9 个视图文件迁移到 extend/base/admin/view/system/timer_config/。index.js 补 toolbar: ['refresh', 'export'] 声明(C 类页面,去掉默认的 add/delete)。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
312 lines
13 KiB
PHP
312 lines
13 KiB
PHP
$(function(){
|
||
// ============ 节点叠加层状态(模块级) ============
|
||
// currentNodeId: 当前选中的节点 ID;为空表示纯配置视图(配置基础层)
|
||
// nodeMap: { node_id: {node_id, is_master, ip_address, last_heartbeat_at} }
|
||
// onlineNodeCount: 在线节点总数(用于 all 模式全网预估提示)
|
||
var currentNodeId = '';
|
||
var nodeMap = {};
|
||
var onlineNodeCount = 0;
|
||
var tableId = init.tableRenderId;
|
||
|
||
// ============ 归属可视化(D19 + run_type + 主节点判断)============
|
||
// 入参:run_type、status、当前选中节点是否主节点
|
||
// 返回:{text, class} 用于渲染徽章
|
||
function calcOwnership(runType, status, isMaster) {
|
||
if (status == 0) {
|
||
return { text: '已停用', cls: 'layui-badge layui-bg-gray' };
|
||
}
|
||
switch (runType) {
|
||
case 'all':
|
||
// 所有节点都跑(含当前节点)
|
||
return { text: '当前自动', cls: 'layui-badge layui-bg-green' };
|
||
case 'main':
|
||
if (isMaster == 1) {
|
||
return { text: '当前(主)自动', cls: 'layui-badge layui-bg-green' };
|
||
}
|
||
// 非主节点:此任务不会在当前节点调度
|
||
return { text: '仅主节点', cls: 'layui-badge layui-bg-orange' };
|
||
case 'auto':
|
||
// 多节点竞争执行(谁抢到谁跑)
|
||
return { text: '竞争', cls: 'layui-badge layui-bg-blue' };
|
||
case 'manual':
|
||
// 仅手动触发
|
||
return { text: '仅手动', cls: 'layui-badge layui-bg-orange' };
|
||
default:
|
||
return { text: runType || '-', cls: 'layui-badge' };
|
||
}
|
||
}
|
||
|
||
// ============ 加载节点列表(hostList 接口)============
|
||
function loadNodeList(callback) {
|
||
ua.request.get({
|
||
url: ua.url('system.timer_config/hostList'),
|
||
prefix: true,
|
||
}, function (res) {
|
||
var list = (res && res.data) ? res.data : [];
|
||
onlineNodeCount = list.length;
|
||
nodeMap = {};
|
||
var $sel = $('#node_filter');
|
||
// 保留首项(全局配置)
|
||
$sel.find('option').not(':first').remove();
|
||
$.each(list, function (i, item) {
|
||
nodeMap[item.node_id] = item;
|
||
var masterTag = item.is_master == 1 ? ' [主]' : '';
|
||
var ip = item.ip_address || '';
|
||
$sel.append('<option value="' + item.node_id + '">' + item.node_id + masterTag + ' (' + ip + ')</option>');
|
||
});
|
||
// 重新渲染 select
|
||
if (window.layui && layui.form) {
|
||
layui.form.render('select', 'timerNodeFilter');
|
||
}
|
||
if (typeof callback === 'function') {
|
||
callback(list);
|
||
}
|
||
}, function (res) {
|
||
// hostList 失败:保持纯配置视图(不阻塞页面)
|
||
$('#node_hint').attr('class', 'layui-badge').text('节点列表加载失败:' + (res && res.msg ? res.msg : '未知错误')).show();
|
||
});
|
||
}
|
||
|
||
// ============ 更新节点 hint 提示 ============
|
||
function updateNodeHint() {
|
||
var $hint = $('#node_hint');
|
||
if (!currentNodeId) {
|
||
$hint.hide();
|
||
return;
|
||
}
|
||
var node = nodeMap[currentNodeId];
|
||
if (!node) {
|
||
$hint.hide();
|
||
return;
|
||
}
|
||
var master = node.is_master == 1 ? '主节点' : '非主节点';
|
||
var ip = node.ip_address || '';
|
||
$hint.attr('class', 'layui-badge layui-bg-blue')
|
||
.text('当前视角:' + node.node_id + ' / ' + master + (ip ? ' / ' + ip : ''))
|
||
.show();
|
||
}
|
||
|
||
// ============ 表格 cols(基础配置层 + 节点叠加列始终在,按节点上下文渲染)============
|
||
var cols = [[
|
||
{ type: 'checkbox' },
|
||
{ field: 'id', title: 'ID', width: 80 },
|
||
{ field: 'task_name', title: '任务名称', minWidth: 180 },
|
||
{
|
||
field: 'run_type',
|
||
search: 'select',
|
||
selectList: ua.getDataBrage('select_list_run_type'),
|
||
title: '运行类型',
|
||
width: 110,
|
||
templet: function (d) {
|
||
var map = ua.getDataBrage('select_list_run_type');
|
||
return map[d.run_type] || d.run_type;
|
||
}
|
||
},
|
||
{
|
||
field: 'status',
|
||
search: 'select',
|
||
selectList: ua.getDataBrage('select_list_status'),
|
||
title: '状态',
|
||
width: 90,
|
||
templet: ua.table.switch
|
||
},
|
||
// 并发数:NULL=继承代码默认;正整数=覆盖;行内可改(edit:'text' → modify)
|
||
{
|
||
field: 'concurrency',
|
||
title: '并发',
|
||
width: 110,
|
||
align: 'center',
|
||
search: false,
|
||
edit: 'text',
|
||
templet: function (d) {
|
||
if (d.concurrency === null || d.concurrency === undefined || d.concurrency === '') {
|
||
return '<span class="layui-badge layui-bg-gray">默认</span>';
|
||
}
|
||
return d.concurrency;
|
||
}
|
||
},
|
||
// 手动触发目标节点(只读展示)
|
||
{
|
||
field: 'trigger_node_id',
|
||
title: '触发节点',
|
||
width: 140,
|
||
search: false,
|
||
templet: function (d) {
|
||
if (!d.trigger_node_id) {
|
||
return '<span class="layui-text-em">-</span>';
|
||
}
|
||
return d.trigger_node_id;
|
||
}
|
||
},
|
||
// 归属列(节点叠加):无选中节点时显示"全局"占位,有节点时按 run_type+主节点算
|
||
{
|
||
field: '_ownership',
|
||
title: '归属',
|
||
width: 130,
|
||
align: 'center',
|
||
search: false,
|
||
templet: function (d) {
|
||
if (!currentNodeId) {
|
||
return '<span class="layui-badge layui-bg-gray">全局</span>';
|
||
}
|
||
var node = nodeMap[currentNodeId];
|
||
var isMaster = node ? node.is_master : 0;
|
||
var info = calcOwnership(d.run_type, d.status, isMaster);
|
||
return '<span class="' + info.cls + '">' + info.text + '</span>';
|
||
}
|
||
},
|
||
{
|
||
field: 'is_synced',
|
||
search: 'select',
|
||
selectList: ua.getDataBrage('select_list_is_synced'),
|
||
title: '同步状态',
|
||
width: 100,
|
||
templet: function (d) {
|
||
if (d.is_synced == 1) {
|
||
return '<span class="layui-badge layui-bg-green">已同步</span>';
|
||
}
|
||
return '<span class="layui-badge">未同步</span>';
|
||
}
|
||
},
|
||
{ field: 'last_execute_node', title: '最后执行节点', width: 140, search: false },
|
||
{
|
||
field: 'last_execute_time',
|
||
title: '最后执行时间',
|
||
width: 170,
|
||
search: 'range',
|
||
templet: function (d) {
|
||
if (!d.last_execute_time || d.last_execute_time == 0) {
|
||
return '<span class="layui-text-em">暂无</span>';
|
||
}
|
||
var date = new Date(d.last_execute_time * 1000);
|
||
var Y = date.getFullYear();
|
||
var m = ('0' + (date.getMonth() + 1)).slice(-2);
|
||
var day = ('0' + date.getDate()).slice(-2);
|
||
var H = ('0' + date.getHours()).slice(-2);
|
||
var i = ('0' + date.getMinutes()).slice(-2);
|
||
var s = ('0' + date.getSeconds()).slice(-2);
|
||
return Y + '-' + m + '-' + day + ' ' + H + ':' + i + ':' + s;
|
||
}
|
||
},
|
||
{
|
||
width: 250, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
|
||
[{
|
||
class: 'layui-btn layui-btn-primary layui-btn-xs',
|
||
method: 'tab',
|
||
field: 'id',
|
||
text: '详情',
|
||
title: '查看详情',
|
||
auth: 'read',
|
||
url: init.readUrl,
|
||
icon: ''
|
||
}],
|
||
'edit',
|
||
[{
|
||
// 手动触发:定向选中节点(method:none,自处理 click + POST body 带 trigger_node_id)
|
||
// D19:跨 run_type、穿透 status(调试工作流)
|
||
class: 'layui-btn layui-btn-warm layui-btn-xs timer-trigger-btn',
|
||
method: 'none',
|
||
field: 'id',
|
||
text: '触发',
|
||
title: '手动触发到选中节点',
|
||
auth: 'trigger',
|
||
url: init.triggerUrl,
|
||
icon: '',
|
||
extend: '',
|
||
// 节点叠加层启用:必须选中节点才显示触发按钮
|
||
_if: function () {
|
||
return currentNodeId !== '';
|
||
}
|
||
}]
|
||
]
|
||
},
|
||
]];
|
||
|
||
// ============ 初始化表格(配置基础层,永远渲染)============
|
||
// C 类页面:有限操作(add/delete 已在控制器禁用),仅保留 refresh + export
|
||
ua.table.render({
|
||
init: init,
|
||
toolbar: ['refresh', 'export'],
|
||
cols: cols,
|
||
});
|
||
|
||
ua.listen();
|
||
|
||
// ============ 行内编辑(concurrency)校验 + all 模式预估提示 ============
|
||
// ua.table 已通过 listenEdit 绑定 edit 事件并 POST 到 modifyUrl;
|
||
// 这里挂钩 layui table edit 事件做前置校验 + all 模式预估提示
|
||
if (window.layui && layui.table) {
|
||
layui.table.on('edit(' + tableId + '_LayFilter)', function (obj) {
|
||
if (obj.field === 'concurrency') {
|
||
var val = obj.value;
|
||
// 空:行内编辑不允许置空(继承默认请走 edit 表单),modify 接口也会拒绝
|
||
if (val === '' || !/^[1-9]\d*$/.test(val)) {
|
||
ua.msg.error('concurrency 必须是正整数(继承默认请走编辑表单)');
|
||
layui.table.reloadData(tableId);
|
||
return false;
|
||
}
|
||
// all 模式预估提示:全网并发 = 在线节点数 × 此值
|
||
var rowData = obj.data;
|
||
if (rowData.run_type === 'all' && onlineNodeCount > 0) {
|
||
var total = onlineNodeCount * parseInt(val, 10);
|
||
setTimeout(function () {
|
||
layer.tips(
|
||
'all 模式:全网预估 = ' + onlineNodeCount + ' 节点 × ' + val + ' = ' + total + ' 并发',
|
||
'.layui-table-edit-tips',
|
||
{ tips: [2, '#FF5722'], time: 6000 }
|
||
);
|
||
}, 100);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ============ 手动触发按钮(定向选中节点)============
|
||
// method:'none' 不绑定默认行为,自处理 click
|
||
$(document).on('click', '.timer-trigger-btn', function (e) {
|
||
e.preventDefault();
|
||
var taskId = $(this).attr('data-id');
|
||
if (!taskId) {
|
||
ua.msg.error('未找到任务 ID');
|
||
return;
|
||
}
|
||
if (!currentNodeId) {
|
||
// 兜底(_if 已阻止渲染,但用户切节点后表格未 reload 时仍可能进入)
|
||
ua.msg.error('请先在顶部选择要触发的节点');
|
||
return;
|
||
}
|
||
var node = nodeMap[currentNodeId] || {};
|
||
var master = node.is_master == 1 ? '主节点' : '非主节点';
|
||
var ip = node.ip_address || '';
|
||
ua.msg.confirm(
|
||
'确定将此任务触发到节点 [' + currentNodeId + ' / ' + master + (ip ? ' / ' + ip : '') + '] 执行?\n(跨 run_type、穿透 status)',
|
||
function () {
|
||
ua.request.post({
|
||
url: ua.url(init.triggerUrl + '?id=' + taskId),
|
||
prefix: true,
|
||
data: { trigger_node_id: currentNodeId },
|
||
}, function (res) {
|
||
ua.msg.success(res.msg || '触发成功,等待定时器执行', function () {
|
||
layui.table.reloadData(tableId);
|
||
});
|
||
});
|
||
}
|
||
);
|
||
});
|
||
|
||
// ============ 节点选择器联动 ============
|
||
if (window.layui && layui.form) {
|
||
layui.form.on('select(node_filter)', function (data) {
|
||
currentNodeId = data.value || '';
|
||
updateNodeHint();
|
||
// 通知后端 reload(后端可选记录 node 上下文;归属列由前端 JS 算,不依赖后端)
|
||
layui.table.reloadData(tableId, {
|
||
where: { node_id: currentNodeId }
|
||
});
|
||
});
|
||
}
|
||
|
||
// ============ 首次加载节点列表(异步,不阻塞表格渲染)============
|
||
loadNodeList();
|
||
});
|