git-sync-web 自动提交 2026-09-09 10:33:29

This commit is contained in:
tamakyi
2026-09-09 10:33:29 +08:00
parent 1bf1f72901
commit 7c9be4b44b
3 changed files with 92 additions and 8 deletions
+47
View File
@@ -64,6 +64,7 @@ function card(r) {
${r.conflicts && r.conflicts.length ? `<span class="badge b-conflict">冲突 ${r.conflicts.length}</span>` : ''} ${r.conflicts && r.conflicts.length ? `<span class="badge b-conflict">冲突 ${r.conflicts.length}</span>` : ''}
</div> </div>
<div class="last">上次同步:${esc(last)}</div> <div class="last">上次同步:${esc(last)}</div>
${r.last_sync && r.last_sync.detail ? `<div class="last-detail" title="${esc(r.last_sync.detail)}">${esc(r.last_sync.detail)}</div>` : ''}
${r.status === 'error' && r.last_error ? `<div class="err" title="${esc(r.last_error)}">${esc(r.last_error)}</div>` : ''} ${r.status === 'error' && r.last_error ? `<div class="err" title="${esc(r.last_error)}">${esc(r.last_error)}</div>` : ''}
<div class="card-btns"> <div class="card-btns">
<button class="btn" data-act="sync" ${r.running ? '' : 'disabled'}>立即同步</button> <button class="btn" data-act="sync" ${r.running ? '' : 'disabled'}>立即同步</button>
@@ -106,6 +107,7 @@ function connect() {
if (i >= 0) repos[i] = msg.data; else repos.push(msg.data); if (i >= 0) repos[i] = msg.data; else repos.push(msg.data);
render(); render();
} else if (msg.type === 'log') { } else if (msg.type === 'log') {
appendLiveLine(msg);
if (current === msg.repo && !$('#tab-log').hidden) appendLogLine(`[${msg.time}] ${msg.msg}`); if (current === msg.repo && !$('#tab-log').hidden) appendLogLine(`[${msg.time}] ${msg.msg}`);
} }
}; };
@@ -115,8 +117,52 @@ function setConn(ok) {
const el = $('#conn'); const el = $('#conn');
el.textContent = ok ? '● 已连接' : '● 未连接'; el.textContent = ok ? '● 已连接' : '● 未连接';
el.className = 'conn ' + (ok ? 'on' : 'off'); el.className = 'conn ' + (ok ? 'on' : 'off');
const live = $('#live-conn');
if (live) {
live.textContent = ok ? '● 已连接' : '● 未连接';
live.className = 'conn ' + (ok ? 'on' : 'off');
}
} }
// ---------- 全局实时活动面板 ----------
const LIVE_MAX = 400;
function appendLiveLine(msg) {
const box = $('#live-log');
if (!box) return;
let level = 'info';
if (msg.level === 'error') level = 'err';
else if (msg.level === 'warn') level = 'warn';
const line = document.createElement('div');
line.className = 'live-line lv-' + level;
line.innerHTML = `<span class="ll-time">${esc(msg.time)}</span><span class="ll-repo">${esc(msg.repo)}</span><span class="ll-msg">${esc(msg.msg)}</span>`;
box.appendChild(line);
while (box.childElementCount > LIVE_MAX) box.removeChild(box.firstChild);
box.scrollTop = box.scrollHeight;
}
function clearLive() {
const box = $('#live-log');
if (box) box.innerHTML = '';
}
// 载入各仓库最近日志作为初始上下文,避免面板一开始空白
async function seedLive() {
const names = repos.map(r => r.name);
await Promise.all(names.map(async (name) => {
try {
const d = await api(`/api/repos/${name}/log?lines=8`);
if (d.lines && d.lines.length) {
d.lines.forEach(t => {
const m = t.match(/^\[(.+?)\] (.*)$/);
appendLiveLine({ time: m ? m[1] : '', repo: name, msg: m ? m[2] : t, level: 'info', seed: true });
});
}
} catch (_) { /* 忽略 */ }
}));
}
$('#btn-live-clear').addEventListener('click', clearLive);
// ---------- 登录 ---------- // ---------- 登录 ----------
function showLogin() { function showLogin() {
const el = $('#login'); const el = $('#login');
@@ -411,6 +457,7 @@ async function refresh() {
const d = await api('/api/status'); const d = await api('/api/status');
repos = d.repos || []; repos = d.repos || [];
render(); render();
seedLive();
} catch (e) { /* 忽略,SSE 会重试 */ } } catch (e) { /* 忽略,SSE 会重试 */ }
} }
+12 -1
View File
@@ -17,12 +17,23 @@
</div> </div>
</header> </header>
<main> <main class="layout">
<section class="main-col">
<div id="grid" class="grid"></div> <div id="grid" class="grid"></div>
<div id="empty" class="empty" hidden> <div id="empty" class="empty" hidden>
<p>还没有任何仓库配置。</p> <p>还没有任何仓库配置。</p>
<button class="btn btn-primary" id="btn-empty-new">创建第一个配置</button> <button class="btn btn-primary" id="btn-empty-new">创建第一个配置</button>
</div> </div>
</section>
<aside id="live" class="live">
<div class="live-head">
<h2>实时活动</h2>
<span id="live-conn" class="conn">连接中…</span>
<button class="btn btn-sm" id="btn-live-clear" title="清空本面板">清空</button>
</div>
<div class="live-hint">所有仓库的同步(手动与自动)实时显示在此,无需点进详情</div>
<div id="live-log" class="live-log"></div>
</aside>
</main> </main>
<div id="modal" class="modal" hidden> <div id="modal" class="modal" hidden>
+28 -2
View File
@@ -23,10 +23,36 @@ header { display: flex; justify-content: space-between; align-items: center; pad
.btn-danger:hover { background: var(--red-bg); } .btn-danger:hover { background: var(--red-bg); }
.btn-icon { border: none; background: none; font-size: 18px; cursor: pointer; color: var(--muted); } .btn-icon { border: none; background: none; font-size: 18px; cursor: pointer; color: var(--muted); }
main { max-width: 1200px; margin: 24px auto; padding: 0 24px; } main { max-width: 1320px; margin: 24px auto; padding: 0 24px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; } .layout { display: flex; gap: 20px; align-items: flex-start; }
.main-col { flex: 1; min-width: 0; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.empty { text-align: center; color: var(--muted); padding: 80px 0; } .empty { text-align: center; color: var(--muted); padding: 80px 0; }
/* 全局实时活动面板 */
.live { width: 360px; flex: none; position: sticky; top: 80px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); display: flex; flex-direction: column; max-height: calc(100vh - 104px); }
.live-head { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border); }
.live-head h2 { margin: 0; font-size: 15px; font-weight: 500; flex: 1; }
.live-head .conn { margin: 0; }
.btn-sm { border: 1px solid var(--border); background: #fff; border-radius: 7px; padding: 3px 10px; font-size: 12px; cursor: pointer; color: var(--muted); }
.btn-sm:hover { border-color: #b6bcc4; background: #f3f4f6; }
.live-hint { font-size: 11px; color: var(--muted); padding: 8px 14px; border-bottom: 1px solid var(--border); }
.live-log { flex: 1; overflow-y: auto; padding: 8px 10px; font-family: Consolas, "Cascadia Code", monospace; font-size: 11.5px; line-height: 1.5; }
.live-line { display: flex; gap: 6px; padding: 2px 0; border-bottom: 1px solid #f2f2f2; }
.live-line .ll-time { color: #9aa0a6; flex: none; }
.live-line .ll-repo { color: var(--blue); flex: none; font-weight: 600; max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.live-line .ll-msg { color: var(--text); min-width: 0; word-break: break-word; }
.live-line.lv-err .ll-msg { color: var(--red); }
.live-line.lv-warn .ll-msg { color: var(--amber); }
.live-line.lv-err .ll-repo, .live-line.lv-warn .ll-repo { color: #b06a00; }
.last-detail { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 980px) {
.layout { flex-direction: column; }
.live { width: 100%; position: static; max-height: 360px; }
}
.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; display: flex; flex-direction: column; gap: 8px; } .card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; display: flex; flex-direction: column; gap: 8px; }
.card.st-conflict { border-color: #e8c98a; background: #fffdf6; } .card.st-conflict { border-color: #e8c98a; background: #fffdf6; }
.card.st-error { border-color: #eec9c9; } .card.st-error { border-color: #eec9c9; }