git-sync-web 自动提交 2026-09-09 13:58:07

This commit is contained in:
tamakyi
2026-09-09 13:58:08 +08:00
parent bba6a3362e
commit f161424dcd
10 changed files with 214 additions and 13 deletions
+165 -8
View File
@@ -434,14 +434,38 @@ function updateConflictActions() {
}
// ---------- 差异显示(网页端) ----------
// diffPath 非空时只显示单个文件的差异(编辑器「对比此文件」进入)
let diffPath = '';
// diffText 保存当前差异原文,供「复制 / 下载 .patch」使用
let diffText = '';
function updateDiffFilterNote() {
const el = $('#diff-filter');
if (!el) return;
if (!diffPath) { el.hidden = true; el.textContent = ''; return; }
el.hidden = false;
el.innerHTML = `仅显示:${esc(diffPath)} <a href="#" id="diff-filter-clear" style="color:inherit">✕ 显示全部</a>`;
const a = $('#diff-filter-clear');
if (a) a.addEventListener('click', (e) => {
e.preventDefault();
diffPath = '';
updateDiffFilterNote();
if (current) loadDiff(current);
});
}
function loadDiff(name) {
const scope = $('#f-diff-scope').value;
const box = $('#diffbox');
const stat = $('#diff-stat');
box.textContent = '加载中…';
stat.innerHTML = '';
api(`/api/repos/${name}/diff?scope=${encodeURIComponent(scope)}`).then(d => {
renderDiff(box, d.text || '(无差异)');
updateDiffFilterNote();
let url = `/api/repos/${name}/diff?scope=${encodeURIComponent(scope)}`;
if (diffPath) url += `&path=${encodeURIComponent(diffPath)}`;
api(url).then(d => {
diffText = d.text || '';
renderDiff(box, diffText || '(无差异)');
if (d.files && d.files.length) {
stat.innerHTML = d.files.map(f =>
`<span class="df ${f.add ? 'df-add' : ''} ${f.del ? 'df-del' : ''}">${esc(f.path)} <b style="color:var(--green)">+${f.add}</b> <b style="color:var(--red)">-${f.del}</b></span>`
@@ -470,33 +494,81 @@ function renderDiff(box, text) {
// ---------- 文件编辑(网页端) ----------
let fileCurrent = null;
const STATUS_LABEL = { M: '已修改', A: '已新增', D: '已删除', '?': '未跟踪' };
function loadTree(name) {
const list = $('#file-list');
list.textContent = '加载中…';
api(`/api/repos/${name}/tree`).then(d => {
const files = d.files || [];
if (!files.length) { list.textContent = '(无文件)'; return; }
list.innerHTML = files.map(f =>
`<div class="file-item" data-path="${esc(f.path)}" title="${esc(f.path)}">${esc(f.path)}</div>`
).join('');
list.innerHTML = files.map(f => {
const st = f.status || '';
const cls = st === '?' ? 'Q' : st;
const badge = st
? `<span class="fi-status s-${esc(cls)}" title="${esc(STATUS_LABEL[st] || st)}">${esc(st)}</span>`
: '';
return `<div class="file-item" data-path="${esc(f.path)}" title="${esc(f.path)}"><span class="fi-name">${esc(f.path)}</span>${badge}</div>`;
}).join('');
}).catch(err => { list.textContent = '文件列表加载失败:' + err.message; });
}
// ---------- 草稿缓存(localStorage,防刷新丢稿) ----------
const draftKey = (name, path) => `gitautosync:draft:${name}:${path}`;
let draftServer = ''; // 当前文件在磁盘上的原文,用于「放弃草稿」回滚
let draftTimer = null;
function showDraftNote(on) {
const el = $('#file-draft');
if (!el) return;
if (!on) { el.hidden = true; el.innerHTML = ''; return; }
el.hidden = false;
el.innerHTML = '已恢复未保存的草稿 <a href="#" id="draft-discard" style="color:inherit">放弃草稿</a>';
const a = $('#draft-discard');
if (a) a.addEventListener('click', (e) => {
e.preventDefault();
localStorage.removeItem(draftKey(current, fileCurrent));
$('#file-editor').value = draftServer;
showDraftNote(false);
});
}
// 编辑时防抖缓存草稿;内容与磁盘一致时自动清除
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 (v === draftServer) localStorage.removeItem(draftKey(current, fileCurrent));
else localStorage.setItem(draftKey(current, fileCurrent), v);
}, 600);
}
async function loadFile(name, path) {
const editor = $('#file-editor');
editor.value = '加载中…';
editor.disabled = false;
$('#file-current').textContent = path;
fileCurrent = path;
showDraftNote(false);
try {
const d = await api(`/api/repos/${name}/file?path=${encodeURIComponent(path)}`);
if (d.binary) {
editor.value = `(二进制文件,大小 ${d.size} 字节,无法在网页编辑)`;
editor.disabled = true;
} else {
editor.value = d.content || '';
editor.disabled = false;
draftServer = '';
return;
}
draftServer = d.content || '';
const saved = localStorage.getItem(draftKey(name, path));
if (saved !== null && saved !== draftServer) {
editor.value = saved;
showDraftNote(true);
} else {
editor.value = draftServer;
}
editor.disabled = false;
} catch (err) {
editor.value = '读取失败:' + err.message;
editor.disabled = true;
@@ -517,6 +589,10 @@ async function saveFile(commit) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path, content })
});
// 写盘成功 → 草稿作废
localStorage.removeItem(draftKey(name, path));
showDraftNote(false);
draftServer = content;
if (commit) {
await api(`/api/repos/${name}/commit`, {
method: 'POST',
@@ -535,7 +611,40 @@ async function saveFile(commit) {
$('#f-diff-scope').addEventListener('change', () => { if (current && !$('#tab-diff').hidden) loadDiff(current); });
$('#btn-diff-refresh').addEventListener('click', () => { if (current) loadDiff(current); });
// 差异:复制 / 下载 .patch
$('#btn-diff-copy').addEventListener('click', async () => {
if (!diffText) { alert('当前没有可复制的差异内容'); return; }
try {
await navigator.clipboard.writeText(diffText);
alert('差异内容已复制到剪贴板');
} catch (err) {
alert('复制失败(浏览器可能限制了剪贴板权限):' + err.message);
}
});
$('#btn-diff-download').addEventListener('click', () => {
if (!diffText) { alert('当前没有可下载的差异内容'); return; }
const scope = $('#f-diff-scope').value;
const blob = new Blob([diffText], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${current || 'repo'}-${scope}.patch`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
});
$('#btn-file-refresh').addEventListener('click', () => { if (current) loadTree(current); });
// 编辑器「对比此文件」→ 跳到差异 tab 并只看该文件
$('#btn-file-diff').addEventListener('click', () => {
if (!current || !fileCurrent) { alert('请先在左侧选择一个文件'); return; }
diffPath = fileCurrent;
$('#f-diff-scope').value = 'working';
showTab('diff');
});
$('#file-list').addEventListener('click', (e) => {
const item = e.target.closest('.file-item');
if (!item || !current) return;
@@ -544,6 +653,15 @@ $('#file-list').addEventListener('click', (e) => {
$('#btn-file-save').addEventListener('click', () => saveFile(false));
$('#btn-file-commit').addEventListener('click', () => saveFile(true));
// 编辑器:输入时缓存草稿,Ctrl/Cmd + S 直接保存
$('#file-editor').addEventListener('input', scheduleDraft);
$('#file-editor').addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
e.preventDefault();
saveFile(false);
}
});
// ---------- 统计 ----------
function dateStr(d) {
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
@@ -607,6 +725,7 @@ function fillForm(r) {
$('#f-autocommit').checked = r ? !!r.auto_commit : false;
$('#f-cron').value = r ? (r.cron || '') : '';
$('#f-log').value = r ? (r.log_file || '') : '';
previewCron();
}
function readForm() {
@@ -674,6 +793,43 @@ $('#btn-sync-all').addEventListener('click', () => {
repos.filter(r => r.running).forEach(r => post(`/api/repos/${r.name}/sync`).catch(() => {}));
});
// ---------- 暗色模式 ----------
const THEME_KEY = 'gitautosync:theme';
function applyTheme(dark) {
document.documentElement.classList.toggle('dark', !!dark);
const btn = $('#btn-theme');
if (btn) btn.textContent = dark ? '☀️' : '🌙';
}
function initTheme() {
applyTheme(localStorage.getItem(THEME_KEY) === 'dark');
}
$('#btn-theme').addEventListener('click', () => {
const dark = !document.documentElement.classList.contains('dark');
localStorage.setItem(THEME_KEY, dark ? 'dark' : 'light');
applyTheme(dark);
});
// ---------- Cron 下次执行预览 ----------
let cronTimer = null;
async function previewCron() {
const el = $('#cron-next');
if (!el) return;
const expr = $('#f-cron').value.trim();
if (!expr) { el.textContent = ''; el.className = 'cron-next'; return; }
try {
const d = await api(`/api/cron-next?expr=${encodeURIComponent(expr)}`);
el.textContent = '下次执行:' + d.next;
el.className = 'cron-next good';
} catch (err) {
el.textContent = '表达式无效:' + err.message;
el.className = 'cron-next bad';
}
}
$('#f-cron').addEventListener('input', () => {
if (cronTimer) clearTimeout(cronTimer);
cronTimer = setTimeout(previewCron, 400);
});
// ---------- 初始化 ----------
async function refresh() {
try {
@@ -684,6 +840,7 @@ async function refresh() {
} catch (e) { /* 忽略,SSE 会重试 */ }
}
initTheme();
refresh();
connect();
setInterval(refresh, 20000);