mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 12:45:32 +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-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"
|
||||
data-auth-index="{:auth('system.timer_config/index')}"
|
||||
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-modify="{:auth('system.timer_config/modify')}"
|
||||
data-auth-trigger="{:auth('system.timer_config/trigger')}"
|
||||
lay-filter="currentTable">
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,78 +1,309 @@
|
||||
$(function(){
|
||||
ua.table.render({
|
||||
init: init,
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'id', title: 'ID', width: 80},
|
||||
{field: 'task_name', title: '任务名称', minWidth: 160},
|
||||
{field: 'run_type', search: 'select', selectList: ua.getDataBrage('select_list_run_type'), title: '运行类型', width: 130, templet: function(d){
|
||||
// ============ 节点叠加层状态(模块级) ============
|
||||
// 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: 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">未同步</span>';
|
||||
}},
|
||||
{field: 'last_execute_node', title: '最后执行节点', width: 140},
|
||||
{field: 'last_execute_time', title: '最后执行时间', width: 170, templet: function(d){
|
||||
if(!d.last_execute_time || d.last_execute_time == 0){
|
||||
}
|
||||
},
|
||||
{ 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 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: 200, 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',
|
||||
[{
|
||||
class: 'layui-btn layui-btn-warm layui-btn-xs',
|
||||
method: 'request',
|
||||
field: 'id',
|
||||
text: '触发',
|
||||
title: '手动触发',
|
||||
auth: 'trigger',
|
||||
url: init.triggerUrl,
|
||||
icon: '',
|
||||
extend: '',
|
||||
callback: function(o){
|
||||
// 只对 manual 类型显示,通过 CSS 控制显隐
|
||||
}
|
||||
}]
|
||||
]
|
||||
},
|
||||
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 !== '';
|
||||
}
|
||||
}]
|
||||
]
|
||||
},
|
||||
]];
|
||||
|
||||
]],
|
||||
// ============ 初始化表格(配置基础层,永远渲染)============
|
||||
ua.table.render({
|
||||
init: init,
|
||||
cols: cols,
|
||||
});
|
||||
|
||||
// 控制触发按钮显隐:只有 run_type=manual 时显示
|
||||
ua.listen();
|
||||
|
||||
// 监听表格渲染完成后处理触发按钮显隐
|
||||
$(document).on('renderComplete', function(){
|
||||
$('table tbody tr').each(function(){
|
||||
var runType = $(this).find('td[data-field="run_type"] .layui-table-cell').text().trim();
|
||||
if(runType !== 'manual'){
|
||||
$(this).find('.layui-btn[title="手动触发"]').hide();
|
||||
// ============ 行内编辑(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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user