claude-coding-flow 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flow.js +21 -8
- package/commands/bug-fix.md +51 -23
- package/commands/code-gen.md +54 -22
- package/commands/doc-gen.md +128 -46
- package/dashboard/db.py +69 -4
- package/dashboard/main.py +86 -4
- package/dashboard/static/app.js +156 -24
- package/dashboard/static/index.html +12 -9
- package/dashboard/static/style.css +64 -20
- package/package.json +1 -1
package/dashboard/static/app.js
CHANGED
|
@@ -16,7 +16,10 @@ const TAB_CONFIG = {
|
|
|
16
16
|
let currentTab = "doc-gen";
|
|
17
17
|
let _ctxTaskId = null;
|
|
18
18
|
let _ctxModuleId = null;
|
|
19
|
+
let _ctxModuleType = "";
|
|
20
|
+
let _ctxModuleTaskCount = 0;
|
|
19
21
|
let _ctxTabType = null;
|
|
22
|
+
let _editingFilePath = null;
|
|
20
23
|
|
|
21
24
|
// ── Tab Switching ──
|
|
22
25
|
|
|
@@ -68,8 +71,9 @@ async function loadTabContent(tab) {
|
|
|
68
71
|
|
|
69
72
|
function renderModuleShell(mod, innerHtml, color) {
|
|
70
73
|
const tasks = mod.tasks || [];
|
|
74
|
+
const modType = mod.type || "code-gen";
|
|
71
75
|
return `<div class="module-section">
|
|
72
|
-
<div class="module-header" data-module-id="${mod.id}" onclick="toggleModule('${mod.id}')">
|
|
76
|
+
<div class="module-header" data-module-id="${mod.id}" data-type="${modType}" data-task-count="${tasks.length}" onclick="toggleModule('${mod.id}')">
|
|
73
77
|
<div class="module-left">
|
|
74
78
|
<span class="material-icons module-expand" id="expand-${mod.id}">expand_more</span>
|
|
75
79
|
<span class="module-name">${esc(mod.name)}</span>
|
|
@@ -111,7 +115,15 @@ async function renderTaskChain(tasks, tabType) {
|
|
|
111
115
|
await Promise.all(relPromises);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
|
|
118
|
+
// Build chain parent: prev_task_id first, then related_task_id (for doc-gen iterations)
|
|
119
|
+
const taskIds = new Set(tasks.map(t => t.id));
|
|
120
|
+
function chainParent(t) {
|
|
121
|
+
if (t.prev_task_id && taskIds.has(t.prev_task_id)) return { id: t.prev_task_id, label: "迭代" };
|
|
122
|
+
if (t.related_task_id && taskIds.has(t.related_task_id)) return { id: t.related_task_id, label: "迭代" };
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const roots = tasks.filter(t => !chainParent(t));
|
|
115
127
|
const visited = new Set();
|
|
116
128
|
|
|
117
129
|
for (const root of roots) {
|
|
@@ -119,7 +131,9 @@ async function renderTaskChain(tasks, tabType) {
|
|
|
119
131
|
while (cur) {
|
|
120
132
|
if (visited.has(cur.id)) break;
|
|
121
133
|
visited.add(cur.id);
|
|
122
|
-
const
|
|
134
|
+
const nextInfo = tasks.map(t => ({ task: t, parent: chainParent(t) })).find(x => x.parent && x.parent.id === cur.id);
|
|
135
|
+
const next = nextInfo ? nextInfo.task : null;
|
|
136
|
+
const linkLabel = nextInfo ? nextInfo.parent.label : "迭代";
|
|
123
137
|
|
|
124
138
|
lines.push(`<div class="chain-node">
|
|
125
139
|
<span class="chain-dot ${cur.status}"></span>
|
|
@@ -130,7 +144,7 @@ async function renderTaskChain(tasks, tabType) {
|
|
|
130
144
|
if (next) {
|
|
131
145
|
lines.push(`<div class="chain-link">
|
|
132
146
|
<span class="material-icons chain-arrow">arrow_downward</span>
|
|
133
|
-
<span class="chain-link-label"
|
|
147
|
+
<span class="chain-link-label">${linkLabel}</span>
|
|
134
148
|
</div>`);
|
|
135
149
|
}
|
|
136
150
|
cur = next;
|
|
@@ -149,7 +163,7 @@ async function renderTaskChain(tasks, tabType) {
|
|
|
149
163
|
return `<div class="task-chain">${lines.join("")}</div>`;
|
|
150
164
|
}
|
|
151
165
|
|
|
152
|
-
function _renderCrossBadge(relations) {
|
|
166
|
+
function _renderCrossBadge(relations, tabType) {
|
|
153
167
|
if (!relations || relations.length === 0) return "";
|
|
154
168
|
const badges = relations.map(r => {
|
|
155
169
|
if (r.type === "based-on") {
|
|
@@ -187,8 +201,14 @@ function renderCodeGenCard(task) {
|
|
|
187
201
|
|
|
188
202
|
const progress = calcProgress(task);
|
|
189
203
|
|
|
204
|
+
const bugBadges = (task.bug_fix_details || []).map(b => {
|
|
205
|
+
const statusText = b.status === "resolved" ? "已解决" : "未解决";
|
|
206
|
+
const cls = b.status === "resolved" ? "resolved" : "unresolved";
|
|
207
|
+
return `<span class="cross-badge traces-to" title="Bug: ${esc(b.title)} (${statusText})">Bug: ${esc(b.title)} (${statusText})</span>`;
|
|
208
|
+
}).join("");
|
|
209
|
+
|
|
190
210
|
return `<div class="task-card" data-id="${task.id}" data-type="code-gen">
|
|
191
|
-
<div class="task-header" onclick="
|
|
211
|
+
<div class="task-header" onclick="loadTaskDetail('${task.id}')">
|
|
192
212
|
<div>
|
|
193
213
|
<div class="task-title">${esc(task.title)}</div>
|
|
194
214
|
<div class="task-meta">${task.id}</div>
|
|
@@ -197,11 +217,11 @@ function renderCodeGenCard(task) {
|
|
|
197
217
|
<span class="status-badge ${task.status}">${statusLabel(task.status)}</span>
|
|
198
218
|
</div>
|
|
199
219
|
</div>
|
|
220
|
+
${bugBadges ? `<div style="padding:0 20px 4px">${bugBadges}</div>` : ""}
|
|
200
221
|
<div class="phases-row">${phases}</div>
|
|
201
222
|
<div class="progress-bar-wrap"><div class="progress-bar blue ${task.status === 'failed' ? 'failed' : ''}" style="width:${progress}%"></div></div>
|
|
202
223
|
<div class="task-footer">
|
|
203
224
|
<span>${task.created_at || ''}</span>
|
|
204
|
-
<a class="log-toggle" onclick="event.stopPropagation();loadTaskDetail('${task.id}')">日志详情</a>
|
|
205
225
|
</div>
|
|
206
226
|
<div class="task-detail-content hidden" id="detail-${task.id}"></div>
|
|
207
227
|
</div>`;
|
|
@@ -225,11 +245,12 @@ function renderDocGenCard(task) {
|
|
|
225
245
|
const sketchFolder = task.sketch_folder || "无";
|
|
226
246
|
const outputDoc = task.output_doc || "未生成";
|
|
227
247
|
const imgCount = task._image_count || 0;
|
|
248
|
+
const figmaCount = task._figma_count || 0;
|
|
228
249
|
|
|
229
250
|
const progress = calcProgress(task);
|
|
230
251
|
|
|
231
252
|
return `<div class="task-card" data-id="${task.id}" data-type="doc-gen">
|
|
232
|
-
<div class="task-header" onclick="
|
|
253
|
+
<div class="task-header" onclick="loadTaskDetail('${task.id}')">
|
|
233
254
|
<div>
|
|
234
255
|
<div class="task-title">${esc(task.title)}</div>
|
|
235
256
|
<div class="task-meta">${task.id}</div>
|
|
@@ -240,13 +261,14 @@ function renderDocGenCard(task) {
|
|
|
240
261
|
</div>
|
|
241
262
|
<div class="artifact-tags">
|
|
242
263
|
<span class="artifact-tag"><span class="material-icons">article</span>${esc(reqDoc)}</span>
|
|
243
|
-
|
|
264
|
+
${imgCount > 0 ? `<span class="artifact-tag"><span class="material-icons">image</span>${imgCount} 张图片</span>` : ""}
|
|
265
|
+
${figmaCount > 0 ? `<span class="artifact-tag figma"><span class="material-icons">design_services</span>${figmaCount} 个 Figma</span>` : ""}
|
|
266
|
+
${imgCount === 0 && figmaCount === 0 ? `<span class="artifact-tag"><span class="material-icons">image</span>无图片</span>` : ""}
|
|
244
267
|
<span class="artifact-tag"><span class="material-icons">auto_awesome</span>${esc(outputDoc)}</span>
|
|
245
268
|
</div>
|
|
246
269
|
<div class="progress-bar-wrap"><div class="progress-bar amber" style="width:${progress}%"></div></div>
|
|
247
270
|
<div class="task-footer">
|
|
248
271
|
<span>${task.created_at || ''}</span>
|
|
249
|
-
<a class="log-toggle" onclick="event.stopPropagation();loadTaskDetail('${task.id}')">日志详情</a>
|
|
250
272
|
</div>
|
|
251
273
|
<div class="task-detail-content hidden" id="detail-${task.id}"></div>
|
|
252
274
|
</div>`;
|
|
@@ -283,7 +305,7 @@ function renderBugFixCard(task) {
|
|
|
283
305
|
}
|
|
284
306
|
|
|
285
307
|
return `<div class="bug-card" data-id="${task.id}" data-type="bug-fix">
|
|
286
|
-
<div class="bug-header">
|
|
308
|
+
<div class="bug-header" onclick="loadTaskDetail('${task.id}')" style="cursor:pointer">
|
|
287
309
|
<span class="material-icons bug-status-icon ${resolved ? 'resolved' : ''}">${resolved ? 'check_circle' : 'error_outline'}</span>
|
|
288
310
|
<div class="bug-info">
|
|
289
311
|
<div class="bug-title">${esc(task.title)}</div>
|
|
@@ -292,12 +314,11 @@ function renderBugFixCard(task) {
|
|
|
292
314
|
</div>
|
|
293
315
|
<span class="status-badge ${resolved ? 'resolved' : 'unresolved'}">${resolved ? '已解决' : '未解决'}</span>
|
|
294
316
|
</div>
|
|
295
|
-
<div class="phases-row" style="padding-left:
|
|
317
|
+
<div class="phases-row" style="padding-left:52px">${phases}</div>
|
|
296
318
|
<div class="progress-bar-wrap"><div class="progress-bar red" style="width:${progress}%"></div></div>
|
|
297
319
|
<div class="bug-footer">
|
|
298
320
|
<span>${task.id}</span>
|
|
299
321
|
<span>${task.created_at || ''}</span>
|
|
300
|
-
<a class="log-toggle" onclick="event.stopPropagation();loadTaskDetail('${task.id}')">日志详情</a>
|
|
301
322
|
</div>
|
|
302
323
|
<div class="task-detail-content hidden" id="detail-${task.id}"></div>
|
|
303
324
|
</div>`;
|
|
@@ -334,6 +355,12 @@ document.addEventListener("contextmenu", e => {
|
|
|
334
355
|
if (modHeader && !e.target.closest(".task-card") && !e.target.closest(".bug-card")) {
|
|
335
356
|
e.preventDefault();
|
|
336
357
|
_ctxModuleId = modHeader.dataset.moduleId;
|
|
358
|
+
_ctxModuleType = modHeader.dataset.type || "";
|
|
359
|
+
_ctxModuleTaskCount = parseInt(modHeader.dataset.taskCount || "0");
|
|
360
|
+
// doc-gen 不显示菜单,有任务的模块也不显示
|
|
361
|
+
if (_ctxModuleType === "doc-gen" || _ctxModuleTaskCount > 0) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
337
364
|
showCtx("ctx-module", e.clientX, e.clientY);
|
|
338
365
|
return;
|
|
339
366
|
}
|
|
@@ -398,6 +425,7 @@ document.querySelectorAll(".ctx-menu").forEach(menu => {
|
|
|
398
425
|
else if (action === "view-detail") viewDetail();
|
|
399
426
|
else if (action === "view-snapshot") viewSnapshot();
|
|
400
427
|
else if (action === "delete-task") deleteTask();
|
|
428
|
+
else if (action === "delete-module") deleteModule();
|
|
401
429
|
else if (action === "view-req-doc") viewReqDoc();
|
|
402
430
|
else if (action === "view-images") viewImages();
|
|
403
431
|
else if (action === "view-dev-doc") viewDevDoc();
|
|
@@ -420,6 +448,17 @@ async function deleteTask() {
|
|
|
420
448
|
else showToast("删除失败");
|
|
421
449
|
}
|
|
422
450
|
|
|
451
|
+
async function deleteModule() {
|
|
452
|
+
if (!_ctxModuleId) return;
|
|
453
|
+
if (!confirm(`确定删除模块 ${_ctxModuleId}?`)) return;
|
|
454
|
+
const res = await fetch(`/api/modules/${_ctxModuleId}`, { method: "DELETE" });
|
|
455
|
+
if (res.ok) { showToast("已删除模块"); loadTabContent(currentTab); }
|
|
456
|
+
else {
|
|
457
|
+
const data = await res.json().catch(() => ({}));
|
|
458
|
+
showToast(data.detail || "删除失败");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
423
462
|
// ── View Detail Modal ──
|
|
424
463
|
|
|
425
464
|
async function viewDetail() {
|
|
@@ -455,7 +494,7 @@ async function viewSnapshot() {
|
|
|
455
494
|
const res = await fetch(`/api/tasks/${_ctxTaskId}/snapshots/files/changes.diff`);
|
|
456
495
|
if (res.ok) {
|
|
457
496
|
const data = await res.json();
|
|
458
|
-
body.innerHTML =
|
|
497
|
+
body.innerHTML = renderDiffByFile(data.content || "");
|
|
459
498
|
} else {
|
|
460
499
|
body.innerHTML = '<div class="no-data">加载 diff 失败</div>';
|
|
461
500
|
}
|
|
@@ -480,6 +519,8 @@ async function viewFile(taskId, snapName, filepath) {
|
|
|
480
519
|
|
|
481
520
|
document.getElementById("modal-file-title").textContent = filepath;
|
|
482
521
|
document.getElementById("modal-file-body").innerHTML = renderFileContent(filepath, content);
|
|
522
|
+
_editingFilePath = null;
|
|
523
|
+
resetFileEditButtons(false);
|
|
483
524
|
openModal("modal-file");
|
|
484
525
|
}
|
|
485
526
|
|
|
@@ -490,11 +531,13 @@ async function viewReqDoc() {
|
|
|
490
531
|
const art = await fetch(`/api/tasks/${_ctxTaskId}/doc-artifacts`).then(r => r.json());
|
|
491
532
|
if (!art.requirement_doc) { showToast("无需求文档"); return; }
|
|
492
533
|
|
|
493
|
-
const res = await fetch(`/api/tasks/${_ctxTaskId}/doc-artifacts/
|
|
534
|
+
const res = await fetch(`/api/tasks/${_ctxTaskId}/doc-artifacts/prds/${art.requirement_doc}`);
|
|
494
535
|
if (!res.ok) { showToast("加载失败"); return; }
|
|
495
536
|
const data = await res.json();
|
|
496
537
|
document.getElementById("modal-file-title").textContent = art.requirement_doc;
|
|
497
538
|
document.getElementById("modal-file-body").innerHTML = `<div class="log-md-content">${renderDiffMarkers(marked.parse(data.content || ''))}</div>`;
|
|
539
|
+
_editingFilePath = null;
|
|
540
|
+
resetFileEditButtons(false);
|
|
498
541
|
openModal("modal-file");
|
|
499
542
|
}
|
|
500
543
|
|
|
@@ -510,9 +553,59 @@ async function viewDevDoc() {
|
|
|
510
553
|
const data = await res.json();
|
|
511
554
|
document.getElementById("modal-file-title").textContent = art.develop_doc;
|
|
512
555
|
document.getElementById("modal-file-body").innerHTML = `<div class="log-md-content">${renderDiffMarkers(marked.parse(data.content || ''))}</div>`;
|
|
556
|
+
_editingFilePath = { taskId: _ctxTaskId, type: "develop", path: art.develop_doc, rawContent: data.content || "" };
|
|
557
|
+
resetFileEditButtons(true);
|
|
513
558
|
openModal("modal-file");
|
|
514
559
|
}
|
|
515
560
|
|
|
561
|
+
function resetFileEditButtons(showEdit) {
|
|
562
|
+
document.getElementById("btn-edit-file").classList.toggle("hidden", !showEdit);
|
|
563
|
+
document.getElementById("btn-save-file").classList.add("hidden");
|
|
564
|
+
document.getElementById("btn-cancel-edit").classList.add("hidden");
|
|
565
|
+
document.getElementById("modal-file-body").classList.remove("hidden");
|
|
566
|
+
document.getElementById("file-editor").classList.add("hidden");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function closeModal(id) {
|
|
570
|
+
document.getElementById(id).classList.add("hidden");
|
|
571
|
+
_editingFilePath = null;
|
|
572
|
+
resetFileEditButtons(false);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function toggleFileEdit() {
|
|
576
|
+
if (!_editingFilePath) return;
|
|
577
|
+
const editor = document.getElementById("file-editor");
|
|
578
|
+
editor.value = _editingFilePath.rawContent;
|
|
579
|
+
document.getElementById("modal-file-body").classList.add("hidden");
|
|
580
|
+
editor.classList.remove("hidden");
|
|
581
|
+
document.getElementById("btn-edit-file").classList.add("hidden");
|
|
582
|
+
document.getElementById("btn-save-file").classList.remove("hidden");
|
|
583
|
+
document.getElementById("btn-cancel-edit").classList.remove("hidden");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function cancelFileEdit() {
|
|
587
|
+
resetFileEditButtons(true);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function saveFileEdit() {
|
|
591
|
+
if (!_editingFilePath) return;
|
|
592
|
+
const content = document.getElementById("file-editor").value;
|
|
593
|
+
const { taskId, type, path } = _editingFilePath;
|
|
594
|
+
const res = await fetch(`/api/tasks/${taskId}/doc-artifacts/${type}/${path}`, {
|
|
595
|
+
method: "PUT",
|
|
596
|
+
headers: { "Content-Type": "application/json" },
|
|
597
|
+
body: JSON.stringify({ content }),
|
|
598
|
+
});
|
|
599
|
+
if (res.ok) {
|
|
600
|
+
_editingFilePath.rawContent = content;
|
|
601
|
+
document.getElementById("modal-file-body").innerHTML = `<div class="log-md-content">${renderDiffMarkers(marked.parse(content))}</div>`;
|
|
602
|
+
resetFileEditButtons(true);
|
|
603
|
+
showToast("已保存");
|
|
604
|
+
} else {
|
|
605
|
+
showToast("保存失败");
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
516
609
|
// ── Doc-gen: View Sketch Images ──
|
|
517
610
|
|
|
518
611
|
async function viewImages() {
|
|
@@ -521,7 +614,7 @@ async function viewImages() {
|
|
|
521
614
|
const body = document.getElementById("modal-gallery-body");
|
|
522
615
|
|
|
523
616
|
if (!art.images || art.images.length === 0) {
|
|
524
|
-
body.innerHTML = '<div class="no-data"
|
|
617
|
+
body.innerHTML = '<div class="no-data">暂无图片</div>';
|
|
525
618
|
} else {
|
|
526
619
|
body.innerHTML = `<div class="image-grid">
|
|
527
620
|
${art.images.map(img =>
|
|
@@ -556,6 +649,49 @@ function renderDiff(content) {
|
|
|
556
649
|
return `<div class="diff-viewer">${html}</div>`;
|
|
557
650
|
}
|
|
558
651
|
|
|
652
|
+
function renderDiffByFile(content) {
|
|
653
|
+
const lines = content.split("\n");
|
|
654
|
+
const files = [];
|
|
655
|
+
let current = null;
|
|
656
|
+
for (const line of lines) {
|
|
657
|
+
if (line.startsWith("diff --git ")) {
|
|
658
|
+
const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
659
|
+
const fileName = match ? match[2] : line;
|
|
660
|
+
current = { name: fileName, lines: [] };
|
|
661
|
+
files.push(current);
|
|
662
|
+
}
|
|
663
|
+
if (current) current.lines.push(line);
|
|
664
|
+
}
|
|
665
|
+
if (files.length <= 1) return renderDiff(content);
|
|
666
|
+
|
|
667
|
+
let idx = 0;
|
|
668
|
+
const fileItems = files.map(f => {
|
|
669
|
+
const addCount = f.lines.filter(l => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
670
|
+
const delCount = f.lines.filter(l => l.startsWith("-") && !l.startsWith("---")).length;
|
|
671
|
+
return `<div class="snap-file-item" onclick="toggleDiffFile(this, 'diff-file-${idx}')">
|
|
672
|
+
<span class="material-icons">description</span>
|
|
673
|
+
<span style="flex:1">${esc(f.name)}</span>
|
|
674
|
+
<span style="color:var(--green-600);font-size:11px">+${addCount}</span>
|
|
675
|
+
<span style="color:var(--red-600);font-size:11px">-${delCount}</span>
|
|
676
|
+
<span class="material-icons" style="font-size:16px;color:var(--g400)">expand_more</span>
|
|
677
|
+
</div>
|
|
678
|
+
<div id="diff-file-${idx++}" style="display:none">${renderDiff(f.lines.join("\n"))}</div>`;
|
|
679
|
+
}).join("");
|
|
680
|
+
return fileItems;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function toggleDiffFile(item, diffId) {
|
|
684
|
+
const el = document.getElementById(diffId);
|
|
685
|
+
const icon = item.querySelector('.material-icons:last-child');
|
|
686
|
+
if (el.style.display === "none") {
|
|
687
|
+
el.style.display = "block";
|
|
688
|
+
icon.textContent = "expand_less";
|
|
689
|
+
} else {
|
|
690
|
+
el.style.display = "none";
|
|
691
|
+
icon.textContent = "expand_more";
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
559
695
|
function renderFileContent(filepath, content) {
|
|
560
696
|
const ext = filepath.split(".").pop().toLowerCase();
|
|
561
697
|
const langMap = {
|
|
@@ -577,19 +713,15 @@ function openModal(id) {
|
|
|
577
713
|
document.getElementById(id).classList.remove("hidden");
|
|
578
714
|
}
|
|
579
715
|
|
|
580
|
-
function closeModal(id) {
|
|
581
|
-
document.getElementById(id).classList.add("hidden");
|
|
582
|
-
}
|
|
583
|
-
|
|
584
716
|
document.querySelectorAll(".modal-overlay").forEach(el => {
|
|
585
|
-
el.addEventListener("
|
|
586
|
-
if (e.target === el) el.
|
|
717
|
+
el.addEventListener("mousedown", e => {
|
|
718
|
+
if (e.target === el) closeModal(el.id);
|
|
587
719
|
});
|
|
588
720
|
});
|
|
589
721
|
|
|
590
722
|
document.addEventListener("keydown", e => {
|
|
591
723
|
if (e.key === "Escape") {
|
|
592
|
-
document.querySelectorAll(".modal-overlay:not(.hidden)").forEach(el => el.
|
|
724
|
+
document.querySelectorAll(".modal-overlay:not(.hidden)").forEach(el => closeModal(el.id));
|
|
593
725
|
}
|
|
594
726
|
});
|
|
595
727
|
|
|
@@ -606,7 +738,7 @@ function esc(text) {
|
|
|
606
738
|
}
|
|
607
739
|
|
|
608
740
|
function statusLabel(status) {
|
|
609
|
-
return { running: "运行中", completed: "已完成", failed: "失败" }[status] || status;
|
|
741
|
+
return { pending: "进行中", running: "运行中", completed: "已完成", failed: "失败" }[status] || status;
|
|
610
742
|
}
|
|
611
743
|
|
|
612
744
|
function calcProgress(task) {
|
|
@@ -99,7 +99,6 @@
|
|
|
99
99
|
|
|
100
100
|
<!-- 右键菜单: code-gen 任务 -->
|
|
101
101
|
<div id="ctx-code-gen" class="ctx-menu hidden">
|
|
102
|
-
<div class="ctx-item" data-action="copy-id"><span class="material-icons">content_copy</span>拷贝 ID</div>
|
|
103
102
|
<div class="ctx-item" data-action="view-detail"><span class="material-icons">info</span>查看详情</div>
|
|
104
103
|
<div class="ctx-item" data-action="view-snapshot"><span class="material-icons">code</span>查看代码快照</div>
|
|
105
104
|
<div class="ctx-sep"></div>
|
|
@@ -109,23 +108,21 @@
|
|
|
109
108
|
<!-- 右键菜单: doc-gen 任务 -->
|
|
110
109
|
<div id="ctx-doc-gen" class="ctx-menu hidden">
|
|
111
110
|
<div class="ctx-item" data-action="view-req-doc"><span class="material-icons">article</span>查看需求文档</div>
|
|
112
|
-
<div class="ctx-item" data-action="view-images"><span class="material-icons">image</span
|
|
111
|
+
<div class="ctx-item" data-action="view-images"><span class="material-icons">image</span>查看图片</div>
|
|
113
112
|
<div class="ctx-item" data-action="view-dev-doc"><span class="material-icons">auto_awesome</span>查看开发文档</div>
|
|
114
|
-
<div class="ctx-sep"></div>
|
|
115
|
-
<div class="ctx-item" data-action="copy-id"><span class="material-icons">content_copy</span>拷贝 ID</div>
|
|
116
113
|
</div>
|
|
117
114
|
|
|
118
115
|
<!-- 右键菜单: bug-fix 任务 -->
|
|
119
116
|
<div id="ctx-bug-fix" class="ctx-menu hidden">
|
|
120
117
|
<div class="ctx-item" data-action="view-detail"><span class="material-icons">info</span>查看详情</div>
|
|
121
|
-
<div class="ctx-item" data-action="
|
|
118
|
+
<div class="ctx-item" data-action="view-snapshot"><span class="material-icons">code</span>查看代码快照</div>
|
|
122
119
|
<div class="ctx-sep"></div>
|
|
123
120
|
<div class="ctx-item danger" data-action="delete-task"><span class="material-icons">delete</span>删除任务</div>
|
|
124
121
|
</div>
|
|
125
122
|
|
|
126
123
|
<!-- 右键菜单: 模块 -->
|
|
127
124
|
<div id="ctx-module" class="ctx-menu hidden">
|
|
128
|
-
<div class="ctx-item" data-action="
|
|
125
|
+
<div class="ctx-item danger" data-action="delete-module"><span class="material-icons">delete</span>删除模块</div>
|
|
129
126
|
</div>
|
|
130
127
|
|
|
131
128
|
<!-- 通用弹框: 详情 -->
|
|
@@ -150,14 +147,20 @@
|
|
|
150
147
|
</div>
|
|
151
148
|
</div>
|
|
152
149
|
|
|
153
|
-
<!-- 通用弹框:
|
|
150
|
+
<!-- 通用弹框: 文件查看/编辑 -->
|
|
154
151
|
<div id="modal-file" class="modal-overlay hidden">
|
|
155
152
|
<div class="modal-box wide">
|
|
156
153
|
<div class="modal-head">
|
|
157
154
|
<span class="modal-title" id="modal-file-title">文件内容</span>
|
|
158
|
-
<
|
|
155
|
+
<div class="modal-head-right">
|
|
156
|
+
<button class="head-btn" id="btn-edit-file" onclick="toggleFileEdit()"><span class="material-icons" style="font-size:16px">edit</span>编辑</button>
|
|
157
|
+
<button class="head-btn head-btn-save hidden" id="btn-save-file" onclick="saveFileEdit()"><span class="material-icons" style="font-size:16px">save</span>保存</button>
|
|
158
|
+
<button class="head-btn head-btn-cancel hidden" id="btn-cancel-edit" onclick="cancelFileEdit()"><span class="material-icons" style="font-size:16px">close</span>取消</button>
|
|
159
|
+
<span class="material-icons modal-close" onclick="closeModal('modal-file')">close</span>
|
|
160
|
+
</div>
|
|
159
161
|
</div>
|
|
160
162
|
<div class="modal-body" id="modal-file-body"></div>
|
|
163
|
+
<textarea class="file-editor hidden" id="file-editor"></textarea>
|
|
161
164
|
</div>
|
|
162
165
|
</div>
|
|
163
166
|
|
|
@@ -165,7 +168,7 @@
|
|
|
165
168
|
<div id="modal-gallery" class="modal-overlay hidden">
|
|
166
169
|
<div class="modal-box wide">
|
|
167
170
|
<div class="modal-head">
|
|
168
|
-
<span class="modal-title"
|
|
171
|
+
<span class="modal-title">图片</span>
|
|
169
172
|
<span class="material-icons modal-close" onclick="closeModal('modal-gallery')">close</span>
|
|
170
173
|
</div>
|
|
171
174
|
<div class="modal-body" id="modal-gallery-body"></div>
|
|
@@ -116,7 +116,7 @@ body {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
.nav-item:hover {
|
|
119
|
-
background:
|
|
119
|
+
background: #1e2230;
|
|
120
120
|
color: var(--g200);
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -297,7 +297,7 @@ body {
|
|
|
297
297
|
display: flex;
|
|
298
298
|
align-items: center;
|
|
299
299
|
justify-content: space-between;
|
|
300
|
-
padding:
|
|
300
|
+
padding: 10px 20px;
|
|
301
301
|
background: var(--surface);
|
|
302
302
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
303
303
|
cursor: pointer;
|
|
@@ -319,7 +319,7 @@ body {
|
|
|
319
319
|
.module-expand.open { transform: rotate(180deg); }
|
|
320
320
|
|
|
321
321
|
.module-name {
|
|
322
|
-
font-size:
|
|
322
|
+
font-size: 15px; font-weight: 600; color: var(--g900);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
.module-id {
|
|
@@ -333,7 +333,7 @@ body {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
.module-count {
|
|
336
|
-
font-size:
|
|
336
|
+
font-size: 13px; color: var(--g500);
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
/* ── Module Drawer ── */
|
|
@@ -393,7 +393,7 @@ body {
|
|
|
393
393
|
}
|
|
394
394
|
|
|
395
395
|
.chain-title {
|
|
396
|
-
font-size:
|
|
396
|
+
font-size: 13px; font-weight: 500; color: var(--g700);
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
.chain-arrow {
|
|
@@ -457,10 +457,10 @@ body {
|
|
|
457
457
|
transition: background .15s;
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
.task-card:hover { background:
|
|
460
|
+
.task-card:hover { background: #f4f6f9; }
|
|
461
461
|
|
|
462
462
|
.task-header {
|
|
463
|
-
padding:
|
|
463
|
+
padding: 10px 20px;
|
|
464
464
|
display: flex;
|
|
465
465
|
align-items: center;
|
|
466
466
|
justify-content: space-between;
|
|
@@ -468,12 +468,12 @@ body {
|
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
.task-title {
|
|
471
|
-
font-size:
|
|
471
|
+
font-size: 14px; font-weight: 600; color: var(--g900);
|
|
472
472
|
}
|
|
473
473
|
|
|
474
474
|
.task-meta {
|
|
475
|
-
font-size:
|
|
476
|
-
margin-top:
|
|
475
|
+
font-size: 12px; color: var(--g500);
|
|
476
|
+
margin-top: 2px;
|
|
477
477
|
display: flex; align-items: center; gap: 6px;
|
|
478
478
|
}
|
|
479
479
|
|
|
@@ -484,15 +484,15 @@ body {
|
|
|
484
484
|
/* ── Phase Chips ── */
|
|
485
485
|
|
|
486
486
|
.phases-row {
|
|
487
|
-
padding:
|
|
488
|
-
display: flex; flex-wrap: wrap; gap:
|
|
487
|
+
padding: 6px 20px 10px;
|
|
488
|
+
display: flex; flex-wrap: wrap; gap: 5px;
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
.phase-chip {
|
|
492
492
|
display: inline-flex; align-items: center;
|
|
493
493
|
padding: 3px 10px;
|
|
494
494
|
border-radius: 14px;
|
|
495
|
-
font-size:
|
|
495
|
+
font-size: 12px; font-weight: 500;
|
|
496
496
|
white-space: nowrap;
|
|
497
497
|
transition: transform .12s;
|
|
498
498
|
}
|
|
@@ -508,12 +508,13 @@ body {
|
|
|
508
508
|
/* ── Status Badge ── */
|
|
509
509
|
|
|
510
510
|
.status-badge {
|
|
511
|
-
font-size:
|
|
511
|
+
font-size: 12px;
|
|
512
512
|
padding: 3px 12px;
|
|
513
513
|
border-radius: 12px;
|
|
514
514
|
font-weight: 600;
|
|
515
515
|
letter-spacing: .2px;
|
|
516
516
|
}
|
|
517
|
+
.status-badge.pending { background: var(--g100); color: var(--g600); }
|
|
517
518
|
.status-badge.running { background: var(--blue-50); color: var(--blue-600); }
|
|
518
519
|
.status-badge.completed { background: var(--green-100); color: var(--green-700); }
|
|
519
520
|
.status-badge.failed { background: var(--red-100); color: var(--red-600); }
|
|
@@ -575,18 +576,25 @@ body {
|
|
|
575
576
|
|
|
576
577
|
.artifact-tag .material-icons { font-size: 14px; }
|
|
577
578
|
|
|
579
|
+
.artifact-tag.figma {
|
|
580
|
+
background: #f0e6ff;
|
|
581
|
+
color: #7c3aed;
|
|
582
|
+
border-color: #e0d0ff;
|
|
583
|
+
}
|
|
584
|
+
|
|
578
585
|
/* ── Bug Card ── */
|
|
579
586
|
|
|
580
587
|
.bug-card {
|
|
581
588
|
border-top: 1px solid var(--g100);
|
|
582
|
-
padding:
|
|
589
|
+
padding: 0;
|
|
583
590
|
transition: background .15s;
|
|
584
591
|
}
|
|
585
592
|
|
|
586
|
-
.bug-card:hover { background:
|
|
593
|
+
.bug-card:hover { background: #f4f6f9; }
|
|
587
594
|
|
|
588
595
|
.bug-header {
|
|
589
596
|
display: flex; align-items: flex-start; gap: 12px;
|
|
597
|
+
padding: 10px 20px;
|
|
590
598
|
}
|
|
591
599
|
|
|
592
600
|
.bug-status-icon {
|
|
@@ -598,12 +606,12 @@ body {
|
|
|
598
606
|
.bug-info { flex: 1; min-width: 0; }
|
|
599
607
|
|
|
600
608
|
.bug-title {
|
|
601
|
-
font-size:
|
|
609
|
+
font-size: 14px; font-weight: 600; color: var(--g900);
|
|
602
610
|
}
|
|
603
611
|
|
|
604
612
|
.bug-desc {
|
|
605
|
-
font-size:
|
|
606
|
-
margin-top:
|
|
613
|
+
font-size: 13px; color: var(--g600);
|
|
614
|
+
margin-top: 3px;
|
|
607
615
|
line-height: 1.5;
|
|
608
616
|
}
|
|
609
617
|
|
|
@@ -625,6 +633,7 @@ body {
|
|
|
625
633
|
|
|
626
634
|
.bug-footer {
|
|
627
635
|
margin-top: 8px;
|
|
636
|
+
padding: 0 20px 12px;
|
|
628
637
|
font-size: 11px; color: var(--g500);
|
|
629
638
|
display: flex; gap: 14px;
|
|
630
639
|
}
|
|
@@ -642,7 +651,7 @@ body {
|
|
|
642
651
|
border: 1px solid var(--g200);
|
|
643
652
|
}
|
|
644
653
|
|
|
645
|
-
.ctx-menu.hidden { display: none; }
|
|
654
|
+
.ctx-menu.hidden, .hidden, button.hidden { display: none !important; }
|
|
646
655
|
|
|
647
656
|
.ctx-sep {
|
|
648
657
|
height: 1px;
|
|
@@ -714,6 +723,9 @@ body {
|
|
|
714
723
|
padding: 16px 20px;
|
|
715
724
|
border-bottom: 1px solid var(--g100);
|
|
716
725
|
}
|
|
726
|
+
.modal-head-right {
|
|
727
|
+
display: flex; align-items: center; gap: 8px;
|
|
728
|
+
}
|
|
717
729
|
|
|
718
730
|
.modal-title {
|
|
719
731
|
font-size: 14px; font-weight: 600; color: var(--g900);
|
|
@@ -1032,3 +1044,35 @@ body {
|
|
|
1032
1044
|
.panel-header { padding: 16px 20px 12px; }
|
|
1033
1045
|
.panel-content { padding: 12px 16px 32px; }
|
|
1034
1046
|
}
|
|
1047
|
+
|
|
1048
|
+
/* ── File Editor ── */
|
|
1049
|
+
|
|
1050
|
+
.head-btn {
|
|
1051
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1052
|
+
padding: 4px 12px; border-radius: 6px;
|
|
1053
|
+
background: rgba(255,255,255,0.06);
|
|
1054
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
1055
|
+
color: #b0b8c8; font-size: 13px; cursor: pointer;
|
|
1056
|
+
transition: all 0.15s;
|
|
1057
|
+
}
|
|
1058
|
+
.head-btn:hover { background: rgba(255,255,255,0.1); color: #e0e4ec; }
|
|
1059
|
+
.head-btn-save { color: #34d399; border-color: rgba(52,211,153,0.3); }
|
|
1060
|
+
.head-btn-save:hover { background: rgba(52,211,153,0.1); }
|
|
1061
|
+
.head-btn-cancel { color: #f87171; border-color: rgba(248,113,113,0.3); }
|
|
1062
|
+
.head-btn-cancel:hover { background: rgba(248,113,113,0.1); }
|
|
1063
|
+
.file-editor {
|
|
1064
|
+
width: 100%;
|
|
1065
|
+
min-height: 400px;
|
|
1066
|
+
padding: 16px;
|
|
1067
|
+
background: #0d1117;
|
|
1068
|
+
color: #c9d1d9;
|
|
1069
|
+
border: 1px solid #30363d;
|
|
1070
|
+
border-radius: 6px;
|
|
1071
|
+
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
1072
|
+
font-size: 13px;
|
|
1073
|
+
line-height: 1.6;
|
|
1074
|
+
resize: vertical;
|
|
1075
|
+
outline: none;
|
|
1076
|
+
tab-size: 2;
|
|
1077
|
+
}
|
|
1078
|
+
.file-editor:focus { border-color: #58a6ff; }
|