git-sync-web 自动提交 2026-09-09 14:55:18

This commit is contained in:
tamakyi
2026-09-09 14:55:18 +08:00
parent d48bf772c7
commit a1ccd4fe6a
5 changed files with 523 additions and 192 deletions
+241 -34
View File
@@ -56,6 +56,77 @@ async function api(url, opts) {
}
const post = (url) => api(url, { method: 'POST' });
// ---------- Toast 轻提示(替代原生 alert,避免打断操作) ----------
function toast(msg, type) {
const box = $('#toasts');
// 兜底:容器缺失时不能递归调用自身
if (!box) { console.warn('toast 容器缺失:', msg); return; }
const el = document.createElement('div');
el.className = 'toast t-' + (type || 'info');
el.textContent = msg;
el.title = '点击关闭';
el.addEventListener('click', () => el.remove());
box.appendChild(el);
while (box.childElementCount > 5) box.removeChild(box.firstChild); // 最多堆叠 5 条
const life = type === 'error' ? 6000 : 3200;
setTimeout(() => {
el.classList.add('out');
setTimeout(() => el.remove(), 320);
}, life);
}
// ---------- 编辑器抽象:统一原生 textarea 与 CodeMirror 的读写 ----------
// CodeMirror 由 cm-boot.js 在脚本加载后接管并写入 Editor.cm;
// 库缺失 / 初始化失败时 Editor.cm 保持 null,自动回退原生 textarea,功能不受影响。
const Editor = {
cm: null,
get el() { return $('#file-editor'); },
get value() {
if (this.cm) return this.cm.getValue();
return this.el ? this.el.value : '';
},
set value(v) {
const s = v == null ? '' : String(v);
if (this.cm) this.cm.setValue(s);
else if (this.el) this.el.value = s;
},
get disabled() {
if (this.cm) return this.cm.getOption('readOnly') === 'nocursor';
return !!(this.el && this.el.disabled);
},
set disabled(b) {
if (this.cm) this.cm.setOption('readOnly', b ? 'nocursor' : false);
if (this.el) this.el.disabled = !!b;
},
// 按扩展名选择语法高亮模式
setMode(path) {
if (!this.cm) return;
const ext = String(path || '').split('.').pop().toLowerCase();
const map = {
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript',
json: 'application/json', ts: 'text/typescript', tsx: 'text/typescript',
html: 'htmlmixed', htm: 'htmlmixed', vue: 'htmlmixed', xml: 'xml', svg: 'xml',
css: 'css', scss: 'css', less: 'css',
go: 'go', py: 'python', sh: 'shell', bash: 'shell', zsh: 'shell',
c: 'clike', h: 'clike', cpp: 'clike', cc: 'clike', hpp: 'clike',
java: 'clike', kt: 'clike', cs: 'clike', scala: 'clike',
yml: 'yaml', yaml: 'yaml', sql: 'sql',
md: 'markdown', markdown: 'markdown',
ini: 'properties', properties: 'properties', env: 'properties',
cfg: 'properties', conf: 'properties', toml: 'properties',
diff: 'diff', patch: 'diff',
};
this.cm.setOption('mode', map[ext] || 'text/plain');
},
setTheme(dark) {
if (this.cm) this.cm.setOption('theme', dark ? 'material-darker' : 'eclipse');
},
// 弹窗 / tab 切换后需要重算尺寸,否则 CodeMirror 可能不显示内容
refresh() { if (this.cm) setTimeout(() => this.cm.refresh(), 30); },
onSave: null, // 由 cm-boot.js 的快捷键触发
onChange: null, // 由 cm-boot.js 的 change 事件触发(草稿缓存)
};
// ---------- 渲染 ----------
function render() {
const grid = $('#grid');
@@ -115,7 +186,7 @@ $('#grid').addEventListener('click', async (e) => {
} else if (act === 'detail') {
openDetail(name);
}
} catch (err) { alert(err.message); }
} catch (err) { toast(err.message, 'error'); }
});
// ---------- SSE 实时事件 ----------
@@ -241,7 +312,7 @@ function showTab(t) {
if (t === 'log' && current) loadLog(current);
if (t === 'history' && current) loadHistory(current);
if (t === 'diff' && current) loadDiff(current);
if (t === 'file' && current) loadTree(current);
if (t === 'file' && current) { loadTree(current); Editor.refresh(); }
if (t === 'conflict' && current) { loadConflicts(current); updateConflictActions(); }
if (t === 'stats' && current) loadStats(current);
}
@@ -387,9 +458,9 @@ async function doAction(action, okMsg) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action })
});
alert(okMsg);
toast(okMsg);
loadConflicts(current);
} catch (err) { alert(err.message); }
} catch (err) { toast(err.message, 'error'); }
}
$('#btn-abort').addEventListener('click', () => {
if (confirm('确定放弃本次合并/变基?仓库将回到冲突前状态。')) doAction('abort', '已放弃本次合并');
@@ -406,17 +477,17 @@ $('#btn-force-push').addEventListener('click', async () => {
if (!current) return;
try {
await post(`/api/repos/${current}/force-push`);
alert('已发起强制推送(--force-with-lease)');
toast('已发起强制推送(--force-with-lease)');
updateConflictActions();
} catch (err) { alert(err.message); }
} catch (err) { toast(err.message, 'error'); }
});
$('#btn-abort-push').addEventListener('click', async () => {
if (!current) return;
try {
await post(`/api/repos/${current}/abort-push`);
alert('已放弃强制推送');
toast('已放弃强制推送');
updateConflictActions();
} catch (err) { alert(err.message); }
} catch (err) { toast(err.message, 'error'); }
});
// 根据当前仓库快照切换推送冲突操作区显隐(B3)
@@ -476,18 +547,156 @@ function loadDiff(name) {
}).catch(err => { box.textContent = '差异加载失败:' + err.message; });
}
// ---------- 差异渲染:统一 / 并排 / 词级 ----------
// 把 unified diff 解析成「文件 → 行记录」,统一与并排两种视图共用同一份结构
function parseUnifiedDiff(text) {
const lines = String(text).split('\n');
const files = [];
let cur = null;
let oldNo = 0, newNo = 0;
const push = (header) => { cur = { header, rows: [] }; files.push(cur); return cur; };
for (const line of lines) {
if (line.startsWith('diff --git') || line.startsWith('Index: ')) { push(line); continue; }
if (!cur && line === '') continue; // 开头空行忽略
if (!cur) push('');
if (line.startsWith('@@')) {
const m = /@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
if (m) { oldNo = parseInt(m[1], 10); newNo = parseInt(m[2], 10); }
cur.rows.push({ type: 'hunk', text: line });
continue;
}
if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('index ') ||
line.startsWith('new file') || line.startsWith('deleted file') || line.startsWith('similarity ') ||
line.startsWith('rename ') || line.startsWith('old mode') || line.startsWith('new mode')) {
cur.rows.push({ type: 'meta', text: line });
continue;
}
if (line.startsWith('+')) { cur.rows.push({ type: 'add', text: line.slice(1), no: newNo++ }); continue; }
if (line.startsWith('-')) { cur.rows.push({ type: 'del', text: line.slice(1), no: oldNo++ }); continue; }
if (line.startsWith(' ')) { cur.rows.push({ type: 'ctx', text: line.slice(1), no: oldNo++ }); newNo++; continue; }
if (line === '') { cur.rows.push({ type: 'ctx', text: '', no: oldNo++ }); newNo++; continue; }
cur.rows.push({ type: 'meta', text: line }); // \ No newline 等
}
return files;
}
// 词级:把 [-删除-] / {+新增+} 渲染成高亮片段(textContent 拼接,天然防 XSS)
function fillWord(el, text) {
const re = /\[-([\s\S]*?)-\]|\{\+([\s\S]*?)\+\}/g;
let last = 0, m;
el.textContent = '';
while ((m = re.exec(text)) !== null) {
if (m.index > last) el.appendChild(document.createTextNode(text.slice(last, m.index)));
const span = document.createElement('span');
const isDel = m[1] !== undefined;
span.className = isDel ? 'wd-del' : 'wd-add';
span.textContent = isDel ? m[1] : m[2];
el.appendChild(span);
last = re.lastIndex;
}
if (last < text.length) el.appendChild(document.createTextNode(text.slice(last)));
if (!el.childNodes.length) el.appendChild(document.createTextNode(text));
}
// 并排:把连续的 del/add 段配对成行,多余的一侧留空
function buildSplitRows(rows) {
const out = [];
let i = 0;
while (i < rows.length) {
const r = rows[i];
if (r.type === 'hunk' || r.type === 'meta') { out.push({ kind: r.type, text: r.text }); i++; continue; }
if (r.type === 'ctx') { out.push({ kind: 'ctx', left: r, right: r }); i++; continue; }
const dels = [], adds = [];
while (i < rows.length && rows[i].type === 'del') dels.push(rows[i++]);
while (i < rows.length && rows[i].type === 'add') adds.push(rows[i++]);
const n = Math.max(dels.length, adds.length);
for (let k = 0; k < n; k++) {
const l = dels[k] || null, a = adds[k] || null;
out.push({ kind: l && a ? 'chg' : (l ? 'del' : 'add'), left: l, right: a });
}
}
return out;
}
function fileHeaderOf(h) {
const m = /^diff --git a\/(.+?) b\/(.+)$/.exec(h);
return m ? m[2] : h;
}
function sideCell(cls, r, word) {
const c = document.createElement('div');
c.className = 'scol ' + cls;
const ln = document.createElement('span');
ln.className = 'ln';
ln.textContent = r ? (r.no == null ? '' : r.no) : '';
const t = document.createElement('span');
t.className = 'stxt';
if (r) { if (word) fillWord(t, r.text); else t.textContent = r.text; }
c.appendChild(ln);
c.appendChild(t);
return c;
}
function buildSplit(f, word) {
const frag = document.createDocumentFragment();
buildSplitRows(f.rows).forEach(r => {
if (r.kind === 'hunk' || r.kind === 'meta') {
const d = document.createElement('div');
d.className = 'diff-line diff-hunk';
d.textContent = r.text === '' ? ' ' : r.text;
frag.appendChild(d);
return;
}
const row = document.createElement('div');
row.className = 'srow s-' + r.kind;
row.appendChild(sideCell('sl', r.left, word));
row.appendChild(sideCell('sr', r.right, word));
frag.appendChild(row);
});
return frag;
}
function buildUnified(f, word) {
const frag = document.createDocumentFragment();
f.rows.forEach(r => {
const d = document.createElement('div');
d.className = 'diff-line';
if (r.type === 'add') d.classList.add('diff-add');
else if (r.type === 'del') d.classList.add('diff-del');
else if (r.type === 'hunk') d.classList.add('diff-hunk');
const prefix = r.type === 'add' ? '+' : r.type === 'del' ? '-' : r.type === 'ctx' ? ' ' : '';
const txt = prefix + r.text;
if (word) fillWord(d, txt); else d.textContent = txt === '' ? ' ' : txt;
frag.appendChild(d);
});
return frag;
}
function renderDiff(box, text) {
box.innerHTML = '';
const lines = String(text).split('\n');
const viewEl = $('#f-diff-view');
const wordEl = $('#f-diff-word');
const split = !!viewEl && viewEl.value === 'split';
const word = !!wordEl && wordEl.checked;
const files = parseUnifiedDiff(text);
const frag = document.createDocumentFragment();
lines.forEach(line => {
const pre = document.createElement('div');
pre.className = 'diff-line';
if (line.startsWith('+')) pre.classList.add('diff-add');
else if (line.startsWith('-')) pre.classList.add('diff-del');
else if (line.startsWith('@@')) pre.classList.add('diff-hunk');
pre.textContent = line === '' ? ' ' : line;
frag.appendChild(pre);
if (!files.length) {
const d = document.createElement('div');
d.className = 'diff-line';
d.textContent = String(text) || '(无差异)';
frag.appendChild(d);
box.appendChild(frag);
return;
}
files.forEach(f => {
if (f.header) {
const h = document.createElement('div');
h.className = 'diff-file-head';
h.textContent = fileHeaderOf(f.header);
h.title = f.header;
frag.appendChild(h);
}
frag.appendChild(split ? buildSplit(f, word) : buildUnified(f, word));
});
box.appendChild(frag);
}
@@ -537,9 +746,8 @@ function scheduleDraft() {
if (!current || !fileCurrent) return;
if (draftTimer) clearTimeout(draftTimer);
draftTimer = setTimeout(() => {
const editor = $('#file-editor');
if (!editor || editor.disabled) return;
const v = editor.value;
if (Editor.disabled) return;
const v = Editor.value;
if (v === draftServer) localStorage.removeItem(draftKey(current, fileCurrent));
else localStorage.setItem(draftKey(current, fileCurrent), v);
}, 600);
@@ -578,10 +786,9 @@ async function loadFile(name, path) {
async function saveFile(commit) {
const name = current;
const path = fileCurrent;
if (!name || !path) { alert('请先在左侧选择一个文件'); return; }
const editor = $('#file-editor');
if (editor.disabled) { alert('当前文件不可编辑(二进制或读取失败)'); return; }
const content = editor.value;
if (!name || !path) { toast('请先在左侧选择一个文件'); return; }
if (Editor.disabled) { toast('当前文件不可编辑(二进制或读取失败)'); return; }
const content = Editor.value;
try {
// 先写盘
await api(`/api/repos/${name}/file`, {
@@ -599,14 +806,14 @@ async function saveFile(commit) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path, message: $('#file-msg').value })
});
alert('已保存并提交');
toast('已保存并提交');
} else {
// 保存后自动切到差异预览
$('#f-diff-scope').value = 'working';
showTab('diff');
alert('已保存到磁盘(未提交)。可在「差异」tab 查看改动,或稍后由自动同步提交。');
toast('已保存到磁盘(未提交)。可在「差异」tab 查看改动,或稍后由自动同步提交。');
}
} catch (err) { alert('保存失败:' + err.message); }
} catch (err) { toast('保存失败:' + err.message); }
}
$('#f-diff-scope').addEventListener('change', () => { if (current && !$('#tab-diff').hidden) loadDiff(current); });
@@ -614,16 +821,16 @@ $('#btn-diff-refresh').addEventListener('click', () => { if (current) loadDiff(c
// 差异:复制 / 下载 .patch
$('#btn-diff-copy').addEventListener('click', async () => {
if (!diffText) { alert('当前没有可复制的差异内容'); return; }
if (!diffText) { toast('当前没有可复制的差异内容'); return; }
try {
await navigator.clipboard.writeText(diffText);
alert('差异内容已复制到剪贴板');
toast('差异内容已复制到剪贴板');
} catch (err) {
alert('复制失败(浏览器可能限制了剪贴板权限):' + err.message);
toast('复制失败(浏览器可能限制了剪贴板权限):' + err.message);
}
});
$('#btn-diff-download').addEventListener('click', () => {
if (!diffText) { alert('当前没有可下载的差异内容'); return; }
if (!diffText) { toast('当前没有可下载的差异内容'); return; }
const scope = $('#f-diff-scope').value;
const blob = new Blob([diffText], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
@@ -639,7 +846,7 @@ $('#btn-file-refresh').addEventListener('click', () => { if (current) loadTree(c
// 编辑器「对比此文件」→ 跳到差异 tab 并只看该文件
$('#btn-file-diff').addEventListener('click', () => {
if (!current || !fileCurrent) { alert('请先在左侧选择一个文件'); return; }
if (!current || !fileCurrent) { toast('请先在左侧选择一个文件'); return; }
diffPath = fileCurrent;
$('#f-diff-scope').value = 'working';
showTab('diff');
@@ -773,7 +980,7 @@ $('#cfg-form').addEventListener('submit', async (e) => {
}
closeModal();
refresh();
} catch (err) { alert('保存失败:' + err.message); }
} catch (err) { toast('保存失败:' + err.message); }
});
$('#btn-delete').addEventListener('click', async () => {
@@ -783,7 +990,7 @@ $('#btn-delete').addEventListener('click', async () => {
await api(`/api/repos/${current}`, { method: 'DELETE' });
closeModal();
refresh();
} catch (err) { alert(err.message); }
} catch (err) { toast(err.message, 'error'); }
});
// ---------- 顶部按钮 ----------
+184 -155
View File
@@ -1,204 +1,214 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<html lang="zh-CN" data-page-node-id="nRvVfOmQzU2twwSAvqUwfl">
<head data-page-node-id="BuWdwjwKPwwSCpow9JSC3n">
<meta charset="utf-8" data-page-node-id="tfHCUSqYqPzGPxp06Gd1tM">
<meta name="viewport" content="width=device-width, initial-scale=1" data-page-node-id="jb078uU5LM9D2ifPdaUWAq">
<title>Git Sync 控制台</title>
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/style.css" data-page-node-id="ORLXZi9XzxrEfwCcuWSocY">
<!-- CodeMirror(本地 vendor,离线可用;浅色/深色主题均按类名隔离,可同时加载) -->
<link rel="stylesheet" href="/vendor/cm/codemirror.min.css">
<link rel="stylesheet" href="/vendor/cm/eclipse.css">
<link rel="stylesheet" href="/vendor/cm/material-darker.css">
<link rel="stylesheet" href="/vendor/cm/codemirror.min.css" data-page-node-id="DEsUZn7j7yjeQYLdcwspqH">
<link rel="stylesheet" href="/vendor/cm/eclipse.css" data-page-node-id="MaPxkDtHnCAcvhei4GQ7iw">
<link rel="stylesheet" href="/vendor/cm/material-darker.css" data-page-node-id="ChKgT48SiWzjCMcWsGLrje">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
</head>
<body>
<header>
<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>
<body data-page-node-id="pZxw2WnQAZycSEktQenh8f">
<header data-page-node-id="XYY4eaCzyeDqRPq55yjUTY">
<div class="brand" data-page-node-id="xYRyIDur3LHCyDOfcx2CSX"><span class="brand-dot" data-page-node-id="GP41MDt2LXjJaYhsp8InyW"></span>Git Sync 控制台</div>
<div class="header-actions" data-page-node-id="XruBG1DkVAqNNsyOl4J41n">
<span id="conn" class="conn" data-page-node-id="aGv9c7D5e6Izpi92PjfxBz">连接中…</span>
<button class="btn btn-icon" id="btn-theme" title="切换深色 / 浅色模式" data-page-node-id="AP0nSHePHnQDDMylbBKMkA">🌙</button>
<button class="btn" id="btn-start-all" title="启动所有仓库的自动同步" data-page-node-id="QThBRVKpwwHv4WTWoGl0XA">全部启动</button>
<button class="btn" id="btn-stop-all" title="暂停所有仓库的自动同步" data-page-node-id="skWzYCenmPbVdRXS3TNxMK">全部暂停</button>
<button class="btn" id="btn-sync-all" data-page-node-id="6ag20ptiBl3F5MUA22HF68">全部同步</button>
<button class="btn btn-primary" id="btn-new" data-page-node-id="c0DooX9PlqcLzk8PGhoZR3">+ 新建配置</button>
</div>
</header>
<main class="layout">
<section class="main-col">
<div id="grid" class="grid"></div>
<div id="empty" class="empty" hidden>
<p>还没有任何仓库配置。</p>
<button class="btn btn-primary" id="btn-empty-new">创建第一个配置</button>
<main class="layout" data-page-node-id="9QmwKKoxuxwUQhg1t97g3S">
<section class="main-col" data-page-node-id="sKKLeDH2PGoGGIHbEhFid9">
<div id="grid" class="grid" data-page-node-id="xoQF8aiwcrnknxwxd80TI1"></div>
<div id="empty" class="empty" hidden data-page-node-id="C97b1VODLutQfNM13MXmAQ">
<p data-page-node-id="5nTKGjtKBDEFtovt25xVkQ">还没有任何仓库配置。</p>
<button class="btn btn-primary" id="btn-empty-new" data-page-node-id="SOs5r2CqrkMlyKyIZCGjkc">创建第一个配置</button>
</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>
<aside id="live" class="live" data-page-node-id="sgJJOcfDk9vPJs2DHcddtg">
<div class="live-head" data-page-node-id="znZimXC9iQsawFUMoA0lCu">
<h2 data-page-node-id="nGI3EfHmGzvRvuwP0oie2v">实时活动</h2>
<span id="live-conn" class="conn" data-page-node-id="Kg4dQkALsqywtSpd37Zz3B">连接中…</span>
<button class="btn btn-sm" id="btn-live-clear" title="清空本面板" data-page-node-id="KmUck5XGik1k7kP9zyfV2B">清空</button>
</div>
<div class="live-hint">所有仓库的同步(手动与自动)实时显示在此,无需点进详情</div>
<div id="live-log" class="live-log"></div>
<div class="live-hint" data-page-node-id="camBOmhyguvcYnuUJmA2E7">所有仓库的同步(手动与自动)实时显示在此,无需点进详情</div>
<div id="live-log" class="live-log" data-page-node-id="VBl8S1HvgHrCW0882nIFt2"></div>
</aside>
</main>
<div id="modal" class="modal" hidden>
<div class="modal-box">
<div class="modal-head">
<h2 id="m-title">详情</h2>
<button class="btn-icon" id="m-close" title="关闭">✕</button>
<div id="modal" class="modal" hidden data-page-node-id="txBcrv6ZrmZhFBqb699zeN">
<div class="modal-box" data-page-node-id="KdrABdnmnNUg8UQdCa7Nqv">
<div class="modal-head" data-page-node-id="QdJ8BjaSvq7DxthECgP16z">
<h2 id="m-title" data-page-node-id="5kDCaJqI0QPGtb0QllYLBm">详情</h2>
<button class="btn-icon" id="m-close" title="关闭" data-page-node-id="cEUcru162HY4bHMBMNTEfI">✕</button>
</div>
<div class="tabs" id="m-tabs">
<button data-tab="log" class="tab active">日志</button>
<button data-tab="history" class="tab">历史</button>
<button data-tab="diff" class="tab">差异</button>
<button data-tab="file" class="tab">文件</button>
<button data-tab="conflict" class="tab">冲突</button>
<button data-tab="stats" class="tab">统计</button>
<button data-tab="config" class="tab">配置</button>
<div class="tabs" id="m-tabs" data-page-node-id="CuT5FO4bado4BoZKGRXLGA">
<button data-tab="log" class="tab active" data-page-node-id="UirzBTtYe9llMyNeM4Q7cS">日志</button>
<button data-tab="history" class="tab" data-page-node-id="CPGPmAmMmERkIKBuv5Dw20">历史</button>
<button data-tab="diff" class="tab" data-page-node-id="hxChwjabjboAjspxC9VQyT">差异</button>
<button data-tab="file" class="tab" data-page-node-id="TotQXXPG6HhUSfeSm8XHz2">文件</button>
<button data-tab="conflict" class="tab" data-page-node-id="ujZu5YIEdpocOLfXLYhPv8">冲突</button>
<button data-tab="stats" class="tab" data-page-node-id="iIUiptYinU7iEa6VFcmceV">统计</button>
<button data-tab="config" class="tab" data-page-node-id="LRRFTfdian98kzNy3ca7U3">配置</button>
</div>
<div class="tab-body">
<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>
<div class="tab-body" data-page-node-id="BrgjnFG38oxkdJfBGHBRIX">
<div id="tab-log" class="tab-pane" data-page-node-id="LeZ5xD39VJIgWzOgMDLmji">
<div class="log-tools" data-page-node-id="VeZ2yA8bXiB5QkTnVxho88">
<label data-page-node-id="mu1MsfnHFce2PblMBUHuBz">级别
<select id="log-level" data-page-node-id="qRmCfGUzlRUbqzPfRd3GMb">
<option value="all" data-page-node-id="MBqUhLyyEFwF5KlLC2KzZq">全部</option>
<option value="error" data-page-node-id="PAJzGYoj7CRpKDjFKGgRF9">仅错误</option>
<option value="warn" data-page-node-id="m58wjzleIydsdSo49RQDUn">仅警告</option>
<option value="info" data-page-node-id="AGIjFdNqVxkFMzLjdbM76q">仅信息</option>
</select>
</label>
<input id="log-search" class="log-search" placeholder="搜索日志内容(关键词)">
<button class="btn btn-sm" id="btn-log-clear" title="清除过滤条件">清除</button>
<input id="log-search" class="log-search" placeholder="搜索日志内容(关键词)" data-page-node-id="diPbIGZMP5CvkxPAGMsEnR">
<button class="btn btn-sm" id="btn-log-clear" title="清除过滤条件" data-page-node-id="WTCY4f4B9KEUbcx59P7S1K">清除</button>
</div>
<pre id="logbox" class="logbox">加载中…</pre>
<pre id="logbox" class="logbox" data-page-node-id="xXJ1Cc3SnDycFy75rRr8FY">加载中…</pre>
</div>
<div id="tab-history" class="tab-pane" hidden>
<div id="commit-list" class="commit-list">加载中…</div>
<div id="tab-history" class="tab-pane" hidden data-page-node-id="StRYtav5tS7RxMlmHkgMSV">
<div id="commit-list" class="commit-list" data-page-node-id="vLBDjSJ9KvjCVXYgjXFluN">加载中…</div>
</div>
<div id="tab-diff" class="tab-pane" hidden>
<div class="diff-head">
<label>范围
<select id="f-diff-scope">
<option value="working">工作区(未暂存)</option>
<option value="staged">暂存区</option>
<option value="all">全部(含已提交未推送)</option>
<option value="remote">与远程对比</option>
<div id="tab-diff" class="tab-pane" hidden data-page-node-id="8ZEuDbkKtSFB1H2vNCD76h">
<div class="diff-head" data-page-node-id="pXqbBCb7m3PfpYSKGgGBJX">
<label data-page-node-id="9logygXL6fEhQpwTrZDwHR">范围
<select id="f-diff-scope" data-page-node-id="tjqhjYveDmQDIx4B9g1B58">
<option value="working" data-page-node-id="hmF9tmY9YCefzG7ppCmi7K">工作区(未暂存)</option>
<option value="staged" data-page-node-id="O5a6sN0qQaMZQPE6tGLqhU">暂存区</option>
<option value="all" data-page-node-id="xCr8XjV5khXje91R7F0axX">全部(含已提交未推送)</option>
<option value="remote" data-page-node-id="YWBratpqmpHwx7GuFLa2cC">与远程对比</option>
</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>
<label data-page-node-id="HM7TUuaIIeWw55c6H6p8et">视图
<select id="f-diff-view" data-page-node-id="pPFl4XiZHHV6OW02F6izDl">
<option value="unified" data-page-node-id="UBjVfLe9r8IoW6n4Br722d">统一</option>
<option value="split" data-page-node-id="52oaT6kkwDaWCwPJgQesYV">并排</option>
</select>
</label>
<label class="check-inline" title="按单词而非整行显示改动" data-page-node-id="Tn8hpGEewmZoD4cEfS4KUV"><input type="checkbox" id="f-diff-word" data-page-node-id="lxIM4byqutKFfAsBwujha6"> 词级</label>
<button class="btn btn-sm" id="btn-diff-refresh" data-page-node-id="3nrxtZxQJZHYXHAE7NR1Qq">刷新</button>
<button class="btn btn-sm" id="btn-diff-copy" data-page-node-id="oNXn145DgiaPDu4Aw4mOxG">复制</button>
<button class="btn btn-sm" id="btn-diff-download" data-page-node-id="ibtJzrmQVfhLYTIWAdCeqI">下载 .patch</button>
<span id="diff-filter" class="diff-filter" hidden data-page-node-id="XcHdGYotv6fgDrzsjgMerW"></span>
</div>
<div id="diff-stat" class="diff-stat"></div>
<pre id="diffbox" class="diffbox">加载中…</pre>
<div id="diff-stat" class="diff-stat" data-page-node-id="2hoANSTC9oMHaGMrEQ4HIl"></div>
<pre id="diffbox" class="diffbox" data-page-node-id="19VEbYBBwIIk0rsVl1M5FX">加载中…</pre>
</div>
<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 id="tab-file" class="tab-pane" hidden data-page-node-id="kAFseOkCuTSGBdvb4EmQZd">
<div class="file-head" data-page-node-id="UjmP651Cok2T5VcpTyNEEA">
<button class="btn btn-sm" id="btn-file-refresh" data-page-node-id="RF7EO9SpUEx62Gxu2GDfnJ">刷新文件列表</button>
<button class="btn btn-sm" id="btn-file-diff" title="只看当前文件的差异" data-page-node-id="4EiZjZ2rCfX5ZqD5ACDd7U">对比此文件</button>
<span id="file-current" class="file-current" data-page-node-id="Y29ei6OnYN1W2AWCe6Jueg"></span>
<span id="file-draft" class="file-draft" hidden data-page-node-id="gQRdJvI800DgkQ6h6trjuL"></span>
</div>
<div class="file-body">
<div id="file-list" class="file-list">加载中…</div>
<div class="file-edit-col">
<div class="file-body" data-page-node-id="RXmkN5Ep4iegYWG3iWBiON">
<div id="file-list" class="file-list" data-page-node-id="F75SFyDQShim9rZFPnjuJC">加载中…</div>
<div class="file-edit-col" data-page-node-id="xh1qou2SVboDfvKTLcXFEA">
<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>
<button class="btn btn-sm btn-primary" id="btn-file-commit">保存并提交</button>
<div class="file-actions" data-page-node-id="GAMAnkYKeMi93EVEx8eYOn">
<input id="file-msg" class="file-msg" placeholder="提交说明(可选)" data-page-node-id="aS2jRAr1kAkqElk1T4Cldz">
<button class="btn btn-sm" id="btn-file-save" data-page-node-id="gaVGyD6YkFFElkXJFBtLk5">保存</button>
<button class="btn btn-sm btn-primary" id="btn-file-commit" data-page-node-id="1VDju8pdiboyM0zBgwff21">保存并提交</button>
</div>
</div>
</div>
</div>
<div id="tab-conflict" class="tab-pane" hidden>
<div class="manual-actions">
<button class="btn btn-sm" id="btn-fetch">获取</button>
<button class="btn btn-sm" id="btn-pull">拉取</button>
<button class="btn btn-sm" id="btn-push">推送</button>
<div id="tab-conflict" class="tab-pane" hidden data-page-node-id="EFoL9Iv3JY4DBXK2ovqVl3">
<div class="manual-actions" data-page-node-id="OSsThrKzK22Q1tIcZ0a2bu">
<button class="btn btn-sm" id="btn-fetch" data-page-node-id="JEWw9Jx1x6PWh5EzLd7Bp9">获取</button>
<button class="btn btn-sm" id="btn-pull" data-page-node-id="D3F7c7ZkTj2GaQqrBtshfW">拉取</button>
<button class="btn btn-sm" id="btn-push" data-page-node-id="qTA9vsiTSuWz4dwilu7Prf">推送</button>
</div>
<div class="conflict-actions" id="pull-conflict-actions">
<button class="btn btn-danger" id="btn-abort">放弃合并</button>
<button class="btn btn-primary" id="btn-resolved">我已在本地解决,继续</button>
<div class="conflict-actions" id="pull-conflict-actions" data-page-node-id="u1mYEBVApFghZDqR2RFg7V">
<button class="btn btn-danger" id="btn-abort" data-page-node-id="iFRU44FOA3NXieww4PmbAR">放弃合并</button>
<button class="btn btn-primary" id="btn-resolved" data-page-node-id="C4SMNurpXaOJ8jByGjS2zg">我已在本地解决,继续</button>
</div>
<div class="conflict-actions" id="push-conflict-actions" hidden>
<span class="hint" id="push-conflict-hint"></span>
<button class="btn btn-danger" id="btn-force-push">强制推送(--force-with-lease)</button>
<button class="btn" id="btn-abort-push">放弃强制推送</button>
<div class="conflict-actions" id="push-conflict-actions" hidden data-page-node-id="C2kukXRbyT5HAgGznQBEOF">
<span class="hint" id="push-conflict-hint" data-page-node-id="meqqutN8Y8BlHoIUOsDuc3"></span>
<button class="btn btn-danger" id="btn-force-push" data-page-node-id="20FtTVRLlB3HrObTPUgS7I">强制推送(--force-with-lease)</button>
<button class="btn" id="btn-abort-push" data-page-node-id="kr5vGtshzwTYewuRpjenau">放弃强制推送</button>
</div>
<div id="conflict-list"></div>
<div id="conflict-list" data-page-node-id="5cossjGefHTRs2BQmv7ZiE"></div>
</div>
<div id="tab-stats" class="tab-pane" hidden>
<div id="stat-summary" class="stat-summary"></div>
<canvas id="stat-chart" height="120"></canvas>
<div id="tab-stats" class="tab-pane" hidden data-page-node-id="AmEsRLuCU7oz0ILjoFOSan">
<div id="stat-summary" class="stat-summary" data-page-node-id="B3x3zXCqPMHxf9Sbo22JsT"></div>
<canvas id="stat-chart" height="120" data-page-node-id="IzH3GbmZh2ogX04vsMxXz5"></canvas>
</div>
<div id="tab-config" class="tab-pane" hidden>
<form id="cfg-form" autocomplete="off">
<div class="form-grid">
<label>配置名称
<input name="name" id="f-name" required pattern="[A-Za-z0-9_\-]+" title="仅字母、数字、下划线、连字符">
<div id="tab-config" class="tab-pane" hidden data-page-node-id="Q0lO9KCy3xEzAkMXn5oQoc">
<form id="cfg-form" autocomplete="off" data-page-node-id="02yHvyuCTFoEYiXQz2Dvv5">
<div class="form-grid" data-page-node-id="SXw7lpnaPt4PKbbHGpCIJT">
<label data-page-node-id="qyzGnJE1Gt2AFnpJFtmmWe">配置名称
<input name="name" id="f-name" required pattern="[A-Za-z0-9_\-]+" title="仅字母、数字、下划线、连字符" data-page-node-id="NF2A817ZBdsM4puREMglkE">
</label>
<label>分支
<input name="branch" id="f-branch" value="main">
<label data-page-node-id="QC3jvGyY6uBpIUe0dtLRyy">分支
<input name="branch" id="f-branch" value="main" data-page-node-id="uIDYoqF9rtqJ0kqijuojOd">
</label>
<label class="full">仓库路径
<input name="repo_path" id="f-path" placeholder="C:\path\to\repo 或 /c/path/to/repo" required>
<label class="full" data-page-node-id="AEwFVqzONKMsh1CjmIWTfB">仓库路径
<input name="repo_path" id="f-path" placeholder="C:\path\to\repo 或 /c/path/to/repo" required data-page-node-id="V8Tqs5XjcAN6ezkffD7A0A">
</label>
<label class="full">远程仓库(每行一个,格式 名称=URL)
<label class="full" data-page-node-id="SZ5RWmnnD0vgIfUoyNcCAP">远程仓库(每行一个,格式 名称=URL)
<textarea name="remotes" id="f-remotes" rows="3" placeholder="origin=https://github.com/user/repo.git"></textarea>
</label>
<label>操作模式
<select name="mode" id="f-mode">
<option value="both">both(拉取并推送)</option>
<option value="push">push(仅推送)</option>
<option value="pull">pull(仅拉取)</option>
<label data-page-node-id="QUBANIDAtqHXOLlp2iHfUw">操作模式
<select name="mode" id="f-mode" data-page-node-id="PYGTzl4KFnOy5QLTnl9bil">
<option value="both" data-page-node-id="4uWdIiNUcKH1klnQC7Yyfk">both(拉取并推送)</option>
<option value="push" data-page-node-id="Fx5PMaVHG9UGFzmyKtNoVC">push(仅推送)</option>
<option value="pull" data-page-node-id="g0oRcyHrhBbgOpG3htuVkZ">pull(仅拉取)</option>
</select>
</label>
<label>检查间隔(秒)
<input name="interval" id="f-interval" type="number" min="5" value="60">
<label data-page-node-id="SR3Eyg3vhv2DEOD4ux6HbU">检查间隔(秒)
<input name="interval" id="f-interval" type="number" min="5" value="60" data-page-node-id="t4WQ8TOUFADFvrxWDqDLOO">
</label>
<label>认证方式
<select name="auth_method" id="f-auth">
<option value="ssh">SSH</option>
<option value="https">HTTPS(支持重试)</option>
<label data-page-node-id="pVjnoOnEBz06wQUDFuGnSG">认证方式
<select name="auth_method" id="f-auth" data-page-node-id="fsjBIeHacR0Y68FPjYrlIE">
<option value="ssh" data-page-node-id="D1rhiol1YcEJjSQptztATh">SSH</option>
<option value="https" data-page-node-id="SC5nYAdz2JBiHIks7TFSaN">HTTPS(支持重试)</option>
</select>
</label>
<label>冲突策略
<select name="conflict_strategy" id="f-conflict">
<option value="merge">merge(冲突后等待人工解决)</option>
<option value="rebase">rebase(变基)</option>
<option value="abort">abort(冲突后自动放弃)</option>
<label data-page-node-id="HzJHn4COhFnLGwn6aIiBKP">冲突策略
<select name="conflict_strategy" id="f-conflict" data-page-node-id="tJK7z7t0ojAK0xkHaM80nd">
<option value="merge" data-page-node-id="BdiprVQi0oHS9aTDD4LQ2l">merge(冲突后等待人工解决)</option>
<option value="rebase" data-page-node-id="jCngOAxfELckp4A3mrk1qI">rebase(变基)</option>
<option value="abort" data-page-node-id="aGj68eybADJcGx5G1g85UB">abort(冲突后自动放弃)</option>
</select>
</label>
<label>推送策略(non-fast-forward)
<select name="push_strategy" id="f-push">
<option value="manual">manual(冲突后等待人工决策)</option>
<option value="rebase">rebase(变基到远程后重试)</option>
<option value="force">force(--force-with-lease 强制推送)</option>
<label data-page-node-id="gwR8fc4feWQMTOGVirMjCO">推送策略(non-fast-forward)
<select name="push_strategy" id="f-push" data-page-node-id="HCS9CZyBErr1VeYSJxKStW">
<option value="manual" data-page-node-id="yurv3dbmJx7LKFDzzvzIUP">manual(冲突后等待人工决策)</option>
<option value="rebase" data-page-node-id="E3ifgThEynHNJhBnLn0a88">rebase(变基到远程后重试)</option>
<option value="force" data-page-node-id="lhFHAEDyn49ABT02QDys8o">force(--force-with-lease 强制推送)</option>
</select>
</label>
<label>重试次数(HTTPS)
<input name="retry_count" id="f-retry" type="number" min="0" value="3">
<label data-page-node-id="Nv2HjS10oANWojhUmKVQE4">重试次数(HTTPS)
<input name="retry_count" id="f-retry" type="number" min="0" value="3" data-page-node-id="UEgQOQBxHxqIfR5kYyZctp">
</label>
<label>重试间隔(秒,HTTPS)
<input name="retry_interval" id="f-retry-interval" type="number" min="0" value="5">
<label data-page-node-id="jNiYqEK7BpWhx0mg0CvdDY">重试间隔(秒,HTTPS)
<input name="retry_interval" id="f-retry-interval" type="number" min="0" value="5" data-page-node-id="sFEylqamHk8vW9H2D0alce">
</label>
<label class="full check-row">
<input type="checkbox" id="f-autocommit">
<span>自动提交模式:检测到未提交改动时自动 commit,并随同步推送到远程</span>
<label class="full check-row" data-page-node-id="gWPFS7fXyat6FiuobDoynQ">
<input type="checkbox" id="f-autocommit" data-page-node-id="5ADMjJXNR5EGPDmhwiCBlm">
<span data-page-node-id="z0tpoI6jpAbJCEwUJ4cSxO">自动提交模式:检测到未提交改动时自动 commit,并随同步推送到远程</span>
</label>
<label class="full">Cron 表达式(可选,5 段:分 时 日 月 周;填写后优先于固定间隔)
<input name="cron" id="f-cron" placeholder="*/30 * * * * 表示每 30 分钟,留空使用固定间隔">
<span id="cron-next" class="cron-next"></span>
<label class="full" data-page-node-id="tei066TFL8N9sqInjysFvD">Cron 表达式(可选,5 段:分 时 日 月 周;填写后优先于固定间隔)
<input name="cron" id="f-cron" placeholder="*/30 * * * * 表示每 30 分钟,留空使用固定间隔" data-page-node-id="6W3Ua8pPoGLuaNh7rGcDOw">
<span id="cron-next" class="cron-next" data-page-node-id="WgXuBnZPqGWvOBGRxzIVCn"></span>
</label>
<label class="full">日志文件路径(留空使用默认 logs/配置名.log)
<input name="log_file" id="f-log">
<label class="full" data-page-node-id="fsjZ6wLid5NUBV0yAHKdII">日志文件路径(留空使用默认 logs/配置名.log)
<input name="log_file" id="f-log" data-page-node-id="KJpv7jEWYnOQZ1PtQMetfP">
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">保存</button>
<button type="button" class="btn btn-danger" id="btn-delete" hidden>删除配置</button>
<div class="form-actions" data-page-node-id="1GfIJLKdxSyPwBqpqCCMb9">
<button type="submit" class="btn btn-primary" data-page-node-id="E2123htNE6lmoNzZTg7AwV">保存</button>
<button type="button" class="btn" id="btn-clone" hidden data-page-node-id="H6BdDiraff2cjD9EWjF8Eh">克隆此配置</button>
<button type="button" class="btn btn-danger" id="btn-delete" hidden data-page-node-id="iyYF2JZ87tE2mGPCkbHX5w">删除配置</button>
</div>
</form>
</div>
@@ -206,23 +216,42 @@
</div>
</div>
<div id="login" class="modal" hidden>
<div class="modal-box login-box">
<div class="modal-head">
<h2>Git Sync 控制台</h2>
<div id="login" class="modal" hidden data-page-node-id="bALcA8zunmLBixpK2bI55B">
<div class="modal-box login-box" data-page-node-id="kcGN8ZqtOQx7KUIgGpoGCN">
<div class="modal-head" data-page-node-id="TPAEpzey0DyD3tbzQcX6AD">
<h2 data-page-node-id="SxFYrLjYGUJGks4LhmWFGa">Git Sync 控制台</h2>
</div>
<div class="login-body">
<label>访问密码
<input type="password" id="login-password">
<div class="login-body" data-page-node-id="loM04avwKu3ehaXbaPV1dc">
<label data-page-node-id="PQlrAv5lABcwv1V9A2kAZf">访问密码
<input type="password" id="login-password" data-page-node-id="BQQm1tLuMFSt6HhCgtUSzi">
</label>
<p id="login-err" class="login-err" hidden></p>
<div class="form-actions">
<button class="btn btn-primary" id="btn-login">登录</button>
<p id="login-err" class="login-err" hidden data-page-node-id="c5KlyY0GNpRJlp2ZCDLC2P"></p>
<div class="form-actions" data-page-node-id="dKo7nNYzQBz8MQZa7x51Bf">
<button class="btn btn-primary" id="btn-login" data-page-node-id="EOo505vSrX2QIGGOnpcDxp">登录</button>
</div>
</div>
</div>
</div>
<!-- Toast 轻提示容器(替代原生 alert,固定在右下角,不打断操作) -->
<div id="toasts" class="toasts" data-page-node-id="sGCNypYh5YHNDe7nZ7TUeA"></div>
<script src="/app.js"></script>
<!-- CodeMirror:本地 vendor,必须在 app.js 之后加载以便降级检测 -->
<script src="/vendor/cm/codemirror.min.js"></script>
<script src="/vendor/cm/mode/javascript.min.js"></script>
<script src="/vendor/cm/mode/xml.min.js"></script>
<script src="/vendor/cm/mode/css.min.js"></script>
<script src="/vendor/cm/mode/htmlmixed.min.js"></script>
<script src="/vendor/cm/mode/go.min.js"></script>
<script src="/vendor/cm/mode/markdown.min.js"></script>
<script src="/vendor/cm/mode/shell.min.js"></script>
<script src="/vendor/cm/mode/python.min.js"></script>
<script src="/vendor/cm/mode/clike.min.js"></script>
<script src="/vendor/cm/mode/yaml.min.js"></script>
<script src="/vendor/cm/mode/sql.min.js"></script>
<script src="/vendor/cm/mode/properties.min.js"></script>
<script src="/vendor/cm/mode/diff.min.js"></script>
<script src="/cm-boot.js"></script>
</body>
</html>
+39
View File
@@ -206,3 +206,42 @@ html.dark .file-editor { background: #12151a; }
.cron-next { font-size: 12px; color: var(--muted); min-height: 16px; }
.cron-next.bad { color: var(--red); }
.cron-next.good { color: var(--green); }
/* ---------- Toast 轻提示(替代 alert) ---------- */
.toasts { position: fixed; right: 20px; bottom: 20px; display: flex; flex-direction: column; gap: 8px; z-index: 9999; pointer-events: none; }
.toast { pointer-events: auto; cursor: pointer; max-width: 380px; padding: 10px 14px; border-radius: 8px; font-size: 13px; color: #fff; background: #333; box-shadow: 0 6px 18px rgba(0, 0, 0, .18); animation: toast-in .22s ease-out; }
.toast.t-info { background: #2f6feb; }
.toast.t-success { background: #2da44e; }
.toast.t-warn { background: #bf8700; }
.toast.t-error { background: #cf222e; }
.toast.out { opacity: 0; transform: translateY(6px); transition: all .3s; }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* ---------- 差异:并排视图 ---------- */
.diff-file-head { font: 12px Consolas, monospace; color: var(--muted); padding: 4px 6px; background: var(--gray-bg); border-radius: 6px; margin: 8px 0 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.srow { display: flex; border-top: 1px solid var(--border); }
.srow .scol { flex: 1; min-width: 0; display: flex; gap: 8px; padding: 0 6px; font: 12px/1.5 Consolas, monospace; white-space: pre-wrap; word-break: break-all; }
.srow .sl { border-right: 1px solid var(--border); }
.srow .ln { flex: none; width: 44px; text-align: right; color: var(--muted); user-select: none; opacity: .75; }
.srow .stxt { flex: 1; }
.srow.s-del .sl { background: var(--red-bg); color: var(--red); }
.srow.s-add .sr { background: var(--green-bg); color: var(--green); }
.srow.s-chg .sl { background: var(--red-bg); color: var(--red); }
.srow.s-chg .sr { background: var(--green-bg); color: var(--green); }
/* ---------- 差异:词级高亮 ---------- */
.wd-add { background: var(--green-bg); color: var(--green); border-radius: 3px; }
.wd-del { background: var(--red-bg); color: var(--red); border-radius: 3px; text-decoration: line-through; }
.diff-head label.check-inline { flex-direction: row; align-items: center; gap: 4px; }
.diff-head label.check-inline input { width: auto; margin: 0; }
/* ---------- CodeMirror 适配 ---------- */
.CodeMirror { height: 420px; border: 1px solid var(--border); border-radius: 8px; font: 12.5px/1.55 Consolas, monospace; }
.CodeMirror-scroll { min-height: 380px; }
/* ---------- 冲突文件内联编辑 ---------- */
.cf-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
.cf-head h4 { margin: 0; flex: 1; }
.cf-ta { width: 100%; min-height: 260px; border: 1px solid var(--border); border-radius: 8px; padding: 10px; font: 12.5px/1.55 Consolas, monospace; color: var(--text); background: #fff; white-space: pre; overflow: auto; }
html.dark .cf-ta { background: #12151a; }
.cf-actions { display: flex; gap: 8px; margin-top: 6px; }