aifastdb-devplan 1.0.3 → 1.1.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/dist/dev-plan-document-store.d.ts.map +1 -1
- package/dist/dev-plan-document-store.js +3 -0
- package/dist/dev-plan-document-store.js.map +1 -1
- package/dist/dev-plan-graph-store.d.ts +10 -0
- package/dist/dev-plan-graph-store.d.ts.map +1 -1
- package/dist/dev-plan-graph-store.js +113 -0
- package/dist/dev-plan-graph-store.js.map +1 -1
- package/dist/dev-plan-interface.d.ts +8 -0
- package/dist/dev-plan-interface.d.ts.map +1 -1
- package/dist/mcp-server/index.js +18 -0
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/visualize/server.js +154 -3
- package/dist/visualize/server.js.map +1 -1
- package/dist/visualize/template.d.ts.map +1 -1
- package/dist/visualize/template.js +1070 -67
- package/dist/visualize/template.js.map +1 -1
- package/package.json +1 -1
|
@@ -18,8 +18,41 @@ function getVisualizationHTML(projectName) {
|
|
|
18
18
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
19
19
|
body { background: #111827; color: #e5e7eb; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; }
|
|
20
20
|
|
|
21
|
+
/* App Layout */
|
|
22
|
+
.app-layout { display: flex; height: 100vh; overflow: hidden; }
|
|
23
|
+
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
|
24
|
+
|
|
25
|
+
/* Sidebar */
|
|
26
|
+
.sidebar { width: 48px; background: #0f172a; border-right: 1px solid #1e293b; flex-shrink: 0; display: flex; flex-direction: column; transition: width 0.25s ease; overflow: hidden; z-index: 40; }
|
|
27
|
+
.sidebar.expanded { width: 200px; }
|
|
28
|
+
.sidebar-header { height: 56px; display: flex; align-items: center; justify-content: center; border-bottom: 1px solid #1e293b; cursor: pointer; flex-shrink: 0; overflow: hidden; transition: all 0.2s; padding: 0 8px; }
|
|
29
|
+
.sidebar-header:hover { background: #1e293b; }
|
|
30
|
+
.sidebar-logo { font-size: 18px; font-weight: 900; background: linear-gradient(90deg, #38bdf8, #818cf8, #a78bfa, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; white-space: nowrap; line-height: 1; }
|
|
31
|
+
.sidebar-logo-full { display: none; }
|
|
32
|
+
.sidebar-logo-short { display: block; }
|
|
33
|
+
.sidebar.expanded .sidebar-header { justify-content: flex-start; padding: 0 16px; }
|
|
34
|
+
.sidebar.expanded .sidebar-logo-full { display: block; }
|
|
35
|
+
.sidebar.expanded .sidebar-logo-short { display: none; }
|
|
36
|
+
.sidebar-nav { flex: 1; padding: 8px 0; display: flex; flex-direction: column; gap: 2px; }
|
|
37
|
+
.sidebar-footer { padding: 8px 0; border-top: 1px solid #1e293b; }
|
|
38
|
+
.nav-item { position: relative; display: flex; align-items: center; height: 40px; padding: 0 12px; cursor: pointer; color: #6b7280; transition: all 0.2s; white-space: nowrap; overflow: hidden; gap: 12px; border-left: 3px solid transparent; }
|
|
39
|
+
.nav-item:hover { background: #1e293b; color: #d1d5db; }
|
|
40
|
+
.nav-item.active { color: #a5b4fc; background: rgba(99,102,241,0.1); border-left-color: #6366f1; }
|
|
41
|
+
.nav-item.disabled { cursor: default; opacity: 0.5; }
|
|
42
|
+
.nav-item.disabled:hover { background: #1e293b; }
|
|
43
|
+
.nav-item-icon { width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
|
|
44
|
+
.nav-item-text { font-size: 13px; font-weight: 500; opacity: 0; transition: opacity 0.2s; }
|
|
45
|
+
.sidebar.expanded .nav-item-text { opacity: 1; }
|
|
46
|
+
.nav-item-badge { font-size: 9px; padding: 1px 6px; border-radius: 4px; background: #374151; color: #6b7280; margin-left: auto; opacity: 0; transition: opacity 0.2s; }
|
|
47
|
+
.sidebar.expanded .nav-item-badge { opacity: 1; }
|
|
48
|
+
|
|
49
|
+
/* Sidebar tooltip (collapsed mode) */
|
|
50
|
+
.nav-item .nav-tooltip { position: absolute; left: 52px; top: 50%; transform: translateY(-50%); background: #1f2937; border: 1px solid #374151; color: #e5e7eb; padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity 0.15s; z-index: 50; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
|
|
51
|
+
.sidebar:not(.expanded) .nav-item:hover .nav-tooltip { opacity: 1; }
|
|
52
|
+
|
|
21
53
|
/* Header */
|
|
22
|
-
.header { background:
|
|
54
|
+
.header { background: transparent; border-bottom: none; padding: 8px 24px; display: flex; align-items: center; justify-content: space-between; height: 44px; position: absolute; top: 0; left: 0; right: 0; z-index: 10; pointer-events: none; }
|
|
55
|
+
.header * { pointer-events: auto; }
|
|
23
56
|
.header h1 { font-size: 20px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
|
|
24
57
|
.header h1 .icon { font-size: 24px; }
|
|
25
58
|
.header .project-name { color: #818cf8; }
|
|
@@ -36,18 +69,13 @@ function getVisualizationHTML(projectName) {
|
|
|
36
69
|
.progress-fill { height: 100%; background: linear-gradient(90deg, #10b981, #3b82f6); border-radius: 4px; transition: width 0.5s; }
|
|
37
70
|
|
|
38
71
|
/* Controls */
|
|
39
|
-
.controls {
|
|
40
|
-
.filter-
|
|
41
|
-
.filter-
|
|
42
|
-
.filter-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
.refresh-btn { padding: 6px 14px; border-radius: 6px; border: none; background: #4f46e5; color: #fff; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 6px; transition: background 0.2s; }
|
|
47
|
-
.refresh-btn:hover { background: #4338ca; }
|
|
48
|
-
|
|
49
|
-
/* Graph — 明确高度 = 视口 - header(56) - controls(44) - legend(40) */
|
|
50
|
-
.graph-container { position: relative; height: calc(100vh - 140px); background: #111827; }
|
|
72
|
+
.controls { display: none; }
|
|
73
|
+
.filter-check { display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 12px; color: #9ca3af; user-select: none; }
|
|
74
|
+
.filter-check input { accent-color: #6366f1; width: 13px; height: 13px; cursor: pointer; }
|
|
75
|
+
.filter-check:hover { color: #d1d5db; }
|
|
76
|
+
|
|
77
|
+
/* Graph — flex 自适应高度 */
|
|
78
|
+
.graph-container { position: relative; flex: 1; background: #111827; min-height: 0; }
|
|
51
79
|
#graph { width: 100%; height: 100%; }
|
|
52
80
|
|
|
53
81
|
.loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(17,24,39,0.9); z-index: 20; }
|
|
@@ -55,18 +83,22 @@ function getVisualizationHTML(projectName) {
|
|
|
55
83
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
56
84
|
|
|
57
85
|
/* Detail Panel */
|
|
58
|
-
.panel { position: absolute; top: 12px; right: 12px; width:
|
|
59
|
-
.panel.show { display:
|
|
60
|
-
.panel-
|
|
86
|
+
.panel { position: absolute; top: 12px; right: 12px; width: 340px; max-height: calc(100vh - 180px); background: #1f2937; border: 1px solid #374151; border-radius: 12px; box-shadow: 0 20px 40px rgba(0,0,0,0.5); z-index: 10; display: none; overflow: hidden; min-width: 280px; max-width: calc(100vw - 40px); transition: none; }
|
|
87
|
+
.panel.show { display: flex; flex-direction: column; }
|
|
88
|
+
.panel-resize-handle { position: absolute; top: 0; left: -4px; width: 8px; height: 100%; cursor: col-resize; z-index: 15; background: transparent; }
|
|
89
|
+
.panel-resize-handle:hover, .panel-resize-handle.active { background: linear-gradient(90deg, transparent, rgba(99,102,241,0.4), transparent); }
|
|
90
|
+
.panel-resize-handle::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 3px; height: 32px; background: #4b5563; border-radius: 2px; opacity: 0; transition: opacity 0.2s; }
|
|
91
|
+
.panel-resize-handle:hover::after, .panel-resize-handle.active::after { opacity: 1; background: #6366f1; }
|
|
92
|
+
.panel-header { padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; cursor: default; user-select: none; }
|
|
61
93
|
.panel-header.project { background: linear-gradient(135deg, #d97706, #f59e0b); }
|
|
62
94
|
.panel-header.module { background: linear-gradient(135deg, #059669, #10b981); }
|
|
63
95
|
.panel-header.main-task { background: linear-gradient(135deg, #4f46e5, #6366f1); }
|
|
64
96
|
.panel-header.sub-task { background: linear-gradient(135deg, #7c3aed, #8b5cf6); }
|
|
65
97
|
.panel-header.document { background: linear-gradient(135deg, #7c3aed, #a78bfa); }
|
|
66
|
-
.panel-title { font-weight: 600; font-size: 14px; color: #fff; }
|
|
98
|
+
.panel-title { font-weight: 600; font-size: 14px; color: #fff; pointer-events: none; }
|
|
67
99
|
.panel-close { background: rgba(255,255,255,0.2); border: none; color: #fff; width: 28px; height: 28px; border-radius: 6px; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
|
|
68
100
|
.panel-close:hover { background: rgba(255,255,255,0.3); }
|
|
69
|
-
.panel-body { padding: 16px; }
|
|
101
|
+
.panel-body { padding: 16px; overflow-y: auto; flex: 1; }
|
|
70
102
|
.panel-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; border-bottom: 1px solid #374151; }
|
|
71
103
|
.panel-row:last-child { border-bottom: none; }
|
|
72
104
|
.panel-label { color: #9ca3af; }
|
|
@@ -83,8 +115,28 @@ function getVisualizationHTML(projectName) {
|
|
|
83
115
|
.panel-progress-bar { width: 100%; height: 6px; background: #374151; border-radius: 3px; overflow: hidden; margin-top: 4px; }
|
|
84
116
|
.panel-progress-fill { height: 100%; background: #10b981; border-radius: 3px; }
|
|
85
117
|
|
|
118
|
+
/* Sub-task List in Panel */
|
|
119
|
+
.subtask-section { margin-top: 12px; border-top: 1px solid #374151; padding-top: 10px; }
|
|
120
|
+
.subtask-section-title { font-size: 12px; color: #9ca3af; font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; }
|
|
121
|
+
.subtask-list { list-style: none; padding: 0; margin: 0; }
|
|
122
|
+
.subtask-item { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-bottom: 1px solid rgba(55,65,81,0.5); font-size: 12px; }
|
|
123
|
+
.subtask-item:last-child { border-bottom: none; }
|
|
124
|
+
.subtask-icon { width: 16px; height: 16px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 10px; }
|
|
125
|
+
.subtask-icon.completed { background: #064e3b; color: #6ee7b7; }
|
|
126
|
+
.subtask-icon.in_progress { background: #1e3a5f; color: #93c5fd; }
|
|
127
|
+
.subtask-icon.pending { background: #374151; color: #6b7280; }
|
|
128
|
+
.subtask-icon.cancelled { background: #451a03; color: #fbbf24; }
|
|
129
|
+
.subtask-name { color: #d1d5db; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
130
|
+
.subtask-name.completed { color: #6ee7b7; text-decoration: line-through; text-decoration-color: rgba(110,231,183,0.3); }
|
|
131
|
+
.subtask-name.cancelled { color: #9ca3af; text-decoration: line-through; }
|
|
132
|
+
.subtask-id { color: #6b7280; font-size: 10px; flex-shrink: 0; font-family: monospace; }
|
|
133
|
+
.subtask-time { color: #6ee7b7; font-size: 10px; flex-shrink: 0; opacity: 0.75; margin-left: auto; }
|
|
134
|
+
|
|
86
135
|
/* Legend */
|
|
87
|
-
.legend { background: #1f2937; border-top: 1px solid #374151; padding:
|
|
136
|
+
.legend { background: #1f2937; border-top: 1px solid #374151; padding: 6px 24px; display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 10px 20px; font-size: 12px; color: #9ca3af; }
|
|
137
|
+
.legend-filters { display: flex; align-items: center; gap: 8px; }
|
|
138
|
+
.legend-divider { width: 1px; height: 18px; background: #374151; }
|
|
139
|
+
.legend-sep { width: 100%; height: 0; }
|
|
88
140
|
.legend-item { display: flex; align-items: center; gap: 6px; }
|
|
89
141
|
.legend-icon { width: 12px; height: 12px; }
|
|
90
142
|
.legend-icon.star { background: #f59e0b; clip-path: polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%); }
|
|
@@ -97,66 +149,301 @@ function getVisualizationHTML(projectName) {
|
|
|
97
149
|
.legend-line.thin { background: #6b7280; height: 1px; }
|
|
98
150
|
.legend-line.dashed { border-top: 2px dashed #6b7280; background: none; height: 0; }
|
|
99
151
|
.legend-line.dotted { border-top: 2px dotted #10b981; background: none; height: 0; }
|
|
152
|
+
.legend-line.task-doc { border-top: 2px dashed #b45309; background: none; height: 0; }
|
|
153
|
+
|
|
154
|
+
/* Document Content in Panel */
|
|
155
|
+
.doc-section { margin-top: 12px; border-top: 1px solid #374151; padding-top: 10px; }
|
|
156
|
+
.doc-section-title { font-size: 12px; color: #9ca3af; font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; }
|
|
157
|
+
.doc-content { background: #111827; border: 1px solid #374151; border-radius: 8px; padding: 12px; font-size: 12px; line-height: 1.7; color: #d1d5db; overflow-x: auto; }
|
|
158
|
+
.doc-content h1, .doc-content h2, .doc-content h3, .doc-content h4 { color: #f3f4f6; margin: 12px 0 6px 0; }
|
|
159
|
+
.doc-content h1 { font-size: 16px; border-bottom: 1px solid #374151; padding-bottom: 4px; }
|
|
160
|
+
.doc-content h2 { font-size: 14px; border-bottom: 1px solid rgba(55,65,81,0.5); padding-bottom: 3px; }
|
|
161
|
+
.doc-content h3 { font-size: 13px; }
|
|
162
|
+
.doc-content h4 { font-size: 12px; color: #d1d5db; }
|
|
163
|
+
.doc-content p { margin: 6px 0; }
|
|
164
|
+
.doc-content code { background: #1e293b; color: #a5b4fc; padding: 1px 5px; border-radius: 3px; font-size: 11px; font-family: 'Cascadia Code', 'Fira Code', Consolas, monospace; }
|
|
165
|
+
.doc-content pre { background: #0f172a; border: 1px solid #1e293b; border-radius: 6px; padding: 10px; overflow-x: auto; margin: 8px 0; }
|
|
166
|
+
.doc-content pre code { background: none; padding: 0; color: #e2e8f0; display: block; white-space: pre; }
|
|
167
|
+
.doc-content ul, .doc-content ol { padding-left: 20px; margin: 6px 0; }
|
|
168
|
+
.doc-content li { margin: 2px 0; }
|
|
169
|
+
.doc-content blockquote { border-left: 3px solid #4f46e5; padding-left: 10px; color: #9ca3af; margin: 8px 0; font-style: italic; }
|
|
170
|
+
.doc-content table { width: 100%; border-collapse: collapse; margin: 8px 0; font-size: 11px; }
|
|
171
|
+
.doc-content th { background: #1e293b; color: #a5b4fc; padding: 5px 8px; text-align: left; border: 1px solid #374151; font-weight: 600; }
|
|
172
|
+
.doc-content td { padding: 4px 8px; border: 1px solid #374151; }
|
|
173
|
+
.doc-content tr:nth-child(even) { background: rgba(30,41,59,0.3); }
|
|
174
|
+
.doc-content a { color: #818cf8; text-decoration: none; }
|
|
175
|
+
.doc-content a:hover { text-decoration: underline; }
|
|
176
|
+
.doc-content hr { border: none; border-top: 1px solid #374151; margin: 10px 0; }
|
|
177
|
+
.doc-content strong { color: #f3f4f6; }
|
|
178
|
+
.doc-content em { color: #c4b5fd; }
|
|
179
|
+
.doc-loading { text-align: center; color: #6b7280; padding: 16px; font-size: 12px; }
|
|
180
|
+
.doc-error { text-align: center; color: #f87171; padding: 12px; font-size: 12px; }
|
|
181
|
+
.doc-toggle { background: none; border: 1px solid #4b5563; color: #9ca3af; padding: 2px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; }
|
|
182
|
+
.doc-toggle:hover { border-color: #6b7280; color: #d1d5db; }
|
|
183
|
+
|
|
184
|
+
/* Page Views */
|
|
185
|
+
.page-view { display: none; }
|
|
186
|
+
.page-view.active { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
187
|
+
.page-graph.active { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
188
|
+
|
|
189
|
+
/* Stats Dashboard */
|
|
190
|
+
.stats-page { padding: 24px; overflow-y: auto; background: #111827; flex: 1; }
|
|
191
|
+
.stats-header { margin-bottom: 24px; }
|
|
192
|
+
.stats-header h2 { font-size: 22px; font-weight: 700; color: #f3f4f6; margin-bottom: 4px; }
|
|
193
|
+
.stats-header p { font-size: 13px; color: #6b7280; }
|
|
194
|
+
|
|
195
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 28px; }
|
|
196
|
+
.stat-card { background: #1f2937; border: 1px solid #374151; border-radius: 12px; padding: 20px; position: relative; overflow: hidden; }
|
|
197
|
+
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; }
|
|
198
|
+
.stat-card.purple::before { background: linear-gradient(90deg, #6366f1, #8b5cf6); }
|
|
199
|
+
.stat-card.green::before { background: linear-gradient(90deg, #059669, #10b981); }
|
|
200
|
+
.stat-card.blue::before { background: linear-gradient(90deg, #2563eb, #3b82f6); }
|
|
201
|
+
.stat-card.amber::before { background: linear-gradient(90deg, #d97706, #f59e0b); }
|
|
202
|
+
.stat-card.rose::before { background: linear-gradient(90deg, #e11d48, #f43f5e); }
|
|
203
|
+
.stat-card-icon { font-size: 28px; margin-bottom: 8px; }
|
|
204
|
+
.stat-card-value { font-size: 32px; font-weight: 800; color: #f3f4f6; line-height: 1; }
|
|
205
|
+
.stat-card-label { font-size: 12px; color: #9ca3af; margin-top: 6px; }
|
|
206
|
+
.stat-card-sub { font-size: 11px; color: #6b7280; margin-top: 4px; }
|
|
207
|
+
|
|
208
|
+
.stats-section { margin-bottom: 28px; }
|
|
209
|
+
.stats-section-title { font-size: 15px; font-weight: 600; color: #e5e7eb; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
|
|
210
|
+
.stats-section-title .sec-icon { font-size: 18px; }
|
|
211
|
+
|
|
212
|
+
/* Overall Progress Ring */
|
|
213
|
+
.progress-ring-wrap { display: flex; align-items: center; gap: 24px; background: #1f2937; border: 1px solid #374151; border-radius: 12px; padding: 24px; margin-bottom: 28px; }
|
|
214
|
+
.progress-ring-info { flex: 1; }
|
|
215
|
+
.progress-ring-info h3 { font-size: 18px; font-weight: 700; color: #f3f4f6; margin-bottom: 4px; }
|
|
216
|
+
.progress-ring-info p { font-size: 13px; color: #9ca3af; margin-bottom: 12px; }
|
|
217
|
+
.progress-ring-info .motivate { font-size: 14px; color: #fbbf24; font-weight: 600; }
|
|
218
|
+
.ring-svg { flex-shrink: 0; }
|
|
219
|
+
|
|
220
|
+
/* Priority Bars */
|
|
221
|
+
.priority-bars { display: flex; flex-direction: column; gap: 12px; background: #1f2937; border: 1px solid #374151; border-radius: 12px; padding: 20px; }
|
|
222
|
+
.priority-row { display: flex; align-items: center; gap: 12px; }
|
|
223
|
+
.priority-label { width: 32px; font-size: 12px; font-weight: 700; text-align: center; padding: 2px 0; border-radius: 4px; flex-shrink: 0; }
|
|
224
|
+
.priority-label.P0 { background: #7f1d1d; color: #fca5a5; }
|
|
225
|
+
.priority-label.P1 { background: #78350f; color: #fde68a; }
|
|
226
|
+
.priority-label.P2 { background: #1e3a5f; color: #93c5fd; }
|
|
227
|
+
.priority-bar-track { flex: 1; height: 10px; background: #374151; border-radius: 5px; overflow: hidden; }
|
|
228
|
+
.priority-bar-fill { height: 100%; border-radius: 5px; transition: width 0.5s; }
|
|
229
|
+
.priority-bar-fill.P0 { background: linear-gradient(90deg, #dc2626, #f87171); }
|
|
230
|
+
.priority-bar-fill.P1 { background: linear-gradient(90deg, #d97706, #fbbf24); }
|
|
231
|
+
.priority-bar-fill.P2 { background: linear-gradient(90deg, #2563eb, #60a5fa); }
|
|
232
|
+
.priority-nums { font-size: 11px; color: #9ca3af; width: 70px; text-align: right; flex-shrink: 0; }
|
|
233
|
+
|
|
234
|
+
/* Phase List */
|
|
235
|
+
.phase-list { display: flex; flex-direction: column; gap: 8px; }
|
|
236
|
+
.phase-item { background: #1f2937; border: 1px solid #374151; border-radius: 10px; padding: 14px 16px; display: flex; align-items: center; gap: 14px; }
|
|
237
|
+
.phase-status-icon { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
|
|
238
|
+
.phase-status-icon.completed { background: #064e3b; color: #6ee7b7; }
|
|
239
|
+
.phase-status-icon.in_progress { background: #1e3a5f; color: #93c5fd; }
|
|
240
|
+
.phase-status-icon.pending { background: #374151; color: #6b7280; }
|
|
241
|
+
.phase-info { flex: 1; min-width: 0; }
|
|
242
|
+
.phase-info-title { font-size: 13px; font-weight: 600; color: #e5e7eb; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
243
|
+
.phase-info-sub { font-size: 11px; color: #6b7280; margin-top: 2px; }
|
|
244
|
+
.phase-bar-mini { width: 80px; height: 6px; background: #374151; border-radius: 3px; overflow: hidden; flex-shrink: 0; }
|
|
245
|
+
.phase-bar-mini-fill { height: 100%; background: #10b981; border-radius: 3px; }
|
|
246
|
+
.phase-pct { font-size: 12px; font-weight: 700; color: #9ca3af; width: 40px; text-align: right; flex-shrink: 0; }
|
|
247
|
+
.phase-item-wrap { background: #1f2937; border: 1px solid #374151; border-radius: 10px; overflow: hidden; }
|
|
248
|
+
.phase-item-main { display: flex; align-items: center; gap: 14px; padding: 14px 16px; cursor: pointer; transition: background 0.15s; }
|
|
249
|
+
.phase-item-main:hover { background: rgba(55,65,81,0.3); }
|
|
250
|
+
.phase-expand-icon { width: 16px; font-size: 10px; color: #6b7280; flex-shrink: 0; transition: transform 0.2s; text-align: center; }
|
|
251
|
+
.phase-item-wrap.expanded .phase-expand-icon { transform: rotate(90deg); }
|
|
252
|
+
.phase-subtasks { display: none; border-top: 1px solid #374151; padding: 6px 0; }
|
|
253
|
+
.phase-item-wrap.expanded .phase-subtasks { display: block; }
|
|
254
|
+
.phase-sub-item { display: flex; align-items: center; gap: 8px; padding: 4px 16px 4px 62px; font-size: 12px; }
|
|
255
|
+
.phase-sub-icon { width: 14px; height: 14px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; flex-shrink: 0; }
|
|
256
|
+
.phase-sub-icon.completed { background: #064e3b; color: #6ee7b7; }
|
|
257
|
+
.phase-sub-icon.in_progress { background: #1e3a5f; color: #93c5fd; }
|
|
258
|
+
.phase-sub-icon.pending { background: #374151; color: #6b7280; }
|
|
259
|
+
.phase-sub-icon.cancelled { background: #451a03; color: #fbbf24; }
|
|
260
|
+
.phase-sub-name { color: #d1d5db; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
261
|
+
.phase-sub-name.completed { color: #6ee7b7; text-decoration: line-through; text-decoration-color: rgba(110,231,183,0.3); }
|
|
262
|
+
.phase-sub-id { color: #4b5563; font-size: 10px; font-family: monospace; flex-shrink: 0; }
|
|
263
|
+
.phase-time { color: #6ee7b7; font-size: 10px; opacity: 0.8; }
|
|
264
|
+
.phase-sub-time { color: #6ee7b7; font-size: 10px; opacity: 0.7; flex-shrink: 0; margin-left: auto; }
|
|
265
|
+
.phase-time { color: #6ee7b7; font-size: 10px; }
|
|
266
|
+
.phase-sub-time { color: #6ee7b7; font-size: 10px; flex-shrink: 0; margin-left: auto; }
|
|
267
|
+
|
|
268
|
+
/* Module Cards */
|
|
269
|
+
.module-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
|
|
270
|
+
.module-card { background: #1f2937; border: 1px solid #374151; border-radius: 10px; padding: 16px; }
|
|
271
|
+
.module-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
272
|
+
.module-card-dot { width: 10px; height: 10px; border-radius: 50%; background: #10b981; flex-shrink: 0; }
|
|
273
|
+
.module-card-name { font-size: 13px; font-weight: 600; color: #e5e7eb; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
274
|
+
.module-card-bar { width: 100%; height: 6px; background: #374151; border-radius: 3px; overflow: hidden; margin-bottom: 8px; }
|
|
275
|
+
.module-card-bar-fill { height: 100%; background: linear-gradient(90deg, #059669, #10b981); border-radius: 3px; }
|
|
276
|
+
.module-card-stats { display: flex; justify-content: space-between; font-size: 11px; color: #6b7280; }
|
|
100
277
|
|
|
101
278
|
/* Debug bar */
|
|
102
|
-
.debug { position:
|
|
279
|
+
.debug { position: absolute; bottom: 0; left: 12px; background: rgba(31,41,55,0.9); border: 1px solid #374151; border-radius: 8px 8px 0 0; padding: 8px 12px; font-size: 11px; color: #9ca3af; z-index: 30; max-width: 400px; }
|
|
103
280
|
.debug .ok { color: #10b981; }
|
|
104
281
|
.debug .err { color: #f87171; }
|
|
105
282
|
</style>
|
|
106
283
|
</head>
|
|
107
284
|
<body>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<div class="
|
|
112
|
-
<
|
|
285
|
+
<div class="app-layout">
|
|
286
|
+
<!-- Sidebar -->
|
|
287
|
+
<div class="sidebar" id="sidebar">
|
|
288
|
+
<div class="sidebar-header" onclick="toggleSidebar()" title="展开/收起导航">
|
|
289
|
+
<span class="sidebar-logo sidebar-logo-short">Ai</span>
|
|
290
|
+
<span class="sidebar-logo sidebar-logo-full">AiFastDb-DevPlan</span>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="sidebar-nav">
|
|
293
|
+
<div class="nav-item active" data-page="graph" onclick="navTo('graph')">
|
|
294
|
+
<span class="nav-item-icon">🔗</span>
|
|
295
|
+
<span class="nav-item-text">图谱可视化</span>
|
|
296
|
+
<span class="nav-tooltip">图谱可视化</span>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="nav-item disabled" data-page="tasks" onclick="navTo('tasks')">
|
|
299
|
+
<span class="nav-item-icon">📋</span>
|
|
300
|
+
<span class="nav-item-text">任务看板</span>
|
|
301
|
+
<span class="nav-item-badge">即将推出</span>
|
|
302
|
+
<span class="nav-tooltip">任务看板 (即将推出)</span>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="nav-item disabled" data-page="docs" onclick="navTo('docs')">
|
|
305
|
+
<span class="nav-item-icon">📄</span>
|
|
306
|
+
<span class="nav-item-text">文档浏览</span>
|
|
307
|
+
<span class="nav-item-badge">即将推出</span>
|
|
308
|
+
<span class="nav-tooltip">文档浏览 (即将推出)</span>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="nav-item" data-page="stats" onclick="navTo('stats')">
|
|
311
|
+
<span class="nav-item-icon">📊</span>
|
|
312
|
+
<span class="nav-item-text">统计仪表盘</span>
|
|
313
|
+
<span class="nav-tooltip">统计仪表盘</span>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="sidebar-footer">
|
|
317
|
+
<div class="nav-item disabled" data-page="settings" onclick="navTo('settings')">
|
|
318
|
+
<span class="nav-item-icon">⚙️</span>
|
|
319
|
+
<span class="nav-item-text">项目设置</span>
|
|
320
|
+
<span class="nav-item-badge">即将推出</span>
|
|
321
|
+
<span class="nav-tooltip">项目设置 (即将推出)</span>
|
|
322
|
+
</div>
|
|
113
323
|
</div>
|
|
114
324
|
</div>
|
|
115
325
|
|
|
116
|
-
<!--
|
|
117
|
-
<div class="
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
326
|
+
<!-- Main Content -->
|
|
327
|
+
<div class="main-content">
|
|
328
|
+
|
|
329
|
+
<!-- ===== PAGE: Graph ===== -->
|
|
330
|
+
<div class="page-view page-graph active" id="pageGraph">
|
|
331
|
+
<!-- Header -->
|
|
332
|
+
<div class="header">
|
|
333
|
+
<h1><span class="icon">🔗</span> DevPlan 图谱 <span class="project-name">${projectName}</span></h1>
|
|
334
|
+
<div class="stats-bar" id="statsBar">
|
|
335
|
+
<div class="stat"><span>加载中...</span></div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!-- Graph -->
|
|
340
|
+
<div class="graph-container">
|
|
341
|
+
<div class="loading" id="loading"><div><div class="spinner"></div><p style="margin-top:12px;color:#9ca3af;">加载图谱数据...</p></div></div>
|
|
342
|
+
<div id="graph"></div>
|
|
343
|
+
<div class="panel" id="panel">
|
|
344
|
+
<div class="panel-resize-handle" id="panelResizeHandle"></div>
|
|
345
|
+
<div class="panel-header" id="panelHeader">
|
|
346
|
+
<span class="panel-title" id="panelTitle">节点详情</span>
|
|
347
|
+
<button class="panel-close" onclick="closePanel()">✕</button>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="panel-body" id="panelBody"></div>
|
|
350
|
+
</div>
|
|
351
|
+
<!-- Debug info -->
|
|
352
|
+
<div class="debug" id="debug">状态: 正在加载 vis-network...</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<!-- Legend + Filters (merged) -->
|
|
356
|
+
<div class="legend">
|
|
357
|
+
<!-- 筛选复选框 -->
|
|
358
|
+
<label class="filter-check"><input type="checkbox" checked data-type="module" onchange="toggleFilter('module')"> 模块</label>
|
|
359
|
+
<label class="filter-check"><input type="checkbox" checked data-type="main-task" onchange="toggleFilter('main-task')"> 主任务</label>
|
|
360
|
+
<label class="filter-check"><input type="checkbox" checked data-type="sub-task" onchange="toggleFilter('sub-task')"> 子任务</label>
|
|
361
|
+
<label class="filter-check"><input type="checkbox" checked data-type="document" onchange="toggleFilter('document')"> 文档</label>
|
|
362
|
+
<div class="legend-divider"></div>
|
|
363
|
+
<!-- 图例 -->
|
|
364
|
+
<div class="legend-item"><div class="legend-icon star"></div> 项目</div>
|
|
365
|
+
<div class="legend-item"><div class="legend-icon diamond"></div> 模块</div>
|
|
366
|
+
<div class="legend-item"><div class="legend-icon circle"></div> 主任务</div>
|
|
367
|
+
<div class="legend-item"><div class="legend-icon dot"></div> 子任务</div>
|
|
368
|
+
<div class="legend-item"><div class="legend-icon square"></div> 文档</div>
|
|
369
|
+
<div class="legend-divider"></div>
|
|
370
|
+
<div class="legend-item"><div class="legend-line solid"></div> 主任务</div>
|
|
371
|
+
<div class="legend-item"><div class="legend-line thin"></div> 子任务</div>
|
|
372
|
+
<div class="legend-item"><div class="legend-line dashed"></div> 文档</div>
|
|
373
|
+
<div class="legend-item"><div class="legend-line dotted"></div> 模块关联</div>
|
|
374
|
+
<div class="legend-item"><div class="legend-line task-doc"></div> 任务-文档</div>
|
|
375
|
+
</div>
|
|
124
376
|
</div>
|
|
125
|
-
<div class="sep"></div>
|
|
126
|
-
<button class="refresh-btn" onclick="loadData()">↻ 刷新</button>
|
|
127
|
-
</div>
|
|
128
377
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
378
|
+
<!-- ===== PAGE: Stats Dashboard ===== -->
|
|
379
|
+
<div class="page-view" id="pageStats">
|
|
380
|
+
<div class="stats-page" id="statsPageContent">
|
|
381
|
+
<div class="stats-header">
|
|
382
|
+
<h2>📊 项目仪表盘 — ${projectName}</h2>
|
|
383
|
+
<p>项目开发进度总览与关键指标</p>
|
|
384
|
+
</div>
|
|
385
|
+
<!-- 内容由 JS 动态渲染 -->
|
|
386
|
+
<div id="statsContent"><div style="text-align:center;padding:60px;color:#6b7280;">加载中...</div></div>
|
|
137
387
|
</div>
|
|
138
|
-
<div class="panel-body" id="panelBody"></div>
|
|
139
388
|
</div>
|
|
140
|
-
</div>
|
|
141
389
|
|
|
142
|
-
<!-- Legend -->
|
|
143
|
-
<div class="legend">
|
|
144
|
-
<div class="legend-item"><div class="legend-icon star"></div> 项目</div>
|
|
145
|
-
<div class="legend-item"><div class="legend-icon diamond"></div> 模块</div>
|
|
146
|
-
<div class="legend-item"><div class="legend-icon circle"></div> 主任务</div>
|
|
147
|
-
<div class="legend-item"><div class="legend-icon dot"></div> 子任务</div>
|
|
148
|
-
<div class="legend-item"><div class="legend-icon square"></div> 文档</div>
|
|
149
|
-
<div style="width:1px;height:16px;background:#374151;"></div>
|
|
150
|
-
<div class="legend-item"><div class="legend-line solid"></div> 主任务</div>
|
|
151
|
-
<div class="legend-item"><div class="legend-line thin"></div> 子任务</div>
|
|
152
|
-
<div class="legend-item"><div class="legend-line dashed"></div> 文档</div>
|
|
153
|
-
<div class="legend-item"><div class="legend-line dotted"></div> 模块关联</div>
|
|
154
390
|
</div>
|
|
155
|
-
|
|
156
|
-
<!-- Debug info -->
|
|
157
|
-
<div class="debug" id="debug">状态: 正在加载 vis-network...</div>
|
|
391
|
+
</div>
|
|
158
392
|
|
|
159
393
|
<script>
|
|
394
|
+
// ========== Sidebar ==========
|
|
395
|
+
function toggleSidebar() {
|
|
396
|
+
var sidebar = document.getElementById('sidebar');
|
|
397
|
+
if (!sidebar) return;
|
|
398
|
+
sidebar.classList.toggle('expanded');
|
|
399
|
+
var isExpanded = sidebar.classList.contains('expanded');
|
|
400
|
+
// 记住偏好
|
|
401
|
+
try { localStorage.setItem('devplan_sidebar_expanded', isExpanded ? '1' : '0'); } catch(e) {}
|
|
402
|
+
// 通知 vis-network 重新适配尺寸
|
|
403
|
+
setTimeout(function() { if (network) network.redraw(); }, 300);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
var currentPage = 'graph';
|
|
407
|
+
var pageMap = { graph: 'pageGraph', stats: 'pageStats' };
|
|
408
|
+
|
|
409
|
+
function navTo(page) {
|
|
410
|
+
// 仅支持已实现的页面
|
|
411
|
+
if (!pageMap[page]) return;
|
|
412
|
+
if (page === currentPage) return;
|
|
413
|
+
|
|
414
|
+
// 切换页面视图
|
|
415
|
+
var oldView = document.getElementById(pageMap[currentPage]);
|
|
416
|
+
var newView = document.getElementById(pageMap[page]);
|
|
417
|
+
if (oldView) oldView.classList.remove('active');
|
|
418
|
+
if (newView) newView.classList.add('active');
|
|
419
|
+
|
|
420
|
+
// 切换导航高亮
|
|
421
|
+
var items = document.querySelectorAll('.nav-item[data-page]');
|
|
422
|
+
for (var i = 0; i < items.length; i++) {
|
|
423
|
+
items[i].classList.remove('active');
|
|
424
|
+
if (items[i].getAttribute('data-page') === page) items[i].classList.add('active');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
currentPage = page;
|
|
428
|
+
|
|
429
|
+
// 按需加载页面数据
|
|
430
|
+
if (page === 'stats') loadStatsPage();
|
|
431
|
+
if (page === 'graph' && network) {
|
|
432
|
+
setTimeout(function() { network.redraw(); network.fit(); }, 100);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 恢复 sidebar 偏好
|
|
437
|
+
(function() {
|
|
438
|
+
try {
|
|
439
|
+
var saved = localStorage.getItem('devplan_sidebar_expanded');
|
|
440
|
+
if (saved === '1') {
|
|
441
|
+
var sidebar = document.getElementById('sidebar');
|
|
442
|
+
if (sidebar) { sidebar.classList.add('expanded'); }
|
|
443
|
+
}
|
|
444
|
+
} catch(e) {}
|
|
445
|
+
})();
|
|
446
|
+
|
|
160
447
|
// ========== Debug ==========
|
|
161
448
|
var dbg = document.getElementById('debug');
|
|
162
449
|
function log(msg, ok) {
|
|
@@ -249,6 +536,7 @@ function edgeStyle(edge) {
|
|
|
249
536
|
if (label === 'has_sub_task') return { width: 1, color: { color: '#4b5563', highlight: '#818cf8' }, dashes: false, arrows: { to: { enabled: true, scaleFactor: 0.4 } } };
|
|
250
537
|
if (label === 'has_document') return { width: 1, color: { color: '#4b5563', highlight: '#a78bfa' }, dashes: [5, 5], arrows: { to: { enabled: true, scaleFactor: 0.4 } } };
|
|
251
538
|
if (label === 'module_has_task') return { width: 1.5, color: { color: '#065f46', highlight: '#34d399' }, dashes: [2, 4], arrows: { to: { enabled: true, scaleFactor: 0.5 } } };
|
|
539
|
+
if (label === 'task_has_doc') return { width: 1.5, color: { color: '#b45309', highlight: '#f59e0b' }, dashes: [4, 3], arrows: { to: { enabled: true, scaleFactor: 0.5 } } };
|
|
252
540
|
return { width: 1, color: { color: '#374151' }, dashes: false };
|
|
253
541
|
}
|
|
254
542
|
|
|
@@ -449,6 +737,61 @@ function renderGraph() {
|
|
|
449
737
|
}
|
|
450
738
|
|
|
451
739
|
// ========== Detail Panel ==========
|
|
740
|
+
|
|
741
|
+
/** 根据主任务节点 ID,从 allNodes/allEdges 中查找其所有子任务节点 */
|
|
742
|
+
function getSubTasksForMainTask(mainTaskNodeId) {
|
|
743
|
+
var subTaskIds = [];
|
|
744
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
745
|
+
var e = allEdges[i];
|
|
746
|
+
if (e.from === mainTaskNodeId && e.label === 'has_sub_task') {
|
|
747
|
+
subTaskIds.push(e.to);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
var subTasks = [];
|
|
751
|
+
var idSet = {};
|
|
752
|
+
for (var i = 0; i < subTaskIds.length; i++) idSet[subTaskIds[i]] = true;
|
|
753
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
754
|
+
if (idSet[allNodes[i].id]) {
|
|
755
|
+
subTasks.push(allNodes[i]);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return subTasks;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function getRelatedDocsForTask(taskNodeId) {
|
|
762
|
+
var docIds = [];
|
|
763
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
764
|
+
var e = allEdges[i];
|
|
765
|
+
if (e.from === taskNodeId && e.label === 'task_has_doc') {
|
|
766
|
+
docIds.push(e.to);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
var docs = [];
|
|
770
|
+
var idSet = {};
|
|
771
|
+
for (var i = 0; i < docIds.length; i++) idSet[docIds[i]] = true;
|
|
772
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
773
|
+
if (idSet[allNodes[i].id]) docs.push(allNodes[i]);
|
|
774
|
+
}
|
|
775
|
+
return docs;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function getRelatedTasksForDoc(docNodeId) {
|
|
779
|
+
var taskIds = [];
|
|
780
|
+
for (var i = 0; i < allEdges.length; i++) {
|
|
781
|
+
var e = allEdges[i];
|
|
782
|
+
if (e.to === docNodeId && e.label === 'task_has_doc') {
|
|
783
|
+
taskIds.push(e.from);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
var tasks = [];
|
|
787
|
+
var idSet = {};
|
|
788
|
+
for (var i = 0; i < taskIds.length; i++) idSet[taskIds[i]] = true;
|
|
789
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
790
|
+
if (idSet[allNodes[i].id]) tasks.push(allNodes[i]);
|
|
791
|
+
}
|
|
792
|
+
return tasks;
|
|
793
|
+
}
|
|
794
|
+
|
|
452
795
|
function showPanel(nodeId) {
|
|
453
796
|
var node = nodesDataSet.get(nodeId);
|
|
454
797
|
if (!node) return;
|
|
@@ -468,15 +811,72 @@ function showPanel(nodeId) {
|
|
|
468
811
|
html += row('任务ID', p.taskId);
|
|
469
812
|
html += row('优先级', '<span class="status-badge priority-' + (p.priority || 'P2') + '">' + (p.priority || 'P2') + '</span>');
|
|
470
813
|
html += row('状态', statusBadge(p.status));
|
|
814
|
+
if (p.completedAt) { html += row('完成时间', '<span style="color:#6ee7b7;">' + fmtTime(p.completedAt) + '</span>'); }
|
|
471
815
|
if (p.totalSubtasks !== undefined) {
|
|
472
816
|
var pct = p.totalSubtasks > 0 ? Math.round((p.completedSubtasks || 0) / p.totalSubtasks * 100) : 0;
|
|
473
817
|
html += row('子任务', (p.completedSubtasks || 0) + '/' + p.totalSubtasks);
|
|
474
818
|
html += '<div class="panel-progress"><div class="panel-progress-bar"><div class="panel-progress-fill" style="width:' + pct + '%"></div></div></div>';
|
|
475
819
|
}
|
|
820
|
+
|
|
821
|
+
// 查找并显示子任务列表
|
|
822
|
+
var subTasks = getSubTasksForMainTask(nodeId);
|
|
823
|
+
if (subTasks.length > 0) {
|
|
824
|
+
var completedCount = 0;
|
|
825
|
+
for (var si = 0; si < subTasks.length; si++) {
|
|
826
|
+
if ((subTasks[si].properties || {}).status === 'completed') completedCount++;
|
|
827
|
+
}
|
|
828
|
+
html += '<div class="subtask-section">';
|
|
829
|
+
html += '<div class="subtask-section-title"><span>子任务列表</span><span style="color:#6b7280;">' + completedCount + '/' + subTasks.length + '</span></div>';
|
|
830
|
+
html += '<ul class="subtask-list">';
|
|
831
|
+
// 排序:进行中 > 待开始 > 已完成 > 已取消
|
|
832
|
+
var statusOrder = { in_progress: 0, pending: 1, completed: 2, cancelled: 3 };
|
|
833
|
+
subTasks.sort(function(a, b) {
|
|
834
|
+
var sa = (a.properties || {}).status || 'pending';
|
|
835
|
+
var sb = (b.properties || {}).status || 'pending';
|
|
836
|
+
return (statusOrder[sa] || 1) - (statusOrder[sb] || 1);
|
|
837
|
+
});
|
|
838
|
+
for (var si = 0; si < subTasks.length; si++) {
|
|
839
|
+
var st = subTasks[si];
|
|
840
|
+
var stProps = st.properties || {};
|
|
841
|
+
var stStatus = stProps.status || 'pending';
|
|
842
|
+
var stIcon = stStatus === 'completed' ? '✓' : stStatus === 'in_progress' ? '▶' : stStatus === 'cancelled' ? '✗' : '○';
|
|
843
|
+
var stTime = stProps.completedAt ? fmtTime(stProps.completedAt) : '';
|
|
844
|
+
html += '<li class="subtask-item">';
|
|
845
|
+
html += '<span class="subtask-icon ' + stStatus + '">' + stIcon + '</span>';
|
|
846
|
+
html += '<span class="subtask-name ' + stStatus + '" title="' + escHtml(st.label) + '">' + escHtml(st.label) + '</span>';
|
|
847
|
+
if (stTime) { html += '<span class="subtask-time">' + stTime + '</span>'; }
|
|
848
|
+
html += '<span class="subtask-id">' + escHtml(stProps.taskId || '') + '</span>';
|
|
849
|
+
html += '</li>';
|
|
850
|
+
}
|
|
851
|
+
html += '</ul>';
|
|
852
|
+
html += '</div>';
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// 查找并显示关联文档
|
|
856
|
+
var relDocs = getRelatedDocsForTask(nodeId);
|
|
857
|
+
if (relDocs.length > 0) {
|
|
858
|
+
html += '<div class="subtask-section">';
|
|
859
|
+
html += '<div class="subtask-section-title"><span style="color:#f59e0b;">关联文档</span><span style="color:#6b7280;">' + relDocs.length + '</span></div>';
|
|
860
|
+
html += '<ul class="subtask-list">';
|
|
861
|
+
for (var di = 0; di < relDocs.length; di++) {
|
|
862
|
+
var doc = relDocs[di];
|
|
863
|
+
var docProps = doc.properties || {};
|
|
864
|
+
var docLabel = docProps.section || '';
|
|
865
|
+
if (docProps.subSection) docLabel += ' / ' + docProps.subSection;
|
|
866
|
+
html += '<li class="subtask-item" style="cursor:pointer;" onclick="network.selectNodes([\'' + doc.id + '\']);showPanel(\'' + doc.id + '\')">';
|
|
867
|
+
html += '<span class="subtask-icon" style="color:#f59e0b;">📄</span>';
|
|
868
|
+
html += '<span class="subtask-name" title="' + escHtml(doc.label) + '">' + escHtml(doc.label) + '</span>';
|
|
869
|
+
html += '<span class="subtask-id">' + escHtml(docLabel) + '</span>';
|
|
870
|
+
html += '</li>';
|
|
871
|
+
}
|
|
872
|
+
html += '</ul>';
|
|
873
|
+
html += '</div>';
|
|
874
|
+
}
|
|
476
875
|
} else if (node._type === 'sub-task') {
|
|
477
876
|
html += row('任务ID', p.taskId);
|
|
478
877
|
html += row('父任务', p.parentTaskId);
|
|
479
878
|
html += row('状态', statusBadge(p.status));
|
|
879
|
+
if (p.completedAt) { html += row('完成时间', '<span style="color:#6ee7b7;">' + fmtTime(p.completedAt) + '</span>'); }
|
|
480
880
|
} else if (node._type === 'module') {
|
|
481
881
|
html += row('模块ID', p.moduleId);
|
|
482
882
|
html += row('状态', statusBadge(p.status || 'active'));
|
|
@@ -485,38 +885,641 @@ function showPanel(nodeId) {
|
|
|
485
885
|
html += row('类型', p.section);
|
|
486
886
|
if (p.subSection) html += row('子类型', p.subSection);
|
|
487
887
|
html += row('版本', p.version);
|
|
888
|
+
|
|
889
|
+
// 查找并显示关联任务
|
|
890
|
+
var relTasks = getRelatedTasksForDoc(nodeId);
|
|
891
|
+
if (relTasks.length > 0) {
|
|
892
|
+
html += '<div class="subtask-section">';
|
|
893
|
+
html += '<div class="subtask-section-title"><span style="color:#6366f1;">关联任务</span><span style="color:#6b7280;">' + relTasks.length + '</span></div>';
|
|
894
|
+
html += '<ul class="subtask-list">';
|
|
895
|
+
for (var ti = 0; ti < relTasks.length; ti++) {
|
|
896
|
+
var task = relTasks[ti];
|
|
897
|
+
var tProps = task.properties || {};
|
|
898
|
+
var tStatus = tProps.status || 'pending';
|
|
899
|
+
var tIcon = tStatus === 'completed' ? '✓' : tStatus === 'in_progress' ? '▶' : '○';
|
|
900
|
+
html += '<li class="subtask-item" style="cursor:pointer;" onclick="network.selectNodes([\'' + task.id + '\']);showPanel(\'' + task.id + '\')">';
|
|
901
|
+
html += '<span class="subtask-icon ' + tStatus + '">' + tIcon + '</span>';
|
|
902
|
+
html += '<span class="subtask-name" title="' + escHtml(task.label) + '">' + escHtml(task.label) + '</span>';
|
|
903
|
+
html += '<span class="subtask-id">' + escHtml(tProps.taskId || '') + '</span>';
|
|
904
|
+
html += '</li>';
|
|
905
|
+
}
|
|
906
|
+
html += '</ul>';
|
|
907
|
+
html += '</div>';
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// 文档内容区域 — 先显示加载中,稍后异步填充
|
|
911
|
+
html += '<div class="doc-section">';
|
|
912
|
+
html += '<div class="doc-section-title"><span>文档内容</span><button class="doc-toggle" id="docToggleBtn" onclick="toggleDocContent()">收起</button></div>';
|
|
913
|
+
html += '<div id="docContentArea"><div class="doc-loading">加载中...</div></div>';
|
|
914
|
+
html += '</div>';
|
|
488
915
|
} else if (node._type === 'project') {
|
|
489
916
|
html += row('类型', '项目根节点');
|
|
490
917
|
}
|
|
491
918
|
|
|
492
919
|
body.innerHTML = html;
|
|
493
920
|
panel.classList.add('show');
|
|
921
|
+
|
|
922
|
+
// 如果是文档节点,异步加载内容
|
|
923
|
+
if (node._type === 'document') {
|
|
924
|
+
loadDocContent(p.section, p.subSection);
|
|
925
|
+
}
|
|
494
926
|
}
|
|
495
927
|
|
|
496
928
|
function closePanel() { document.getElementById('panel').classList.remove('show'); }
|
|
497
929
|
|
|
930
|
+
// ========== Panel Resize ==========
|
|
931
|
+
var panelDefaultWidth = 340;
|
|
932
|
+
var panelExpandedWidth = 680;
|
|
933
|
+
var panelIsExpanded = false;
|
|
934
|
+
var panelResizing = false;
|
|
935
|
+
|
|
936
|
+
// 双击标题栏切换宽度
|
|
937
|
+
(function() {
|
|
938
|
+
var header = document.getElementById('panelHeader');
|
|
939
|
+
if (!header) return;
|
|
940
|
+
header.addEventListener('dblclick', function(e) {
|
|
941
|
+
// 不要在关闭按钮上触发
|
|
942
|
+
if (e.target.closest && e.target.closest('.panel-close')) return;
|
|
943
|
+
var panel = document.getElementById('panel');
|
|
944
|
+
if (!panel) return;
|
|
945
|
+
panelIsExpanded = !panelIsExpanded;
|
|
946
|
+
var targetWidth = panelIsExpanded ? panelExpandedWidth : panelDefaultWidth;
|
|
947
|
+
panel.style.transition = 'width 0.25s ease';
|
|
948
|
+
panel.style.width = targetWidth + 'px';
|
|
949
|
+
setTimeout(function() { panel.style.transition = 'none'; }, 260);
|
|
950
|
+
});
|
|
951
|
+
})();
|
|
952
|
+
|
|
953
|
+
// 拖拽左边线调整宽度
|
|
954
|
+
(function() {
|
|
955
|
+
var handle = document.getElementById('panelResizeHandle');
|
|
956
|
+
var panel = document.getElementById('panel');
|
|
957
|
+
if (!handle || !panel) return;
|
|
958
|
+
|
|
959
|
+
var startX = 0;
|
|
960
|
+
var startWidth = 0;
|
|
961
|
+
|
|
962
|
+
handle.addEventListener('mousedown', function(e) {
|
|
963
|
+
e.preventDefault();
|
|
964
|
+
e.stopPropagation();
|
|
965
|
+
panelResizing = true;
|
|
966
|
+
startX = e.clientX;
|
|
967
|
+
startWidth = panel.offsetWidth;
|
|
968
|
+
handle.classList.add('active');
|
|
969
|
+
document.body.style.cursor = 'col-resize';
|
|
970
|
+
document.body.style.userSelect = 'none';
|
|
971
|
+
|
|
972
|
+
function onMouseMove(ev) {
|
|
973
|
+
if (!panelResizing) return;
|
|
974
|
+
// 面板在右侧,向左拖 = 增大宽度
|
|
975
|
+
var dx = startX - ev.clientX;
|
|
976
|
+
var newWidth = Math.max(280, Math.min(startWidth + dx, window.innerWidth - 40));
|
|
977
|
+
panel.style.width = newWidth + 'px';
|
|
978
|
+
panelIsExpanded = newWidth > (panelDefaultWidth + 50);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function onMouseUp() {
|
|
982
|
+
panelResizing = false;
|
|
983
|
+
handle.classList.remove('active');
|
|
984
|
+
document.body.style.cursor = '';
|
|
985
|
+
document.body.style.userSelect = '';
|
|
986
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
987
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
991
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
992
|
+
});
|
|
993
|
+
})();
|
|
994
|
+
|
|
498
995
|
function row(label, value) { return '<div class="panel-row"><span class="panel-label">' + label + '</span><span class="panel-value">' + (value || '-') + '</span></div>'; }
|
|
499
996
|
function statusBadge(s) { return '<span class="status-badge status-' + (s || 'pending') + '">' + statusText(s) + '</span>'; }
|
|
500
997
|
function statusText(s) { var m = { completed: '已完成', in_progress: '进行中', pending: '待开始', cancelled: '已取消', active: '活跃', planning: '规划中', deprecated: '已废弃' }; return m[s] || s || '未知'; }
|
|
501
998
|
function escHtml(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
|
|
502
999
|
|
|
1000
|
+
// 格式化时间戳(毫秒)为可读日期时间,当年省略年份
|
|
1001
|
+
function fmtTime(ts) {
|
|
1002
|
+
if (!ts) return '';
|
|
1003
|
+
var d = new Date(ts);
|
|
1004
|
+
var m = String(d.getMonth() + 1).padStart(2, '0');
|
|
1005
|
+
var day = String(d.getDate()).padStart(2, '0');
|
|
1006
|
+
var h = String(d.getHours()).padStart(2, '0');
|
|
1007
|
+
var min = String(d.getMinutes()).padStart(2, '0');
|
|
1008
|
+
var time = m + '-' + day + ' ' + h + ':' + min;
|
|
1009
|
+
if (d.getFullYear() !== new Date().getFullYear()) {
|
|
1010
|
+
time = d.getFullYear() + '-' + time;
|
|
1011
|
+
}
|
|
1012
|
+
return time;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ========== Phase Expand (Stats page) ==========
|
|
1016
|
+
function togglePhaseExpand(el) {
|
|
1017
|
+
var wrap = el.closest('.phase-item-wrap');
|
|
1018
|
+
if (wrap) wrap.classList.toggle('expanded');
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ========== Document Content ==========
|
|
1022
|
+
var docContentVisible = true;
|
|
1023
|
+
|
|
1024
|
+
function toggleDocContent() {
|
|
1025
|
+
var area = document.getElementById('docContentArea');
|
|
1026
|
+
var btn = document.getElementById('docToggleBtn');
|
|
1027
|
+
if (!area) return;
|
|
1028
|
+
docContentVisible = !docContentVisible;
|
|
1029
|
+
area.style.display = docContentVisible ? 'block' : 'none';
|
|
1030
|
+
if (btn) btn.textContent = docContentVisible ? '收起' : '展开';
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function loadDocContent(section, subSection) {
|
|
1034
|
+
var area = document.getElementById('docContentArea');
|
|
1035
|
+
if (!area) return;
|
|
1036
|
+
var url = '/api/doc?section=' + encodeURIComponent(section || '');
|
|
1037
|
+
if (subSection) url += '&subSection=' + encodeURIComponent(subSection);
|
|
1038
|
+
|
|
1039
|
+
fetch(url).then(function(r) {
|
|
1040
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
1041
|
+
return r.json();
|
|
1042
|
+
}).then(function(doc) {
|
|
1043
|
+
if (!doc || !doc.content) {
|
|
1044
|
+
area.innerHTML = '<div class="doc-error">文档内容为空</div>';
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
area.innerHTML = '<div class="doc-content">' + renderMarkdown(doc.content) + '</div>';
|
|
1048
|
+
docContentVisible = true;
|
|
1049
|
+
var btn = document.getElementById('docToggleBtn');
|
|
1050
|
+
if (btn) btn.textContent = '收起';
|
|
1051
|
+
}).catch(function(err) {
|
|
1052
|
+
area.innerHTML = '<div class="doc-error">加载失败: ' + escHtml(err.message) + '</div>';
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** 简易 Markdown 渲染 — 支持标题、粗体、斜体、代码、列表、表格、引用、链接、分隔线 */
|
|
1057
|
+
function renderMarkdown(md) {
|
|
1058
|
+
if (!md) return '';
|
|
1059
|
+
|
|
1060
|
+
// 先处理代码块(防止内部被其他规则干扰)
|
|
1061
|
+
var codeBlocks = [];
|
|
1062
|
+
md = md.replace(/\`\`\`(\\w*)?\\n([\\s\\S]*?)\`\`\`/g, function(m, lang, code) {
|
|
1063
|
+
var idx = codeBlocks.length;
|
|
1064
|
+
codeBlocks.push('<pre><code>' + escHtml(code.replace(/\\n$/, '')) + '</code></pre>');
|
|
1065
|
+
return '%%CODEBLOCK_' + idx + '%%';
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// 按行处理
|
|
1069
|
+
var lines = md.split('\\n');
|
|
1070
|
+
var html = '';
|
|
1071
|
+
var inTable = false;
|
|
1072
|
+
var inList = false;
|
|
1073
|
+
var listType = '';
|
|
1074
|
+
|
|
1075
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1076
|
+
var line = lines[i];
|
|
1077
|
+
|
|
1078
|
+
// 代码块占位符
|
|
1079
|
+
var cbMatch = line.match(/^%%CODEBLOCK_(\\d+)%%$/);
|
|
1080
|
+
if (cbMatch) {
|
|
1081
|
+
if (inList) { html += '</' + listType + '>'; inList = false; }
|
|
1082
|
+
if (inTable) { html += '</table>'; inTable = false; }
|
|
1083
|
+
html += codeBlocks[parseInt(cbMatch[1])];
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// 表格行
|
|
1088
|
+
if (line.match(/^\\|(.+)\\|\\s*$/)) {
|
|
1089
|
+
if (inList) { html += '</' + listType + '>'; inList = false; }
|
|
1090
|
+
// 跳过分隔行
|
|
1091
|
+
if (line.match(/^\\|[\\s\\-:|]+\\|\\s*$/)) continue;
|
|
1092
|
+
var cells = line.split('|').filter(function(c, idx, arr) { return idx > 0 && idx < arr.length - 1; });
|
|
1093
|
+
if (!inTable) {
|
|
1094
|
+
html += '<table>';
|
|
1095
|
+
html += '<tr>' + cells.map(function(c) { return '<th>' + inlineFormat(c.trim()) + '</th>'; }).join('') + '</tr>';
|
|
1096
|
+
inTable = true;
|
|
1097
|
+
} else {
|
|
1098
|
+
html += '<tr>' + cells.map(function(c) { return '<td>' + inlineFormat(c.trim()) + '</td>'; }).join('') + '</tr>';
|
|
1099
|
+
}
|
|
1100
|
+
continue;
|
|
1101
|
+
} else if (inTable) {
|
|
1102
|
+
html += '</table>';
|
|
1103
|
+
inTable = false;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// 空行
|
|
1107
|
+
if (line.trim() === '') {
|
|
1108
|
+
if (inList) { html += '</' + listType + '>'; inList = false; }
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// 标题
|
|
1113
|
+
var hMatch = line.match(/^(#{1,4})\\s+(.+)$/);
|
|
1114
|
+
if (hMatch) {
|
|
1115
|
+
if (inList) { html += '</' + listType + '>'; inList = false; }
|
|
1116
|
+
var level = hMatch[1].length;
|
|
1117
|
+
html += '<h' + level + '>' + inlineFormat(hMatch[2]) + '</h' + level + '>';
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// 分隔线
|
|
1122
|
+
if (line.match(/^(\\*{3,}|-{3,}|_{3,})\\s*$/)) {
|
|
1123
|
+
if (inList) { html += '</' + listType + '>'; inList = false; }
|
|
1124
|
+
html += '<hr>';
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// 引用
|
|
1129
|
+
if (line.match(/^>\\s?/)) {
|
|
1130
|
+
if (inList) { html += '</' + listType + '>'; inList = false; }
|
|
1131
|
+
html += '<blockquote>' + inlineFormat(line.replace(/^>\\s?/, '')) + '</blockquote>';
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// 无序列表
|
|
1136
|
+
var ulMatch = line.match(/^\\s*[-*+]\\s+(.+)$/);
|
|
1137
|
+
if (ulMatch) {
|
|
1138
|
+
if (!inList || listType !== 'ul') {
|
|
1139
|
+
if (inList) html += '</' + listType + '>';
|
|
1140
|
+
html += '<ul>';
|
|
1141
|
+
inList = true;
|
|
1142
|
+
listType = 'ul';
|
|
1143
|
+
}
|
|
1144
|
+
html += '<li>' + inlineFormat(ulMatch[1]) + '</li>';
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// 有序列表
|
|
1149
|
+
var olMatch = line.match(/^\\s*\\d+\\.\\s+(.+)$/);
|
|
1150
|
+
if (olMatch) {
|
|
1151
|
+
if (!inList || listType !== 'ol') {
|
|
1152
|
+
if (inList) html += '</' + listType + '>';
|
|
1153
|
+
html += '<ol>';
|
|
1154
|
+
inList = true;
|
|
1155
|
+
listType = 'ol';
|
|
1156
|
+
}
|
|
1157
|
+
html += '<li>' + inlineFormat(olMatch[1]) + '</li>';
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// 普通段落
|
|
1162
|
+
if (inList) { html += '</' + listType + '>'; inList = false; }
|
|
1163
|
+
html += '<p>' + inlineFormat(line) + '</p>';
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (inList) html += '</' + listType + '>';
|
|
1167
|
+
if (inTable) html += '</table>';
|
|
1168
|
+
|
|
1169
|
+
return html;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/** 行内格式化:粗体、斜体、行内代码、链接 */
|
|
1173
|
+
function inlineFormat(text) {
|
|
1174
|
+
if (!text) return '';
|
|
1175
|
+
// 行内代码
|
|
1176
|
+
text = text.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
|
1177
|
+
// 粗体
|
|
1178
|
+
text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
|
1179
|
+
text = text.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
|
1180
|
+
// 斜体
|
|
1181
|
+
text = text.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
|
1182
|
+
text = text.replace(/_(.+?)_/g, '<em>$1</em>');
|
|
1183
|
+
// 链接
|
|
1184
|
+
text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
1185
|
+
return text;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
503
1188
|
// ========== Filters ==========
|
|
504
1189
|
function toggleFilter(type) {
|
|
505
|
-
var
|
|
506
|
-
if (
|
|
507
|
-
delete hiddenTypes[type];
|
|
508
|
-
btn.classList.add('active');
|
|
509
|
-
} else {
|
|
1190
|
+
var cb = document.querySelector('.filter-check input[data-type="' + type + '"]');
|
|
1191
|
+
if (cb && !cb.checked) {
|
|
510
1192
|
hiddenTypes[type] = true;
|
|
511
|
-
|
|
1193
|
+
} else {
|
|
1194
|
+
delete hiddenTypes[type];
|
|
512
1195
|
}
|
|
513
1196
|
renderGraph();
|
|
514
1197
|
}
|
|
515
1198
|
|
|
1199
|
+
// ========== Auto Refresh ==========
|
|
1200
|
+
var autoRefreshTimer = null;
|
|
1201
|
+
var AUTO_REFRESH_INTERVAL = 15000; // 15秒自动刷新
|
|
1202
|
+
|
|
1203
|
+
function startAutoRefresh() {
|
|
1204
|
+
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
|
|
1205
|
+
autoRefreshTimer = setInterval(function() {
|
|
1206
|
+
log('自动刷新: 检查数据更新...', true);
|
|
1207
|
+
silentRefresh();
|
|
1208
|
+
}, AUTO_REFRESH_INTERVAL);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** 静默刷新:只更新数据,不重建图谱(避免布局跳动) */
|
|
1212
|
+
function silentRefresh() {
|
|
1213
|
+
Promise.all([
|
|
1214
|
+
fetch('/api/graph').then(function(r) { return r.json(); }),
|
|
1215
|
+
fetch('/api/progress').then(function(r) { return r.json(); })
|
|
1216
|
+
]).then(function(results) {
|
|
1217
|
+
var graphRes = results[0];
|
|
1218
|
+
var progressRes = results[1];
|
|
1219
|
+
var newNodes = graphRes.nodes || [];
|
|
1220
|
+
var newEdges = graphRes.edges || [];
|
|
1221
|
+
|
|
1222
|
+
// 检查数据是否有变化(通过节点数量和状态快照比较)
|
|
1223
|
+
var changed = false;
|
|
1224
|
+
if (newNodes.length !== allNodes.length || newEdges.length !== allEdges.length) {
|
|
1225
|
+
changed = true;
|
|
1226
|
+
} else {
|
|
1227
|
+
// 比较每个节点的状态
|
|
1228
|
+
var oldStatusMap = {};
|
|
1229
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
1230
|
+
var n = allNodes[i];
|
|
1231
|
+
oldStatusMap[n.id] = (n.properties || {}).status || '';
|
|
1232
|
+
}
|
|
1233
|
+
for (var i = 0; i < newNodes.length; i++) {
|
|
1234
|
+
var n = newNodes[i];
|
|
1235
|
+
var oldStatus = oldStatusMap[n.id];
|
|
1236
|
+
var newStatus = (n.properties || {}).status || '';
|
|
1237
|
+
if (oldStatus !== newStatus) {
|
|
1238
|
+
changed = true;
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (changed) {
|
|
1245
|
+
log('检测到数据变化, 更新图谱...', true);
|
|
1246
|
+
allNodes = newNodes;
|
|
1247
|
+
allEdges = newEdges;
|
|
1248
|
+
renderStats(progressRes, graphRes);
|
|
1249
|
+
// 仅更新节点样式而非重建整个图谱,以保持当前布局
|
|
1250
|
+
if (nodesDataSet && network) {
|
|
1251
|
+
updateNodeStyles();
|
|
1252
|
+
} else {
|
|
1253
|
+
renderGraph();
|
|
1254
|
+
}
|
|
1255
|
+
} else {
|
|
1256
|
+
log('数据无变化 (' + new Date().toLocaleTimeString() + ')', true);
|
|
1257
|
+
}
|
|
1258
|
+
}).catch(function(err) {
|
|
1259
|
+
log('自动刷新失败: ' + err.message, false);
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/** 增量更新节点样式(不重建布局) */
|
|
1264
|
+
function updateNodeStyles() {
|
|
1265
|
+
try {
|
|
1266
|
+
// 构建当前可见节点的 ID 和新数据映射
|
|
1267
|
+
var newNodeMap = {};
|
|
1268
|
+
for (var i = 0; i < allNodes.length; i++) {
|
|
1269
|
+
newNodeMap[allNodes[i].id] = allNodes[i];
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// 更新已有节点的样式
|
|
1273
|
+
var currentIds = nodesDataSet.getIds();
|
|
1274
|
+
for (var i = 0; i < currentIds.length; i++) {
|
|
1275
|
+
var id = currentIds[i];
|
|
1276
|
+
var newData = newNodeMap[id];
|
|
1277
|
+
if (newData && !hiddenTypes[newData.type]) {
|
|
1278
|
+
var s = nodeStyle(newData);
|
|
1279
|
+
nodesDataSet.update({
|
|
1280
|
+
id: id,
|
|
1281
|
+
label: newData.label,
|
|
1282
|
+
color: s.color,
|
|
1283
|
+
font: s.font,
|
|
1284
|
+
_props: newData.properties || {}
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// 处理新增/删除的节点 — 如果有结构变化,完整重建
|
|
1290
|
+
var visibleNewNodes = allNodes.filter(function(n) { return !hiddenTypes[n.type]; });
|
|
1291
|
+
if (visibleNewNodes.length !== currentIds.length) {
|
|
1292
|
+
renderGraph();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
log('节点样式已更新 (' + new Date().toLocaleTimeString() + ')', true);
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
log('增量更新失败, 完整重建: ' + err.message, false);
|
|
1298
|
+
renderGraph();
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
516
1302
|
// ========== App Start ==========
|
|
517
1303
|
function startApp() {
|
|
518
1304
|
log('vis-network 就绪, 开始加载数据...', true);
|
|
519
1305
|
loadData();
|
|
1306
|
+
startAutoRefresh();
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// ========== Stats Dashboard ==========
|
|
1310
|
+
var statsLoaded = false;
|
|
1311
|
+
|
|
1312
|
+
function loadStatsPage() {
|
|
1313
|
+
var container = document.getElementById('statsContent');
|
|
1314
|
+
if (!container) return;
|
|
1315
|
+
container.innerHTML = '<div style="text-align:center;padding:60px;color:#6b7280;"><div class="spinner" style="margin:0 auto 12px;"></div>加载统计数据...</div>';
|
|
1316
|
+
|
|
1317
|
+
fetch('/api/stats').then(function(r) { return r.json(); }).then(function(data) {
|
|
1318
|
+
statsLoaded = true;
|
|
1319
|
+
renderStatsPage(data);
|
|
1320
|
+
}).catch(function(err) {
|
|
1321
|
+
container.innerHTML = '<div style="text-align:center;padding:60px;color:#f87171;">加载失败: ' + err.message + '<br><button class="refresh-btn" onclick="loadStatsPage()" style="margin-top:12px;">重试</button></div>';
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function renderStatsPage(data) {
|
|
1326
|
+
var container = document.getElementById('statsContent');
|
|
1327
|
+
if (!container) return;
|
|
1328
|
+
|
|
1329
|
+
var pct = data.overallPercent || 0;
|
|
1330
|
+
var totalSub = data.subTaskCount || 0;
|
|
1331
|
+
var doneSub = data.completedSubTasks || 0;
|
|
1332
|
+
var totalMain = data.mainTaskCount || 0;
|
|
1333
|
+
var doneMain = data.completedMainTasks || 0;
|
|
1334
|
+
var docCount = data.docCount || 0;
|
|
1335
|
+
var modCount = data.moduleCount || 0;
|
|
1336
|
+
|
|
1337
|
+
// 激励语
|
|
1338
|
+
var motivate = '';
|
|
1339
|
+
if (pct >= 100) motivate = '🎉 项目已全部完成!太棒了!';
|
|
1340
|
+
else if (pct >= 75) motivate = '🚀 即将大功告成,冲刺阶段!';
|
|
1341
|
+
else if (pct >= 50) motivate = '💪 已过半程,保持节奏!';
|
|
1342
|
+
else if (pct >= 25) motivate = '🌱 稳步推进中,继续加油!';
|
|
1343
|
+
else if (pct > 0) motivate = '🏗️ 万事开头难,已迈出第一步!';
|
|
1344
|
+
else motivate = '📋 项目已规划就绪,开始行动吧!';
|
|
1345
|
+
|
|
1346
|
+
var html = '';
|
|
1347
|
+
|
|
1348
|
+
// ===== 总体进度环 =====
|
|
1349
|
+
var ringR = 54;
|
|
1350
|
+
var ringC = 2 * Math.PI * ringR;
|
|
1351
|
+
var ringOffset = ringC - (pct / 100) * ringC;
|
|
1352
|
+
html += '<div class="progress-ring-wrap">';
|
|
1353
|
+
html += '<svg class="ring-svg" width="140" height="140" viewBox="0 0 140 140">';
|
|
1354
|
+
html += '<circle cx="70" cy="70" r="' + ringR + '" stroke="#374151" stroke-width="10" fill="none"/>';
|
|
1355
|
+
html += '<circle cx="70" cy="70" r="' + ringR + '" stroke="url(#ringGrad)" stroke-width="10" fill="none" stroke-linecap="round" stroke-dasharray="' + ringC + '" stroke-dashoffset="' + ringOffset + '" transform="rotate(-90 70 70)" style="transition:stroke-dashoffset 1s ease;"/>';
|
|
1356
|
+
html += '<defs><linearGradient id="ringGrad" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#10b981"/><stop offset="100%" stop-color="#3b82f6"/></linearGradient></defs>';
|
|
1357
|
+
html += '<text x="70" y="65" text-anchor="middle" fill="#f3f4f6" font-size="28" font-weight="800">' + pct + '%</text>';
|
|
1358
|
+
html += '<text x="70" y="84" text-anchor="middle" fill="#6b7280" font-size="11">完成率</text>';
|
|
1359
|
+
html += '</svg>';
|
|
1360
|
+
html += '<div class="progress-ring-info">';
|
|
1361
|
+
html += '<h3>项目总体进度</h3>';
|
|
1362
|
+
html += '<p>子任务完成 <strong style="color:#10b981;">' + doneSub + '</strong> / ' + totalSub + ',主任务完成 <strong style="color:#3b82f6;">' + doneMain + '</strong> / ' + totalMain + '</p>';
|
|
1363
|
+
html += '<div class="motivate">' + motivate + '</div>';
|
|
1364
|
+
html += '</div></div>';
|
|
1365
|
+
|
|
1366
|
+
// ===== 概览卡片 =====
|
|
1367
|
+
html += '<div class="stats-grid">';
|
|
1368
|
+
html += statCard('📋', totalMain, '主任务', doneMain + ' 已完成', 'blue');
|
|
1369
|
+
html += statCard('✅', doneSub, '已完成子任务', '共 ' + totalSub + ' 个子任务', 'green');
|
|
1370
|
+
html += statCard('📄', docCount, '文档', Object.keys(data.docBySection || {}).length + ' 种类型', 'purple');
|
|
1371
|
+
html += statCard('🧩', modCount, '功能模块', '', 'amber');
|
|
1372
|
+
var remainSub = totalSub - doneSub;
|
|
1373
|
+
html += statCard('⏳', remainSub, '待完成子任务', remainSub > 0 ? '继续努力!' : '全部完成!', 'rose');
|
|
1374
|
+
html += '</div>';
|
|
1375
|
+
|
|
1376
|
+
// ===== 按优先级统计 =====
|
|
1377
|
+
var bp = data.byPriority || {};
|
|
1378
|
+
html += '<div class="stats-section">';
|
|
1379
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🎯</span> 按优先级统计</div>';
|
|
1380
|
+
html += '<div class="priority-bars">';
|
|
1381
|
+
var priorities = ['P0', 'P1', 'P2'];
|
|
1382
|
+
for (var pi = 0; pi < priorities.length; pi++) {
|
|
1383
|
+
var pk = priorities[pi];
|
|
1384
|
+
var pd = bp[pk] || { total: 0, completed: 0 };
|
|
1385
|
+
var ppct = pd.total > 0 ? Math.round(pd.completed / pd.total * 100) : 0;
|
|
1386
|
+
html += '<div class="priority-row">';
|
|
1387
|
+
html += '<span class="priority-label ' + pk + '">' + pk + '</span>';
|
|
1388
|
+
html += '<div class="priority-bar-track"><div class="priority-bar-fill ' + pk + '" style="width:' + ppct + '%"></div></div>';
|
|
1389
|
+
html += '<span class="priority-nums">' + pd.completed + '/' + pd.total + ' (' + ppct + '%)</span>';
|
|
1390
|
+
html += '</div>';
|
|
1391
|
+
}
|
|
1392
|
+
html += '</div></div>';
|
|
1393
|
+
|
|
1394
|
+
// ===== 进行中的任务 =====
|
|
1395
|
+
var inProg = data.inProgressPhases || [];
|
|
1396
|
+
if (inProg.length > 0) {
|
|
1397
|
+
html += '<div class="stats-section">';
|
|
1398
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🔄</span> 进行中 (' + inProg.length + ')</div>';
|
|
1399
|
+
html += '<div class="phase-list">';
|
|
1400
|
+
for (var ii = 0; ii < inProg.length; ii++) {
|
|
1401
|
+
html += phaseItem(inProg[ii], 'in_progress', '▶');
|
|
1402
|
+
}
|
|
1403
|
+
html += '</div></div>';
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// ===== 已完成的里程碑 =====
|
|
1407
|
+
var done = data.completedPhases || [];
|
|
1408
|
+
if (done.length > 0) {
|
|
1409
|
+
html += '<div class="stats-section">';
|
|
1410
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🏆</span> 已完成里程碑 (' + done.length + ')</div>';
|
|
1411
|
+
html += '<div class="phase-list">';
|
|
1412
|
+
for (var di = 0; di < done.length; di++) {
|
|
1413
|
+
html += phaseItem(done[di], 'completed', '✓');
|
|
1414
|
+
}
|
|
1415
|
+
html += '</div></div>';
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// ===== 待开始的任务 =====
|
|
1419
|
+
var pending = data.pendingPhases || [];
|
|
1420
|
+
if (pending.length > 0) {
|
|
1421
|
+
html += '<div class="stats-section">';
|
|
1422
|
+
html += '<div class="stats-section-title"><span class="sec-icon">📌</span> 待开始 (' + pending.length + ')</div>';
|
|
1423
|
+
html += '<div class="phase-list">';
|
|
1424
|
+
for (var qi = 0; qi < pending.length; qi++) {
|
|
1425
|
+
html += phaseItem(pending[qi], 'pending', '○');
|
|
1426
|
+
}
|
|
1427
|
+
html += '</div></div>';
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// ===== 模块概览 =====
|
|
1431
|
+
var mods = data.moduleStats || [];
|
|
1432
|
+
if (mods.length > 0) {
|
|
1433
|
+
html += '<div class="stats-section">';
|
|
1434
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🧩</span> 模块概览</div>';
|
|
1435
|
+
html += '<div class="module-grid">';
|
|
1436
|
+
for (var mi = 0; mi < mods.length; mi++) {
|
|
1437
|
+
var mod = mods[mi];
|
|
1438
|
+
var mpct = mod.subTaskCount > 0 ? Math.round(mod.completedSubTaskCount / mod.subTaskCount * 100) : 0;
|
|
1439
|
+
html += '<div class="module-card">';
|
|
1440
|
+
html += '<div class="module-card-header"><div class="module-card-dot" style="background:' + (mpct >= 100 ? '#10b981' : mpct > 0 ? '#3b82f6' : '#4b5563') + ';"></div><span class="module-card-name">' + escHtml(mod.name) + '</span></div>';
|
|
1441
|
+
html += '<div class="module-card-bar"><div class="module-card-bar-fill" style="width:' + mpct + '%"></div></div>';
|
|
1442
|
+
html += '<div class="module-card-stats"><span>' + mod.completedSubTaskCount + '/' + mod.subTaskCount + ' 子任务</span><span>' + mpct + '%</span></div>';
|
|
1443
|
+
html += '</div>';
|
|
1444
|
+
}
|
|
1445
|
+
html += '</div></div>';
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// ===== 文档分布 =====
|
|
1449
|
+
var docSec = data.docBySection || {};
|
|
1450
|
+
var docKeys = Object.keys(docSec);
|
|
1451
|
+
if (docKeys.length > 0) {
|
|
1452
|
+
html += '<div class="stats-section">';
|
|
1453
|
+
html += '<div class="stats-section-title"><span class="sec-icon">📚</span> 文档分布</div>';
|
|
1454
|
+
html += '<div class="stats-grid">';
|
|
1455
|
+
var secNames = { overview: '概述', core_concepts: '核心概念', api_design: 'API 设计', file_structure: '文件结构', config: '配置', examples: '示例', technical_notes: '技术笔记', api_endpoints: 'API 端点', milestones: '里程碑', changelog: '变更日志', custom: '自定义' };
|
|
1456
|
+
for (var si = 0; si < docKeys.length; si++) {
|
|
1457
|
+
var sk = docKeys[si];
|
|
1458
|
+
html += '<div class="stat-card purple" style="padding:14px;">';
|
|
1459
|
+
html += '<div style="font-size:20px;font-weight:800;color:#a5b4fc;">' + docSec[sk] + '</div>';
|
|
1460
|
+
html += '<div style="font-size:11px;color:#9ca3af;margin-top:4px;">' + (secNames[sk] || sk) + '</div>';
|
|
1461
|
+
html += '</div>';
|
|
1462
|
+
}
|
|
1463
|
+
html += '</div></div>';
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
container.innerHTML = html;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function statCard(icon, value, label, sub, color) {
|
|
1470
|
+
return '<div class="stat-card ' + color + '"><div class="stat-card-icon">' + icon + '</div><div class="stat-card-value">' + value + '</div><div class="stat-card-label">' + label + '</div>' + (sub ? '<div class="stat-card-sub">' + sub + '</div>' : '') + '</div>';
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function phaseItem(task, status, icon) {
|
|
1474
|
+
var ppct = task.percent || 0;
|
|
1475
|
+
var subText = task.total !== undefined ? (task.completed || 0) + '/' + task.total + ' 子任务' : task.taskId;
|
|
1476
|
+
var subs = task.subTasks || [];
|
|
1477
|
+
var rDocsCheck = task.relatedDocs || [];
|
|
1478
|
+
var hasSubs = subs.length > 0 || rDocsCheck.length > 0;
|
|
1479
|
+
var subIcons = { completed: '✓', in_progress: '◉', pending: '○', cancelled: '⊘' };
|
|
1480
|
+
var mainTime = task.completedAt ? fmtTime(task.completedAt) : '';
|
|
1481
|
+
var h = '<div class="phase-item-wrap">';
|
|
1482
|
+
h += '<div class="phase-item-main" ' + (hasSubs ? 'onclick="togglePhaseExpand(this)"' : '') + '>';
|
|
1483
|
+
if (hasSubs) { h += '<div class="phase-expand-icon">▶</div>'; }
|
|
1484
|
+
h += '<div class="phase-status-icon ' + status + '">' + icon + '</div>';
|
|
1485
|
+
h += '<div class="phase-info" style="flex:1;min-width:0;"><div class="phase-info-title">' + escHtml(task.title) + '</div>';
|
|
1486
|
+
h += '<div class="phase-info-sub">' + escHtml(task.taskId) + ' · ' + subText;
|
|
1487
|
+
if (mainTime) { h += ' · <span class="phase-time">✓ ' + mainTime + '</span>'; }
|
|
1488
|
+
h += '</div></div>';
|
|
1489
|
+
h += '<div class="phase-bar-mini"><div class="phase-bar-mini-fill" style="width:' + ppct + '%"></div></div>';
|
|
1490
|
+
h += '<div class="phase-pct">' + ppct + '%</div>';
|
|
1491
|
+
h += '</div>';
|
|
1492
|
+
var rDocs = task.relatedDocs || [];
|
|
1493
|
+
if (hasSubs || rDocs.length > 0) {
|
|
1494
|
+
h += '<div class="phase-subtasks">';
|
|
1495
|
+
for (var si = 0; si < subs.length; si++) {
|
|
1496
|
+
var s = subs[si];
|
|
1497
|
+
var ss = s.status || 'pending';
|
|
1498
|
+
var subTime = s.completedAt ? fmtTime(s.completedAt) : '';
|
|
1499
|
+
h += '<div class="phase-sub-item">';
|
|
1500
|
+
h += '<div class="phase-sub-icon ' + ss + '">' + (subIcons[ss] || '○') + '</div>';
|
|
1501
|
+
h += '<span class="phase-sub-name ' + ss + '">' + escHtml(s.title) + '</span>';
|
|
1502
|
+
if (subTime) { h += '<span class="phase-sub-time">' + subTime + '</span>'; }
|
|
1503
|
+
h += '<span class="phase-sub-id">' + escHtml(s.taskId) + '</span>';
|
|
1504
|
+
h += '</div>';
|
|
1505
|
+
}
|
|
1506
|
+
if (rDocs.length > 0) {
|
|
1507
|
+
h += '<div style="padding:6px 0 2px 8px;font-size:11px;color:#f59e0b;font-weight:600;">关联文档</div>';
|
|
1508
|
+
for (var rd = 0; rd < rDocs.length; rd++) {
|
|
1509
|
+
var rdoc = rDocs[rd];
|
|
1510
|
+
var rdLabel = rdoc.section || '';
|
|
1511
|
+
if (rdoc.subSection) rdLabel += ' / ' + rdoc.subSection;
|
|
1512
|
+
h += '<div class="phase-sub-item">';
|
|
1513
|
+
h += '<div class="phase-sub-icon" style="color:#f59e0b;">📄</div>';
|
|
1514
|
+
h += '<span class="phase-sub-name">' + escHtml(rdoc.title) + '</span>';
|
|
1515
|
+
h += '<span class="phase-sub-id">' + escHtml(rdLabel) + '</span>';
|
|
1516
|
+
h += '</div>';
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
h += '</div>';
|
|
1520
|
+
}
|
|
1521
|
+
h += '</div>';
|
|
1522
|
+
return h;
|
|
520
1523
|
}
|
|
521
1524
|
|
|
522
1525
|
// ========== Init: 动态加载 vis-network ==========
|