@aicupa/plugin-todo-dependency 1.0.1 → 1.0.3
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/package.json +1 -1
- package/service.js +31 -0
- package/view/index.html +131 -47
package/package.json
CHANGED
package/service.js
CHANGED
|
@@ -78,6 +78,37 @@ module.exports = (api) => {
|
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
|
|
81
|
+
async toggleDone({ todoId, done, filePath }) {
|
|
82
|
+
try {
|
|
83
|
+
const content = await api.readFile(filePath)
|
|
84
|
+
const data = JSON.parse(content)
|
|
85
|
+
const todotree = data.todotree
|
|
86
|
+
|
|
87
|
+
function findAndUpdate(nodes) {
|
|
88
|
+
for (const node of nodes) {
|
|
89
|
+
if (node.todo && node.todo.id === todoId) {
|
|
90
|
+
node.todo.done = done
|
|
91
|
+
if (done) {
|
|
92
|
+
node.todo.doneAt = Date.now()
|
|
93
|
+
} else {
|
|
94
|
+
delete node.todo.doneAt
|
|
95
|
+
}
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
if (node.children?.length && findAndUpdate(node.children)) return true
|
|
99
|
+
}
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
findAndUpdate(todotree.tree)
|
|
104
|
+
await api.store('todotree', todotree, filePath)
|
|
105
|
+
await api.reload(filePath)
|
|
106
|
+
return { ok: true }
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { ok: false, error: e.message }
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
81
112
|
async scanAllDeps({ filePath }) {
|
|
82
113
|
try {
|
|
83
114
|
const data = await api.getTree(filePath)
|
package/view/index.html
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
color: #333;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
body.dark #graphUI { color: #
|
|
19
|
+
body.dark #graphUI { color: #cccccc; }
|
|
20
20
|
|
|
21
21
|
.toolbar {
|
|
22
22
|
padding: 10px 16px; background: #fff; border-bottom: 1px solid #e8e8e8;
|
|
23
23
|
display: flex; align-items: center; gap: 10px; flex-shrink: 0;
|
|
24
24
|
}
|
|
25
|
-
.dark .toolbar { background: #
|
|
25
|
+
.dark .toolbar { background: #252526; border-bottom-color: #3c3c3c; }
|
|
26
26
|
.toolbar-title { font-size: 14px; font-weight: 600; flex: 1; }
|
|
27
27
|
|
|
28
28
|
.btn {
|
|
@@ -32,17 +32,17 @@
|
|
|
32
32
|
.btn:active { transform: scale(0.97); }
|
|
33
33
|
.btn-ghost { background: transparent; color: #666; }
|
|
34
34
|
.btn-ghost:hover { background: #f0f0f0; }
|
|
35
|
-
.dark .btn-ghost { color: #
|
|
36
|
-
.dark .btn-ghost:hover { background: #
|
|
35
|
+
.dark .btn-ghost { color: #9d9d9d; }
|
|
36
|
+
.dark .btn-ghost:hover { background: #3c3c3c; }
|
|
37
37
|
|
|
38
38
|
.warning {
|
|
39
39
|
background: #fffbe6; border-bottom: 1px solid #ffe58f; color: #ad6800;
|
|
40
40
|
padding: 6px 16px; font-size: 12px; flex-shrink: 0;
|
|
41
41
|
}
|
|
42
|
-
.dark .warning { background: #
|
|
42
|
+
.dark .warning { background: #3a3000; border-bottom-color: #554400; color: #ffc53d; }
|
|
43
43
|
|
|
44
44
|
#graphArea { flex: 1; overflow: auto; position: relative; background: #f7f8fa; }
|
|
45
|
-
.dark #graphArea { background: #
|
|
45
|
+
.dark #graphArea { background: #1e1e1e; }
|
|
46
46
|
#graphContainer { position: relative; min-width: 100%; min-height: 100%; }
|
|
47
47
|
|
|
48
48
|
.node {
|
|
@@ -51,24 +51,34 @@
|
|
|
51
51
|
padding: 10px 12px; cursor: default;
|
|
52
52
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
|
53
53
|
transition: box-shadow 0.2s, transform 0.15s;
|
|
54
|
+
display: flex; align-items: flex-start; gap: 10px;
|
|
54
55
|
}
|
|
55
56
|
.node:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); transform: translateY(-1px); }
|
|
56
|
-
.dark .node { background: #
|
|
57
|
+
.dark .node { background: #2d2d2d; border-color: #3c3c3c; box-shadow: 0 1px 4px rgba(0,0,0,0.3); }
|
|
57
58
|
.node.done { opacity: 0.55; }
|
|
58
|
-
.node.done .node-content { text-decoration: line-through; }
|
|
59
|
+
.node.done .node-content { text-decoration: line-through; color: #999; }
|
|
59
60
|
.node.not-found { border-style: dashed; opacity: 0.65; }
|
|
60
61
|
|
|
61
|
-
.node-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
.node-check {
|
|
63
|
+
width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0;
|
|
64
|
+
border: 2px solid #d9d9d9; background: #fff; cursor: pointer;
|
|
65
|
+
display: flex; align-items: center; justify-content: center;
|
|
66
|
+
transition: all 0.15s; margin-top: 1px;
|
|
66
67
|
}
|
|
67
|
-
.node-
|
|
68
|
-
.
|
|
69
|
-
.
|
|
70
|
-
.
|
|
71
|
-
|
|
68
|
+
.node-check:hover { border-color: #52c41a; background: #f6ffed; }
|
|
69
|
+
.node-check:active { transform: scale(0.9); }
|
|
70
|
+
.node-check.checked { border-color: #52c41a; background: #52c41a; }
|
|
71
|
+
.node-check.checked::after {
|
|
72
|
+
content: ''; display: block; width: 4px; height: 7px;
|
|
73
|
+
border: solid #fff; border-width: 0 1.5px 1.5px 0;
|
|
74
|
+
transform: rotate(45deg) translate(-0.5px, -0.5px);
|
|
75
|
+
}
|
|
76
|
+
.dark .node-check { border-color: #555; background: #2d2d2d; }
|
|
77
|
+
.dark .node-check:hover { border-color: #73d13d; background: #1e3a1e; }
|
|
78
|
+
.dark .node-check.checked { border-color: #73d13d; background: #73d13d; }
|
|
79
|
+
.dark .node-check.checked::after { border-color: #2d2d2d; }
|
|
80
|
+
|
|
81
|
+
.node-body { flex: 1; min-width: 0; }
|
|
72
82
|
|
|
73
83
|
.node-content {
|
|
74
84
|
font-size: 12px; line-height: 1.5; word-break: break-word;
|
|
@@ -76,16 +86,25 @@
|
|
|
76
86
|
-webkit-box-orient: vertical; overflow: hidden;
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
.node-badge {
|
|
90
|
+
display: inline-block; font-size: 10px; padding: 1px 6px;
|
|
91
|
+
border-radius: 10px; margin-top: 5px;
|
|
92
|
+
}
|
|
93
|
+
.badge-pending { background: #fff7e6; color: #d46b08; }
|
|
94
|
+
.badge-done { background: #f6ffed; color: #389e0d; }
|
|
95
|
+
.badge-blocked { background: #fff1f0; color: #cf1322; }
|
|
96
|
+
.dark .badge-pending { background: #3a3000; color: #ffc53d; }
|
|
97
|
+
.dark .badge-done { background: #1e3a1e; color: #73d13d; }
|
|
98
|
+
.dark .badge-blocked { background: #3a1a1a; color: #ff4d4f; }
|
|
99
|
+
|
|
100
|
+
.node.blocked { border-style: dashed; opacity: 0.7; }
|
|
101
|
+
.dark .node.blocked { opacity: 0.7; }
|
|
102
|
+
|
|
79
103
|
.level-default { border-color: #1890ff; }
|
|
80
104
|
.level-secondary { border-color: #bbb; }
|
|
81
105
|
.level-success { border-color: #52c41a; }
|
|
82
106
|
.level-warning { border-color: #faad14; }
|
|
83
107
|
.level-danger { border-color: #ff4d4f; }
|
|
84
|
-
.level-default .node-id { background: #1890ff; }
|
|
85
|
-
.level-secondary .node-id { background: #999; }
|
|
86
|
-
.level-success .node-id { background: #52c41a; }
|
|
87
|
-
.level-warning .node-id { background: #e8a100; }
|
|
88
|
-
.level-danger .node-id { background: #ff4d4f; }
|
|
89
108
|
|
|
90
109
|
#edgeSvg { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
91
110
|
|
|
@@ -133,14 +152,14 @@
|
|
|
133
152
|
title: '依赖图谱', refresh: '刷新',
|
|
134
153
|
emptyTitle: '暂无依赖关系',
|
|
135
154
|
emptyHint: '右键 Todo 节点,选择「设置依赖」<br>即可建立待办之间的依赖关系',
|
|
136
|
-
done: '已完成', pending: '待完成', notFound: '未找到该 Todo',
|
|
155
|
+
done: '已完成', pending: '待完成', blocked: '阻塞中', notFound: '未找到该 Todo',
|
|
137
156
|
cycleWarning: '检测到循环依赖,部分节点的层级可能不准确',
|
|
138
157
|
},
|
|
139
158
|
en: {
|
|
140
159
|
title: 'Dependency Graph', refresh: 'Refresh',
|
|
141
160
|
emptyTitle: 'No dependencies yet',
|
|
142
161
|
emptyHint: 'Right-click a Todo and select "Set Dependencies"<br>to create dependency relationships',
|
|
143
|
-
done: 'Done', pending: 'Pending', notFound: 'Todo not found',
|
|
162
|
+
done: 'Done', pending: 'Pending', blocked: 'Blocked', notFound: 'Todo not found',
|
|
144
163
|
cycleWarning: 'Cycle detected — some nodes may be positioned incorrectly',
|
|
145
164
|
},
|
|
146
165
|
}
|
|
@@ -294,32 +313,73 @@
|
|
|
294
313
|
return el
|
|
295
314
|
}
|
|
296
315
|
|
|
316
|
+
async function toggleTodoDone(todoId, done) {
|
|
317
|
+
try {
|
|
318
|
+
await callPlugin('toggleDone', { todoId, done, filePath: currentFilePath })
|
|
319
|
+
} catch {}
|
|
320
|
+
autoScan()
|
|
321
|
+
}
|
|
322
|
+
|
|
297
323
|
function renderGraph(nodeIds, edges, positions, todoData, layout) {
|
|
298
324
|
const container = document.getElementById('graphContainer')
|
|
299
325
|
container.innerHTML = ''
|
|
300
326
|
container.style.width = layout.width + 'px'
|
|
301
327
|
container.style.height = layout.height + 'px'
|
|
302
328
|
|
|
303
|
-
|
|
304
|
-
const
|
|
329
|
+
// Build dependency map: nodeId -> [depIds]
|
|
330
|
+
const depsOf = {}
|
|
331
|
+
for (const [from, to] of edges) {
|
|
332
|
+
if (!depsOf[to]) depsOf[to] = []
|
|
333
|
+
depsOf[to].push(from)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isBlocked(id) {
|
|
337
|
+
const deps = depsOf[id]
|
|
338
|
+
if (!deps?.length) return false
|
|
339
|
+
const todo = todoData[id]
|
|
340
|
+
if (todo?.done) return false
|
|
341
|
+
return deps.some(depId => {
|
|
342
|
+
const dep = todoData[depId]
|
|
343
|
+
return !dep || !dep.done
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const edgeColor = isDark ? '#555' : '#c8c8d0'
|
|
348
|
+
const blockedEdgeColor = isDark ? '#5c2020' : '#ffccc7'
|
|
349
|
+
const arrowColor = isDark ? '#777' : '#aaa'
|
|
350
|
+
const blockedArrowColor = isDark ? '#ff4d4f' : '#ff7875'
|
|
305
351
|
const svg = svgEl('svg', { id: 'edgeSvg', width: layout.width, height: layout.height })
|
|
306
352
|
const defs = svgEl('defs', {})
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
353
|
+
function addMarker(id, color) {
|
|
354
|
+
const m = svgEl('marker', {
|
|
355
|
+
id, markerWidth: 8, markerHeight: 6,
|
|
356
|
+
refX: 4, refY: 3, orient: 'auto-start-reverse', markerUnits: 'userSpaceOnUse',
|
|
357
|
+
})
|
|
358
|
+
m.appendChild(svgEl('polygon', { points: '0 0,8 3,0 6', fill: color }))
|
|
359
|
+
defs.appendChild(m)
|
|
360
|
+
}
|
|
361
|
+
addMarker('arrow', arrowColor)
|
|
362
|
+
addMarker('arrow-blocked', blockedArrowColor)
|
|
363
|
+
svg.appendChild(defs)
|
|
313
364
|
|
|
365
|
+
const ARROW_H = 6
|
|
314
366
|
for (const [from, to] of edges) {
|
|
315
367
|
const p1 = positions[from], p2 = positions[to]
|
|
316
368
|
if (!p1 || !p2) continue
|
|
317
369
|
const x1 = p1.x + NODE_W / 2, y1 = p1.y + NODE_H
|
|
318
|
-
const x2 = p2.x + NODE_W / 2, y2 = p2.y
|
|
319
|
-
const
|
|
370
|
+
const x2 = p2.x + NODE_W / 2, y2 = p2.y - ARROW_H
|
|
371
|
+
const dy = Math.abs(y2 - y1)
|
|
372
|
+
const cp = Math.max(dy * 0.4, 30)
|
|
373
|
+
const fromDone = todoData[from]?.done
|
|
374
|
+
const toBlocked = isBlocked(to)
|
|
375
|
+
const isBlockingEdge = toBlocked && !fromDone
|
|
320
376
|
svg.appendChild(svgEl('path', {
|
|
321
377
|
d: `M${x1},${y1} C${x1},${y1 + cp} ${x2},${y2 - cp} ${x2},${y2}`,
|
|
322
|
-
fill: 'none',
|
|
378
|
+
fill: 'none',
|
|
379
|
+
stroke: isBlockingEdge ? blockedEdgeColor : edgeColor,
|
|
380
|
+
'stroke-width': 1.5,
|
|
381
|
+
'stroke-dasharray': isBlockingEdge ? '6,3' : 'none',
|
|
382
|
+
'marker-end': isBlockingEdge ? 'url(#arrow-blocked)' : 'url(#arrow)',
|
|
323
383
|
}))
|
|
324
384
|
}
|
|
325
385
|
container.appendChild(svg)
|
|
@@ -327,27 +387,51 @@
|
|
|
327
387
|
for (const id of nodeIds) {
|
|
328
388
|
const pos = positions[id]; if (!pos) continue
|
|
329
389
|
const todo = todoData[id]
|
|
390
|
+
const blocked = isBlocked(id)
|
|
330
391
|
const el = document.createElement('div'); el.className = 'node'
|
|
331
392
|
if (todo) {
|
|
332
393
|
el.classList.add('level-' + (todo.level || 'default'))
|
|
333
394
|
if (todo.done) el.classList.add('done')
|
|
395
|
+
else if (blocked) el.classList.add('blocked')
|
|
334
396
|
} else el.classList.add('not-found')
|
|
335
397
|
el.style.cssText = `left:${pos.x}px;top:${pos.y}px;width:${NODE_W}px;`
|
|
336
398
|
|
|
337
|
-
const header = document.createElement('div'); header.className = 'node-header'
|
|
338
|
-
const badge = document.createElement('span'); badge.className = 'node-id'
|
|
339
|
-
badge.textContent = '#' + id; header.appendChild(badge)
|
|
340
399
|
if (todo) {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
400
|
+
const check = document.createElement('div')
|
|
401
|
+
check.className = 'node-check' + (todo.done ? ' checked' : '')
|
|
402
|
+
check.addEventListener('click', e => {
|
|
403
|
+
e.stopPropagation()
|
|
404
|
+
toggleTodoDone(id, !todo.done)
|
|
405
|
+
})
|
|
406
|
+
el.appendChild(check)
|
|
345
407
|
}
|
|
346
|
-
|
|
408
|
+
|
|
409
|
+
const body = document.createElement('div'); body.className = 'node-body'
|
|
347
410
|
const content = document.createElement('div'); content.className = 'node-content'
|
|
348
|
-
if (todo)
|
|
349
|
-
|
|
350
|
-
|
|
411
|
+
if (todo) {
|
|
412
|
+
content.textContent = todo.content
|
|
413
|
+
} else {
|
|
414
|
+
content.textContent = t.notFound
|
|
415
|
+
content.style.color = '#ff4d4f'; content.style.fontStyle = 'italic'
|
|
416
|
+
}
|
|
417
|
+
body.appendChild(content)
|
|
418
|
+
|
|
419
|
+
if (todo) {
|
|
420
|
+
const sb = document.createElement('span')
|
|
421
|
+
if (todo.done) {
|
|
422
|
+
sb.className = 'node-badge badge-done'
|
|
423
|
+
sb.textContent = t.done
|
|
424
|
+
} else if (blocked) {
|
|
425
|
+
sb.className = 'node-badge badge-blocked'
|
|
426
|
+
sb.textContent = t.blocked
|
|
427
|
+
} else {
|
|
428
|
+
sb.className = 'node-badge badge-pending'
|
|
429
|
+
sb.textContent = t.pending
|
|
430
|
+
}
|
|
431
|
+
body.appendChild(sb)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
el.appendChild(body); container.appendChild(el)
|
|
351
435
|
}
|
|
352
436
|
}
|
|
353
437
|
|