git-sync-web 自动提交 2026-09-09 14:55:18
This commit is contained in:
@@ -1361,6 +1361,31 @@ func (e *Engine) CommitFile(name, rel, message string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveConflictFile 网页端解决冲突:写入内容 → 暂存 → (可选)结束合并/变基
|
||||
// finish=true 且已无其它冲突文件时才真正结束,避免把带冲突标记的内容提交进去
|
||||
func (e *Engine) ResolveConflictFile(name, rel, content string, finish bool) error {
|
||||
s := e.get(name)
|
||||
if s == nil {
|
||||
return fmt.Errorf("配置不存在: %s", name)
|
||||
}
|
||||
if err := e.WriteFileContent(name, rel, content); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := gitRun(s.cfg.RepoPath, 30*time.Second, "add", "--", rel); err != nil {
|
||||
return fmt.Errorf("git add 失败: %w", err)
|
||||
}
|
||||
e.logf(s, "info", "网页端冲突文件 %s 已暂存", rel)
|
||||
if !finish {
|
||||
return nil
|
||||
}
|
||||
rest, _ := conflictFiles(s.cfg.RepoPath)
|
||||
if len(rest) > 0 {
|
||||
e.pushStatus(s)
|
||||
return fmt.Errorf("已暂存 %s;但仍有未解决的冲突文件:%s", rel, strings.Join(rest, ", "))
|
||||
}
|
||||
return e.ConflictResolve(name)
|
||||
}
|
||||
|
||||
// DiffFile 单文件增删统计
|
||||
type DiffFile struct {
|
||||
Path string `json:"path"`
|
||||
@@ -1375,10 +1400,13 @@ type DiffResult struct {
|
||||
Text string `json:"text"`
|
||||
Files []DiffFile `json:"files"`
|
||||
VsRemote bool `json:"vs_remote"`
|
||||
Word bool `json:"word"`
|
||||
}
|
||||
|
||||
// WorkingDiff 返回工作区/暂存/全部/与远程的差异;rel 非空时只对比该文件
|
||||
func (e *Engine) WorkingDiff(name, scope, rel string) (*DiffResult, error) {
|
||||
// WorkingDiff 返回工作区/暂存/全部/与远程的差异
|
||||
// rel 非空时只对比单个文件(已做路径安全校验)
|
||||
// mode = "word" 时改用 --word-diff=plain,便于词级定位改动
|
||||
func (e *Engine) WorkingDiff(name, scope, rel, mode string) (*DiffResult, error) {
|
||||
s := e.get(name)
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("配置不存在: %s", name)
|
||||
@@ -1402,6 +1430,10 @@ func (e *Engine) WorkingDiff(name, scope, rel string) (*DiffResult, error) {
|
||||
default: // working(未暂存)
|
||||
args = []string{"diff", "--no-color"}
|
||||
}
|
||||
// 词级差异:--word-diff=plain 用 [-x-] / {+x+} 标记行内改动
|
||||
if mode == "word" {
|
||||
args = append(args, "--word-diff=plain")
|
||||
}
|
||||
statArgs := append([]string{}, args...)
|
||||
if scope == "remote" {
|
||||
statArgs = []string{"diff", "--no-color", "--stat", remote + "/" + s.cfg.Branch, "HEAD"}
|
||||
|
||||
@@ -53,6 +53,7 @@ func NewServer(e *Engine, static fs.FS, password string) *Server {
|
||||
mux.HandleFunc("GET /api/repos/{name}/tree", s.wrapRepo(s.treeRepo))
|
||||
mux.HandleFunc("GET /api/repos/{name}/file", s.wrapRepo(s.fileRepo))
|
||||
mux.HandleFunc("PUT /api/repos/{name}/file", s.wrapRepo(s.fileWriteRepo))
|
||||
mux.HandleFunc("POST /api/repos/{name}/resolve-file", s.wrapRepo(s.resolveFileRepo))
|
||||
mux.HandleFunc("POST /api/repos/{name}/commit", s.wrapRepo(s.commitFileRepo))
|
||||
mux.HandleFunc("GET /api/repos/{name}/diff", s.wrapRepo(s.diffRepo))
|
||||
mux.HandleFunc("GET /api/cron-next", s.handleCronNext)
|
||||
@@ -377,6 +378,28 @@ func (s *Server) fileWriteRepo(w http.ResponseWriter, r *http.Request, name stri
|
||||
writeJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
// resolveFileRepo 网页端解决单个冲突文件:写入 + 暂存,finish=true 时尝试结束合并/变基
|
||||
func (s *Server) resolveFileRepo(w http.ResponseWriter, r *http.Request, name string) {
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Finish bool `json:"finish"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeErr(w, 400, "请求体格式错误")
|
||||
return
|
||||
}
|
||||
if req.Path == "" {
|
||||
writeErr(w, 400, "缺少 path")
|
||||
return
|
||||
}
|
||||
if err := s.engine.ResolveConflictFile(name, req.Path, req.Content, req.Finish); err != nil {
|
||||
writeErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) commitFileRepo(w http.ResponseWriter, r *http.Request, name string) {
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
@@ -403,7 +426,8 @@ func (s *Server) diffRepo(w http.ResponseWriter, r *http.Request, name string) {
|
||||
scope = "working"
|
||||
}
|
||||
// path 可选:传入时只对比该文件(用于编辑器「对比此文件」)
|
||||
res, err := s.engine.WorkingDiff(name, scope, r.URL.Query().Get("path"))
|
||||
// mode = word 时返回 --word-diff=plain 结果(词级差异)
|
||||
res, err := s.engine.WorkingDiff(name, scope, r.URL.Query().Get("path"), r.URL.Query().Get("mode"))
|
||||
if err != nil {
|
||||
writeErr(w, 400, err.Error())
|
||||
return
|
||||
|
||||
+241
-34
@@ -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
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user