refactor(timer): TimerConfig 控制器与视图迁移到 Base 层

控制器逻辑移入 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>
This commit is contained in:
augushong
2026-07-25 22:39:20 +08:00
parent 1d704fc32c
commit 009647f96f
16 changed files with 183 additions and 172 deletions

View File

@@ -0,0 +1,179 @@
<?php
namespace base\admin\controller\system;
use app\common\controller\AdminController;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnotation;
use think\App;
use think\facade\Cache;
/**
* @ControllerAnnotation(title="定时任务协调配置表")
*/
class TimerConfigBase extends AdminController
{
use \app\admin\traits\Curd;
public function __construct(App $app)
{
parent::__construct($app);
$this->model = new \app\admin\model\SystemTimerConfig();
$this->assign('select_list_run_type', $this->model::SELECT_LIST_RUN_TYPE, true);
$this->assign('select_list_status', $this->model::SELECT_LIST_STATUS, true);
$this->assign('select_list_is_synced', $this->model::SELECT_LIST_IS_SYNCED, true);
$this->assign('select_list_manual_trigger', $this->model::SELECT_LIST_MANUAL_TRIGGER, true);
// 允许通过行内修改的字段
$this->allowModifyFields = [
'status',
'run_type',
'concurrency',
];
}
/**
* @NodeAnotation(title="添加")
*/
public function add()
{
$this->error('定时任务配置由系统同步生成,不支持手动添加');
}
/**
* @NodeAnotation(title="删除")
*/
public function delete($id)
{
$this->error('定时任务配置由系统管理,不支持删除');
}
/**
* @NodeAnotation(title="编辑")
*/
public function edit($id)
{
$row = $this->model->find($id);
empty($row) && $this->error('数据不存在');
if ($this->request->isPost()) {
$post = $this->request->post();
// 只允许修改 run_type、status、concurrency
$post = array_intersect_key($post, array_flip(['run_type', 'status', 'concurrency']));
if (array_key_exists('concurrency', $post)) {
// 空/null=继承代码默认写 NULL正整数=覆盖0或负数=拒绝M5 防静默吞任务)
$post['concurrency'] = $this->normalizeConcurrency($post['concurrency']);
}
try {
$save = $row->save($post);
} catch (\Exception $e) {
$this->error('保存失败:' . $e->getMessage());
}
if ($save) {
// D18DB commit 后递增脏标记,触发 timer reloadedit 改 concurrency/run_type/status与 trigger 一致)
Cache::inc('timer_config_version');
$this->success('保存成功');
}
$this->error('保存失败');
}
$this->assign('row', $row);
return $this->fetch();
}
/**
* @NodeAnotation(title="手动触发")
*/
public function trigger($id)
{
$this->checkPostRequest();
$row = $this->model->find($id);
if (!$row) {
$this->error('数据不存在');
}
$trigger_node_id = $this->request->post('trigger_node_id');
if (empty($trigger_node_id)) {
$this->error('请选择节点');
}
try {
// 手动触发跨 run_type、穿透 statusD19调试工作流关闭+手动触发+打开)
$row->save([
'manual_trigger' => 1,
'trigger_node_id' => $trigger_node_id,
'last_trigger_time' => time(),
]);
// D18DB commit 成功后才递增 version防脏读commit 前递增→节点 reload 读旧值→永不 reload
Cache::inc('timer_config_version');
} catch (\Exception $e) {
$this->error('触发失败:' . $e->getMessage());
}
$this->success('触发成功,等待定时器执行');
}
/**
* @NodeAnotation(title="节点列表")
*/
public function hostList()
{
$list = \app\admin\model\SystemHost::where('status', 1)
->field('node_id,is_master,ip_address,last_heartbeat_at')
->order('is_master desc, node_id asc')
->select();
$this->success('', $list);
}
/**
* @NodeAnotation(title="属性修改")
*/
public function modify()
{
$this->checkPostRequest();
$post = $this->request->post();
$rule = [
'id|ID' => 'require',
'field|字段' => 'require',
'value|值' => 'require',
];
$this->validate($post, $rule);
if (!in_array($post['field'], $this->allowModifyFields)) {
$this->error('该字段不允许修改:' . $post['field']);
}
// concurrency 行内修改必须为正整数M5 防静默吞任务;继承默认请走 edit 表单传空值)
if ($post['field'] === 'concurrency' && (!is_numeric($post['value']) || (int) $post['value'] < 1)) {
$this->error('concurrency 必须是正整数');
}
$row = $this->model->find($post['id']);
empty($row) && $this->error('数据不存在');
$value = $post['field'] === 'concurrency' ? (int) $post['value'] : $post['value'];
try {
$save = $row->save([$post['field'] => $value]);
} catch (\Exception $e) {
$this->error($e->getMessage());
}
if ($save) {
// D18DB commit 后递增脏标记,触发 timer reload行内改 concurrency/status/run_type同 edit
Cache::inc('timer_config_version');
}
$this->success('保存成功');
}
/**
* 归一化 concurrency 入库值.
* 空字符串/null → null继承代码默认正整数 → int0或负数/非数字 → 拒绝.
*
* @param mixed $value 提交值
* @return int|null
*/
protected function normalizeConcurrency($value)
{
if ($value === '' || $value === null) {
return null;
}
if (!is_numeric($value) || (int) $value < 1) {
$this->error('concurrency 必须是正整数或空(继承默认)');
}
return (int) $value;
}
}

View File

@@ -0,0 +1,12 @@
var init = {
tableElem: '#currentTable',
tableRenderId: 'currentTableRenderId',
indexUrl: 'system.timer_config/index',
addUrl: 'system.timer_config/add' + location.search,
editUrl: 'system.timer_config/edit',
readUrl: 'system.timer_config/read',
deleteUrl: 'system.timer_config/delete',
exportUrl: 'system.timer_config/export',
modifyUrl: 'system.timer_config/modify',
triggerUrl: 'system.timer_config/trigger',
};

View File

@@ -0,0 +1,70 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">任务名称</label>
<div class="layui-input-block">
<input type="text" name="task_name" class="layui-input" lay-verify="required" placeholder="请输入任务名称" value="{$Request.param.task_name|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">运行类型:main/auto/all/manual</label>
<div class="layui-input-block">
<select name="run_type" >
<option value=''></option>
{foreach $select_list_run_type as $k=>$v}
<option value='{$k}' {in name="k" value="$Request.param.run_type"}selected=""{/in}>{$v}</option>
{/foreach}
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态:0=停用,1=启用</label>
<div class="layui-input-block">
{foreach $select_list_status as $k=>$v}
<input type="radio" name="status" value="{$k}" title="{$v}" {in name="k" value="$Request.param.status"}checked=""{/in}>
{/foreach}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">是否已同步:0=未同步,1=已同步</label>
<div class="layui-input-block">
{foreach $select_list_is_synced as $k=>$v}
<input type="radio" name="is_synced" value="{$k}" title="{$v}" {in name="k" value="$Request.param.is_synced"}checked=""{/in}>
{/foreach}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">最后执行节点ID</label>
<div class="layui-input-block">
<input type="text" name="last_execute_node" class="layui-input" placeholder="请输入最后执行节点ID" value="{$Request.param.last_execute_node|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">最后执行时间戳</label>
<div class="layui-input-block">
<input type="text" name="last_execute_time" data-date="" data-date-type="datetime" class="layui-input" placeholder="请输入最后执行时间戳" value="{$Request.param.last_execute_time|default='0'}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">手动触发标记:0=未触发,1=已触发</label>
<div class="layui-input-block">
{foreach $select_list_manual_trigger as $k=>$v}
<input type="radio" name="manual_trigger" value="{$k}" title="{$v}" {in name="k" value="$Request.param.manual_trigger"}checked=""{/in}>
{/foreach}
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})

View File

@@ -0,0 +1,86 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item">
<label class="layui-form-label">任务名称</label>
<div class="layui-input-block">
<input type="text" name="task_name" class="layui-input layui-disabled" disabled value="{$row.task_name|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">运行类型</label>
<div class="layui-input-block">
<select name="run_type" lay-verify="required">
<option value=''></option>
{foreach $select_list_run_type as $k=>$v}
<option value='{$k}' {in name="k" value="$row.run_type"}selected=""{/in}>{$v}</option>
{/foreach}
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
{foreach $select_list_status as $k=>$v}
<input type="radio" name="status" value="{$k}" title="{$v}" {in name="k" value="$row.status"}checked=""{/in}>
{/foreach}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">并发数</label>
<div class="layui-input-block" style="padding-top:8px;">
<div class="layui-inline" style="margin-right:15px;">
<input type="radio" name="concurrency_type" lay-filter="concurrency_type" value="default" title="默认(继承代码)" {if condition="$row.concurrency eq null"}checked{/if}>
<input type="radio" name="concurrency_type" lay-filter="concurrency_type" value="custom" title="自定义" {if condition="$row.concurrency neq null"}checked{/if}>
</div>
<div class="layui-inline">
<input type="number" name="concurrency" value="{$row.concurrency}" min="1" step="1" placeholder="正整数" class="layui-input" style="display:inline-block;width:120px;" lay-affix="concurrency_type">
</div>
<div class="layui-form-mid layui-word-aux">
<span class="layui-badge layui-bg-gray">默认=继承代码</span>
空值=继承;正整数=覆盖0或负数=拒绝(防静默吞任务)
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">同步状态</label>
<div class="layui-input-block" style="padding-top: 8px;">
{if $row.is_synced == 1}
<span class="layui-badge layui-bg-green">已同步</span>
{else/}
<span class="layui-badge">未同步</span>
{/if}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">最后执行节点</label>
<div class="layui-input-block" style="padding-top: 8px;">
{$row.last_execute_node|default='<span class="layui-text-em">暂无</span>'}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">最后执行时间</label>
<div class="layui-input-block" style="padding-top: 8px;">
{notempty name="row.last_execute_time"}
{$row.last_execute_time|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无</span>
{/notempty}
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
{notempty name='$Request.param.backTagId'}
<div class="layui-btn layui-btn-sm page-back-button" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</div>
{/notempty}
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,36 @@
$(function(){
var form = layui.form;
// ============ 并发数:默认/自定义切换联动 ============
// 默认 → number input 禁用 + 提交时清空(写 NULL 继承)
// 自定义 → number input 启用,必须正整数
function syncConcurrencyInput() {
var type = $('input[name="concurrency_type"]:checked').val();
var $input = $('input[name="concurrency"]');
if (type === 'default') {
$input.prop('disabled', true).val('');
} else {
$input.prop('disabled', false);
}
form.render();
}
form.on('radio(concurrency_type)', function () {
syncConcurrencyInput();
});
// 初始化(根据模板 checked 状态同步)
syncConcurrencyInput();
// ============ 提交前归一化 ============
// 默认 → concurrency=''(后端 normalizeConcurrency 写 NULL
// 自定义 → 保留输入值后端校验正整数0/负数/非数字被拒)
ua.listen(function (dataField) {
if (dataField.concurrency_type === 'default') {
dataField.concurrency = '';
}
// 移除辅助字段,避免传给后端
delete dataField.concurrency_type;
return dataField;
});
});

View File

@@ -0,0 +1,29 @@
<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>
</div>

View File

@@ -0,0 +1,311 @@
$(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();
});

View File

@@ -0,0 +1,103 @@
<div class="layuimini-container detail-container">
<div class="layuimini-main">
<div class="layui-card detail-card">
<div class="layui-card-header detail-header">
<div class="layui-row">
<div class="layui-col-md9">
<h2 class="detail-title">#{$row.id} {$title}</h2>
<div class="detail-id">ID: {$row.id}</div>
</div>
<div class="layui-col-md3 text-right detail-actions">
<button class="layui-btn layui-btn-primary" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</button>
<button class="layui-btn" onclick="location.href='{:url("edit", ["id" => $row.id])}'">编辑</button>
</div>
</div>
</div>
<div class="layui-card-body detail-content">
<div class="layui-row layui-col-space12">
<div class="layui-col-md8 detail-main">
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">任务名称</div>
<div class="detail-field-value">
{notempty name="row.task_name"}
{$row.task_name}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">运行类型</div>
<div class="detail-field-value">
{$select_list_run_type[$row.run_type]|default=''}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">同步状态</div>
<div class="detail-field-value">
{if $row.is_synced == 1}
<span class="layui-badge layui-bg-green">已同步</span>
{else/}
<span class="layui-badge">未同步</span>
{/if}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">最后执行节点</div>
<div class="detail-field-value">
{notempty name="row.last_execute_node"}
{$row.last_execute_node}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">最后执行时间</div>
<div class="detail-field-value">
{notempty name="row.last_execute_time"}
{$row.last_execute_time|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">手动触发标记</div>
<div class="detail-field-value">
<span class="layui-badge">{$select_list_manual_trigger[$row.manual_trigger]|default=''}</span>
</div>
</div>
</div>
</div>
<div class="layui-col-md4 detail-side">
<h3 class="detail-side-title">基础信息</h3>
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">ID</div>
<div class="detail-field-value">{$row.id}</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">状态</div>
<div class="detail-field-value">
<span class="layui-badge">{$select_list_status[$row.status]|default=''}</span>
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">创建时间</div>
<div class="detail-field-value">
{notempty name="row.create_time"}
{$row.create_time|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})

View File

@@ -0,0 +1,11 @@
var init = {
tableElem: '#currentTable',
tableRenderId: 'currentTableRenderId',
indexUrl: 'system.timer_log/index',
addUrl: '',
editUrl: '',
readUrl: 'system.timer_log/read',
deleteUrl: '',
exportUrl: '',
modifyUrl: '',
};

View File

@@ -0,0 +1,9 @@
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-index="{:auth('system.timer_log/index')}"
data-auth-read="{:auth('system.timer_log/read')}"
lay-filter="currentTable">
</table>
</div>
</div>

View File

@@ -0,0 +1,99 @@
$(function(){
// 状态徽章模板
var statusTemplet = function(d) {
var statusMap = {
'success': {text: '成功', color: '#5FB878', bgColor: '#e8f8ef'},
'error': {text: '失败', color: '#FF5722', bgColor: '#ffe8e2'},
'running': {text: '运行中', color: '#1E9FFF', bgColor: '#e2f1ff'},
};
var item = statusMap[d.status] || {text: d.status, color: '#999', bgColor: '#f5f5f5'};
return '<span style="display:inline-block;padding:2px 10px;border-radius:3px;color:'+item.color+';background:'+item.bgColor+';font-size:12px;">'+item.text+'</span>';
};
// 时间格式化模板
var timeTemplet = function(d) {
if (!d.start_time || d.start_time === 0) return '<span style="color:#999">-</span>';
var date = new Date(d.start_time * 1000);
var Y = date.getFullYear();
var M = (date.getMonth()+1).toString().padStart(2,'0');
var D = date.getDate().toString().padStart(2,'0');
var h = date.getHours().toString().padStart(2,'0');
var m = date.getMinutes().toString().padStart(2,'0');
var s = date.getSeconds().toString().padStart(2,'0');
return Y+'-'+M+'-'+D+' '+h+':'+m+':'+s;
};
var endTimeTemplet = function(d) {
if (!d.end_time || d.end_time === 0) return '<span style="color:#999">-</span>';
var date = new Date(d.end_time * 1000);
var Y = date.getFullYear();
var M = (date.getMonth()+1).toString().padStart(2,'0');
var D = date.getDate().toString().padStart(2,'0');
var h = date.getHours().toString().padStart(2,'0');
var m = date.getMinutes().toString().padStart(2,'0');
var s = date.getSeconds().toString().padStart(2,'0');
return Y+'-'+M+'-'+D+' '+h+':'+m+':'+s;
};
// 耗时格式化模板
var durationTemplet = function(d) {
if (!d.duration || d.duration === 0) return '<span style="color:#999">-</span>';
if (d.duration < 1000) {
return d.duration + ' ms';
}
return (d.duration / 1000).toFixed(2) + ' s';
};
// 错误信息截断模板
var errorMsgTemplet = function(d) {
if (!d.error_message) return '<span style="color:#999">-</span>';
var text = d.error_message;
if (text.length > 50) {
return '<span title="'+text.replace(/"/g,'&quot;').replace(/'/g,'&#39;')+'" style="cursor:pointer;" lay-event="showError">'+text.substring(0,50)+'...</span>';
}
return text;
};
ua.table.render({
init: init,
cols: [[
{type: 'checkbox'},
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'task_name', title: '任务名称', width: 160, search: 'select', searchUrl: 'system.timer_log/index?selectFields=task_name'},
{field: 'node_id', title: '节点ID', width: 120, search: 'select', searchUrl: 'system.timer_log/index?selectFields=node_id'},
{field: 'run_type', title: '运行类型', width: 100},
{field: 'start_time', title: '开始时间', width: 170, templet: timeTemplet, search: 'range'},
{field: 'end_time', title: '结束时间', width: 170, templet: endTimeTemplet},
{field: 'duration', title: '耗时', width: 100, templet: durationTemplet},
{field: 'status', title: '状态', width: 100, templet: statusTemplet, search: 'select', selectList: {success:'成功', error:'失败', running:'运行中'}},
{field: 'error_message', title: '错误信息', minWidth: 200, templet: errorMsgTemplet},
{
width: 100, 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: ''
}]
]
},
]],
});
// 点击错误信息查看完整内容
ua.table.on('tool(currentTable)', function(obj) {
if (obj.event === 'showError') {
var text = obj.data.error_message || '';
layer.alert('<pre style="white-space:pre-wrap;word-break:break-all;max-height:400px;overflow-y:auto;">'+text.replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre>', {
title: '错误信息详情',
area: ['600px', '400px']
});
}
});
ua.listen();
})

View File

@@ -0,0 +1,139 @@
<div class="layuimini-container detail-container">
<div class="layuimini-main">
<div class="layui-card detail-card">
<div class="layui-card-header detail-header">
<div class="layui-row">
<div class="layui-col-md9">
<h2 class="detail-title">#{$row.id} {$title}</h2>
<div class="detail-id">ID: {$row.id}</div>
</div>
<div class="layui-col-md3 text-right detail-actions">
<button class="layui-btn layui-btn-primary" layuimini-content-href="{$Request.param.backTagId}" data-back="1">返回</button>
</div>
</div>
</div>
<div class="layui-card-body detail-content">
<div class="layui-row layui-col-space12">
<!-- 左侧主体内容 -->
<div class="layui-col-md8 detail-main">
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">任务名称</div>
<div class="detail-field-value">
{notempty name="row.task_name"}
{$row.task_name}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">执行节点ID</div>
<div class="detail-field-value">
{notempty name="row.node_id"}
{$row.node_id}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">运行类型</div>
<div class="detail-field-value">
{notempty name="row.run_type"}
{$row.run_type}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">开始时间</div>
<div class="detail-field-value">
{notempty name="row.start_time"}
{:date('Y-m-d H:i:s', $row['start_time'])}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">结束时间</div>
<div class="detail-field-value">
{notempty name="row.end_time"}
{:date('Y-m-d H:i:s', $row['end_time'])}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">耗时</div>
<div class="detail-field-value">
{notempty name="row.duration"}
{:php echo ($row['duration'] < 1000) ? $row['duration'].' ms' : round($row['duration']/1000, 2).' s';}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">错误信息</div>
<div class="detail-field-value" style="white-space: pre-wrap;">
{notempty name="row.error_message"}
{$row.error_message|raw}
{else/}
<span class="layui-text-em">暂无内容</span>
{/notempty}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">并发分片ID</div>
<div class="detail-field-value">
{notempty name="row.concurrency_id"}
{$row.concurrency_id}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
</div>
</div>
<!-- 右侧基础信息 -->
<div class="layui-col-md4 detail-side">
<h3 class="detail-side-title">基础信息</h3>
<div class="detail-field-group">
<div class="detail-field-item">
<div class="detail-field-label">ID</div>
<div class="detail-field-value">{$row.id}</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">状态</div>
<div class="detail-field-value">
{switch name="row.status"}
{case value="success"}<span style="display:inline-block;padding:2px 10px;border-radius:3px;color:#5FB878;background:#e8f8ef;font-size:12px;">成功</span>{/case}
{case value="error"}<span style="display:inline-block;padding:2px 10px;border-radius:3px;color:#FF5722;background:#ffe8e2;font-size:12px;">失败</span>{/case}
{case value="running"}<span style="display:inline-block;padding:2px 10px;border-radius:3px;color:#1E9FFF;background:#e2f1ff;font-size:12px;">运行中</span>{/case}
{default /}<span style="color:#999">{$row.status}</span>
{/switch}
</div>
</div>
<div class="detail-field-item">
<div class="detail-field-label">创建时间</div>
<div class="detail-field-value">
{notempty name="row.create_time"}
{$row.create_time|date="Y-m-d H:i:s"}
{else/}
<span class="layui-text-em">暂无数据</span>
{/notempty}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
$(function(){
ua.listen();
})