mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-09-05 07:15:31 +08:00
feat(timer): 列表页节点视角下拉与归属可视化
新增节点视角下拉(全局/指定节点),calcOwnership 计算任务归属,触发操作联动节点选择器定向 trigger_node_id,并发列双通道显示(默认/覆盖值)。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,10 +1,28 @@
|
|||||||
<div class="layuimini-container">
|
<div class="layuimini-container">
|
||||||
<div class="layuimini-main">
|
<div class="layuimini-main">
|
||||||
|
<!-- 节点叠加层:选择节点后显示归属/触发;不选=纯配置视图(配置基础层永远在) -->
|
||||||
|
<div class="layui-form timer-node-filter" lay-filter="timerNodeFilter" style="margin: 10px 0;">
|
||||||
|
<div class="layui-inline">
|
||||||
|
<label class="layui-form-label" style="width:90px;">节点视角</label>
|
||||||
|
<div class="layui-input-inline" style="width:240px;">
|
||||||
|
<select name="node_filter" id="node_filter" lay-filter="node_filter">
|
||||||
|
<option value="">全局配置(不选节点)</option>
|
||||||
|
<!-- 动态填充:调 hostList 接口 -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-inline" style="line-height:38px;">
|
||||||
|
<span class="layui-badge layui-bg-blue" id="node_hint" style="display:none;"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table id="currentTable" class="layui-table layui-hide"
|
<table id="currentTable" class="layui-table layui-hide"
|
||||||
data-auth-index="{:auth('system.timer_config/index')}"
|
data-auth-index="{:auth('system.timer_config/index')}"
|
||||||
data-auth-edit="{:auth('system.timer_config/edit')}"
|
data-auth-edit="{:auth('system.timer_config/edit')}"
|
||||||
|
data-auth-read="{:auth('system.timer_config/read')}"
|
||||||
data-auth-export="{:auth('system.timer_config/export')}"
|
data-auth-export="{:auth('system.timer_config/export')}"
|
||||||
data-auth-modify="{:auth('system.timer_config/modify')}"
|
data-auth-modify="{:auth('system.timer_config/modify')}"
|
||||||
|
data-auth-trigger="{:auth('system.timer_config/trigger')}"
|
||||||
lay-filter="currentTable">
|
lay-filter="currentTable">
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,37 +1,195 @@
|
|||||||
$(function(){
|
$(function(){
|
||||||
ua.table.render({
|
// ============ 节点叠加层状态(模块级) ============
|
||||||
init: init,
|
// currentNodeId: 当前选中的节点 ID;为空表示纯配置视图(配置基础层)
|
||||||
cols: [[
|
// nodeMap: { node_id: {node_id, is_master, ip_address, last_heartbeat_at} }
|
||||||
{type: 'checkbox'},
|
// onlineNodeCount: 在线节点总数(用于 all 模式全网预估提示)
|
||||||
{field: 'id', title: 'ID', width: 80},
|
var currentNodeId = '';
|
||||||
{field: 'task_name', title: '任务名称', minWidth: 160},
|
var nodeMap = {};
|
||||||
{field: 'run_type', search: 'select', selectList: ua.getDataBrage('select_list_run_type'), title: '运行类型', width: 130, templet: function(d){
|
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');
|
var map = ua.getDataBrage('select_list_run_type');
|
||||||
return map[d.run_type] || d.run_type;
|
return map[d.run_type] || d.run_type;
|
||||||
}},
|
}
|
||||||
{field: 'status', search: 'select', selectList: ua.getDataBrage('select_list_status'), title: '状态', width: 100, templet: ua.table.switch},
|
},
|
||||||
{field: 'is_synced', search: 'select', selectList: ua.getDataBrage('select_list_is_synced'), title: '同步状态', width: 100, templet: function(d){
|
{
|
||||||
if(d.is_synced == 1){
|
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 layui-bg-green">已同步</span>';
|
||||||
}
|
}
|
||||||
return '<span class="layui-badge">未同步</span>';
|
return '<span class="layui-badge">未同步</span>';
|
||||||
}},
|
}
|
||||||
{field: 'last_execute_node', title: '最后执行节点', width: 140},
|
},
|
||||||
{field: 'last_execute_time', title: '最后执行时间', width: 170, templet: function(d){
|
{ field: 'last_execute_node', title: '最后执行节点', width: 140, search: false },
|
||||||
if(!d.last_execute_time || d.last_execute_time == 0){
|
{
|
||||||
|
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>';
|
return '<span class="layui-text-em">暂无</span>';
|
||||||
}
|
}
|
||||||
var date = new Date(d.last_execute_time * 1000);
|
var date = new Date(d.last_execute_time * 1000);
|
||||||
var Y = date.getFullYear();
|
var Y = date.getFullYear();
|
||||||
var m = ('0' + (date.getMonth()+1)).slice(-2);
|
var m = ('0' + (date.getMonth() + 1)).slice(-2);
|
||||||
var day = ('0' + date.getDate()).slice(-2);
|
var day = ('0' + date.getDate()).slice(-2);
|
||||||
var H = ('0' + date.getHours()).slice(-2);
|
var H = ('0' + date.getHours()).slice(-2);
|
||||||
var i = ('0' + date.getMinutes()).slice(-2);
|
var i = ('0' + date.getMinutes()).slice(-2);
|
||||||
var s = ('0' + date.getSeconds()).slice(-2);
|
var s = ('0' + date.getSeconds()).slice(-2);
|
||||||
return Y+'-'+m+'-'+day+' '+H+':'+i+':'+s;
|
return Y + '-' + m + '-' + day + ' ' + H + ':' + i + ':' + s;
|
||||||
}},
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
width: 200, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
|
width: 250, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
|
||||||
[{
|
[{
|
||||||
class: 'layui-btn layui-btn-primary layui-btn-xs',
|
class: 'layui-btn layui-btn-primary layui-btn-xs',
|
||||||
method: 'tab',
|
method: 'tab',
|
||||||
@@ -44,35 +202,108 @@ $(function(){
|
|||||||
}],
|
}],
|
||||||
'edit',
|
'edit',
|
||||||
[{
|
[{
|
||||||
class: 'layui-btn layui-btn-warm layui-btn-xs',
|
// 手动触发:定向选中节点(method:none,自处理 click + POST body 带 trigger_node_id)
|
||||||
method: 'request',
|
// D19:跨 run_type、穿透 status(调试工作流)
|
||||||
|
class: 'layui-btn layui-btn-warm layui-btn-xs timer-trigger-btn',
|
||||||
|
method: 'none',
|
||||||
field: 'id',
|
field: 'id',
|
||||||
text: '触发',
|
text: '触发',
|
||||||
title: '手动触发',
|
title: '手动触发到选中节点',
|
||||||
auth: 'trigger',
|
auth: 'trigger',
|
||||||
url: init.triggerUrl,
|
url: init.triggerUrl,
|
||||||
icon: '',
|
icon: '',
|
||||||
extend: '',
|
extend: '',
|
||||||
callback: function(o){
|
// 节点叠加层启用:必须选中节点才显示触发按钮
|
||||||
// 只对 manual 类型显示,通过 CSS 控制显隐
|
_if: function () {
|
||||||
|
return currentNodeId !== '';
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
]];
|
||||||
|
|
||||||
]],
|
// ============ 初始化表格(配置基础层,永远渲染)============
|
||||||
|
ua.table.render({
|
||||||
|
init: init,
|
||||||
|
cols: cols,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 控制触发按钮显隐:只有 run_type=manual 时显示
|
|
||||||
ua.listen();
|
ua.listen();
|
||||||
|
|
||||||
// 监听表格渲染完成后处理触发按钮显隐
|
// ============ 行内编辑(concurrency)校验 + all 模式预估提示 ============
|
||||||
$(document).on('renderComplete', function(){
|
// ua.table 已通过 listenEdit 绑定 edit 事件并 POST 到 modifyUrl;
|
||||||
$('table tbody tr').each(function(){
|
// 这里挂钩 layui table edit 事件做前置校验 + all 模式预估提示
|
||||||
var runType = $(this).find('td[data-field="run_type"] .layui-table-cell').text().trim();
|
if (window.layui && layui.table) {
|
||||||
if(runType !== 'manual'){
|
layui.table.on('edit(' + tableId + '_LayFilter)', function (obj) {
|
||||||
$(this).find('.layui-btn[title="手动触发"]').hide();
|
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();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user