@minonl/dia 0.0.14 → 0.0.15
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/dist/cli.js +5 -2
- package/docs/design/graph-web-mockup.html +643 -133
- package/docs/design/graph-web.md +142 -43
- package/docs/index.md +1 -1
- package/docs/status.md +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1899,6 +1899,9 @@ function assertInside(base, p) {
|
|
|
1899
1899
|
return abs;
|
|
1900
1900
|
}
|
|
1901
1901
|
var TREE_SKIP = new Set([".git", "node_modules", ".data"]);
|
|
1902
|
+
function copyFilter(base) {
|
|
1903
|
+
return (p) => !relative3(base, p).split(sep4).some((s) => TREE_SKIP.has(s));
|
|
1904
|
+
}
|
|
1902
1905
|
function walkFiles(root) {
|
|
1903
1906
|
const out = [];
|
|
1904
1907
|
(function rec(dir) {
|
|
@@ -2109,7 +2112,7 @@ function install(from, opts = {}) {
|
|
|
2109
2112
|
if (!alreadyInstalled || hashTree(src.dir) !== hashTree(target)) {
|
|
2110
2113
|
rmSync3(target, { recursive: true, force: true });
|
|
2111
2114
|
mkdirSync7(target, { recursive: true });
|
|
2112
|
-
cpSync(src.dir, target, { recursive: true, dereference: true, filter: (
|
|
2115
|
+
cpSync(src.dir, target, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
|
|
2113
2116
|
}
|
|
2114
2117
|
const rendered = renderContent(src.dir, target, m) ?? planned;
|
|
2115
2118
|
const appliedConfig = apply(m.config ?? []);
|
|
@@ -2881,7 +2884,7 @@ function install2(from, opts = {}) {
|
|
|
2881
2884
|
const staging = join13(idDir, `.staging-${process.pid}-${Date.now().toString(36)}`);
|
|
2882
2885
|
rmSync4(staging, { recursive: true, force: true });
|
|
2883
2886
|
mkdirSync10(staging, { recursive: true });
|
|
2884
|
-
cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: (
|
|
2887
|
+
cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
|
|
2885
2888
|
const stagingHash = hashTree2(staging);
|
|
2886
2889
|
let reuseTarget = false;
|
|
2887
2890
|
if (existsSync11(target)) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>dia graph ·
|
|
6
|
+
<title>dia graph · 视觉设计稿 v0.3</title>
|
|
7
7
|
<style>
|
|
8
8
|
:root{
|
|
9
9
|
--bg:#f6f7f9; --panel:#fff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb;
|
|
@@ -16,36 +16,71 @@
|
|
|
16
16
|
}
|
|
17
17
|
*{box-sizing:border-box}
|
|
18
18
|
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
19
|
-
header{display:flex;align-items:center;gap:16px;padding:
|
|
19
|
+
header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);background:var(--panel);flex-wrap:wrap}
|
|
20
20
|
header h1{font-size:17px;margin:0;letter-spacing:.2px}
|
|
21
21
|
header .sub{color:var(--muted);font-size:12px}
|
|
22
22
|
header .spacer{flex:1}
|
|
23
|
+
.tabs{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
|
|
24
|
+
.tabs button{border:0;background:transparent;color:var(--muted);padding:6px 12px;font:inherit;font-size:12px;cursor:pointer}
|
|
25
|
+
.tabs button.on{background:var(--blue);color:#fff}
|
|
23
26
|
.pill{border:1px solid var(--line);border-radius:999px;padding:4px 10px;font-size:12px;color:var(--muted);background:var(--bg)}
|
|
24
|
-
main{display:grid;grid-template-columns:minmax(0,1fr)
|
|
27
|
+
main{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;padding:18px;max-width:1480px;margin:0 auto}
|
|
25
28
|
.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
|
|
26
29
|
.toolbar{display:flex;gap:8px;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap}
|
|
27
30
|
.seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
|
|
28
31
|
.seg button{border:0;background:transparent;color:var(--muted);padding:6px 10px;font:inherit;font-size:12px;cursor:pointer}
|
|
29
32
|
.seg button.on{background:var(--blue);color:#fff}
|
|
33
|
+
.seg button:disabled{opacity:.35;cursor:not-allowed}
|
|
34
|
+
.win-select{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 8px;font:inherit;font-size:12px}
|
|
35
|
+
.search{position:relative}
|
|
36
|
+
.search input{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 10px;font:inherit;font-size:12px;width:210px}
|
|
37
|
+
.search .q-list{position:absolute;left:0;top:34px;z-index:20;width:280px;max-height:250px;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);display:none}
|
|
38
|
+
.search .q-list.open{display:block}
|
|
39
|
+
.q-item{padding:6px 10px;font-size:12px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
|
|
40
|
+
.q-item:hover,.q-item.active{background:color-mix(in srgb,var(--blue) 18%,transparent)}
|
|
41
|
+
.q-item .k{color:var(--muted)}
|
|
42
|
+
.q-item .q-prio{font-style:normal;font-size:10px;color:var(--muted);margin-left:6px}
|
|
43
|
+
.q-head{padding:6px 10px;font-size:10px;color:var(--muted);border-bottom:1px solid var(--line)}
|
|
44
|
+
.q-empty{padding:10px;font-size:12px;color:var(--muted)}
|
|
45
|
+
.node.hit{outline:2px solid var(--blue);outline-offset:1px}
|
|
46
|
+
.node.dim{opacity:.22}
|
|
47
|
+
.focus-chip{display:none;align-items:center;gap:4px;border:1px solid var(--blue);color:var(--blue);border-radius:999px;padding:3px 4px 3px 10px;font-size:11px}
|
|
48
|
+
.focus-chip.on{display:inline-flex}
|
|
49
|
+
.focus-chip button{border:0;background:transparent;color:var(--blue);cursor:pointer;font:inherit;font-size:11px;padding:0 6px}
|
|
50
|
+
.focus-chip .fd{border:0;background:transparent;color:var(--blue);font:inherit;font-size:11px}
|
|
30
51
|
.toolbar .hint{margin-left:auto;color:var(--muted);font-size:12px}
|
|
31
|
-
.canvas{position:relative;height:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
svg.edges
|
|
35
|
-
|
|
36
|
-
svg.edges line.claims{stroke:var(--cyan);stroke-dasharray:5 4}
|
|
37
|
-
.node{position:absolute;width:206px;border:1px solid var(--line);border-left:4px solid var(--muted);border-radius:10px;background:var(--panel);padding:9px 11px;box-shadow:var(--shadow);cursor:pointer;transition:transform .12s ease,box-shadow .12s ease}
|
|
38
|
-
.node:hover{transform:translateY(-1px);box-shadow:0 10px 28px rgba(17,24,39,.12)}
|
|
39
|
-
.node.task.doing{border-left-color:var(--blue)}
|
|
40
|
-
.node.task.blocked{border-left-color:var(--red)}
|
|
41
|
-
.node.task.review{border-left-color:var(--amber)}
|
|
42
|
-
.node.task.done,.node.task.accepted{border-left-color:var(--green)}
|
|
52
|
+
.canvas{position:relative;height:620px;overflow:hidden}
|
|
53
|
+
.canvas-scroll{position:absolute;inset:0;overflow:auto;padding:10px}
|
|
54
|
+
.lane-rail{position:absolute;left:0;top:0;bottom:0;width:110px;z-index:6;pointer-events:none;background:linear-gradient(90deg,var(--panel) 86%,transparent)}
|
|
55
|
+
svg.edges{position:absolute;inset:0;pointer-events:none;overflow:visible}
|
|
56
|
+
.node{position:absolute;width:190px;border:1px solid var(--line);border-left:1px solid var(--line);border-radius:10px;background:var(--panel);padding:8px 10px;box-shadow:var(--shadow);cursor:pointer}
|
|
43
57
|
.node.path{border-left-color:var(--violet)}
|
|
44
58
|
.node.session{border-left-color:var(--cyan)}
|
|
45
|
-
.node b{display:block;font-size:13px;line-height:1.
|
|
59
|
+
.node b{display:block;font-size:13px;line-height:1.3;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
46
60
|
.node .meta{color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
47
|
-
.
|
|
48
|
-
.
|
|
61
|
+
.progress{height:4px;border-radius:99px;background:var(--line);margin-top:6px;overflow:hidden}
|
|
62
|
+
.progress i{display:block;height:100%;background:var(--blue);background-image:linear-gradient(45deg,rgba(255,255,255,.28) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.28) 50%,rgba(255,255,255,.28) 75%,transparent 75%);background-size:12px 12px;animation:flow 1s linear infinite}
|
|
63
|
+
.progress i.blocked{background-color:var(--red);animation-play-state:paused;opacity:.65}
|
|
64
|
+
.progress i.review{background-color:var(--amber)}
|
|
65
|
+
.progress i.done,.progress i.accepted{background-color:var(--muted);background-image:none;animation:none}
|
|
66
|
+
.progress i.todo{background-color:var(--muted);background-image:none;animation:none}
|
|
67
|
+
@keyframes flow{to{background-position:12px 0}}
|
|
68
|
+
.st{font-weight:500}
|
|
69
|
+
.st.doing{color:var(--ink)} .st.blocked{color:var(--red)} .st.review{color:var(--amber)}
|
|
70
|
+
.st.done,.st.accepted{color:var(--muted)} .st.todo{color:var(--muted)}
|
|
71
|
+
.prio{float:right;font-size:10px;font-weight:600;border-radius:5px;padding:0 6px;color:#fff;margin-left:4px}
|
|
72
|
+
.prio{background:transparent;border:1px solid var(--line)}
|
|
73
|
+
.prio.p0{background:var(--red);border-color:var(--red);color:#fff}
|
|
74
|
+
.prio.p1{border-color:var(--amber);color:var(--amber)}
|
|
75
|
+
.prio.p2{border-color:var(--line);color:var(--muted)}
|
|
76
|
+
.pgroup{position:absolute;left:0;pointer-events:none;border-bottom:1px dashed var(--line)}
|
|
77
|
+
.plane-label{position:absolute;left:20px;width:78px;z-index:6;pointer-events:none;padding:2px 6px 2px 0;background:linear-gradient(90deg,var(--panel) 72%,transparent);transform:translateY(-50%);font-size:13px;font-weight:700;letter-spacing:.1em}
|
|
78
|
+
.plane-label small{display:block;font-size:9px;font-weight:400;letter-spacing:0;opacity:.7;margin-top:2px}
|
|
79
|
+
.legend-overlay{position:absolute;right:12px;bottom:12px;z-index:5;background:color-mix(in srgb,var(--panel) 92%,transparent);border:1px solid var(--line);border-radius:10px;padding:8px 10px;box-shadow:var(--shadow);font-size:11px;color:var(--muted);display:grid;gap:5px;min-width:190px}
|
|
80
|
+
.legend-overlay .row{display:flex;align-items:center;gap:8px;white-space:nowrap}
|
|
81
|
+
.legend-overlay .sample{width:38px;height:10px;flex:0 0 38px}
|
|
82
|
+
.legend-overlay b{color:var(--ink);font-weight:600}
|
|
83
|
+
.canvas.simple .legend-overlay .detail-row{display:none}
|
|
49
84
|
aside{display:flex;flex-direction:column;gap:14px}
|
|
50
85
|
.side-title{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--line)}
|
|
51
86
|
.side-title h2{font-size:13px;margin:0}
|
|
@@ -54,21 +89,68 @@
|
|
|
54
89
|
.item{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
55
90
|
.item:last-child{border-bottom:0}
|
|
56
91
|
.item .k{color:var(--muted)}
|
|
57
|
-
.dot{display:inline-block;width:7px;height:7px;border-radius:99px;background:var(--muted);margin-right:6px;vertical-align:1px}
|
|
58
|
-
.dot.blue{background:var(--blue)} .dot.red{background:var(--red)} .dot.green{background:var(--green)} .dot.amber{background:var(--amber)}
|
|
59
|
-
.legend{padding:10px 14px 14px;color:var(--muted);font-size:12px;display:grid;gap:6px}
|
|
60
|
-
.legend i{display:inline-block;width:18px;height:2px;margin-right:8px;vertical-align:3px}
|
|
61
92
|
.attention{padding:8px 14px 14px}
|
|
62
93
|
.attention .row{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
63
94
|
.attention .row:last-child{border-bottom:0}
|
|
64
95
|
.tag{font-size:10px;border-radius:6px;padding:1px 6px;background:rgba(220,38,38,.12);color:var(--red);white-space:nowrap}
|
|
65
96
|
.tag.amber{background:rgba(217,119,6,.12);color:var(--amber)}
|
|
66
|
-
.
|
|
67
|
-
.
|
|
68
|
-
.
|
|
69
|
-
.
|
|
70
|
-
.
|
|
97
|
+
.attention .row{cursor:pointer}
|
|
98
|
+
.quick{display:flex;gap:6px;margin-top:6px}
|
|
99
|
+
.quick button,.btn{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:7px;padding:3px 8px;font:inherit;font-size:11px;cursor:pointer}
|
|
100
|
+
.quick button.primary,.btn.primary{background:var(--blue);border-color:var(--blue);color:#fff}
|
|
101
|
+
.backdrop{position:fixed;inset:0;background:rgba(0,0,0,.35);opacity:0;pointer-events:none;transition:opacity .18s;z-index:40}
|
|
102
|
+
.backdrop.show{opacity:1;pointer-events:auto}
|
|
103
|
+
.drawer{position:fixed;top:0;right:0;bottom:0;width:440px;max-width:92vw;background:var(--panel);border-left:1px solid var(--line);box-shadow:-12px 0 40px rgba(0,0,0,.25);transform:translateX(100%);transition:transform .2s ease;z-index:50;display:flex;flex-direction:column}
|
|
104
|
+
.drawer.show{transform:none}
|
|
105
|
+
.drawer-head{display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line)}
|
|
106
|
+
.drawer-head h3{font-size:15px;margin:0 0 3px}
|
|
107
|
+
.drawer-head .meta{color:var(--muted);font-size:11px}
|
|
108
|
+
.drawer-head .close{margin-left:auto;border:0;background:transparent;color:var(--muted);font-size:18px;cursor:pointer}
|
|
109
|
+
.drawer-tabs{display:flex;gap:6px;padding:10px 16px;border-bottom:1px solid var(--line)}
|
|
110
|
+
.drawer-tabs button{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:7px;padding:4px 10px;font:inherit;font-size:12px;cursor:pointer}
|
|
111
|
+
.drawer-tabs button.on{background:var(--blue);border-color:var(--blue);color:#fff}
|
|
112
|
+
.drawer-body{padding:14px 16px 24px;overflow:auto}
|
|
113
|
+
.drawer-body h4{font-size:12px;margin:14px 0 6px;color:var(--muted);letter-spacing:.05em;text-transform:uppercase}
|
|
114
|
+
.kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
|
|
71
115
|
.kv .k{color:var(--muted)}
|
|
116
|
+
.iv-seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;margin-bottom:12px}
|
|
117
|
+
.iv-seg button{flex:1;border:0;background:transparent;color:var(--muted);padding:7px 8px;font:inherit;font-size:12px;cursor:pointer}
|
|
118
|
+
.iv-seg button.on{background:var(--blue);color:#fff}
|
|
119
|
+
.iv-row{display:grid;gap:6px;margin-bottom:10px}
|
|
120
|
+
.iv-row label{font-size:11px;color:var(--muted)}
|
|
121
|
+
.iv-row select,.iv-row textarea{width:100%;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--ink);padding:8px;font:inherit;font-size:12px}
|
|
122
|
+
.iv-row textarea{min-height:96px;resize:vertical}
|
|
123
|
+
.iv-status{color:var(--green);font-size:11px;margin-left:8px}
|
|
124
|
+
.rel-list .item{display:flex;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
125
|
+
.rel-list .item:last-child{border-bottom:0}
|
|
126
|
+
.rel-list .k{color:var(--muted)}
|
|
127
|
+
.kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
|
|
128
|
+
.kv .k{color:var(--muted)}
|
|
129
|
+
/* timeline */
|
|
130
|
+
.timeline{padding:14px 16px 18px;overflow:hidden;position:relative}
|
|
131
|
+
.tl-grid{display:grid;grid-template-columns:190px minmax(0,1fr);gap:8px;align-items:start}
|
|
132
|
+
.tl-labels{overflow:hidden;position:relative}
|
|
133
|
+
.tl-label-head{height:34px;display:flex;align-items:flex-end;font-size:11px;color:var(--muted);padding-bottom:6px}
|
|
134
|
+
.tl-labels-body{will-change:transform}
|
|
135
|
+
.tl-label-row{height:38px;box-sizing:border-box;padding:6px 8px 4px 0;border-top:1px dashed var(--line);font-size:12px;overflow:hidden}
|
|
136
|
+
.tl-label-row b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
137
|
+
.tl-label-row span{color:var(--muted);font-size:10px}
|
|
138
|
+
.tl-scroll{overflow:auto;max-height:620px;scrollbar-width:thin}
|
|
139
|
+
.scroll-hint{position:absolute;right:18px;top:10px;z-index:6;background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:3px 9px;font-size:10px;color:var(--muted);box-shadow:var(--shadow)}
|
|
140
|
+
.tl-head{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:end;color:var(--muted);font-size:11px;margin-bottom:8px;position:sticky;top:0;z-index:4;background:var(--panel)}
|
|
141
|
+
.tl-axis{position:relative;height:34px;width:var(--tlw,900px)}
|
|
142
|
+
.tl-axis span{position:absolute;transform:translateX(-50%);white-space:nowrap;line-height:1.2}
|
|
143
|
+
.tl-axis span small{display:block;font-size:9px;opacity:.55}
|
|
144
|
+
.tl-body{position:relative}
|
|
145
|
+
.tl-row{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:start;min-height:38px;border-top:1px dashed var(--line)}
|
|
146
|
+
.tl-track{position:relative;height:28px;overflow:hidden;width:var(--tlw,900px);margin-top:4px}
|
|
147
|
+
.tl-bar{position:absolute;top:7px;height:14px;border-radius:5px;background:var(--blue);opacity:.9;cursor:pointer}
|
|
148
|
+
.tl-bar .fill{position:absolute;left:0;top:0;bottom:0;border-radius:5px;background:rgba(255,255,255,.45)}
|
|
149
|
+
.tl-bar.blocked{background:var(--red)} .tl-bar.done,.tl-bar.accepted,.tl-bar.todo{background:var(--muted)}
|
|
150
|
+
.tl-bar.review{background:var(--amber)}
|
|
151
|
+
.tl-bar.critical{outline:2px solid var(--amber);outline-offset:2px}
|
|
152
|
+
.tl-today{position:absolute;top:0;bottom:0;pointer-events:none;z-index:2;border-left:1px dashed var(--red);color:var(--red);font-size:10px;padding-left:4px}
|
|
153
|
+
.tl-caption{color:var(--muted);font-size:12px;padding:10px 0 2px}
|
|
72
154
|
footer{padding:0 22px 22px;color:var(--muted);font-size:11px}
|
|
73
155
|
@media (max-width:980px){ main{grid-template-columns:1fr} .canvas{height:560px} }
|
|
74
156
|
</style>
|
|
@@ -76,34 +158,79 @@
|
|
|
76
158
|
<body>
|
|
77
159
|
<header>
|
|
78
160
|
<h1>dia graph</h1>
|
|
79
|
-
<span class="sub">只读依赖图 · 视觉设计稿 v0.
|
|
80
|
-
<
|
|
161
|
+
<span class="sub">只读依赖图 + 时间线 · 视觉设计稿 v0.3</span>
|
|
162
|
+
<div class="tabs" id="tabs">
|
|
163
|
+
<button class="on" data-v="graph">关系图</button>
|
|
164
|
+
<button data-v="time">时间线</button>
|
|
165
|
+
</div>
|
|
81
166
|
<span class="spacer"></span>
|
|
82
|
-
<span class="pill"
|
|
167
|
+
<span class="pill">静态 mockup · 无网络</span>
|
|
168
|
+
<span class="pill" id="route">#/graph</span>
|
|
169
|
+
<span class="pill" id="updated">updated 18:42</span>
|
|
83
170
|
</header>
|
|
84
171
|
|
|
85
172
|
<main>
|
|
86
173
|
<section class="panel">
|
|
87
174
|
<div class="toolbar">
|
|
88
|
-
<div
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
175
|
+
<div id="graph-tools" style="display:flex;gap:8px;align-items:center">
|
|
176
|
+
<div class="seg" id="layout">
|
|
177
|
+
<button class="on" data-l="priority">优先级视图</button>
|
|
178
|
+
<button data-l="dep">依赖视图</button>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="seg" id="mode">
|
|
181
|
+
<button class="on" data-m="simple">简约</button>
|
|
182
|
+
<button data-m="detail">详细</button>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="search">
|
|
185
|
+
<input id="q" type="search" placeholder="搜索 ID / 名称 / owner" autocomplete="off">
|
|
186
|
+
<div class="q-list" id="q-list"></div>
|
|
187
|
+
</div>
|
|
188
|
+
<span class="focus-chip" id="focus-chip"><span id="focus-text"></span><select id="focus-fd" class="fd"><option value="1">1 层</option><option value="2" selected>2 层</option><option value="all">全链路</option></select><button id="focus-exit">退出</button></span>
|
|
189
|
+
</div>
|
|
190
|
+
<div id="time-tools" style="display:none;gap:8px;align-items:center">
|
|
191
|
+
<div class="seg" id="zoom">
|
|
192
|
+
<button data-z="-">-</button>
|
|
193
|
+
<button data-z="reset">100%</button>
|
|
194
|
+
<button data-z="+">+</button>
|
|
195
|
+
</div>
|
|
93
196
|
</div>
|
|
94
|
-
<
|
|
95
|
-
<
|
|
96
|
-
<
|
|
197
|
+
<select class="win-select" id="win" title="按任务开始时间过滤,结束时间不限(两个视图共用)">
|
|
198
|
+
<option value="7">近 7 天</option>
|
|
199
|
+
<option value="14">近 14 天</option>
|
|
200
|
+
<option value="30">近 30 天</option>
|
|
201
|
+
<option value="0">全部时间</option>
|
|
202
|
+
</select>
|
|
203
|
+
<span class="hint" id="hint">按优先级分行(P0 最高);行内从左到右 = 依赖顺序。简约只看任务和依赖,详细展开具体关联。</span>
|
|
204
|
+
</div>
|
|
205
|
+
<div id="view-graph">
|
|
206
|
+
<div class="canvas" id="canvas">
|
|
207
|
+
<div class="canvas-scroll" id="canvas-scroll">
|
|
208
|
+
<svg class="edges" id="edges"></svg>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="lane-rail" id="lane-rail"></div>
|
|
211
|
+
<div class="legend-overlay" id="legend">
|
|
212
|
+
<div class="row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-dep" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#2563eb"/></marker></defs><line x1="1" y1="5" x2="30" y2="5" stroke="#2563eb" stroke-width="1.6" marker-end="url(#lg-dep)"/></svg><span><b>→ depends_on</b> 单向</span></div>
|
|
213
|
+
<div class="row detail-row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-touch" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#7c3aed"/></marker></defs><line x1="1" y1="5" x2="30" y2="5" stroke="#7c3aed" stroke-width="1.6" stroke-dasharray="6 4" marker-end="url(#lg-touch)"/></svg><span><b>→ touches</b> 单向</span></div>
|
|
214
|
+
<div class="row detail-row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-claim" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#0891b2"/></marker></defs><line x1="1" y1="5" x2="30" y2="5" stroke="#0891b2" stroke-width="1.6" stroke-dasharray="2 3" marker-end="url(#lg-claim)"/></svg><span><b>→ claims</b> 单向</span></div>
|
|
215
|
+
<div class="row detail-row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-collab" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#64748b"/></marker></defs><line x1="3" y1="5" x2="32" y2="5" stroke="#64748b" stroke-width="1.6" stroke-dasharray="1 3" marker-start="url(#lg-collab)" marker-end="url(#lg-collab)"/></svg><span><b>↔ collaborates_with</b> 双向(示例)</span></div>
|
|
216
|
+
</div>
|
|
97
217
|
</div>
|
|
98
|
-
<span class="hint">点击节点查看详情 · 边:依赖 / 触碰 / claim</span>
|
|
99
218
|
</div>
|
|
100
|
-
<div
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
219
|
+
<div id="view-time" style="display:none">
|
|
220
|
+
<div class="timeline">
|
|
221
|
+
<div class="scroll-hint">← 左右滚动 →</div>
|
|
222
|
+
<div class="tl-grid">
|
|
223
|
+
<div class="tl-labels">
|
|
224
|
+
<div class="tl-label-head">任务</div>
|
|
225
|
+
<div class="tl-labels-body" id="tl-labels-body"></div>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="tl-scroll" id="tl-scroll">
|
|
228
|
+
<div class="tl-head"><div class="tl-axis" id="tl-axis"></div></div>
|
|
229
|
+
<div class="tl-body" id="tl-body"></div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="tl-caption">橙色描边 = 关键路径(数据根迁移 → install.sh 拆分 → launchd);红色虚线 = 今天。</div>
|
|
233
|
+
</div>
|
|
107
234
|
</div>
|
|
108
235
|
</section>
|
|
109
236
|
|
|
@@ -111,110 +238,493 @@
|
|
|
111
238
|
<div class="panel">
|
|
112
239
|
<div class="side-title"><h2>概览</h2><span>live data</span></div>
|
|
113
240
|
<div class="list">
|
|
114
|
-
<div class="item"><span class="k"><i class="dot
|
|
115
|
-
<div class="item"><span class="k"><i class="dot red"></i>阻塞</span><b>
|
|
116
|
-
<div class="item"><span class="k"><i class="dot amber"></i>待评审</span><b>
|
|
117
|
-
<div class="item"><span class="k"><i class="dot
|
|
241
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>任务</span><b id="stat-tasks">100</b></div>
|
|
242
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--red)"></i>阻塞</span><b id="stat-blocked">0</b></div>
|
|
243
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--amber)"></i>待评审</span><b id="stat-review">0</b></div>
|
|
244
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>完成</span><b id="stat-done">0</b></div>
|
|
118
245
|
<div class="item"><span class="k">claims</span><b>3</b></div>
|
|
119
246
|
</div>
|
|
120
247
|
</div>
|
|
121
|
-
|
|
122
248
|
<div class="panel">
|
|
123
249
|
<div class="side-title"><h2>需要人</h2><span>attention</span></div>
|
|
124
250
|
<div class="attention">
|
|
125
|
-
<div class="row"><span class="tag">阻塞</span><div><b>
|
|
126
|
-
<div class="row"><span class="tag amber">claim</span><div><b>src/collab.ts</b><br><span class="k">session-a 持有中</span></div></div>
|
|
127
|
-
<div class="row"><span class="tag amber">评审</span><div><b>graph TUI</b><br><span class="k">等待独立 verifier</span></div></div>
|
|
251
|
+
<div class="row" data-task="t-3"><span class="tag">阻塞</span><div><b>launchd 安装脚本</b><br><span class="k">等待 install.sh 拆分</span><div class="quick"><button class="primary" onclick="event.stopPropagation();openDrawer('t-3','intervene')">介入</button><button onclick="event.stopPropagation();openDrawer('t-3','task')">查看</button></div></div></div>
|
|
252
|
+
<div class="row" data-task="t-4"><span class="tag amber">claim</span><div><b>src/collab.ts</b><br><span class="k">session-a 持有中</span><div class="quick"><button class="primary" onclick="event.stopPropagation();openDrawer('t-4','intervene')">联系持有者</button></div></div></div>
|
|
253
|
+
<div class="row" data-task="t-4"><span class="tag amber">评审</span><div><b>graph TUI</b><br><span class="k">等待独立 verifier</span><div class="quick"><button class="primary" onclick="event.stopPropagation();openDrawer('t-4','intervene')">开始评审</button></div></div></div>
|
|
128
254
|
</div>
|
|
129
255
|
</div>
|
|
130
|
-
|
|
131
|
-
<div class="panel">
|
|
132
|
-
<div class="side-title"><h2>图例</h2><span>legend</span></div>
|
|
133
|
-
<div class="legend">
|
|
134
|
-
<div><i style="background:var(--blue)"></i>depends_on(任务依赖)</div>
|
|
135
|
-
<div><i style="background:var(--violet)"></i>touches(触碰路径)</div>
|
|
136
|
-
<div><i style="background:var(--cyan)"></i>claims(会话声明)</div>
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
<div class="panel detail" id="detail">
|
|
141
|
-
<div class="side-title"><h2>节点详情</h2><button onclick="document.getElementById('detail').classList.remove('show')" style="border:0;background:transparent;color:var(--muted);cursor:pointer">关闭</button></div>
|
|
142
|
-
<div class="detail" style="display:block" id="detail-body"></div>
|
|
143
|
-
</div>
|
|
144
256
|
</aside>
|
|
145
257
|
</main>
|
|
146
|
-
<
|
|
258
|
+
<div class="backdrop" id="backdrop" onclick="closeDrawer()"></div>
|
|
259
|
+
<aside class="drawer" id="drawer" aria-hidden="true">
|
|
260
|
+
<div class="drawer-head">
|
|
261
|
+
<div><h3 id="drawer-title">任务详情</h3><div class="meta" id="drawer-sub"></div></div>
|
|
262
|
+
<button class="close" onclick="closeDrawer()">×</button>
|
|
263
|
+
</div>
|
|
264
|
+
<div class="drawer-tabs">
|
|
265
|
+
<button data-dtab="task" onclick="setDrawerTab('task')">任务</button>
|
|
266
|
+
<button data-dtab="intervene" onclick="setDrawerTab('intervene')">介入</button>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="drawer-body" id="drawer-body"></div>
|
|
269
|
+
</aside>
|
|
270
|
+
<footer>颜色只表达警告/异常:红=阻塞/P0,橙=待评审/P1;其余状态用中性灰 + 单一蓝色进度。关系图按依赖深度分层,时间线独立展示执行情况。</footer>
|
|
147
271
|
|
|
148
272
|
<script>
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{id:'
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
function
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
273
|
+
const paths = ['install.sh','src/collab.ts','docs/msg.md','tests/collab.test.ts','kits/dia-msg'];
|
|
274
|
+
const modules = ['collab','graph','claim','release','docs','review','persona','kit'];
|
|
275
|
+
const statusList = ['todo','doing','review','blocked','done','accepted'];
|
|
276
|
+
const tasks = [];
|
|
277
|
+
for (let i=0;i<100;i++){
|
|
278
|
+
const prio = i<6?'P0':i<24?'P1':i<62?'P2':'P3';
|
|
279
|
+
const status = statusList[(i*7)%statusList.length];
|
|
280
|
+
const progress = status==='done'||status==='accepted'?100:status==='todo'?0:(i*13)%95;
|
|
281
|
+
const startD = -15 + ((i*7)%24);
|
|
282
|
+
const endD = Math.min(15, startD + 1 + ((i*3)%8));
|
|
283
|
+
const id = 't-'+String(i+1).padStart(3,'0');
|
|
284
|
+
const deps = i===0 ? [] : ['t-'+String(i-((i%5)+1)).padStart(3,'0')];
|
|
285
|
+
tasks.push({id, label:modules[i%modules.length]+' '+String(i+1).padStart(3,'0'), status, prio, progress, start:startD, end:endD, owner:i%3===0?null:(i%2?'ses_a':'ses_b'), deps});
|
|
286
|
+
}
|
|
287
|
+
const pad=n=>'t-'+String(n+1).padStart(3,'0');
|
|
288
|
+
[[6,1],[7,2],[8,3],[24,7],[25,8],[62,25],[63,26]].forEach(([i,j])=>{ if(tasks[i]&&!tasks[i].deps.includes(pad(j))) tasks[i].deps.push(pad(j)); });
|
|
289
|
+
const taskEdges = [];
|
|
290
|
+
tasks.forEach(t=>t.deps.forEach(d=>taskEdges.push({from:t.id,to:d,kind:'depends_on'})));
|
|
291
|
+
for(let i=0;i<20;i++) taskEdges.push({from:tasks[i*5].id,to:paths[i%paths.length],kind:'touches'});
|
|
292
|
+
for(let i=0;i<12;i++) taskEdges.push({from:i%2?'ses_a':'ses_b',to:paths[i%paths.length],kind:'claims'});
|
|
293
|
+
taskEdges.push({from:'t-001',to:'t-002',kind:'collaborates_with'});
|
|
294
|
+
|
|
295
|
+
const state={view:'graph', layout:'priority', mode:'simple', range:'30d', zoom:28, window:7, q:'', focus:null, fd:2, from:null, to:null};
|
|
296
|
+
const canvas=document.getElementById('canvas'), layer=document.getElementById('canvas-scroll'), rail=document.getElementById('lane-rail'), svg=document.getElementById('edges');
|
|
297
|
+
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
298
|
+
const stIcon={doing:'▶',blocked:'⏸',review:'◐',done:'✓',accepted:'✓',todo:'○'};
|
|
299
|
+
|
|
300
|
+
function focusSet(id,depth){
|
|
301
|
+
const by=new Map(tasks.map(t=>[t.id,t]));
|
|
302
|
+
const max=depth==='all'?Infinity:Math.max(1,Number(depth)||2);
|
|
303
|
+
const out=new Set([id]); let frontier=[id];
|
|
304
|
+
for(let step=0; step<max && frontier.length; step++){
|
|
305
|
+
const next=[];
|
|
306
|
+
frontier.forEach(cur=>{
|
|
307
|
+
const t=by.get(cur); if(!t) return;
|
|
308
|
+
(t.deps||[]).forEach(x=>{ if(by.has(x)&&!out.has(x)){ out.add(x); next.push(x); } });
|
|
309
|
+
tasks.forEach(o=>{ if((o.deps||[]).includes(cur)&&!out.has(o.id)){ out.add(o.id); next.push(o.id); } });
|
|
310
|
+
});
|
|
311
|
+
frontier=next;
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
function visibleTasks(){
|
|
316
|
+
if(state.focus){ const keep=focusSet(state.focus,state.fd); return tasks.filter(t=>keep.has(t.id)); }
|
|
317
|
+
return tasks.filter(t=>state.window<=0 || t.start>=-state.window);
|
|
318
|
+
}
|
|
319
|
+
function searchMatches(){
|
|
320
|
+
const q=(state.q||'').trim().toLowerCase();
|
|
321
|
+
if(!q) return null;
|
|
322
|
+
return tasks.filter(t=>t.id.toLowerCase().includes(q)||t.label.toLowerCase().includes(q)||String(t.owner||'').toLowerCase().includes(q));
|
|
323
|
+
}
|
|
324
|
+
function syncStats(){
|
|
325
|
+
const list=visibleTasks(), st=id=>document.getElementById(id);
|
|
326
|
+
if(!st('stat-tasks')) return;
|
|
327
|
+
st('stat-tasks').textContent = list.length===tasks.length ? String(tasks.length) : list.length+' / '+tasks.length;
|
|
328
|
+
st('stat-blocked').textContent=String(list.filter(t=>t.status==='blocked').length);
|
|
329
|
+
st('stat-review').textContent=String(list.filter(t=>t.status==='review').length);
|
|
330
|
+
st('stat-done').textContent=String(list.filter(t=>t.status==='done'||t.status==='accepted').length);
|
|
331
|
+
}
|
|
332
|
+
function updateHint(){
|
|
333
|
+
const h=document.getElementById('hint'); if(!h||state.view!=='graph') return;
|
|
334
|
+
const n=visibleTasks().length;
|
|
335
|
+
if(state.focus){
|
|
336
|
+
h.textContent='显示 '+n+' 个任务 · '+(state.fd==='all'?'全链路':state.fd+' 层');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
h.textContent='显示 '+n+' / '+tasks.length+' 个任务 · '+(state.layout==='dep'?'从左到右 = 依赖深度':'按优先级分行(P0 最高)');
|
|
340
|
+
}
|
|
341
|
+
function depths(){
|
|
342
|
+
const list=visibleTasks();
|
|
343
|
+
const by=new Map(list.map(t=>[t.id,t])), memo=new Map();
|
|
344
|
+
const depth=t=>{
|
|
345
|
+
if(!t) return 0;
|
|
346
|
+
if(memo.has(t.id)) return memo.get(t.id);
|
|
347
|
+
memo.set(t.id,0);
|
|
348
|
+
const d=t.deps.length?Math.max(...t.deps.map(x=>{ const dt=by.get(x); return dt?depth(dt):-1; }))+1:0;
|
|
349
|
+
memo.set(t.id,d); return d;
|
|
350
|
+
};
|
|
351
|
+
list.forEach(depth); return memo;
|
|
352
|
+
}
|
|
353
|
+
function levels(){
|
|
354
|
+
return [...new Set(visibleTasks().map(t=>t.prio))].sort((a,b)=>{
|
|
355
|
+
const na=parseInt(String(a).replace(/^P/i,''),10), nb=parseInt(String(b).replace(/^P/i,''),10);
|
|
356
|
+
return (Number.isFinite(na)?na:99)-(Number.isFinite(nb)?nb:99)||String(a).localeCompare(String(b));
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function rowsFor(){
|
|
360
|
+
const list=visibleTasks(), d=depths();
|
|
361
|
+
const gap=state.mode==='detail'?230:145;
|
|
362
|
+
const h=state.mode==='detail'?205:110;
|
|
363
|
+
return levels().map((prio,i)=>{
|
|
364
|
+
const rowTasks=list.filter(t=>t.prio===prio).sort((a,b)=>(d.get(a.id)-d.get(b.id))||a.label.localeCompare(b.label));
|
|
365
|
+
return {prio,y:36+i*gap,h,tasks:rowTasks};
|
|
366
|
+
}).filter(r=>r.tasks.length);
|
|
367
|
+
}
|
|
368
|
+
let cardMetrics={mid:52,h:58}, clickTimer=null;
|
|
369
|
+
function measureCard(){
|
|
370
|
+
const probe=card(tasks[0],-9999,-9999);
|
|
371
|
+
probe.style.visibility='hidden';
|
|
372
|
+
layer.appendChild(probe);
|
|
373
|
+
const prog=probe.querySelector('.progress');
|
|
374
|
+
const mid=prog?prog.offsetTop+prog.offsetHeight/2+probe.clientTop:44;
|
|
375
|
+
const h=probe.offsetHeight||58;
|
|
376
|
+
probe.remove();
|
|
377
|
+
return {mid,h};
|
|
378
|
+
}
|
|
379
|
+
function bandCenter(r, bandH){ return r.y-12+bandH/2; }
|
|
380
|
+
function cardTop(r){
|
|
381
|
+
const bandH=state.mode==='detail'?230:145;
|
|
382
|
+
const center=bandCenter(r,bandH);
|
|
383
|
+
// 详细:进度条中心线对齐泳道中心;简约:卡片盒整体垂直居中
|
|
384
|
+
return state.mode==='detail'?center-cardMetrics.mid:center-cardMetrics.h/2;
|
|
385
|
+
}
|
|
386
|
+
function edgeOffset(){ return state.mode==='detail'?cardMetrics.mid:cardMetrics.h/2; }
|
|
387
|
+
function pos(){
|
|
388
|
+
const rows=rowsFor(), p=new Map();
|
|
389
|
+
rows.forEach(r=>r.tasks.forEach((t,i)=>p.set(t.id,{x:170+i*230,y:cardTop(r)})));
|
|
390
|
+
return {p,rows};
|
|
391
|
+
}
|
|
392
|
+
function card(t,x,y,showPrio=false){
|
|
393
|
+
const el=document.createElement('div'); el.className='node task '+t.status; el.style.left=x+'px'; el.style.top=y+'px';
|
|
394
|
+
const rel=taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
|
|
395
|
+
const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
|
|
396
|
+
const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
|
|
397
|
+
return '<div class="meta" style="margin-top:2px">↳ '+esc(kind)+': '+esc(other)+'</div>';
|
|
398
|
+
}).slice(0,4).join('');
|
|
399
|
+
const badge=showPrio?'<span class="prio '+esc(t.prio.toLowerCase())+'">'+esc(t.prio)+'</span>':'';
|
|
400
|
+
el.innerHTML='<b>'+esc(t.label)+badge+'</b>'+
|
|
401
|
+
'<div class="meta">'+esc(t.id)+' · <span class="st '+esc(t.status)+'">'+stIcon[t.status]+' '+esc(t.status)+'</span> · '+t.progress+'%</div>'+
|
|
402
|
+
'<div class="progress"><i class="'+esc(t.status)+'" style="width:'+t.progress+'%"></i></div>'+
|
|
403
|
+
(state.mode==='detail'?'<div class="meta" style="margin-top:5px">'+(t.owner?'@'+esc(t.owner):'unassigned')+' · dep '+t.deps.length+'</div>'+rel:'');
|
|
404
|
+
el.dataset.id=t.id;
|
|
405
|
+
el.onclick=()=>{ clearTimeout(clickTimer); clickTimer=setTimeout(()=>openDrawer(t.id,'task'),180); };
|
|
406
|
+
el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer); focusOn(t.id); };
|
|
185
407
|
return el;
|
|
186
408
|
}
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
409
|
+
function edgePath(a,b){
|
|
410
|
+
const o=edgeOffset(), x1=a.x+190,y1=a.y+o,x2=b.x,y2=b.y+o;
|
|
411
|
+
if(x2>=x1) return `M${x1} ${y1} H${x1+Math.max(24,(x2-x1)/2)} V${y2} H${x2}`;
|
|
412
|
+
return `M${x1} ${y1} H${x1-30} V${y2} H${x2}`;
|
|
413
|
+
}
|
|
414
|
+
function drawGraph(){
|
|
415
|
+
canvas.classList.toggle('simple', state.mode==='simple');
|
|
416
|
+
if(state.layout==='dep'){ drawDepGraph(); return; }
|
|
417
|
+
cardMetrics=measureCard();
|
|
418
|
+
const {p,rows}=pos();
|
|
419
|
+
[...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
|
|
420
|
+
rail.style.display=''; rail.querySelectorAll('.plane-label').forEach(e=>e.remove());
|
|
421
|
+
const maxX=Math.max(600, ...rows.map(r=>170+(r.tasks.length-1)*230+190+10));
|
|
422
|
+
const maxY=rows.length?Math.max(...rows.map(r=>r.y+r.h))+40:600;
|
|
423
|
+
const bandH=state.mode==='detail'?230:145;
|
|
424
|
+
rows.forEach((r,i)=>{
|
|
425
|
+
const g=document.createElement('div'); g.className='pgroup '+r.prio.toLowerCase();
|
|
426
|
+
g.style.top=(r.y-12)+'px'; g.style.width=maxX+'px'; g.style.height=bandH+'px';
|
|
427
|
+
const alpha=Math.max(0.02, 0.075-i*0.012);
|
|
428
|
+
g.style.background='color-mix(in srgb, var(--blue) '+Math.round(alpha*100)+'%, transparent)';
|
|
429
|
+
layer.appendChild(g);
|
|
430
|
+
const label=document.createElement('div'); label.className='plane-label '+r.prio.toLowerCase();
|
|
431
|
+
label.dataset.y=String(r.y-12+bandH/2); label.style.top=(r.y-12+bandH/2-layer.scrollTop)+'px';
|
|
432
|
+
label.style.color=i===0?'var(--red)':i===1?'var(--amber)':'var(--muted)';
|
|
433
|
+
label.innerHTML=esc(r.prio)+' · '+r.tasks.length+(i===0?'<small>紧急</small>':i===1?'<small>高</small>':'<small>常规</small>');
|
|
434
|
+
rail.appendChild(label);
|
|
435
|
+
r.tasks.forEach((t,j)=>layer.appendChild(card(t,170+j*230,cardTop(r))));
|
|
436
|
+
});
|
|
192
437
|
svg.setAttribute('width',maxX); svg.setAttribute('height',maxY);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
438
|
+
const defs=`<defs>
|
|
439
|
+
<marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
|
|
440
|
+
<marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
|
|
441
|
+
</defs>`;
|
|
442
|
+
const lines=taskEdges.filter(e=>e.kind==='depends_on'||e.kind==='collaborates_with').map(e=>{
|
|
443
|
+
const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
|
|
444
|
+
const style=e.kind==='collaborates_with'
|
|
445
|
+
? 'stroke="#64748b" stroke-dasharray="1 3" marker-start="url(#ar-collab)" marker-end="url(#ar-collab)"'
|
|
446
|
+
: 'stroke="#2563eb" marker-end="url(#ar-dep)"';
|
|
447
|
+
return `<path d="${edgePath(a,b)}" fill="none" stroke-width="1.6" ${style}/>`;
|
|
448
|
+
}).join('');
|
|
449
|
+
svg.innerHTML=defs+lines;
|
|
450
|
+
document.getElementById('updated').textContent='updated '+new Date().toLocaleTimeString();
|
|
451
|
+
syncStats(); updateHint(); applySearch();
|
|
452
|
+
}
|
|
453
|
+
let drawerTask=null, drawerTab='task', ivChannel='tui';
|
|
454
|
+
function relations(t){
|
|
455
|
+
return taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
|
|
456
|
+
const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
|
|
457
|
+
const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
|
|
458
|
+
return '<div class="item"><span class="k">'+esc(kind)+'</span><span>'+esc(other)+'</span></div>';
|
|
459
|
+
}).join('') || '<p class="k">无边</p>';
|
|
460
|
+
}
|
|
461
|
+
function taskHtml(t){
|
|
462
|
+
return '<div class="kv"><div class="k">status</div><div>'+esc(t.status)+'</div>'+
|
|
463
|
+
'<div class="k">priority</div><div>'+esc(t.prio)+'</div>'+
|
|
464
|
+
'<div class="k">owner</div><div>'+esc(t.owner||'-')+'</div>'+
|
|
465
|
+
'<div class="k">progress</div><div>'+t.progress+'%</div>'+
|
|
466
|
+
'<div class="k">deps</div><div>'+esc(t.deps.join(', ')||'-')+'</div></div>'+
|
|
467
|
+
'<h4>关联</h4><div class="rel-list">'+relations(t)+'</div>';
|
|
468
|
+
}
|
|
469
|
+
function targets(ch){
|
|
470
|
+
if(ch==='dingtalk') return [['group','乐语助人前端组 · 群聊'],['dm','与 session-a 私聊']];
|
|
471
|
+
if(ch==='msg') return [['ses_a','session-a'],['ses_b','session-b']];
|
|
472
|
+
return [['ses_a','session-a · opencode TUI (online)'],['ses_b','session-b · dsh TUI (online)']];
|
|
473
|
+
}
|
|
474
|
+
function interveneHtml(t){
|
|
475
|
+
const chs=[['tui','TUI 会话'],['dingtalk','钉钉消息链'],['msg','dia msg']];
|
|
476
|
+
const opts=targets(ivChannel).map(([v,l])=>'<option value="'+v+'">'+esc(l)+'</option>').join('');
|
|
477
|
+
return '<div class="iv-seg">'+chs.map(([v,l])=>'<button class="'+(ivChannel===v?'on':'')+'" onclick="setChannel(\''+v+'\')">'+l+'</button>').join('')+'</div>'+
|
|
478
|
+
'<div class="iv-row"><label>目标会话 / 消息链</label><select id="iv-target">'+opts+'</select></div>'+
|
|
479
|
+
'<div class="iv-row"><label>介入消息</label><textarea id="iv-text" placeholder="例如:这个任务卡在什么点?需要我做什么?"></textarea></div>'+
|
|
480
|
+
'<div><button class="btn primary" onclick="sendIntervention()">发送介入</button><span class="iv-status" id="iv-status"></span></div>'+
|
|
481
|
+
'<p class="k" style="margin-top:12px">设计稿:发送仅演示。真实接入分别走 TUI 注入 / 钉钉消息链 / dia msg;同一任务可切换通道。</p>';
|
|
482
|
+
}
|
|
483
|
+
function openDrawer(id, tab){
|
|
484
|
+
drawerTask=tasks.find(t=>t.id===id); if(!drawerTask) return;
|
|
485
|
+
drawerTab=tab||'task';
|
|
486
|
+
document.getElementById('drawer-title').textContent='任务详情';
|
|
487
|
+
document.getElementById('drawer-sub').textContent=drawerTask.label+' · '+drawerTask.id+' · '+drawerTask.prio+' · '+drawerTask.status;
|
|
488
|
+
document.getElementById('drawer').classList.add('show');
|
|
489
|
+
document.getElementById('drawer').setAttribute('aria-hidden','false');
|
|
490
|
+
document.getElementById('backdrop').classList.add('show');
|
|
491
|
+
setDrawerTab(drawerTab);
|
|
492
|
+
}
|
|
493
|
+
function setDrawerTab(tab){
|
|
494
|
+
drawerTab=tab;
|
|
495
|
+
document.querySelectorAll('[data-dtab]').forEach(b=>b.classList.toggle('on',b.dataset.dtab===tab));
|
|
496
|
+
const body=document.getElementById('drawer-body');
|
|
497
|
+
body.innerHTML=tab==='intervene'?interveneHtml(drawerTask):taskHtml(drawerTask);
|
|
498
|
+
}
|
|
499
|
+
function setChannel(ch){ ivChannel=ch; setDrawerTab('intervene'); }
|
|
500
|
+
function sendIntervention(){
|
|
501
|
+
const text=(document.getElementById('iv-text')||{}).value||'';
|
|
502
|
+
if(!text.trim()){ document.getElementById('iv-status').textContent='请先输入内容'; return; }
|
|
503
|
+
document.getElementById('iv-status').textContent='已排队(设计稿)';
|
|
504
|
+
}
|
|
505
|
+
function closeDrawer(){
|
|
506
|
+
document.getElementById('drawer').classList.remove('show');
|
|
507
|
+
document.getElementById('drawer').setAttribute('aria-hidden','true');
|
|
508
|
+
document.getElementById('backdrop').classList.remove('show');
|
|
509
|
+
}
|
|
510
|
+
function dayDate(d){
|
|
511
|
+
const base=new Date('2026-11-06T00:00:00');
|
|
512
|
+
const dt=new Date(base.getTime()+d*86400000);
|
|
513
|
+
return ('0'+(dt.getMonth()+1)).slice(-2)+'-'+('0'+dt.getDate()).slice(-2);
|
|
514
|
+
}
|
|
515
|
+
function isoDate(d){ const base=new Date('2026-11-06T00:00:00'); const dt=new Date(base.getTime()+d*86400000); return dt.toISOString().slice(0,10); }
|
|
516
|
+
function timelineWindow(){
|
|
517
|
+
if(state.from!==null&&state.to!==null) return {from:state.from,to:state.to};
|
|
518
|
+
const list=visibleTasks();
|
|
519
|
+
const w=state.window>0?state.window:14;
|
|
520
|
+
const from=-w;
|
|
521
|
+
const maxEnd=list.length?Math.max(...list.map(t=>t.end)):w;
|
|
522
|
+
return {from,to:Math.max(0,maxEnd)+1};
|
|
523
|
+
}
|
|
524
|
+
function drawTimeline(){
|
|
525
|
+
const win=timelineWindow(), span=Math.max(1,win.to-win.from);
|
|
526
|
+
const x=v=>((v-win.from)/span)*100;
|
|
527
|
+
const chartW=Math.max(640, span*state.zoom);
|
|
528
|
+
document.getElementById('view-time').style.setProperty('--tlw', chartW+'px');
|
|
529
|
+
const axis=document.getElementById('tl-axis'); axis.innerHTML='';
|
|
530
|
+
const step=span<=10?1:span<=18?2:3;
|
|
531
|
+
for(let d=win.from;d<=win.to;d+=step){
|
|
532
|
+
const el=document.createElement('span'); el.style.left=x(d)+'%';
|
|
533
|
+
el.innerHTML=dayDate(d)+'<small>'+(d===0?'今天':(d>0?'+'+d+'d':d+'d'))+'</small>';
|
|
534
|
+
if(d===win.from) el.style.transform='translateX(0)'; else if(d+step>win.to) el.style.transform='translateX(-100%)';
|
|
535
|
+
if(d===0)el.style.color='var(--red)'; axis.appendChild(el);
|
|
536
|
+
}
|
|
537
|
+
const body=document.getElementById('tl-body'); body.innerHTML=''; const labelsBody=document.getElementById('tl-labels-body'); labelsBody.innerHTML=''; const critical=new Set(['t-1','t-2','t-3']);
|
|
538
|
+
visibleTasks().slice().sort((a,b)=>a.start-b.start).forEach(t=>{
|
|
539
|
+
const row=document.createElement('div'); row.className='tl-row';
|
|
540
|
+
row.innerHTML='<div class="tl-track"></div>';
|
|
541
|
+
const label=document.createElement('div'); label.className='tl-label-row'; label.innerHTML='<b>'+esc(t.label)+'</b><span>'+esc(t.id)+' · '+esc(t.prio)+' · '+esc(t.status)+'</span>'; labelsBody.appendChild(label);
|
|
542
|
+
const track=row.querySelector('.tl-track');
|
|
543
|
+
const bar=document.createElement('div'); bar.className='tl-bar '+t.status+(critical.has(t.id)?' critical':'');
|
|
544
|
+
bar.style.left=x(t.start)+'%'; bar.style.width=Math.max(1.5,x(t.end)-x(t.start))+'%';
|
|
545
|
+
bar.title=dayDate(t.start)+' → '+dayDate(t.end)+' ('+t.progress+'%)';
|
|
546
|
+
bar.innerHTML='<div class="fill" style="width:'+t.progress+'%"></div>'; bar.onclick=()=>openDrawer(t.id,'task'); track.appendChild(bar);
|
|
547
|
+
if(win.from<=0&&win.to>=0){ const line=document.createElement('div'); line.className='tl-today'; line.style.left=x(0)+'%'; line.style.top='0'; line.style.bottom='0'; line.textContent=''; track.appendChild(line); }
|
|
548
|
+
body.appendChild(row);
|
|
549
|
+
});
|
|
550
|
+
syncStats();
|
|
551
|
+
}
|
|
552
|
+
function drawDepGraph(){
|
|
553
|
+
cardMetrics=measureCard();
|
|
554
|
+
const list=visibleTasks(), d=depths(), groups=new Map();
|
|
555
|
+
list.forEach(t=>{ const k=d.get(t.id)||0; groups.set(k,[...(groups.get(k)||[]),t]); });
|
|
556
|
+
const p=new Map(); const rows=[];
|
|
557
|
+
const gap=14;
|
|
558
|
+
[...groups.keys()].sort((a,b)=>a-b).forEach((depth,ci)=>{
|
|
559
|
+
const all=groups.get(depth).sort((a,b)=>a.prio.localeCompare(b.prio)||a.label.localeCompare(b.label));
|
|
560
|
+
rows.push({depth,all,x:30+ci*230});
|
|
561
|
+
});
|
|
562
|
+
[...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
|
|
563
|
+
rail.innerHTML=''; rail.style.display='none';
|
|
564
|
+
let maxY=0;
|
|
565
|
+
// 先渲染卡片,再按实测高度逐列堆叠,避免详细模式卡片相互重叠
|
|
566
|
+
rows.forEach(r=>{
|
|
567
|
+
let y=60;
|
|
568
|
+
r.all.forEach(t=>{
|
|
569
|
+
const el=card(t,r.x,y,true);
|
|
570
|
+
el.style.top=y+'px';
|
|
571
|
+
layer.appendChild(el);
|
|
572
|
+
p.set(t.id,{x:r.x,y});
|
|
573
|
+
y+=el.offsetHeight+gap;
|
|
574
|
+
});
|
|
575
|
+
maxY=Math.max(maxY,y);
|
|
576
|
+
});
|
|
577
|
+
maxY=Math.max(600,maxY);
|
|
578
|
+
const maxX=Math.max(600,...rows.map(r=>r.x+200+10));
|
|
579
|
+
svg.setAttribute('width',maxX); svg.setAttribute('height',maxY+40);
|
|
580
|
+
const defs=`<defs>
|
|
581
|
+
<marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
|
|
582
|
+
<marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
|
|
583
|
+
</defs>`;
|
|
584
|
+
const lines=taskEdges.filter(e=>e.kind==='depends_on'||e.kind==='collaborates_with').map(e=>{
|
|
585
|
+
const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
|
|
586
|
+
const style=e.kind==='collaborates_with'?'stroke="#64748b" stroke-dasharray="1 3" marker-start="url(#ar-collab)" marker-end="url(#ar-collab)"':'stroke="#2563eb" marker-end="url(#ar-dep)"';
|
|
587
|
+
return `<path d="${edgePath(a,b)}" fill="none" stroke-width="1.6" ${style}/>`;
|
|
196
588
|
}).join('');
|
|
589
|
+
svg.innerHTML=defs+lines;
|
|
590
|
+
document.getElementById('updated').textContent='updated '+new Date().toLocaleTimeString();
|
|
591
|
+
syncStats(); updateHint(); applySearch();
|
|
592
|
+
}
|
|
593
|
+
function syncLaneLabels(){
|
|
594
|
+
rail.querySelectorAll('.plane-label').forEach(el=>{
|
|
595
|
+
el.style.top=(Number(el.dataset.y||0)-layer.scrollTop)+'px';
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function applySearch(){
|
|
599
|
+
const ms=searchMatches();
|
|
600
|
+
layer.querySelectorAll('.node.task').forEach(el=>{
|
|
601
|
+
el.classList.remove('hit','dim');
|
|
602
|
+
if(!ms) return;
|
|
603
|
+
if(ms.some(t=>t.id===el.dataset.id)) el.classList.add('hit'); else el.classList.add('dim');
|
|
604
|
+
});
|
|
605
|
+
const box=document.getElementById('q-list');
|
|
606
|
+
if(box && box.classList.contains('open')) renderSearchList(ms);
|
|
607
|
+
}
|
|
608
|
+
function renderSearchList(ms){
|
|
609
|
+
const box=document.getElementById('q-list'); if(!box) return;
|
|
610
|
+
const q=(state.q||'').trim();
|
|
611
|
+
const browse=!q;
|
|
612
|
+
const pool=browse?visibleTasks().slice().sort((a,b)=>a.prio.localeCompare(b.prio)||a.start-b.start):ms;
|
|
613
|
+
if(!pool||!pool.length){
|
|
614
|
+
box.innerHTML='<div class="q-empty">'+(browse?'当前时间窗内没有任务':'没有匹配的任务')+'</div>';
|
|
615
|
+
box.classList.add('open'); return;
|
|
616
|
+
}
|
|
617
|
+
const head=browse?('<div class="q-head">可见 '+pool.length+' 个 · ↑↓ 选择,Enter 聚焦</div>')
|
|
618
|
+
:('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter 聚焦</div>');
|
|
619
|
+
box.innerHTML=head+pool.slice(0,20).map(t=>'<div class="q-item" data-id="'+esc(t.id)+'"><span>'+esc(t.label)+'<i class="q-prio">'+esc(t.prio)+'</i></span><span class="k">'+esc(t.id)+'</span></div>').join('');
|
|
620
|
+
box.classList.add('open');
|
|
621
|
+
box.querySelectorAll('.q-item').forEach(el=>el.onclick=()=>focusOn(el.dataset.id));
|
|
622
|
+
}
|
|
623
|
+
function searchListItems(){
|
|
624
|
+
const box=document.getElementById('q-list');
|
|
625
|
+
return box?[...box.querySelectorAll('.q-item')]:[];
|
|
626
|
+
}
|
|
627
|
+
function moveSearchCursor(delta){
|
|
628
|
+
let items=searchListItems();
|
|
629
|
+
if(!items.length){ renderSearchList(searchMatches()); items=searchListItems(); if(!items.length) return; }
|
|
630
|
+
let idx=items.findIndex(el=>el.classList.contains('active'));
|
|
631
|
+
idx=idx<0?0:(idx+delta+items.length)%items.length;
|
|
632
|
+
items.forEach((el,i)=>el.classList.toggle('active',i===idx));
|
|
633
|
+
items[idx].scrollIntoView({block:'nearest'});
|
|
634
|
+
}
|
|
635
|
+
function focusOn(id){
|
|
636
|
+
state.focus=id; state.fd=state.fd||2; state.q='';
|
|
637
|
+
const qi=document.getElementById('q'); if(qi) qi.value='';
|
|
638
|
+
go(state.view,state.mode,state.range,state.zoom);
|
|
639
|
+
}
|
|
640
|
+
function readRoute(){
|
|
641
|
+
const raw=location.hash.replace(/^#\/?/,''); const [path,qs]=raw.split('?');
|
|
642
|
+
const params=new URLSearchParams(qs||location.search);
|
|
643
|
+
const range=(params.get('range')||'30d');
|
|
644
|
+
const zoom=Math.min(84,Math.max(14,parseInt(params.get('zoom')||'28',10)||28));
|
|
645
|
+
const sx=Number(params.get('sx')||0);
|
|
646
|
+
const fromRaw=params.get('from'), toRaw=params.get('to');
|
|
647
|
+
const toOff=v=>v?Math.round((new Date(v+'T00:00:00').getTime()-new Date('2026-11-06T00:00:00').getTime())/86400000):null;
|
|
648
|
+
const winRaw=params.get('w');
|
|
649
|
+
const rangeRaw=params.get('range');
|
|
650
|
+
const win=winRaw!==null?Math.max(0,parseInt(winRaw,10)||0):(rangeRaw?Math.max(0,parseInt(rangeRaw,10)||0):7);
|
|
651
|
+
const fdRaw=params.get('fd');
|
|
652
|
+
const fd=fdRaw==='all'?'all':Math.min(9,Math.max(1,parseInt(fdRaw||'2',10)||2));
|
|
653
|
+
return {view:(path==='timeline'||params.get('view')==='timeline')?'time':'graph', layout:params.get('layout')==='dep'?'dep':'priority', mode:params.get('mode')==='detail'?'detail':'simple', range, zoom, sx, window:win, q:params.get('q')||'', focus:params.get('focus')||null, fd, from:toOff(fromRaw), to:toOff(toRaw), task:params.get('task'), tab:params.get('tab')==='intervene'?'intervene':'task'};
|
|
654
|
+
}
|
|
655
|
+
function routeString(view,mode,range,zoom,from,to){
|
|
656
|
+
const p=view==='time'?'timeline':'graph'; const qs=new URLSearchParams();
|
|
657
|
+
if(view==='graph'&&mode==='detail') qs.set('mode','detail');
|
|
658
|
+
if(view==='graph'&&state.layout==='dep') qs.set('layout','dep');
|
|
659
|
+
if(view==='graph'){
|
|
660
|
+
if(state.window!==7) qs.set('w',String(state.window));
|
|
661
|
+
if(state.q&&state.q.trim()) qs.set('q',state.q.trim());
|
|
662
|
+
if(state.focus){ qs.set('focus',state.focus); if(state.fd!==2) qs.set('fd',String(state.fd)); }
|
|
663
|
+
}
|
|
664
|
+
if(view==='time'){
|
|
665
|
+
if(from!==null&&to!==null){ qs.set('from',isoDate(from)); qs.set('to',isoDate(to)); }
|
|
666
|
+
if(state.window!==7) qs.set('w',String(state.window));
|
|
667
|
+
if(zoom&&zoom!==28) qs.set('zoom',String(zoom));
|
|
668
|
+
}
|
|
669
|
+
return '#/'+p+(qs.toString()?'?'+qs.toString():'');
|
|
670
|
+
}
|
|
671
|
+
function applyRoute(){
|
|
672
|
+
const r=readRoute(); state.view=r.view; state.layout=r.layout; state.mode=r.mode; state.range=r.range; state.zoom=r.zoom; state.sx=r.sx||0; state.window=r.window; state.q=r.q; state.focus=r.focus; state.fd=r.fd; state.from=r.from; state.to=r.to;
|
|
673
|
+
document.querySelectorAll('#tabs button').forEach(x=>x.classList.toggle('on',x.dataset.v===r.view));
|
|
674
|
+
document.querySelectorAll('#mode button').forEach(x=>x.classList.toggle('on',x.dataset.m===r.mode));
|
|
675
|
+
document.querySelectorAll('#layout button').forEach(x=>x.classList.toggle('on',x.dataset.l===r.layout));
|
|
676
|
+
document.getElementById('view-graph').style.display=r.view==='graph'?'':'none';
|
|
677
|
+
document.getElementById('view-time').style.display=r.view==='time'?'':'none';
|
|
678
|
+
document.getElementById('graph-tools').style.display=r.view==='graph'?'flex':'none';
|
|
679
|
+
document.getElementById('time-tools').style.display=r.view==='time'?'flex':'none';
|
|
680
|
+
const zbtn=document.querySelector('#zoom button[data-z="reset"]'); if(zbtn) zbtn.textContent=Math.round(r.zoom/28*100)+'%';
|
|
681
|
+
const dec=document.querySelector('#zoom button[data-z="-"]'), inc=document.querySelector('#zoom button[data-z="+"]'); if(dec) dec.disabled=r.zoom<=14; if(inc) inc.disabled=r.zoom>=84;
|
|
682
|
+
if(r.view!=='graph') document.getElementById('hint').textContent='时间线:左右滚动查看;-/+缩放;橙框 = 关键路径';
|
|
683
|
+
const winSel=document.getElementById('win'); if(winSel) winSel.value=String(r.window);
|
|
684
|
+
const qIn=document.getElementById('q'); if(qIn && qIn.value!==r.q) qIn.value=r.q;
|
|
685
|
+
const chip=document.getElementById('focus-chip');
|
|
686
|
+
if(chip){ chip.classList.toggle('on',!!r.focus); if(r.focus) document.getElementById('focus-text').textContent='聚焦 '+r.focus+' ·'; }
|
|
687
|
+
const fdEl=document.getElementById('focus-fd'); if(fdEl) fdEl.value=String(r.fd);
|
|
688
|
+
const rp=document.getElementById('route'); if(rp) rp.textContent=location.hash||routeString(r.view,r.mode,r.range,state.zoom,state.from,state.to);
|
|
689
|
+
document.title='dia graph · '+(r.view==='time'?'时间线':'关系图');
|
|
690
|
+
if(r.view==='time'){ drawTimeline(); const sc=document.getElementById('tl-scroll'), lb=document.getElementById('tl-labels-body'); sc.scrollTop=0; lb.style.transform='translateY(0px)'; if(state.sx) sc.scrollLeft=state.sx; } else drawGraph();
|
|
691
|
+
if(r.task) openDrawer(r.task,r.tab);
|
|
692
|
+
syncLaneLabels();
|
|
693
|
+
}
|
|
694
|
+
function go(view,mode,range,zoom){ const h=routeString(view,mode,range||state.range,zoom||state.zoom,state.from,state.to); if(location.hash===h) applyRoute(); else location.hash=h; }
|
|
695
|
+
document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>go(b.dataset.v==='time'?'time':'graph',state.mode,state.range));
|
|
696
|
+
document.querySelectorAll('#layout button').forEach(b=>b.onclick=()=>{state.layout=b.dataset.l;go(state.view,state.mode,state.range,state.zoom);});
|
|
697
|
+
const winSelEl=document.getElementById('win');
|
|
698
|
+
if(winSelEl) winSelEl.onchange=()=>{ state.window=Number(winSelEl.value); go(state.view,state.mode,state.range,state.zoom); };
|
|
699
|
+
const qInput=document.getElementById('q');
|
|
700
|
+
if(qInput){
|
|
701
|
+
qInput.oninput=()=>{ state.q=qInput.value; applySearch(); renderSearchList(searchMatches()); };
|
|
702
|
+
qInput.onkeydown=e=>{
|
|
703
|
+
if(e.key==='ArrowDown'){ e.preventDefault(); moveSearchCursor(1); }
|
|
704
|
+
else if(e.key==='ArrowUp'){ e.preventDefault(); moveSearchCursor(-1); }
|
|
705
|
+
else if(e.key==='Enter'){
|
|
706
|
+
const items=searchListItems();
|
|
707
|
+
const active=items.find(el=>el.classList.contains('active'))||items[0];
|
|
708
|
+
if(active) focusOn(active.dataset.id);
|
|
709
|
+
} else if(e.key==='Escape'){ state.q=''; qInput.value=''; applySearch(); qInput.blur(); }
|
|
710
|
+
};
|
|
711
|
+
qInput.onfocus=()=>{ renderSearchList(searchMatches()); };
|
|
712
|
+
qInput.onclick=()=>{ renderSearchList(searchMatches()); };
|
|
713
|
+
qInput.onblur=()=>{ setTimeout(()=>{ const box=document.getElementById('q-list'); if(box) box.classList.remove('open'); },160); };
|
|
197
714
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
document.querySelectorAll('
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
});
|
|
212
|
-
document.querySelectorAll('#density button').forEach(b=>b.onclick=()=>{
|
|
213
|
-
document.querySelectorAll('#density button').forEach(x=>x.classList.remove('on')); b.classList.add('on');
|
|
214
|
-
const compact=b.dataset.d==='compact';
|
|
215
|
-
document.querySelectorAll('.node').forEach(el=>{el.style.padding=compact?'5px 8px':'9px 11px';el.style.width=compact?'180px':'206px';});
|
|
216
|
-
});
|
|
217
|
-
draw();
|
|
715
|
+
const fdSel=document.getElementById('focus-fd');
|
|
716
|
+
if(fdSel) fdSel.onchange=()=>{ state.fd=fdSel.value==='all'?'all':Number(fdSel.value); go(state.view,state.mode,state.range,state.zoom); };
|
|
717
|
+
const fxBtn=document.getElementById('focus-exit');
|
|
718
|
+
if(fxBtn) fxBtn.onclick=()=>{ state.focus=null; go(state.view,state.mode,state.range,state.zoom); };
|
|
719
|
+
document.querySelectorAll('#mode button').forEach(b=>b.onclick=()=>go(state.view,b.dataset.m,state.range,state.zoom));
|
|
720
|
+
document.querySelectorAll('#zoom button').forEach(b=>b.onclick=()=>{ const z=b.dataset.z==='+'?Math.min(84,state.zoom+8):b.dataset.z==='-'?Math.max(14,state.zoom-8):28; go('time',state.mode,state.range,z); });
|
|
721
|
+
layer.addEventListener('scroll',syncLaneLabels,{passive:true});
|
|
722
|
+
const tlScroller=document.getElementById('tl-scroll');
|
|
723
|
+
tlScroller.addEventListener('scroll',()=>{ const lb=document.getElementById('tl-labels-body'); if(lb) lb.style.transform='translateY(-'+tlScroller.scrollTop+'px)'; },{passive:true});
|
|
724
|
+
document.querySelectorAll('.attention .row[data-task]').forEach(r=>r.onclick=()=>openDrawer(r.dataset.task,'intervene'));
|
|
725
|
+
window.addEventListener('hashchange',applyRoute);
|
|
726
|
+
applyRoute();
|
|
727
|
+
if(document.fonts&&document.fonts.ready){ document.fonts.ready.then(()=>{ if(state.view==='graph'){ if(state.layout==='dep') drawDepGraph(); else drawGraph(); } }); }
|
|
218
728
|
</script>
|
|
219
729
|
</body>
|
|
220
730
|
</html>
|
package/docs/design/graph-web.md
CHANGED
|
@@ -1,61 +1,160 @@
|
|
|
1
|
-
# dia graph Web 视觉设计稿(v0.
|
|
1
|
+
# dia graph Web 视觉设计稿(v0.3)
|
|
2
2
|
|
|
3
|
-
> 静态 mockup:[`graph-web-mockup.html`](graph-web-mockup.html)
|
|
4
|
-
> 数据契约:`dia graph --json`(schema 1
|
|
3
|
+
> 静态 mockup:[`graph-web-mockup.html`](graph-web-mockup.html)(浏览器直接打开,无网络/无依赖)
|
|
4
|
+
> 数据契约:`dia graph --json`(schema 1);时间线所需 start/end 为后续扩展字段。
|
|
5
5
|
|
|
6
|
-
## 1.
|
|
6
|
+
## 1. 两个视图
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
┌ header: dia graph · 只读 · 更新时间 ──────────────────────────┐
|
|
10
|
-
├ 工具栏:状态过滤 | 密度 | 提示 │
|
|
11
|
-
├ 主画布(卡片 + SVG 边) ┌ 右侧 320px ────┐
|
|
12
|
-
│ sessions | todo | doing | review | paths │ 概览 / 需要人 │
|
|
13
|
-
│ │ 图例 / 节点详情 │
|
|
14
|
-
└ footer: 数据来源 / 约束 ───────────────────┴────────────────┘
|
|
15
|
-
```
|
|
8
|
+
顶部 tab:
|
|
16
9
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- 节点卡片颜色只表达 **kind + status**,不用于装饰。
|
|
10
|
+
- **关系图**:回答“谁依赖谁、先后顺序、优先级”;
|
|
11
|
+
- **时间线**:独立甘特图,回答“什么时候做、做到哪、是否延期”。
|
|
20
12
|
|
|
21
|
-
|
|
13
|
+
### 关系图:按依赖深度分层,不按状态分列
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
- 纵轴 = 优先级,**P0 最高、P1 中间、P2 最下**;每个优先级是一条**通栏泳道色带**,泳道之间用虚线分隔,不再用闭合方框;
|
|
16
|
+
- P0/P1/P2 标签固定在左侧泳道栏、垂直居中;**横向滚动时标签吸住左边缘**(JS 同步 scrollLeft),泳道底色按级别计算(P0 最深,向下递减),可自然扩展 P3/P4;
|
|
17
|
+
- 组内从左到右 = 依赖顺序(按依赖深度排序);
|
|
18
|
+
- **状态不占布局**:用内联图标/文本 + 进度条表达;
|
|
19
|
+
- `简约`:只显示 task + `depends_on`;
|
|
20
|
+
- `详细`:展开 owner、session、path、touches、claims,并在卡片里列具体关联(`depends_on / depended_by / touches / claims / collaborates_with`)。
|
|
21
|
+
- 图例固定在画布右下角:只显示当前模式会用到的边;`→` 单向、`↔` 双向,线型与图上一致。
|
|
22
|
+
|
|
23
|
+
### 时间线:独立甘特
|
|
24
|
+
|
|
25
|
+
- 每行一个 task,横轴为相对今天的天数(`-8d … today … +4d`);
|
|
26
|
+
- 条长 = start/end,内部填充 = progress;
|
|
27
|
+
- 颜色 = status;橙色描边 = 关键路径;红色虚线 = 今天;
|
|
28
|
+
- 点击条/行可看任务详情。
|
|
29
|
+
|
|
30
|
+
## 2. 边语义
|
|
31
|
+
|
|
32
|
+
| 关系 | 方向 | 线型 | 颜色 |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `depends_on` | 单向 `→` | 实线 | 蓝 |
|
|
35
|
+
| `touches` | 单向 `→` | 虚线 `6 4` | 紫 |
|
|
36
|
+
| `claims` | 单向 `→` | 点线 `2 3` | 青 |
|
|
37
|
+
| `collaborates_with` | 双向 `↔` | 点线 `1 3` + 双箭头 | 灰(视觉示例,未进 M1 契约) |
|
|
38
|
+
|
|
39
|
+
## 3. 为什么不再按状态列/行
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
状态列会把“关系”切碎,且 TODO/DONE、REVIEW/BLOCKED 混在一列没有语义。现在:
|
|
31
42
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- 不做:编辑、拖拽改依赖、写 claim(只读)。
|
|
43
|
+
- 依赖顺序是主结构(拓扑层);
|
|
44
|
+
- 状态是属性(颜色/进度);
|
|
45
|
+
- 优先级是行分组(P0/P1/P2 各一行,最高在最上,虚线框圈组);
|
|
46
|
+
- 时间放在独立时间线,不挤在关系图里。
|
|
37
47
|
|
|
38
|
-
## 4.
|
|
48
|
+
## 4. 与数据契约的映射
|
|
39
49
|
|
|
40
|
-
| UI |
|
|
50
|
+
| UI | 数据 |
|
|
41
51
|
|---|---|
|
|
42
|
-
| 卡片 | `nodes[]
|
|
52
|
+
| task 卡片 | `nodes[kind=task]` + status/owner |
|
|
43
53
|
| 依赖边 | `edges[kind=depends_on]` |
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
54
|
+
| touches / claims | `edges` 对应 kind |
|
|
55
|
+
| 进度 | 后续 `task.progress`(0-100) |
|
|
56
|
+
| 时间线 | 后续 `task.start/end`(相对/ISO) |
|
|
57
|
+
| 关键路径 | 后续由依赖图最长路径计算 |
|
|
48
58
|
|
|
49
|
-
## 5.
|
|
59
|
+
## 5. 实现约束
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
`tests/static.test.ts` 禁止框架 import `node:http` / `fetch` / 网络模块。因此:
|
|
52
62
|
|
|
53
|
-
1.
|
|
54
|
-
2.
|
|
55
|
-
|
|
56
|
-
- 方案 B:独立可选包 `@dia/graph-web`(明确例外 + 只绑 loopback + 单独评审),不改核心框架规则。
|
|
63
|
+
1. 本稿是静态 HTML,不进入 `src/`;
|
|
64
|
+
2. 真实实现优先 `dia graph --export-html` 生成静态文件;
|
|
65
|
+
3. live Web 需独立可选包 + 单独评审(loopback only)。
|
|
57
66
|
|
|
58
67
|
## 6. 后续
|
|
59
68
|
|
|
60
|
-
-
|
|
61
|
-
-
|
|
69
|
+
- 先确认“依赖深度分层 + 简约/详细 + 时间线”结构;
|
|
70
|
+
- 确认后做 `--export-html`(零网络),再评估 live 轮询。
|
|
71
|
+
|
|
72
|
+
## 7. URL 路由
|
|
73
|
+
|
|
74
|
+
静态稿支持 hash 路由(`file://` 直接可用):
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
#/graph
|
|
78
|
+
#/graph?mode=simple
|
|
79
|
+
#/graph?mode=detail
|
|
80
|
+
#/timeline
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- `view` 也可用 query 形式:`?view=timeline&mode=detail`;
|
|
84
|
+
- 切换 tab / 简约详细会同步更新 hash,便于分享/回链;
|
|
85
|
+
- 真正的独立 path(`/graph`、`/timeline`)需要部署层 rewrite 到同一 HTML,或导出时生成
|
|
86
|
+
`graph.html` / `timeline.html`;静态文件本身不能凭空提供两个 path。
|
|
87
|
+
|
|
88
|
+
## 8. 颜色语义(v0.4)
|
|
89
|
+
|
|
90
|
+
原则:**颜色只表达警告/异常,正常状态去色**。
|
|
91
|
+
|
|
92
|
+
| 颜色 | 用途 | 不再用于 |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| 红 | blocked、P0、异常/失败 | 普通任务状态 |
|
|
95
|
+
| 橙 | review、claim 提醒、P1 | 完成态 |
|
|
96
|
+
| 蓝 | 只有“进行中”的进度条 | done/accepted/todo |
|
|
97
|
+
| 灰 | todo / done / accepted / P2 / 中性文本 | 警告/异常 |
|
|
98
|
+
|
|
99
|
+
- 优先级:P0 红实心;P1 橙描边;P2 灰描边(不抢眼);
|
|
100
|
+
- 状态文本:只有 blocked / review 带颜色,其余中性;
|
|
101
|
+
- 进度条:doing 蓝色条纹动画;blocked 红色暂停/置灰;review 橙色;done/accepted 灰色静态;
|
|
102
|
+
- 时间线同一套语义;关键路径只用橙色描边。
|
|
103
|
+
|
|
104
|
+
## 9. 任务抽屉与介入(v0.5)
|
|
105
|
+
|
|
106
|
+
关系图里的卡片**全部是 task**,不再叫“节点详情”。
|
|
107
|
+
|
|
108
|
+
- 点击 task 卡片 → 右侧抽屉「任务详情」,两个 tab:
|
|
109
|
+
- **任务**:status / priority / owner / progress / deps / 关联;
|
|
110
|
+
- **介入**:通道切换 + 目标会话 + 输入框 + 发送(设计稿只演示)。
|
|
111
|
+
- 「需要人」每行带快速操作:
|
|
112
|
+
- 阻塞 → `介入` / `查看`
|
|
113
|
+
- claim → `联系持有者`
|
|
114
|
+
- 待评审 → `开始评审`
|
|
115
|
+
- 介入通道明确区分:
|
|
116
|
+
1. **TUI 会话**:选择 opencode / dsh 会话,输入后注入;
|
|
117
|
+
2. **钉钉消息链**:选择群聊/私聊,输入后走钉钉;
|
|
118
|
+
3. **dia msg**:选择 session id,走 dia 消息邮箱。
|
|
119
|
+
- 同一任务可以在抽屉里切换通道,方便“会话里没人就发钉钉 / 需要留痕就 dia msg”。
|
|
120
|
+
- 深链:`#/graph?task=t-4&tab=intervene` 可直接打开任务抽屉。
|
|
121
|
+
|
|
122
|
+
## 10. 规模样例(100 tasks / 30 天)
|
|
123
|
+
|
|
124
|
+
mockup 数据改为 **100 个任务、时间跨度 30 天**,用来验证真实规模下的布局:
|
|
125
|
+
|
|
126
|
+
- 关系图有两种布局:**优先级视图**(P0/P1/P2/P3 泳道)和**依赖视图**(按依赖深度从左到右);
|
|
127
|
+
两种视图**都不折叠任务**(没有 `+N 个` 聚合,任何卡片都能点开),靠时间窗 + 搜索 + 聚焦控制规模;
|
|
128
|
+
- mock 数据补齐跨优先级依赖,确保两种视图都能看到跨层关系;依赖边只连接当前可见任务;
|
|
129
|
+
- 卡片垂直基准分两种模式:**详细** = 进度条中心线(关系虚线/箭头也锚在进度条中心,同一行连线与进度条同高);
|
|
130
|
+
**简约** = 卡片盒整体垂直居中(连线锚在卡片盒中心);两种模式都和泳道中心线/左侧级别标签对齐;
|
|
131
|
+
- 卡片偏移量按实际渲染测量(含字体加载后重排),不写死魔法数;泳道顶部/右侧空白按实际内容收紧。
|
|
132
|
+
- 时间线:按当前时间窗过滤后纵向滚动;轴显示绝对日期(`MM-DD`)并保留相对天数
|
|
133
|
+
(`-7d / 今天 / +Nd`)。
|
|
134
|
+
- 泳道标签在**外层固定 rail**,不参与横向滚动;只随纵向 `scrollTop` 同步位置;
|
|
135
|
+
- 泳道标签为全宽色带 + 虚线分隔,P0/P1/P2/P3 由数据动态生成。
|
|
136
|
+
- 依赖视图按实测卡片高度逐列堆叠,`详细` 模式下卡片不会互相重叠;依赖视图没有泳道,左侧不留级别标签遮罩/占位;
|
|
137
|
+
- 关系视图参数:`#/graph?layout=dep`(依赖视图)/ 默认优先级视图;
|
|
138
|
+
- 时间窗参数(两个视图共用):`#/graph?w=7|14|30|0`(0 = 全部);时间线 `#/timeline?w=7`;
|
|
139
|
+
显式区间:`#/timeline?from=2026-10-22&to=2026-11-21`。
|
|
140
|
+
|
|
141
|
+
## 10.1 规模控制:时间窗 + 搜索 + 聚焦
|
|
142
|
+
|
|
143
|
+
无折叠后,用三种手段控制一次看多少:
|
|
144
|
+
|
|
145
|
+
- **时间窗(默认近 7 天,按开始时间,结束时间不限)**:只显示 `start >= 今天 - N` 的任务,
|
|
146
|
+
长任务即使结束在很久以后也照常显示;时间线轴自动从 `-N` 延伸到最晚的 `end`;
|
|
147
|
+
- **搜索**:聚焦输入框就出下拉(空时列出当前可见任务,按优先级/开始时间排序),输入 ID / 名称 / owner 实时过滤;
|
|
148
|
+
`↑↓` 选择、`Enter` 聚焦、`Esc` 清空;命中卡片高亮描边、其余置灰,下拉项点击也可聚焦;
|
|
149
|
+
- **聚焦(类 nx graph focus)**:双击卡片或点搜索结果 → 只保留该任务的依赖邻域,
|
|
150
|
+
芯片可切 `1 层 / 2 层 / 全链路`,「退出」回全量;聚焦状态写进 URL(`focus=t-003&fd=2`);
|
|
151
|
+
- 被过滤掉的任务不计入概览统计,标题显示 `显示 N / 总数`。
|
|
152
|
+
|
|
153
|
+
## 11. 时间线滚动与缩放
|
|
154
|
+
|
|
155
|
+
- 左侧任务列是**独立固定 pane**(不依赖 sticky),横向滚动时始终可见;任务名顶部对齐;
|
|
156
|
+
- 时间区右上角显示「← 左右滚动 →」提示,避免看不出可滚动;
|
|
157
|
+
- 右侧时间轴在 `tl-scroll` 内横向滚动,表头与内容同步;
|
|
158
|
+
- `zoom` = 每天像素宽度(默认 28,范围 **14–84**,即 50%–300%),控制条 `- / 100% / +`,到边界禁用;
|
|
159
|
+
- URL:`#/timeline?w=7&zoom=40`;
|
|
160
|
+
- 任务纵向滚动,时间窗默认近 7 天开始(结束不限),绝对日期 + 相对天数同时显示。
|
package/docs/index.md
CHANGED
|
@@ -92,5 +92,5 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
|
|
|
92
92
|
|
|
93
93
|
| 文档 | 一句话 |
|
|
94
94
|
|---|---|
|
|
95
|
-
| [dia graph Web 视觉设计稿(v0.
|
|
95
|
+
| [dia graph Web 视觉设计稿(v0.3)](design/graph-web.md) | 静态 mockup:graph-web-mockup.html(浏览器直接打开,无网络/无依赖) |
|
|
96
96
|
|
package/docs/status.md
CHANGED
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
| D0.21 | worktree 抽取 `packages/wt` | **完成** | 7 处 python→node;`dia wt`;4 套测试原样迁移全绿 |
|
|
91
91
|
| D0.23 | doctor 平台版本自检 | **完成** | `platform:opencode` / `platform:dsh` 报版本(缺宿主 = warn,不判红);`tests/active-doctor.test.ts` |
|
|
92
92
|
| D0.44 | 配置迁移走 LLM 任务 | **完成** | `docs/migrations/llm-migration.md`:升级只改声明;迁移=任务+判据+回滚;禁止代码里堆版本分支(安全敏感/原子场景例外) |
|
|
93
|
+
| D0.64 | 复制过滤按相对路径判断 | **完成** | 绝对路径匹配 `node_modules` 会误伤 npm 全局安装(kit 整树被过滤 → `E_KIT_EMPTY`);改 `copyFilter(base)` 用相对路径,persona/kit 共用 |
|
|
93
94
|
| D0.63 | 运行时数据 `.data/` 不进快照、不参与哈希 | **完成** | `TREE_SKIP`(kit/persona 共用):安装复制与 `hashTree`/verify 都跳过 `.data`,防运行时写入造成 verify 假漂移 |
|
|
94
95
|
| D0.62 | persona 旧版技能软链视为受管 | **完成** | `linkManaged` 同时认 `~/.dia/kits/**` 与 `~/.dia/personas/**`;`persona update` 升级不再需要 `--force` |
|
|
95
96
|
| D0.61 | kit 空内容拒绝 + doctor 只查引用 kit | **完成** | `kit add` 拒绝空清单/空树(E_MANIFEST/E_KIT_EMPTY);doctor 按 persona lock 的 kits 校验,孤儿不再拖红 |
|