diff --git a/static/app.js b/static/app.js index a77df40..a6e2791 100644 --- a/static/app.js +++ b/static/app.js @@ -13,6 +13,8 @@ const state = { history: {}, // { scriptPath: [record, ...] } scripts: [], // 配置中的全部脚本 autoscroll: true, + expandedLogs: new Set(), // 已展开的历史日志 record ID(re-render 后恢复) + lastLiveKey: null, // 上次渲染的"当前任务身份"(用于判断是否需要重建 DOM) }; // ============ Helpers ============ @@ -119,6 +121,7 @@ function renderRunning() { stateBadge.className = "badge badge-grey"; body.innerHTML = `

当前没有任务在执行

`; sizeEl.textContent = ""; + state.lastLiveKey = null; return; } @@ -126,30 +129,49 @@ function renderRunning() { stateBadge.textContent = "执行中"; stateBadge.className = "badge badge-warn"; - body.innerHTML = ` -
-
仓库
${escapeHtml(c.task.repo)}
-
分支
${escapeHtml(c.task.ref)}
-
Commit
${escapeHtml(shortCommit(c.task.commit))}
-
脚本
${escapeHtml(c.task.script)}
-
开始
${escapeHtml(fmtFullTime(c.startedAt))}
-
-

-  `;
-
-  const logBox = $("#live-log");
+  const liveKey = `${c.task.script}|${c.task.repo}|${c.task.ref}|${c.startedAt}`;
   // 仅渲染尾部以避免长日志卡顿
   const fullLog = c.log || "";
   const tail = fullLog.length > LOG_TAIL_CHARS
     ? fullLog.slice(fullLog.length - LOG_TAIL_CHARS)
     : fullLog;
+
+  if (state.lastLiveKey !== liveKey) {
+    // 任务身份变化:重建结构并贴底
+    body.innerHTML = `
+      
+
仓库
${escapeHtml(c.task.repo)}
+
分支
${escapeHtml(c.task.ref)}
+
Commit
${escapeHtml(shortCommit(c.task.commit))}
+
脚本
${escapeHtml(c.task.script)}
+
开始
${escapeHtml(fmtFullTime(c.startedAt))}
+
+

+    `;
+    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 atBottom = distanceFromBottom <= 24;
   logBox.textContent = tail || "(等待脚本输出…)";
   sizeEl.textContent = fullLog.length
     ? `${(fullLog.length / 1024).toFixed(1)} KB`
     : "";
-
-  if (state.autoscroll) {
-    logBox.scrollTop = logBox.scrollHeight;
+  if (state.autoscroll && atBottom) {
+    requestAnimationFrame(() => {
+      logBox.scrollTop = logBox.scrollHeight;
+    });
   }
 }
 
@@ -195,7 +217,7 @@ function renderHistory() {
               ${escapeHtml(r.reason || "")}
               
                 ${r.log
-                  ? ``
+                  ? ``
                   : `无日志`}
               
             
@@ -225,14 +247,29 @@ function renderHistory() {
     `;
   }).join("");
 
-  // 绑定展开日志按钮
+  // 恢复展开状态(re-render 后保持已展开的日志)
+  root.querySelectorAll(".toggle-log").forEach((btn) => {
+    const expanded = state.expandedLogs.has(btn.dataset.id);
+    btn.textContent = expanded ? "收起日志" : "查看日志";
+    const t = document.getElementById(btn.dataset.target);
+    if (t) t.style.display = expanded ? "block" : "none";
+  });
+
+  // 绑定展开日志按钮(以 state.expandedLogs 为单一事实源)
   root.querySelectorAll(".toggle-log").forEach((btn) => {
     btn.addEventListener("click", () => {
       const target = document.getElementById(btn.dataset.target);
       if (!target) return;
-      const visible = target.style.display !== "none";
-      target.style.display = visible ? "none" : "block";
-      btn.textContent = visible ? "查看日志" : "收起日志";
+      const id = btn.dataset.id;
+      if (state.expandedLogs.has(id)) {
+        state.expandedLogs.delete(id);
+        target.style.display = "none";
+        btn.textContent = "查看日志";
+      } else {
+        state.expandedLogs.add(id);
+        target.style.display = "block";
+        btn.textContent = "收起日志";
+      }
     });
   });
 }
@@ -246,6 +283,14 @@ $("#refresh-btn").addEventListener("click", () => {
 $("#log-autoscroll").addEventListener("click", (e) => {
   state.autoscroll = !state.autoscroll;
   e.currentTarget.classList.toggle("active", state.autoscroll);
+  if (state.autoscroll) {
+    const logBox = $("#live-log");
+    if (logBox) {
+      requestAnimationFrame(() => {
+        logBox.scrollTop = logBox.scrollHeight;
+      });
+    }
+  }
 });
 
 pollQueue();