diff --git a/app/admin/view/system/timer_config/index.html b/app/admin/view/system/timer_config/index.html index e1a373d..d1c7866 100644 --- a/app/admin/view/system/timer_config/index.html +++ b/app/admin/view/system/timer_config/index.html @@ -1,10 +1,28 @@
+ +
+
+ +
+ +
+
+
+ +
+
+
diff --git a/app/admin/view/system/timer_config/index.js b/app/admin/view/system/timer_config/index.js index 748db9a..003202b 100644 --- a/app/admin/view/system/timer_config/index.js +++ b/app/admin/view/system/timer_config/index.js @@ -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(''); + }); + // 重新渲染 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 '默认'; + } + return d.concurrency; + } + }, + // 手动触发目标节点(只读展示) + { + field: 'trigger_node_id', + title: '触发节点', + width: 140, + search: false, + templet: function (d) { + if (!d.trigger_node_id) { + return '-'; + } + return d.trigger_node_id; + } + }, + // 归属列(节点叠加):无选中节点时显示"全局"占位,有节点时按 run_type+主节点算 + { + field: '_ownership', + title: '归属', + width: 130, + align: 'center', + search: false, + templet: function (d) { + if (!currentNodeId) { + return '全局'; + } + var node = nodeMap[currentNodeId]; + var isMaster = node ? node.is_master : 0; + var info = calcOwnership(d.run_type, d.status, isMaster); + return '' + info.text + ''; + } + }, + { + field: 'is_synced', + search: 'select', + selectList: ua.getDataBrage('select_list_is_synced'), + title: '同步状态', + width: 100, + templet: function (d) { + if (d.is_synced == 1) { return '已同步'; } return '未同步'; - }}, - {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 '暂无'; } 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(); +});