git-sync-web 自动提交 2026-09-09 13:46:40

This commit is contained in:
tamakyi
2026-09-09 13:46:41 +08:00
parent b921569b72
commit bba6a3362e
7 changed files with 234 additions and 24 deletions
+76 -13
View File
@@ -9,6 +9,31 @@ const STATUS = {
};
const RESULT_TEXT = { ok: '成功', error: '失败', conflict: '冲突' };
// ---------- 相对时间(卡片「上次同步」) ----------
const STALE_HOURS = 24; // 超过该小时数且仍在运行 → 标记「久未同步」
function parseTs(s) {
if (!s) return null;
// 后端时间格式 "2006-01-02 15:04:05",补齐为 ISO 本地时间
const t = new Date(String(s).replace(' ', 'T'));
return isNaN(t.getTime()) ? null : t;
}
function relTime(s) {
const t = parseTs(s);
if (!t) return String(s || '');
const min = Math.floor((Date.now() - t.getTime()) / 60000);
if (min < 1) return '刚刚';
if (min < 60) return min + ' 分钟前';
const hr = Math.floor(min / 60);
if (hr < 24) return hr + ' 小时前';
const day = Math.floor(hr / 24);
if (day < 30) return day + ' 天前';
return String(s).slice(0, 10);
}
function isStale(s) {
const t = parseTs(s);
return !!t && (Date.now() - t.getTime()) > STALE_HOURS * 3600 * 1000;
}
let repos = [];
let current = null;
let creating = false;
@@ -45,7 +70,7 @@ function card(r) {
el.className = 'card ' + st.cls;
el.dataset.name = r.name;
const last = r.last_sync
? `${r.last_sync.time.slice(5, 16)} · ${RESULT_TEXT[r.last_sync.result] || r.last_sync.result}`
? `${relTime(r.last_sync.time)} · ${RESULT_TEXT[r.last_sync.result] || r.last_sync.result}`
: '从未同步';
el.innerHTML = `
<div class="card-head">
@@ -63,8 +88,9 @@ function card(r) {
${r.cron ? `<span class="badge" title="${esc(r.cron)}">cron</span>` : ''}
${r.conflicts && r.conflicts.length ? `<span class="badge b-conflict">冲突 ${r.conflicts.length}</span>` : ''}
${r.push_conflict ? `<span class="badge b-conflict">推送冲突</span>` : ''}
${r.running && r.last_sync && isStale(r.last_sync.time) ? '<span class="badge b-stale">久未同步</span>' : ''}
</div>
<div class="last">上次同步:${esc(last)}</div>
<div class="last" title="${r.last_sync ? esc(r.last_sync.time) : ''}">上次同步:${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>` : ''}
<div class="card-btns">
@@ -111,7 +137,7 @@ function connect() {
if (current === msg.repo && !$('#tab-diff').hidden) loadDiff(msg.repo);
} 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.level || 'info'}] ${msg.msg}`);
}
};
}
@@ -159,8 +185,15 @@ async function seedLive() {
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 });
// 兼容新格式 [时间] [级别] 消息 与历史格式 [时间] 消息
const m = t.match(/^\[(.+?)\]\s*(?:\[(info|warn|error)\]\s*)?(.*)$/i);
appendLiveLine({
time: m ? m[1] : '',
repo: name,
msg: m ? m[3] : t,
level: m && m[2] ? m[2].toLowerCase() : 'info',
seed: true,
});
});
}
} catch (_) { /* 忽略 */ }
@@ -250,22 +283,52 @@ $('#m-close').addEventListener('click', closeModal);
$('#modal').addEventListener('click', (e) => { if (e.target === $('#modal')) closeModal(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
// ---------- 日志 ----------
// ---------- 日志(级别过滤 + 关键词搜索) ----------
let logLines = [];
let logLevel = 'all';
let logQuery = '';
// 落盘格式:[时间] [级别] 消息;历史日志无级别前缀时按 info 处理
function logLevelOf(line) {
const m = /^\[[^\]]+\]\s*\[(info|warn|error)\]/i.exec(line);
return m ? m[1].toLowerCase() : 'info';
}
function renderLog() {
const box = $('#logbox');
const q = logQuery.trim().toLowerCase();
const out = logLines.filter(l =>
(logLevel === 'all' || logLevelOf(l) === logLevel) &&
(!q || l.toLowerCase().includes(q))
);
if (!logLines.length) box.textContent = '(暂无日志)';
else if (!out.length) box.textContent = '(当前过滤条件下无匹配行)';
else box.textContent = out.join('\n');
box.scrollTop = box.scrollHeight;
}
function loadLog(name) {
$('#logbox').textContent = '加载中…';
api(`/api/repos/${name}/log?lines=500`).then(d => {
$('#logbox').textContent = d.lines && d.lines.length ? d.lines.join('\n') : '(暂无日志)';
$('#logbox').scrollTop = $('#logbox').scrollHeight;
}).catch(err => { $('#logbox').textContent = '日志加载失败:' + err.message; });
logLines = d.lines && d.lines.length ? d.lines : [];
renderLog();
}).catch(err => { logLines = []; $('#logbox').textContent = '日志加载失败:' + err.message; });
}
function appendLogLine(line) {
const el = $('#logbox');
if (el.textContent === '(暂无日志)') el.textContent = '';
el.textContent += (el.textContent ? '\n' : '') + line;
el.scrollTop = el.scrollHeight;
logLines.push(line);
if (logLines.length > 2000) logLines.shift();
renderLog();
}
$('#log-level').addEventListener('change', (e) => { logLevel = e.target.value; renderLog(); });
$('#log-search').addEventListener('input', (e) => { logQuery = e.target.value; renderLog(); });
$('#btn-log-clear').addEventListener('click', () => {
logLevel = 'all';
logQuery = '';
$('#log-level').value = 'all';
$('#log-search').value = '';
renderLog();
});
// ---------- 提交历史 ----------
function loadHistory(name) {
const list = $('#commit-list');
+24 -5
View File
@@ -12,6 +12,7 @@
<div class="brand"><span class="brand-dot"></span>Git Sync 控制台</div>
<div class="header-actions">
<span id="conn" class="conn">连接中…</span>
<button class="btn btn-icon" id="btn-theme" title="切换深色 / 浅色模式">🌙</button>
<button class="btn" id="btn-sync-all">全部同步</button>
<button class="btn btn-primary" id="btn-new">+ 新建配置</button>
</div>
@@ -52,9 +53,21 @@
<button data-tab="config" class="tab">配置</button>
</div>
<div class="tab-body">
<div id="tab-log" class="tab-pane">
<pre id="logbox" class="logbox">加载中…</pre>
<div id="tab-log" class="tab-pane">
<div class="log-tools">
<label>级别
<select id="log-level">
<option value="all">全部</option>
<option value="error">仅错误</option>
<option value="warn">仅警告</option>
<option value="info">仅信息</option>
</select>
</label>
<input id="log-search" class="log-search" placeholder="搜索日志内容(关键词)">
<button class="btn btn-sm" id="btn-log-clear" title="清除过滤条件">清除</button>
</div>
<pre id="logbox" class="logbox">加载中…</pre>
</div>
<div id="tab-history" class="tab-pane" hidden>
<div id="commit-list" class="commit-list">加载中…</div>
</div>
@@ -69,6 +82,9 @@
</select>
</label>
<button class="btn btn-sm" id="btn-diff-refresh">刷新</button>
<button class="btn btn-sm" id="btn-diff-copy">复制</button>
<button class="btn btn-sm" id="btn-diff-download">下载 .patch</button>
<span id="diff-filter" class="diff-filter" hidden></span>
</div>
<div id="diff-stat" class="diff-stat"></div>
<pre id="diffbox" class="diffbox">加载中…</pre>
@@ -76,12 +92,14 @@
<div id="tab-file" class="tab-pane" hidden>
<div class="file-head">
<button class="btn btn-sm" id="btn-file-refresh">刷新文件列表</button>
<button class="btn btn-sm" id="btn-file-diff" title="只看当前文件的差异">对比此文件</button>
<span id="file-current" class="file-current"></span>
<span id="file-draft" class="file-draft" hidden></span>
</div>
<div class="file-body">
<div id="file-list" class="file-list">加载中…</div>
<div class="file-edit-col">
<textarea id="file-editor" class="file-editor" placeholder="选择左侧文件进行编辑" spellcheck="false"></textarea>
<textarea id="file-editor" class="file-editor" placeholder="选择左侧文件进行编辑(Ctrl/Cmd + S 保存)" spellcheck="false"></textarea>
<div class="file-actions">
<input id="file-msg" class="file-msg" placeholder="提交说明(可选)">
<button class="btn btn-sm" id="btn-file-save">保存</button>
@@ -167,8 +185,9 @@
<span>自动提交模式:检测到未提交改动时自动 commit,并随同步推送到远程</span>
</label>
<label class="full">Cron 表达式(可选,5 段:分 时 日 月 周;填写后优先于固定间隔)
<input name="cron" id="f-cron" placeholder="*/30 * * * * 表示每 30 分钟,留空使用固定间隔">
</label>
<input name="cron" id="f-cron" placeholder="*/30 * * * * 表示每 30 分钟,留空使用固定间隔">
<span id="cron-next" class="cron-next"></span>
</label>
<label class="full">日志文件路径(留空使用默认 logs/配置名.log)
<input name="log_file" id="f-log">
</label>
+53
View File
@@ -153,3 +153,56 @@ textarea { font-family: Consolas, monospace; resize: vertical; }
.file-body { flex-direction: column; }
.file-list { width: 100%; max-height: 200px; }
}
/* ---------- 暗色模式 ---------- */
html.dark {
--bg: #16181c; --card: #1e2127; --border: #30363d; --text: #d8dee6; --muted: #8b949e;
--blue: #58a6ff; --green: #7ee787; --green-bg: #12261a; --red: #ff7b72; --red-bg: #2d1618;
--amber: #e3b341; --amber-bg: #2b2313; --gray-bg: #24272e;
}
html.dark header { background: var(--card); }
html.dark .btn { background: var(--card); }
html.dark .btn:hover { background: var(--gray-bg); border-color: #4b535d; }
html.dark .btn-sm { background: var(--card); color: var(--text); }
html.dark .btn-danger { background: var(--card); }
html.dark .btn-primary { background: var(--blue); border-color: var(--blue); color: #0d1117; }
html.dark .btn-primary:hover { background: #3d8bfd; border-color: #3d8bfd; }
html.dark .logbox,
html.dark .conflict-file pre,
html.dark .commit-diff,
html.dark .diffbox,
html.dark .file-editor:disabled { background: #12151a; }
html.dark .commit-head:hover { background: var(--gray-bg); }
html.dark .diff-line.diff-hunk { background: #17242f; }
html.dark input, html.dark select, html.dark textarea { background: #12151a; }
html.dark .file-list { background: var(--card); }
html.dark .file-item:hover { background: var(--gray-bg); }
html.dark .file-editor { background: #12151a; }
/* ---------- 日志过滤 ---------- */
.log-tools { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.log-tools label { font-size: 12px; color: var(--muted); display: flex; gap: 6px; align-items: center; }
.log-search { flex: 1; min-width: 140px; }
/* ---------- 差异:单文件过滤提示 ---------- */
.diff-filter { font-size: 12px; color: var(--amber); background: var(--amber-bg); border-radius: 6px; padding: 2px 8px; }
.diff-filter[hidden] { display: none; }
/* ---------- 文件编辑:草稿提示与状态徽标 ---------- */
.file-draft { font-size: 12px; color: var(--amber); background: var(--amber-bg); border-radius: 6px; padding: 2px 8px; }
.file-draft[hidden] { display: none; }
.file-item { display: flex; align-items: center; gap: 6px; }
.file-item .fi-name { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.fi-status { flex: none; font-size: 11px; padding: 0 5px; border-radius: 4px; background: var(--gray-bg); color: var(--muted); }
.fi-status.s-M { background: var(--amber-bg); color: var(--amber); }
.fi-status.s-A { background: var(--green-bg); color: var(--green); }
.fi-status.s-D { background: var(--red-bg); color: var(--red); }
.fi-status.s-Q { background: var(--gray-bg); color: var(--muted); }
/* ---------- 卡片:久未同步 ---------- */
.badge.b-stale { background: var(--amber-bg); color: var(--amber); }
/* ---------- 配置:cron 下次执行预览 ---------- */
.cron-next { font-size: 12px; color: var(--muted); min-height: 16px; }
.cron-next.bad { color: var(--red); }
.cron-next.good { color: var(--green); }