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();
+});