update
This commit is contained in:
@@ -277,3 +277,57 @@ a { color: var(--accent); text-decoration: none; }
|
|||||||
.status-timeout { color: var(--warn); background: rgba(224, 164, 88, 0.12); }
|
.status-timeout { color: var(--warn); background: rgba(224, 164, 88, 0.12); }
|
||||||
.status-skipped { color: var(--skipped); background: rgba(154, 163, 178, 0.12); }
|
.status-skipped { color: var(--skipped); background: rgba(154, 163, 178, 0.12); }
|
||||||
.status-running { color: var(--accent); background: rgba(91, 141, 239, 0.12); }
|
.status-running { color: var(--accent); background: rgba(91, 141, 239, 0.12); }
|
||||||
|
|
||||||
|
/* ============ Running actions + Modal ============ */
|
||||||
|
.running-actions { margin-top: 12px; display: flex; gap: 8px; }
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.modal[hidden] { display: none; }
|
||||||
|
.modal-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
.modal-dialog {
|
||||||
|
position: relative;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
width: min(900px, 92vw);
|
||||||
|
height: min(72vh, 720px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.modal-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
.modal-head h3 { margin: 0; font-size: 14px; font-weight: 600; color: var(--text); }
|
||||||
|
.modal-log {
|
||||||
|
flex: 1;
|
||||||
|
height: auto;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.modal-foot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--surface-2);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|||||||
+49
-31
@@ -15,6 +15,7 @@ const state = {
|
|||||||
autoscroll: true,
|
autoscroll: true,
|
||||||
expandedLogs: new Set(), // 已展开的历史日志 record ID(re-render 后恢复)
|
expandedLogs: new Set(), // 已展开的历史日志 record ID(re-render 后恢复)
|
||||||
lastLiveKey: null, // 上次渲染的"当前任务身份"(用于判断是否需要重建 DOM)
|
lastLiveKey: null, // 上次渲染的"当前任务身份"(用于判断是否需要重建 DOM)
|
||||||
|
modalOpen: false, // 实时日志弹窗是否打开
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============ Helpers ============
|
// ============ Helpers ============
|
||||||
@@ -67,6 +68,7 @@ async function pollQueue() {
|
|||||||
setConn(true, "已连接");
|
setConn(true, "已连接");
|
||||||
renderQueue();
|
renderQueue();
|
||||||
renderRunning();
|
renderRunning();
|
||||||
|
if (state.modalOpen) refreshModalLog();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setConn(false, "连接失败");
|
setConn(false, "连接失败");
|
||||||
console.warn("queue poll failed:", e);
|
console.warn("queue poll failed:", e);
|
||||||
@@ -114,13 +116,11 @@ function renderQueue() {
|
|||||||
function renderRunning() {
|
function renderRunning() {
|
||||||
const body = $("#running-body");
|
const body = $("#running-body");
|
||||||
const stateBadge = $("#running-state");
|
const stateBadge = $("#running-state");
|
||||||
const sizeEl = $("#log-size");
|
|
||||||
|
|
||||||
if (!state.current) {
|
if (!state.current) {
|
||||||
stateBadge.textContent = "空闲";
|
stateBadge.textContent = "空闲";
|
||||||
stateBadge.className = "badge badge-grey";
|
stateBadge.className = "badge badge-grey";
|
||||||
body.innerHTML = `<p class="empty">当前没有任务在执行</p>`;
|
body.innerHTML = `<p class="empty">当前没有任务在执行</p>`;
|
||||||
sizeEl.textContent = "";
|
|
||||||
state.lastLiveKey = null;
|
state.lastLiveKey = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -130,38 +130,48 @@ function renderRunning() {
|
|||||||
stateBadge.className = "badge badge-warn";
|
stateBadge.className = "badge badge-warn";
|
||||||
|
|
||||||
const liveKey = `${c.task.script}|${c.task.repo}|${c.task.ref}|${c.startedAt}`;
|
const liveKey = `${c.task.script}|${c.task.repo}|${c.task.ref}|${c.startedAt}`;
|
||||||
// 仅渲染尾部以避免长日志卡顿
|
if (state.lastLiveKey === liveKey) return;
|
||||||
const fullLog = c.log || "";
|
state.lastLiveKey = liveKey;
|
||||||
|
|
||||||
|
body.innerHTML = `
|
||||||
|
<dl class="running-meta">
|
||||||
|
<dt>仓库</dt><dd>${escapeHtml(c.task.repo)}</dd>
|
||||||
|
<dt>分支</dt><dd>${escapeHtml(c.task.ref)}</dd>
|
||||||
|
<dt>Commit</dt><dd>${escapeHtml(shortCommit(c.task.commit))}</dd>
|
||||||
|
<dt>脚本</dt><dd>${escapeHtml(c.task.script)}</dd>
|
||||||
|
<dt>开始</dt><dd>${escapeHtml(fmtFullTime(c.startedAt))}</dd>
|
||||||
|
</dl>
|
||||||
|
<div class="running-actions">
|
||||||
|
<button id="open-log-btn" class="btn-ghost" type="button">查看实时日志</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
$("#open-log-btn").addEventListener("click", openLogModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Live log modal ============
|
||||||
|
function openLogModal() {
|
||||||
|
if (!state.current) return;
|
||||||
|
$("#log-modal").hidden = false;
|
||||||
|
state.modalOpen = true;
|
||||||
|
refreshModalLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLogModal() {
|
||||||
|
$("#log-modal").hidden = true;
|
||||||
|
state.modalOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshModalLog() {
|
||||||
|
const logBox = $("#modal-log");
|
||||||
|
const sizeEl = $("#log-size");
|
||||||
|
if (!logBox) return;
|
||||||
|
|
||||||
|
const fullLog = state.current ? (state.current.log || "") : "";
|
||||||
const tail = fullLog.length > LOG_TAIL_CHARS
|
const tail = fullLog.length > LOG_TAIL_CHARS
|
||||||
? fullLog.slice(fullLog.length - LOG_TAIL_CHARS)
|
? fullLog.slice(fullLog.length - LOG_TAIL_CHARS)
|
||||||
: fullLog;
|
: fullLog;
|
||||||
|
|
||||||
if (state.lastLiveKey !== liveKey) {
|
// 仅当贴底或已开启自动滚动时才追尾,避免用户向上滚动时被打断
|
||||||
// 任务身份变化:重建结构并贴底
|
|
||||||
body.innerHTML = `
|
|
||||||
<dl class="running-meta">
|
|
||||||
<dt>仓库</dt><dd>${escapeHtml(c.task.repo)}</dd>
|
|
||||||
<dt>分支</dt><dd>${escapeHtml(c.task.ref)}</dd>
|
|
||||||
<dt>Commit</dt><dd>${escapeHtml(shortCommit(c.task.commit))}</dd>
|
|
||||||
<dt>脚本</dt><dd>${escapeHtml(c.task.script)}</dd>
|
|
||||||
<dt>开始</dt><dd>${escapeHtml(fmtFullTime(c.startedAt))}</dd>
|
|
||||||
</dl>
|
|
||||||
<pre class="log-box" id="live-log"></pre>
|
|
||||||
`;
|
|
||||||
state.lastLiveKey = liveKey;
|
|
||||||
const logBox = $("#live-log");
|
|
||||||
logBox.textContent = tail || "(等待脚本输出…)";
|
|
||||||
sizeEl.textContent = fullLog.length
|
|
||||||
? `${(fullLog.length / 1024).toFixed(1)} KB`
|
|
||||||
: "";
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
logBox.scrollTop = logBox.scrollHeight;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同一任务:仅更新日志内容,保持 DOM 节点稳定
|
|
||||||
const logBox = $("#live-log");
|
|
||||||
const distanceFromBottom = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight;
|
const distanceFromBottom = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight;
|
||||||
const atBottom = distanceFromBottom <= 24;
|
const atBottom = distanceFromBottom <= 24;
|
||||||
logBox.textContent = tail || "(等待脚本输出…)";
|
logBox.textContent = tail || "(等待脚本输出…)";
|
||||||
@@ -284,7 +294,7 @@ $("#log-autoscroll").addEventListener("click", (e) => {
|
|||||||
state.autoscroll = !state.autoscroll;
|
state.autoscroll = !state.autoscroll;
|
||||||
e.currentTarget.classList.toggle("active", state.autoscroll);
|
e.currentTarget.classList.toggle("active", state.autoscroll);
|
||||||
if (state.autoscroll) {
|
if (state.autoscroll) {
|
||||||
const logBox = $("#live-log");
|
const logBox = $("#modal-log");
|
||||||
if (logBox) {
|
if (logBox) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
logBox.scrollTop = logBox.scrollHeight;
|
logBox.scrollTop = logBox.scrollHeight;
|
||||||
@@ -293,6 +303,14 @@ $("#log-autoscroll").addEventListener("click", (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 弹窗关闭:点击 backdrop / 关闭按钮 / Esc
|
||||||
|
document.querySelectorAll("#log-modal [data-close]").forEach((el) => {
|
||||||
|
el.addEventListener("click", closeLogModal);
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && state.modalOpen) closeLogModal();
|
||||||
|
});
|
||||||
|
|
||||||
pollQueue();
|
pollQueue();
|
||||||
pollHistory();
|
pollHistory();
|
||||||
setInterval(pollQueue, POLL_QUEUE_MS);
|
setInterval(pollQueue, POLL_QUEUE_MS);
|
||||||
|
|||||||
+15
-4
@@ -41,10 +41,6 @@
|
|||||||
<div id="running-body" class="card-body running-body">
|
<div id="running-body" class="card-body running-body">
|
||||||
<p class="empty">当前没有任务在执行</p>
|
<p class="empty">当前没有任务在执行</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-foot">
|
|
||||||
<button id="log-autoscroll" class="btn-ghost active" type="button">自动滚动</button>
|
|
||||||
<span id="log-size" class="muted"></span>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card" id="history-card">
|
<section class="card" id="history-card">
|
||||||
@@ -56,6 +52,21 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div id="log-modal" class="modal" hidden role="dialog" aria-labelledby="modal-title" aria-modal="true">
|
||||||
|
<div class="modal-backdrop" data-close></div>
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<header class="modal-head">
|
||||||
|
<h3 id="modal-title">实时日志</h3>
|
||||||
|
<button class="btn-ghost" data-close type="button">关闭</button>
|
||||||
|
</header>
|
||||||
|
<pre id="modal-log" class="log-box modal-log">(等待脚本输出…)</pre>
|
||||||
|
<footer class="modal-foot">
|
||||||
|
<button id="log-autoscroll" class="btn-ghost active" type="button">自动滚动</button>
|
||||||
|
<span id="log-size" class="muted"></span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user