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.
@@ -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: #1f2937; border-bottom: 1px solid #374151; padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; height: 56px; }
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 { background: #1f2937; border-bottom: 1px solid #374151; padding: 8px 24px; display: flex; align-items: center; gap: 16px; height: 44px; }
40
- .filter-group { display: flex; gap: 8px; align-items: center; }
41
- .filter-btn { padding: 4px 12px; border-radius: 6px; border: 1px solid #4b5563; background: transparent; color: #d1d5db; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 4px; transition: all 0.2s; }
42
- .filter-btn:hover { border-color: #6b7280; background: #374151; }
43
- .filter-btn.active { border-color: #6366f1; background: #312e81; color: #a5b4fc; }
44
- .filter-btn .dot { width: 8px; height: 8px; border-radius: 50%; }
45
- .sep { width: 1px; height: 20px; background: #374151; }
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: 320px; background: #1f2937; border: 1px solid #374151; border-radius: 12px; box-shadow: 0 20px 40px rgba(0,0,0,0.5); z-index: 10; overflow: hidden; display: none; }
59
- .panel.show { display: block; }
60
- .panel-header { padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; }
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: 8px 24px; display: flex; align-items: center; justify-content: center; gap: 24px; height: 40px; font-size: 12px; color: #9ca3af; }
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: fixed; bottom: 40px; left: 12px; background: rgba(31,41,55,0.9); border: 1px solid #374151; border-radius: 8px; padding: 8px 12px; font-size: 11px; color: #9ca3af; z-index: 30; max-width: 400px; }
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
- <!-- Header -->
109
- <div class="header">
110
- <h1><span class="icon">📊</span> DevPlan 图谱 <span class="project-name">${projectName}</span></h1>
111
- <div class="stats-bar" id="statsBar">
112
- <div class="stat"><span>加载中...</span></div>
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
- <!-- Controls -->
117
- <div class="controls">
118
- <span style="font-size:12px;color:#6b7280;">显示:</span>
119
- <div class="filter-group">
120
- <button class="filter-btn active" data-type="module" onclick="toggleFilter('module')"><span class="dot" style="background:#10b981;"></span> 模块</button>
121
- <button class="filter-btn active" data-type="main-task" onclick="toggleFilter('main-task')"><span class="dot" style="background:#6366f1;"></span> 主任务</button>
122
- <button class="filter-btn active" data-type="sub-task" onclick="toggleFilter('sub-task')"><span class="dot" style="background:#8b5cf6;"></span> 子任务</button>
123
- <button class="filter-btn active" data-type="document" onclick="toggleFilter('document')"><span class="dot" style="background:#a78bfa;"></span> 文档</button>
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()">&#8635; 刷新</button>
127
- </div>
128
377
 
129
- <!-- Graph -->
130
- <div class="graph-container">
131
- <div class="loading" id="loading"><div><div class="spinner"></div><p style="margin-top:12px;color:#9ca3af;">加载图谱数据...</p></div></div>
132
- <div id="graph"></div>
133
- <div class="panel" id="panel">
134
- <div class="panel-header" id="panelHeader">
135
- <span class="panel-title" id="panelTitle">节点详情</span>
136
- <button class="panel-close" onclick="closePanel()">✕</button>
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;">&#x1F4C4;</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 btn = document.querySelector('.filter-btn[data-type="' + type + '"]');
506
- if (hiddenTypes[type]) {
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
- btn.classList.remove('active');
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 ==========