From 11377b67619cba15fab25dda9b11ab31e582469f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=99=AF=E7=AB=8B?= Date: Thu, 21 May 2026 00:17:01 +0800 Subject: [PATCH] =?UTF-8?q?style(queue):=20=E5=AE=8C=E5=96=84=E9=98=9F?= =?UTF-8?q?=E5=88=97=E8=BF=9B=E5=BA=A6=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 队列执行弹窗增加状态点、进度文字、任务状态标记和更大的日志面板,并对日志内容进行 HTML 转义,避免任务输出影响页面结构。 优化进度条和日志区域样式,支持成功、执行中和失败状态的不同视觉反馈,同时保留自动滚动查看最新日志。 为队列表格时间与耗时格式化增加前端兜底函数,修正脚本中转义后的 变量输出,并更新测试确保生成脚本可直接执行。 --- .../stc/public/static/plugs/system/queue.js | 77 +++++++++++++------ .../src/builder/QueueBuilder.php | 64 ++++++++++++--- .../tests/QueueControllerTest.php | 3 + 3 files changed, 110 insertions(+), 34 deletions(-) diff --git a/plugin/think-plugs-static/stc/public/static/plugs/system/queue.js b/plugin/think-plugs-static/stc/public/static/plugs/system/queue.js index df342d2b2..0a3784da3 100644 --- a/plugin/think-plugs-static/stc/public/static/plugs/system/queue.js +++ b/plugin/think-plugs-static/stc/public/static/plugs/system/queue.js @@ -15,12 +15,16 @@ layui.define(function (exports) { let template = [ - '
', + '
', '
', - '
...
', + '
', + ' ', + '
...
', + '
', '
# {{d.code}}
', '
', - '
', + '
执行进度0.00%
', + '
', '
', '
执行日志实时刷新
', ' ', @@ -33,32 +37,49 @@ layui.define(function (exports) { let style = document.createElement('style'); style.id = 'ta-queue-style'; style.innerHTML = [ - '.ta-queue-layer{border-radius:10px;overflow:hidden;}', - '.ta-queue-layer .layui-layer-title{height:48px;line-height:48px;font-weight:600;border-bottom:1px solid #edf0f5;background:#fff;}', - '.ta-queue-layer .layui-layer-content{overflow:hidden;background:#f6f8fb;}', - '.ta-queue-box{padding:18px 20px 20px;background:#f6f8fb;}', - '.ta-queue-status{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:13px 15px;background:#fff;border:1px solid #edf0f5;border-radius:10px;box-shadow:0 4px 18px rgba(15,35,60,.04);}', - '.ta-queue-status-title{min-width:0;flex:1;font-weight:600;}', - '.ta-queue-status-title b{font-weight:600;}', - '.ta-queue-status-code{flex-shrink:0;max-width:220px;color:#8c98a8;font-size:12px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}', - '.ta-queue-progress{height:12px;margin:14px 2px 0;border-radius:999px;background:#e8edf5;overflow:hidden;}', - '.ta-queue-progress .layui-progress-bar{border-radius:999px;}', - '.ta-queue-progress .layui-progress-text{top:-6px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.2);}', - '.ta-queue-log{margin-top:14px;border-radius:10px;background:#222b33;border:1px solid #1a222a;overflow:hidden;box-shadow:inset 0 0 0 1px rgba(255,255,255,.02);}', - '.ta-queue-log-head{height:36px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;background:#172027;color:#dce5ee;font-size:13px;}', - '.ta-queue-log-head small{font-size:12px;font-weight:400;color:#7f8b98;}', - '.ta-queue-log code{display:block;height:185px;margin:0;padding:12px 14px;border:0;background:#222b33;color:#dce5ee;font-family:Consolas,Monaco,Menlo,monospace;font-size:12px;line-height:22px;white-space:normal;overflow:auto;}', - '.ta-queue-log code p{height:22px;line-height:22px;margin:0;color:#dce5ee;}', - '.ta-queue-log code .color-desc{color:#8c98a8!important;}' + '@keyframes taQueuePulse{0%{box-shadow:0 0 0 0 rgba(59,130,246,.28);}70%{box-shadow:0 0 0 8px rgba(59,130,246,0);}100%{box-shadow:0 0 0 0 rgba(59,130,246,0);}}', + '.ta-queue-layer{border-radius:12px;overflow:hidden;box-shadow:0 18px 45px rgba(15,23,42,.22);}', + '.ta-queue-layer .layui-layer-title{height:50px;line-height:50px;padding:0 50px 0 18px;color:#1f2937;font-size:15px;font-weight:700;border-bottom:1px solid var(--ta-border-color,#edf0f5);background:linear-gradient(180deg,#fff,#f8fbfc);}', + '.ta-queue-layer .layui-layer-setwin{top:17px;right:16px;}', + '.ta-queue-layer .layui-layer-content{overflow:hidden;background:var(--ta-body-bg,#f4f7fb);}', + '.ta-queue-box{height:100%;min-height:0;box-sizing:border-box;display:flex;flex-direction:column;padding:18px 20px 20px;background:var(--ta-body-bg,#f4f7fb);}', + '.ta-queue-status{min-height:50px;flex-shrink:0;display:flex;align-items:center;justify-content:space-between;gap:14px;box-sizing:border-box;padding:12px 14px;background:var(--ta-surface,#fff);border:1px solid var(--ta-border-color,#edf0f5);border-radius:12px;box-shadow:0 8px 22px rgba(15,35,60,.06);}', + '.ta-queue-status-main{display:flex;align-items:center;gap:10px;min-width:0;flex:1;}', + '.ta-queue-status-dot{width:8px;height:8px;display:inline-block;flex:0 0 8px;border-radius:50%;background:#94a3b8;}', + '.ta-queue-status-title{min-width:0;flex:1;color:#334155;font-size:13px;font-weight:600;text-align:left!important;}', + '.ta-queue-status-title b{font-weight:700;}', + '.ta-queue-status-code{flex-shrink:0;max-width:220px;padding:2px 8px;color:#8c98a8;font-size:12px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:999px;background:#f3f6fa;}', + '.ta-queue-box[data-queue-state="1"] .ta-queue-status-dot,.ta-queue-box[data-queue-state="2"] .ta-queue-status-dot{background:#3b82f6;animation:taQueuePulse 1.6s ease-out infinite;}', + '.ta-queue-box[data-queue-state="3"] .ta-queue-status{border-color:#b7ebc6;background:linear-gradient(135deg,#f6ffed,#fff);}', + '.ta-queue-box[data-queue-state="3"] .ta-queue-status-dot{background:#16a34a;box-shadow:0 0 0 5px rgba(22,163,74,.12);}', + '.ta-queue-box[data-queue-state="4"] .ta-queue-status{border-color:#ffd6d6;background:linear-gradient(135deg,#fff5f5,#fff);}', + '.ta-queue-box[data-queue-state="4"] .ta-queue-status-dot{background:#ff4d4f;box-shadow:0 0 0 5px rgba(255,77,79,.12);}', + '.ta-queue-box[data-queue-state="4"] .ta-queue-status-title,.ta-queue-box[data-queue-state="4"] .ta-queue-status-title b{color:#ff4d4f!important;}', + '.ta-queue-progress-meta{flex-shrink:0;display:flex;align-items:center;justify-content:space-between;margin:12px 2px 6px;color:#7b8794;font-size:12px;line-height:18px;}', + '.ta-queue-progress-meta b{color:#475569;font-size:12px;font-weight:600;}', + '.ta-queue-progress{flex-shrink:0;height:10px;border-radius:999px;background:#e7edf5;overflow:hidden;}', + '.ta-queue-progress .layui-progress-bar{height:10px;border-radius:999px;box-shadow:0 2px 6px rgba(30,64,175,.22);}', + '.ta-queue-log{flex:1 1 auto;min-height:230px;display:flex;flex-direction:column;margin-top:14px;border-radius:12px;background:#202a33;border:1px solid #19232c;overflow:hidden;box-shadow:inset 0 0 0 1px rgba(255,255,255,.02),0 10px 24px rgba(15,23,42,.12);}', + '.ta-queue-log-head{height:38px;flex:0 0 38px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;background:#15202a;color:#e5edf5;font-size:13px;font-weight:600;}', + '.ta-queue-log-head small{font-size:12px;font-weight:400;color:#94a3b8;}', + '.ta-queue-log code{flex:1 1 auto;display:block;height:auto;min-height:0;margin:0;padding:12px 14px;border:0;box-sizing:border-box;background:#202a33;color:#e5edf5;font-family:Consolas,Monaco,Menlo,monospace;font-size:12px;line-height:22px;white-space:normal;overflow:auto;}', + '.ta-queue-log code p{min-height:22px;height:auto;line-height:22px;margin:0;color:#e5edf5;overflow:visible;text-overflow:clip;white-space:pre-wrap;word-break:break-word;}', + '.ta-queue-log code .color-desc{color:#94a3b8!important;}' ].join(''); document.head.appendChild(style); } + function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, function (char) { + return {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[char]; + }); + } + function Queue(code, doScript, element) { let queue = this; injectStyle(); (this.doAjax = true) && (this.doReload = false) || layer.open({ - type: 1, title: '任务执行进度', area: ['620px', '390px'], skin: 'ta-queue-layer', anim: 2, shadeClose: false, end: function () { + type: 1, title: '任务执行进度', area: ['640px', '520px'], skin: 'ta-queue-layer', anim: 2, shadeClose: false, end: function () { queue.doAjax = queue.doReload && doScript && $.layTable.reload(((element || {}).dataset || {}).tableId || true) && false; }, content: laytpl(template).render({code: code}), success: function ($elem) { new Progress($elem, code, queue, doScript); @@ -74,6 +95,7 @@ layui.define(function (exports) { this.$code = this.$box.find('code'); this.$title = this.$box.find('[data-message-title]'); + this.$progressText = this.$box.find('[data-progress-text]'); this.$percent = this.$box.find('.layui-progress div'); // 设置数据缓存 @@ -84,6 +106,7 @@ layui.define(function (exports) { // 更新任务显示状态 this.SetState = function (status, message) { + that.$box.attr('data-queue-state', status); if (message.indexOf('javascript:') === -1) if (status === 1) { that.$title.html('' + message + '').addClass('text-center'); that.$percent.addClass('layui-bg-blue').removeClass('layui-bg-green layui-bg-red'); @@ -121,7 +144,7 @@ layui.define(function (exports) { continue; } if (text.indexOf('javascript:') === -1) { - lines.push(text.indexOf('>>>') > -1 ? text : percent + text); + lines.push(escapeHtml(text.indexOf('>>>') > -1 ? text : percent + text)); } else if (!that.SetCache(code, idx) && doScript !== false) { that.SetCache(code, idx, 1) $.form.goto(text); @@ -130,14 +153,18 @@ layui.define(function (exports) { if (!isFinite(progress)) { progress = 0; } - that.$code.html(lines.length ? '

' + lines.join('

') + '

' : '

暂无执行日志

').animate({scrollTop: that.$code[0].scrollHeight + 'px'}, 200); + progress = Math.max(0, Math.min(100, progress)); + let percentText = progress.toFixed(2) + '%'; + that.$code.html(lines.length ? '

' + lines.join('

') + '

' : '

暂无执行日志

').animate({scrollTop: that.$code[0].scrollHeight + 'px'}, 200); + that.$progressText.text(percentText); if (status > 0) { that.SetState(status, message); - that.$percent.attr('lay-percent', progress.toFixed(2) + '%') && layui.element.render(); + that.$percent.attr('lay-percent', percentText) && layui.element.render(); status === 3 || status === 4 || setTimeout(that.LoadProgress, Math.floor(Math.random() * 200)); } else { + that.$box.attr('data-queue-state', '0'); that.$title.html('' + message + '').addClass('text-center'); - that.$percent.attr('lay-percent', progress.toFixed(2) + '%') && layui.element.render(); + that.$percent.attr('lay-percent', percentText) && layui.element.render(); setTimeout(that.LoadProgress, Math.floor(Math.random() * 500) + 200); } return false; diff --git a/plugin/think-plugs-system/src/builder/QueueBuilder.php b/plugin/think-plugs-system/src/builder/QueueBuilder.php index a5c62c894..596ea1b68 100644 --- a/plugin/think-plugs-system/src/builder/QueueBuilder.php +++ b/plugin/think-plugs-system/src/builder/QueueBuilder.php @@ -124,16 +124,19 @@ function (d) { SCRIPT, self::json(BuilderLang::text('循环')), self::json(BuilderLang::text('单次')), self::json(BuilderLang::text('任务编号:')), self::json(BuilderLang::text('任务名称:'))))]) ->column(['field' => 'exec_time', 'title' => '任务计划', 'minWidth' => 220, 'templet' => PageBuilder::js(sprintf(<<<'SCRIPT' function (d) { - d.html = %s + d.command + '
' + %s + formatQueueTime(d.exec_time, '' + %s + ''); +%s + d.html = %s + d.command + '
' + %s + formatQueueTimeSafe(d.exec_time, '' + %s + ''); if (d.loops_time > 0) { return d.html + ' ( ' + %s + ' ' + d.loops_time + ' ' + %s + ' ) '; } else { return d.html + ' ( ' + %s + ' ) '; } } -SCRIPT, self::json(BuilderLang::text('执行指令:')), self::json(BuilderLang::text('计划执行:')), self::json(BuilderLang::text('未计划')), self::json(BuilderLang::text('每')), self::json(BuilderLang::text('秒')), self::json(BuilderLang::text('单次任务'))))]) +SCRIPT, self::renderQueueTimeFallbackScript(), self::json(BuilderLang::text('执行指令:')), self::json(BuilderLang::text('计划执行:')), self::json(BuilderLang::text('未计划')), self::json(BuilderLang::text('每')), self::json(BuilderLang::text('秒')), self::json(BuilderLang::text('单次任务'))))]) ->column(['field' => 'loops_time', 'title' => '任务状态', 'minWidth' => 260, 'templet' => PageBuilder::js(sprintf(<<<'SCRIPT' function (d) { +%s +%s d.html = ([ '' + %s + '', '' + %s + '', @@ -142,7 +145,7 @@ function (d) { '' + %s + '' ][d.status] || '') + %s; if (String(d.enter_time || '') !== '' && String(d.enter_time) !== '0' && String(d.enter_time) !== '0.0000') { - d.html += formatQueueTime(d.enter_time) + '' + formatQueueCost(d.enter_time, d.outer_time, d.status) + ''; + d.html += formatQueueTimeSafe(d.enter_time) + '' + formatQueueCostSafe(d.enter_time, d.outer_time, d.status) + ''; d.html += ' ' + %s + ' ' + (d.attempts || 0) + ' ' + %s; } else { d.html += '' + %s + ''; @@ -150,6 +153,8 @@ function (d) { return d.html + '
' + %s + '' + (d.exec_desc || '' + %s + '') + ''; } SCRIPT, + self::renderQueueTimeFallbackScript(), + self::renderQueueCostFallbackScript(), self::json(BuilderLang::text('未知')), self::json(BuilderLang::text('等待')), self::json(BuilderLang::text('执行')), @@ -180,6 +185,47 @@ SCRIPT, ->build(); } + private static function renderQueueTimeFallbackScript(): string + { + return sprintf(<<<'SCRIPT' + var formatQueueTimeSafe = window.formatQueueTime || function (value, emptyHtml) { + var num = Number(value || 0); + if (!isFinite(num) || num <= 0) return emptyHtml || '' + %s + ''; + return layui.util.toDateString(Math.round(num * 1000), 'yyyy-MM-dd HH:mm:ss'); + }; +SCRIPT, self::json(BuilderLang::text('未执行'))); + } + + private static function renderQueueCostFallbackScript(): string + { + return sprintf(<<<'SCRIPT' + var formatQueueCostSafe = window.formatQueueCost || function (start, finish, status) { + var begin = Number(start || 0); + var end = Number(finish || 0); + var cost = 0; + var suffix = ''; + if (!isFinite(begin) || begin <= 0) return ''; + if (isFinite(end) && end > begin) { + cost = end - begin; + } else if (Number(status) === 2) { + cost = Date.now() / 1000 - begin; + suffix = %s; + } + if (cost <= 0) return suffix; + if (cost >= 60) return %s + (cost / 60).toFixed(2) + ' ' + %s + suffix; + if (cost >= 1) return %s + cost.toFixed(2) + ' ' + %s + suffix; + return %s + Math.round(cost * 1000) + ' ms' + suffix; + }; +SCRIPT, + self::json(BuilderLang::text(',执行中')), + self::json(BuilderLang::text(',耗时 ')), + self::json(BuilderLang::text('分钟')), + self::json(BuilderLang::text(',耗时 ')), + self::json(BuilderLang::text('秒')), + self::json(BuilderLang::text(',耗时 ')) + ); + } + private static function renderScript(bool $super): string { $queueStatusUrl = apiuri('system/queue/status'); @@ -189,23 +235,23 @@ $(function () { const queueStatusUrl = %s; const queueStatusEnabled = %s; const queueI18n = %s; - const \$queueMessage = \$('[data-queue-message]'); + const $queueMessage = $('[data-queue-message]'); let queueStatusTimer = 0; let queueStatusRequest = null; const setQueueStatusText = function (text, color, tips) { - if (\$queueMessage.length < 1) return; - \$queueMessage.attr('data-tips-text', tips || text).html('' + text + ''); + if ($queueMessage.length < 1) return; + $queueMessage.attr('data-tips-text', tips || text).html('' + text + ''); }; const loadQueueServiceStatus = function (times, delay) { - if (!queueStatusEnabled || \$queueMessage.length < 1) return; + if (!queueStatusEnabled || $queueMessage.length < 1) return; clearTimeout(queueStatusTimer); if (queueStatusRequest && queueStatusRequest.readyState !== 4) queueStatusRequest.abort(); queueStatusRequest = $.ajax({ url: $.menu.parseUri(queueStatusUrl), type: 'GET', success: function (html) { - \$queueMessage.attr('data-tips-text', queueI18n.clickRefreshServiceStatus).html(html); + $queueMessage.attr('data-tips-text', queueI18n.clickRefreshServiceStatus).html(html); if (times > 1) queueStatusTimer = setTimeout(function () { loadQueueServiceStatus(times - 1, delay); }, delay); }, error: function (xhr) { @@ -249,7 +295,7 @@ $(function () { }); return false; }); - \$queueMessage.off('click.queue-status').on('click.queue-status', function () { + $queueMessage.off('click.queue-status').on('click.queue-status', function () { setQueueStatusText(queueI18n.checking, 'color-desc', queueI18n.refreshingServiceStatus); loadQueueServiceStatus(2, 400); return false; diff --git a/plugin/think-plugs-system/tests/QueueControllerTest.php b/plugin/think-plugs-system/tests/QueueControllerTest.php index 02ab5508a..0d2087aef 100644 --- a/plugin/think-plugs-system/tests/QueueControllerTest.php +++ b/plugin/think-plugs-system/tests/QueueControllerTest.php @@ -51,6 +51,9 @@ class QueueControllerTest extends SqliteIntegrationTestCase $this->assertStringContainsString('批量删除', $html); $this->assertStringContainsString('const queueStatusUrl = "/api/system/queue/status.html";', $html); $this->assertStringContainsString('const queueStatusEnabled = false;', $html); + $this->assertStringContainsString('const $queueMessage = $(\'[data-queue-message]\');', $html); + $this->assertStringContainsString('formatQueueTimeSafe', $html); + $this->assertStringNotContainsString('const \\$queueMessage', $html); $this->assertStringNotContainsString('{$queueStatusUrl}', $html); $this->assertStringNotContainsString('{$enabled}', $html); }