feat: 项目 logo + 侧栏拖动排序 + 合并修复(主检出干净时 in-place)
- 项目 logo:仓库内文件(logo/icon/favicon 等多路径)→git remote 头像→自定义(URL/相对路径)→ 首字母徽章兜底;GET /api/projects/:id/logo(文件流/302),配置面板加 Logo 输入 - 侧栏项目拖动排序:projects.sort_order + POST /api/projects/reorder,乐观更新 - 合并修复:默认分支正被主检出占用时,若工作区干净则直接在主检出 in-place 合并 (用户手动合并的等价操作,安全);脏工作区拒绝并提示提交/暂存或仅通过 - schema: projects.logo / sort_order(ensureColumn 平滑迁移) 测试 75/75(merge 用例改为干净→成功/脏→拒绝) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+59
-7
@@ -312,11 +312,21 @@ function renderSidebar() {
|
||||
ul.innerHTML = `<li class="p-empty">暂无项目<br>点「+ 新建」登记第一个仓库</li>`;
|
||||
return;
|
||||
}
|
||||
ul.innerHTML = S.projects.map((p) => `
|
||||
<li class="${p.id === S.currentProjectId ? 'active' : ''}" data-action="select-project" data-id="${esc(p.id)}">
|
||||
<span class="p-name">${esc(p.name)}</span>
|
||||
<span class="p-path">${esc(p.repoPath)}</span>
|
||||
</li>`).join('');
|
||||
ul.innerHTML = S.projects.map((p) => {
|
||||
const ch = (p.name || '?').trim().charAt(0).toUpperCase();
|
||||
const hue = [...(p.name || '')].reduce((a, c) => (a * 31 + c.charCodeAt(0)) % 360, 7);
|
||||
return `
|
||||
<li class="${p.id === S.currentProjectId ? 'active' : ''}" data-action="select-project" data-id="${esc(p.id)}" draggable="true">
|
||||
<span class="p-logo-wrap">
|
||||
<span class="p-logo-fb" style="--h:${hue}">${esc(ch)}</span>
|
||||
<img class="p-logo" src="/api/projects/${esc(p.id)}/logo?v=${esc(p.logo || '')}" alt="" onload="this.classList.add('ok')" onerror="this.remove()">
|
||||
</span>
|
||||
<span class="p-meta">
|
||||
<span class="p-name">${esc(p.name)}</span>
|
||||
<span class="p-path">${esc(p.repoPath)}</span>
|
||||
</span>
|
||||
</li>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderTopbar() {
|
||||
@@ -1216,6 +1226,7 @@ document.addEventListener('click', (ev) => {
|
||||
if (p) {
|
||||
$('#cfgConcurrency').value = p.concurrency ?? 1;
|
||||
$('#cfgAutonomy').value = AUTONOMY_LABEL[p.autonomy] ? p.autonomy : 'manual';
|
||||
$('#cfgLogo').value = p.logo ?? '';
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1225,11 +1236,13 @@ document.addEventListener('click', (ev) => {
|
||||
const cc = Number($('#cfgConcurrency').value);
|
||||
if (!Number.isInteger(cc) || cc < 1) { toast('最大并发必须是 ≥1 的整数'); break; }
|
||||
const autonomy = $('#cfgAutonomy').value;
|
||||
const logo = $('#cfgLogo').value.trim();
|
||||
act(async () => {
|
||||
await api(`/api/projects/${S.currentProjectId}`, {
|
||||
method: 'PATCH', body: JSON.stringify({ concurrency: cc, autonomy }),
|
||||
method: 'PATCH', body: JSON.stringify({ concurrency: cc, autonomy, logo: logo || null }),
|
||||
});
|
||||
await loadProjects(); // 刷新当前值显示
|
||||
await loadProjects(); // 刷新当前值显示 + logo
|
||||
renderSidebar();
|
||||
$('#configPanel').hidden = true; // 保存成功自动收起
|
||||
}, '配置已保存');
|
||||
break;
|
||||
@@ -1245,6 +1258,45 @@ document.addEventListener('click', (ev) => {
|
||||
}
|
||||
}, true);
|
||||
|
||||
// ── 项目列表拖动排序 ──
|
||||
let dragId = null;
|
||||
$('#projectList').addEventListener('dragstart', (ev) => {
|
||||
const li = ev.target.closest('li[data-id]');
|
||||
if (!li) return;
|
||||
dragId = li.dataset.id;
|
||||
li.classList.add('dragging');
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
$('#projectList').addEventListener('dragend', (ev) => {
|
||||
const li = ev.target.closest('li');
|
||||
if (li) li.classList.remove('dragging');
|
||||
$('#projectList').querySelectorAll('.drop-over').forEach((x) => x.classList.remove('drop-over'));
|
||||
dragId = null;
|
||||
});
|
||||
$('#projectList').addEventListener('dragover', (ev) => {
|
||||
ev.preventDefault();
|
||||
const li = ev.target.closest('li[data-id]');
|
||||
$('#projectList').querySelectorAll('.drop-over').forEach((x) => x.classList.remove('drop-over'));
|
||||
if (li && li.dataset.id !== dragId) li.classList.add('drop-over');
|
||||
});
|
||||
$('#projectList').addEventListener('drop', (ev) => {
|
||||
ev.preventDefault();
|
||||
const li = ev.target.closest('li[data-id]');
|
||||
if (!li || !dragId || li.dataset.id === dragId) return;
|
||||
const ids = S.projects.map((p) => p.id);
|
||||
const from = ids.indexOf(dragId);
|
||||
const to = ids.indexOf(li.dataset.id);
|
||||
if (from < 0 || to < 0) return;
|
||||
ids.splice(to, 0, ids.splice(from, 1)[0]); // 插到目标位置
|
||||
S.projects.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); // 乐观重排
|
||||
renderSidebar();
|
||||
act(async () => {
|
||||
await api('/api/projects/reorder', { method: 'POST', body: JSON.stringify({ order: ids }) });
|
||||
await loadProjects();
|
||||
renderSidebar();
|
||||
}, '项目顺序已保存');
|
||||
});
|
||||
|
||||
// ── 表单提交 ──
|
||||
$('#newProjectForm').addEventListener('submit', (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user