@autodev/codebase 0.0.6 → 1.0.0
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/README.md +163 -70
- package/dist/cli.js +69817 -48443
- package/dist/cli.js.map +1 -1
- package/dist/index.js +21717 -7584
- package/dist/index.js.map +1 -1
- package/dist/static/graph_viewer.html +2333 -0
- package/package.json +18 -16
- package/dist/yoga.wasm +0 -0
- /package/dist/{tree-sitter.wasm → tree-sitter/tree-sitter.wasm} +0 -0
|
@@ -0,0 +1,2333 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Code Dependency Visualization</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.28.1/dist/cytoscape.min.js"></script>
|
|
8
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--primary-color: #4a90d9;
|
|
13
|
+
--secondary-color: #6c757d;
|
|
14
|
+
--success-color: #28a745;
|
|
15
|
+
--info-color: #17a2b8;
|
|
16
|
+
--warning-color: #ffc107;
|
|
17
|
+
--danger-color: #dc3545;
|
|
18
|
+
--light-bg: #f8f9fa;
|
|
19
|
+
--dark-bg: #1e1e1e;
|
|
20
|
+
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
21
|
+
--card-shadow-hover: 0 8px 15px rgba(0, 0, 0, 0.15);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
28
|
+
background: var(--light-bg);
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* 顶部导航栏 */
|
|
33
|
+
.navbar {
|
|
34
|
+
background: linear-gradient(135deg, var(--primary-color) 0%, #357abd 100%);
|
|
35
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
36
|
+
padding: 5px 10px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.navbar h1 {
|
|
40
|
+
color: white;
|
|
41
|
+
margin: 0;
|
|
42
|
+
font-size: 24px;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.stats-bar {
|
|
47
|
+
display: flex;
|
|
48
|
+
gap: 20px;
|
|
49
|
+
align-items: center;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.stat-item {
|
|
53
|
+
background: rgba(255,255,255,0.2);
|
|
54
|
+
padding: 8px 16px;
|
|
55
|
+
border-radius: 20px;
|
|
56
|
+
color: white;
|
|
57
|
+
backdrop-filter: blur(10px);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.stat-item .value {
|
|
61
|
+
font-size: 20px;
|
|
62
|
+
font-weight: bold;
|
|
63
|
+
display: inline;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.stat-item .label {
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
opacity: 0.9;
|
|
69
|
+
display: inline;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* 主容器 */
|
|
73
|
+
.main-container {
|
|
74
|
+
display: flex;
|
|
75
|
+
height: calc(100vh - 80px);
|
|
76
|
+
padding: 20px;
|
|
77
|
+
gap: 20px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* 左侧控制面板 */
|
|
81
|
+
.control-panel {
|
|
82
|
+
width: 220px;
|
|
83
|
+
background: white;
|
|
84
|
+
border-radius: 12px;
|
|
85
|
+
box-shadow: var(--card-shadow);
|
|
86
|
+
padding: 20px;
|
|
87
|
+
overflow-y: auto;
|
|
88
|
+
transition: all 0.3s ease;
|
|
89
|
+
position: relative;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.control-panel.collapsed {
|
|
93
|
+
transform: translateX(-100%);
|
|
94
|
+
width: 0;
|
|
95
|
+
min-width: 0;
|
|
96
|
+
padding: 0;
|
|
97
|
+
margin: 0;
|
|
98
|
+
opacity: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.panel-section {
|
|
102
|
+
margin-bottom: 25px;
|
|
103
|
+
padding-bottom: 20px;
|
|
104
|
+
border-bottom: 1px solid #e9ecef;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.panel-section:last-child {
|
|
108
|
+
border-bottom: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.section-title {
|
|
112
|
+
font-size: 14px;
|
|
113
|
+
font-weight: 600;
|
|
114
|
+
color: #495057;
|
|
115
|
+
margin-bottom: 15px;
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
gap: 8px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.section-title i {
|
|
122
|
+
color: var(--primary-color);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* 搜索框 */
|
|
126
|
+
.search-box {
|
|
127
|
+
position: relative;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.search-box textarea {
|
|
131
|
+
width: 100%;
|
|
132
|
+
min-height: 40px;
|
|
133
|
+
max-height: 120px;
|
|
134
|
+
padding: 10px 70px 10px 15px;
|
|
135
|
+
border: 2px solid #e9ecef;
|
|
136
|
+
border-radius: 8px;
|
|
137
|
+
font-size: 14px;
|
|
138
|
+
transition: all 0.3s ease;
|
|
139
|
+
resize: none;
|
|
140
|
+
overflow-y: auto;
|
|
141
|
+
font-family: inherit;
|
|
142
|
+
line-height: 1.4;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.search-box textarea:focus {
|
|
146
|
+
outline: none;
|
|
147
|
+
border-color: var(--primary-color);
|
|
148
|
+
box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.search-box .search-icon {
|
|
152
|
+
position: absolute;
|
|
153
|
+
right: 15px;
|
|
154
|
+
top: 12px;
|
|
155
|
+
color: #adb5bd;
|
|
156
|
+
pointer-events: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* 清空按钮 */
|
|
160
|
+
.search-box .clear-btn {
|
|
161
|
+
position: absolute;
|
|
162
|
+
right: 40px;
|
|
163
|
+
top: 12px;
|
|
164
|
+
width: 20px;
|
|
165
|
+
height: 20px;
|
|
166
|
+
border: none;
|
|
167
|
+
background: #adb5bd;
|
|
168
|
+
color: white;
|
|
169
|
+
border-radius: 50%;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
display: none;
|
|
172
|
+
align-items: center;
|
|
173
|
+
justify-content: center;
|
|
174
|
+
font-size: 12px;
|
|
175
|
+
transition: all 0.2s ease;
|
|
176
|
+
padding: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.search-box .clear-btn:hover {
|
|
180
|
+
background: #6c757d;
|
|
181
|
+
transform: scale(1.1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.search-box .clear-btn.show {
|
|
185
|
+
display: flex;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* 筛选按钮组 */
|
|
189
|
+
.filter-group {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-direction: column;
|
|
192
|
+
gap: 10px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.filter-btn {
|
|
196
|
+
padding: 10px 15px;
|
|
197
|
+
border: 2px solid #e9ecef;
|
|
198
|
+
background: white;
|
|
199
|
+
border-radius: 8px;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
transition: all 0.3s ease;
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: space-between;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.filter-btn:hover {
|
|
208
|
+
border-color: var(--primary-color);
|
|
209
|
+
background: #f8f9fa;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.filter-btn.active {
|
|
213
|
+
background: var(--primary-color);
|
|
214
|
+
color: white;
|
|
215
|
+
border-color: var(--primary-color);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.filter-btn .count {
|
|
219
|
+
background: rgba(0,0,0,0.1);
|
|
220
|
+
padding: 2px 8px;
|
|
221
|
+
border-radius: 12px;
|
|
222
|
+
font-size: 12px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* 布局选择 */
|
|
226
|
+
.layout-grid {
|
|
227
|
+
display: grid;
|
|
228
|
+
grid-template-columns: repeat(2, 1fr);
|
|
229
|
+
gap: 10px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.layout-btn {
|
|
233
|
+
padding: 10px;
|
|
234
|
+
border: 2px solid #e9ecef;
|
|
235
|
+
background: white;
|
|
236
|
+
border-radius: 8px;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
transition: all 0.3s ease;
|
|
239
|
+
text-align: center;
|
|
240
|
+
font-size: 13px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.layout-btn:hover {
|
|
244
|
+
border-color: var(--primary-color);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.layout-btn.active {
|
|
248
|
+
background: var(--primary-color);
|
|
249
|
+
color: white;
|
|
250
|
+
border-color: var(--primary-color);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* 图例 */
|
|
254
|
+
.legend-item {
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: center;
|
|
257
|
+
gap: 10px;
|
|
258
|
+
padding: 8px 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.legend-color {
|
|
262
|
+
width: 30px;
|
|
263
|
+
height: 30px;
|
|
264
|
+
border-radius: 6px;
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: center;
|
|
267
|
+
justify-content: center;
|
|
268
|
+
font-weight: bold;
|
|
269
|
+
color: white;
|
|
270
|
+
font-size: 12px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.legend-label {
|
|
274
|
+
flex: 1;
|
|
275
|
+
font-size: 13px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* 工具按钮 */
|
|
279
|
+
.tool-buttons {
|
|
280
|
+
display: grid;
|
|
281
|
+
grid-template-columns: repeat(3, 1fr);
|
|
282
|
+
gap: 10px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.tool-btn {
|
|
286
|
+
padding: 12px;
|
|
287
|
+
border: none;
|
|
288
|
+
background: var(--primary-color);
|
|
289
|
+
color: white;
|
|
290
|
+
border-radius: 8px;
|
|
291
|
+
cursor: pointer;
|
|
292
|
+
transition: all 0.3s ease;
|
|
293
|
+
display: flex;
|
|
294
|
+
flex-direction: column;
|
|
295
|
+
align-items: center;
|
|
296
|
+
gap: 5px;
|
|
297
|
+
font-size: 12px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.tool-btn:hover {
|
|
301
|
+
transform: translateY(-2px);
|
|
302
|
+
box-shadow: 0 4px 8px rgba(74, 144, 217, 0.3);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.tool-btn i {
|
|
306
|
+
font-size: 20px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* 中间图形区域 */
|
|
310
|
+
.graph-container {
|
|
311
|
+
flex: 1;
|
|
312
|
+
background: white;
|
|
313
|
+
border-radius: 12px;
|
|
314
|
+
box-shadow: var(--card-shadow);
|
|
315
|
+
position: relative;
|
|
316
|
+
overflow: hidden;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* 边缘恢复按钮 */
|
|
320
|
+
.edge-toggle-btn {
|
|
321
|
+
position: absolute;
|
|
322
|
+
top: 50%;
|
|
323
|
+
transform: translateY(-50%);
|
|
324
|
+
width: 28px;
|
|
325
|
+
height: 80px;
|
|
326
|
+
background: var(--primary-color);
|
|
327
|
+
color: white;
|
|
328
|
+
border: none;
|
|
329
|
+
cursor: pointer;
|
|
330
|
+
display: flex;
|
|
331
|
+
align-items: center;
|
|
332
|
+
justify-content: center;
|
|
333
|
+
font-size: 16px;
|
|
334
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
335
|
+
transition: all 0.3s ease;
|
|
336
|
+
z-index: 100;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* 折叠状态 - 深色背景 */
|
|
340
|
+
.edge-toggle-btn.collapsed {
|
|
341
|
+
background: #2c3e50;
|
|
342
|
+
color: white;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.edge-toggle-btn.collapsed:hover {
|
|
346
|
+
background: #34495e;
|
|
347
|
+
width: 36px;
|
|
348
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* 展开状态 - 浅色背景 */
|
|
352
|
+
.edge-toggle-btn.expanded {
|
|
353
|
+
background: #ecf0f1;
|
|
354
|
+
color: #2c3e50;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.edge-toggle-btn.expanded:hover {
|
|
358
|
+
background: #bdc3c7;
|
|
359
|
+
width: 36px;
|
|
360
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.edge-toggle-btn:hover {
|
|
364
|
+
width: 36px;
|
|
365
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* 左侧边缘按钮 */
|
|
369
|
+
.edge-toggle-btn.left-edge {
|
|
370
|
+
left: 0;
|
|
371
|
+
border-radius: 0 8px 8px 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* 右侧边缘按钮 */
|
|
375
|
+
.edge-toggle-btn.right-edge {
|
|
376
|
+
right: 0;
|
|
377
|
+
border-radius: 8px 0 0 8px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* 边缘按钮始终可见,通过 collapsed/expanded 类控制样式 */
|
|
381
|
+
|
|
382
|
+
#cy {
|
|
383
|
+
width: 100%;
|
|
384
|
+
height: 100%;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* 控制按钮组 */
|
|
388
|
+
.control-buttons {
|
|
389
|
+
position: absolute;
|
|
390
|
+
top: 20px;
|
|
391
|
+
left: 20px;
|
|
392
|
+
display: flex;
|
|
393
|
+
flex-direction: column;
|
|
394
|
+
gap: 10px;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* 导航按钮组 - 右下角 */
|
|
398
|
+
.nav-buttons {
|
|
399
|
+
position: absolute;
|
|
400
|
+
bottom: 20px;
|
|
401
|
+
right: 20px;
|
|
402
|
+
display: flex;
|
|
403
|
+
gap: 10px;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.ctrl-btn {
|
|
407
|
+
width: 40px;
|
|
408
|
+
height: 40px;
|
|
409
|
+
border: none;
|
|
410
|
+
background: white;
|
|
411
|
+
border-radius: 8px;
|
|
412
|
+
box-shadow: var(--card-shadow);
|
|
413
|
+
cursor: pointer;
|
|
414
|
+
display: flex;
|
|
415
|
+
align-items: center;
|
|
416
|
+
justify-content: center;
|
|
417
|
+
transition: all 0.3s ease;
|
|
418
|
+
font-size: 18px;
|
|
419
|
+
color: #495057;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.ctrl-btn:hover:not(:disabled) {
|
|
423
|
+
background: var(--primary-color);
|
|
424
|
+
color: white;
|
|
425
|
+
transform: scale(1.1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.ctrl-btn:disabled {
|
|
429
|
+
opacity: 0.3;
|
|
430
|
+
cursor: not-allowed;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.ctrl-btn.active-nav {
|
|
434
|
+
background: var(--primary-color);
|
|
435
|
+
color: white;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.nav-btn {
|
|
439
|
+
width: 50px;
|
|
440
|
+
height: 50px;
|
|
441
|
+
border: none;
|
|
442
|
+
background: white;
|
|
443
|
+
border-radius: 50%;
|
|
444
|
+
box-shadow: var(--card-shadow);
|
|
445
|
+
cursor: pointer;
|
|
446
|
+
display: flex;
|
|
447
|
+
align-items: center;
|
|
448
|
+
justify-content: center;
|
|
449
|
+
transition: all 0.3s ease;
|
|
450
|
+
font-size: 20px;
|
|
451
|
+
color: #495057;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.nav-btn:hover:not(:disabled) {
|
|
455
|
+
background: var(--primary-color);
|
|
456
|
+
color: white;
|
|
457
|
+
transform: scale(1.1);
|
|
458
|
+
box-shadow: 0 6px 12px rgba(74, 144, 217, 0.4);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.nav-btn:disabled {
|
|
462
|
+
opacity: 0.3;
|
|
463
|
+
cursor: not-allowed;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.nav-btn.active-nav {
|
|
467
|
+
background: var(--primary-color);
|
|
468
|
+
color: white;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* 右侧信息面板 */
|
|
472
|
+
.info-panel {
|
|
473
|
+
width: 350px;
|
|
474
|
+
background: white;
|
|
475
|
+
border-radius: 12px;
|
|
476
|
+
box-shadow: var(--card-shadow);
|
|
477
|
+
padding: 20px;
|
|
478
|
+
overflow-y: auto;
|
|
479
|
+
transition: all 0.3s ease;
|
|
480
|
+
position: relative;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.info-panel.collapsed {
|
|
484
|
+
transform: translateX(100%);
|
|
485
|
+
width: 0;
|
|
486
|
+
min-width: 0;
|
|
487
|
+
padding: 0;
|
|
488
|
+
margin: 0;
|
|
489
|
+
opacity: 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.info-header {
|
|
493
|
+
display: flex;
|
|
494
|
+
align-items: center;
|
|
495
|
+
gap: 10px;
|
|
496
|
+
margin-bottom: 20px;
|
|
497
|
+
padding-bottom: 15px;
|
|
498
|
+
border-bottom: 2px solid #e9ecef;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.info-header h3 {
|
|
502
|
+
margin: 0;
|
|
503
|
+
color: #495057;
|
|
504
|
+
font-size: 18px;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.info-content {
|
|
508
|
+
display: none;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.info-content.active {
|
|
512
|
+
display: block;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.info-row {
|
|
516
|
+
display: flex;
|
|
517
|
+
padding: 10px 0;
|
|
518
|
+
border-bottom: 1px solid #f8f9fa;
|
|
519
|
+
font-size: 14px;
|
|
520
|
+
line-height: 1.5;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.info-label {
|
|
524
|
+
font-weight: 600;
|
|
525
|
+
color: #495057;
|
|
526
|
+
width: 100px;
|
|
527
|
+
flex-shrink: 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.info-value {
|
|
531
|
+
color: #6c757d;
|
|
532
|
+
flex: 1;
|
|
533
|
+
word-break: break-all;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* 连接节点列表样式 */
|
|
537
|
+
.connection-list {
|
|
538
|
+
list-style: none;
|
|
539
|
+
padding: 0;
|
|
540
|
+
margin: 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.connection-item {
|
|
544
|
+
padding: 8px 10px;
|
|
545
|
+
margin-bottom: 6px;
|
|
546
|
+
background: #f8f9fa;
|
|
547
|
+
border-radius: 6px;
|
|
548
|
+
border-left: 3px solid #e9ecef;
|
|
549
|
+
cursor: pointer;
|
|
550
|
+
transition: all 0.2s ease;
|
|
551
|
+
display: flex;
|
|
552
|
+
flex-direction: column;
|
|
553
|
+
gap: 4px;
|
|
554
|
+
font-size: 14px;
|
|
555
|
+
line-height: 1.5;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.connection-main {
|
|
559
|
+
display: flex;
|
|
560
|
+
align-items: center;
|
|
561
|
+
gap: 8px;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.connection-item:hover {
|
|
565
|
+
background: #e9ecef;
|
|
566
|
+
border-left-color: var(--primary-color);
|
|
567
|
+
transform: translateX(2px);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.connection-item.incoming {
|
|
571
|
+
border-left-color: #28a745;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.connection-item.incoming:hover {
|
|
575
|
+
background: #d4edda;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.connection-item.outgoing {
|
|
579
|
+
border-left-color: #ffc107;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.connection-item.outgoing:hover {
|
|
583
|
+
background: #fff3cd;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.connection-type-badge {
|
|
587
|
+
font-size: 10px;
|
|
588
|
+
padding: 2px 6px;
|
|
589
|
+
border-radius: 4px;
|
|
590
|
+
font-weight: 600;
|
|
591
|
+
text-transform: uppercase;
|
|
592
|
+
min-width: 45px;
|
|
593
|
+
text-align: center;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.connection-type-badge.incoming {
|
|
597
|
+
background: #28a745;
|
|
598
|
+
color: white;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.connection-type-badge.outgoing {
|
|
602
|
+
background: #ffc107;
|
|
603
|
+
color: #333;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.connection-node-type {
|
|
607
|
+
font-size: 10px;
|
|
608
|
+
padding: 2px 6px;
|
|
609
|
+
border-radius: 4px;
|
|
610
|
+
font-weight: 600;
|
|
611
|
+
min-width: 24px;
|
|
612
|
+
text-align: center;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.connection-node-type.class {
|
|
616
|
+
background: #4a90d9;
|
|
617
|
+
color: white;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.connection-node-type.function {
|
|
621
|
+
background: #28a745;
|
|
622
|
+
color: white;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.connection-node-type.module {
|
|
626
|
+
background: #ffc107;
|
|
627
|
+
color: #333;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.connection-name {
|
|
631
|
+
flex: 1;
|
|
632
|
+
color: #495057;
|
|
633
|
+
word-break: break-all;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.connection-id {
|
|
637
|
+
font-size: 11px;
|
|
638
|
+
color: #6c757d;
|
|
639
|
+
font-family: 'Courier New', monospace;
|
|
640
|
+
padding-left: 4px;
|
|
641
|
+
word-break: break-all;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.connection-connections {
|
|
645
|
+
font-size: 10px;
|
|
646
|
+
color: #adb5bd;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.connections-header {
|
|
650
|
+
font-size: 11px;
|
|
651
|
+
color: #6c757d;
|
|
652
|
+
padding: 8px 10px;
|
|
653
|
+
background: #f8f9fa;
|
|
654
|
+
border-radius: 6px;
|
|
655
|
+
margin-bottom: 8px;
|
|
656
|
+
font-weight: 600;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.connections-header .count {
|
|
660
|
+
background: var(--primary-color);
|
|
661
|
+
color: white;
|
|
662
|
+
padding: 2px 8px;
|
|
663
|
+
border-radius: 10px;
|
|
664
|
+
margin-left: 5px;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.no-selection {
|
|
668
|
+
text-align: center;
|
|
669
|
+
padding: 40px 20px;
|
|
670
|
+
color: #adb5bd;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.no-selection i {
|
|
674
|
+
font-size: 48px;
|
|
675
|
+
margin-bottom: 15px;
|
|
676
|
+
display: block;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* 语言切换按钮 */
|
|
680
|
+
.lang-switch {
|
|
681
|
+
background: rgba(255,255,255,0.2);
|
|
682
|
+
padding: 8px 16px;
|
|
683
|
+
border-radius: 20px;
|
|
684
|
+
color: white;
|
|
685
|
+
backdrop-filter: blur(10px);
|
|
686
|
+
cursor: pointer;
|
|
687
|
+
transition: all 0.3s ease;
|
|
688
|
+
border: 2px solid transparent;
|
|
689
|
+
font-size: 14px;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.lang-switch:hover {
|
|
693
|
+
background: rgba(255,255,255,0.3);
|
|
694
|
+
border-color: rgba(255,255,255,0.5);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
@media (max-width: 1200px) {
|
|
698
|
+
.info-panel {
|
|
699
|
+
width: 350px;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
@media (max-width: 768px) {
|
|
704
|
+
.control-panel {
|
|
705
|
+
position: absolute;
|
|
706
|
+
left: 0;
|
|
707
|
+
top: 0;
|
|
708
|
+
height: 100%;
|
|
709
|
+
z-index: 1000;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.main-container {
|
|
713
|
+
padding: 10px;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/* 加载动画 */
|
|
718
|
+
.loading-overlay {
|
|
719
|
+
position: fixed;
|
|
720
|
+
top: 0;
|
|
721
|
+
left: 0;
|
|
722
|
+
right: 0;
|
|
723
|
+
bottom: 0;
|
|
724
|
+
background: rgba(255,255,255,0.9);
|
|
725
|
+
display: flex;
|
|
726
|
+
align-items: center;
|
|
727
|
+
justify-content: center;
|
|
728
|
+
z-index: 9999;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.loading-spinner {
|
|
732
|
+
width: 50px;
|
|
733
|
+
height: 50px;
|
|
734
|
+
border: 4px solid #f3f3f3;
|
|
735
|
+
border-top: 4px solid var(--primary-color);
|
|
736
|
+
border-radius: 50%;
|
|
737
|
+
animation: spin 1s linear infinite;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
@keyframes spin {
|
|
741
|
+
0% { transform: rotate(0deg); }
|
|
742
|
+
100% { transform: rotate(360deg); }
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/* Toast 提示 */
|
|
746
|
+
.toast {
|
|
747
|
+
position: fixed;
|
|
748
|
+
bottom: 30px;
|
|
749
|
+
left: 50%;
|
|
750
|
+
transform: translateX(-50%);
|
|
751
|
+
background: #333;
|
|
752
|
+
color: white;
|
|
753
|
+
padding: 12px 24px;
|
|
754
|
+
border-radius: 8px;
|
|
755
|
+
z-index: 10000;
|
|
756
|
+
opacity: 0;
|
|
757
|
+
transition: opacity 0.3s ease;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.toast.show {
|
|
761
|
+
opacity: 1;
|
|
762
|
+
}
|
|
763
|
+
</style>
|
|
764
|
+
</head>
|
|
765
|
+
<body>
|
|
766
|
+
<!-- 加载和文件选择界面 -->
|
|
767
|
+
<div class="loading-overlay" id="loading">
|
|
768
|
+
<div class="loading-content" id="drop-zone" style="text-align: center; padding: 40px; border: 3px dashed #e9ecef; border-radius: 12px; margin: 40px; transition: all 0.3s ease;">
|
|
769
|
+
<h2 style="color: #4a90d9; margin-bottom: 20px;">
|
|
770
|
+
<i class="bi bi-diagram-3"></i> <span data-i18n="title">Code Dependency Visualization</span>
|
|
771
|
+
</h2>
|
|
772
|
+
<div style="margin-bottom: 20px;">
|
|
773
|
+
<p style="color: #6c757d; margin-bottom: 15px;"><span data-i18n="selectFile">Select or drag JSON data file:</span></p>
|
|
774
|
+
<input type="file" id="json-file-input" accept=".json"
|
|
775
|
+
style="padding: 10px; border: 2px solid #e9ecef; border-radius: 8px; width: 80%;">
|
|
776
|
+
<div style="margin-top: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
|
|
777
|
+
<p style="font-size: 13px; color: #495057; margin: 0;">
|
|
778
|
+
<i class="bi bi-arrow-down-up" style="color: #4a90d9;"></i> <span data-i18n="dragFile">Drag .json file here</span>
|
|
779
|
+
</p>
|
|
780
|
+
<p style="font-size: 12px; color: #adb5bd; margin-top: 8px; margin-bottom: 0;">
|
|
781
|
+
<span data-i18n="fileTip">Tip: Generated via --output call-graph.json</span>
|
|
782
|
+
</p>
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
|
|
788
|
+
<!-- Toast 提示 -->
|
|
789
|
+
<div class="toast" id="toast"></div>
|
|
790
|
+
|
|
791
|
+
<!-- 顶部导航栏 -->
|
|
792
|
+
<div class="navbar">
|
|
793
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
794
|
+
<h1><i class="bi bi-diagram-3"></i> <span data-i18n="title">Code Dependency Visualization</span></h1>
|
|
795
|
+
<div class="stats-bar">
|
|
796
|
+
<button class="lang-switch" onclick="toggleLanguage()" id="lang-btn">
|
|
797
|
+
<i class="bi bi-globe"></i> <span id="lang-text">EN</span>
|
|
798
|
+
</button>
|
|
799
|
+
<div class="stat-item">
|
|
800
|
+
<div class="value" id="node-count">-</div>
|
|
801
|
+
<div class="label" data-i18n="nodes">Nodes</div>
|
|
802
|
+
</div>
|
|
803
|
+
<div class="stat-item">
|
|
804
|
+
<div class="value" id="edge-count">-</div>
|
|
805
|
+
<div class="label" data-i18n="edges">Edges</div>
|
|
806
|
+
</div>
|
|
807
|
+
<div class="stat-item">
|
|
808
|
+
<div class="value" id="depth-count">-</div>
|
|
809
|
+
<div class="label" data-i18n="depth">Depth</div>
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
|
|
815
|
+
<!-- 主容器 -->
|
|
816
|
+
<div class="main-container">
|
|
817
|
+
<!-- 左侧控制面板 -->
|
|
818
|
+
<div class="control-panel" id="control-panel">
|
|
819
|
+
<div class="panel-section">
|
|
820
|
+
<div class="section-title">
|
|
821
|
+
<i class="bi bi-search"></i>
|
|
822
|
+
<span data-i18n="searchNodes">Search Nodes</span>
|
|
823
|
+
</div>
|
|
824
|
+
<div class="search-box">
|
|
825
|
+
<textarea id="search-input" data-i18n-placeholder="searchPlaceholder" placeholder="Enter node name (comma-separated for multiple)" rows="1"></textarea>
|
|
826
|
+
<button class="clear-btn" id="search-clear-btn" data-i18n-title="clearSearch" title="Clear search">
|
|
827
|
+
<i class="bi bi-x"></i>
|
|
828
|
+
</button>
|
|
829
|
+
<i class="bi bi-search search-icon"></i>
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
832
|
+
|
|
833
|
+
<div class="panel-section">
|
|
834
|
+
<div class="section-title">
|
|
835
|
+
<i class="bi bi-funnel"></i>
|
|
836
|
+
<span data-i18n="filterType">Filter Type</span>
|
|
837
|
+
</div>
|
|
838
|
+
<div class="filter-group" id="filter-group">
|
|
839
|
+
<div class="filter-btn active" data-filter="all">
|
|
840
|
+
<span data-i18n="all">All</span>
|
|
841
|
+
<span class="count" id="count-all">0</span>
|
|
842
|
+
</div>
|
|
843
|
+
<div class="filter-btn" data-filter="class">
|
|
844
|
+
<span data-i18n="class">Class</span>
|
|
845
|
+
<span class="count" id="count-class">0</span>
|
|
846
|
+
</div>
|
|
847
|
+
<div class="filter-btn" data-filter="function">
|
|
848
|
+
<span data-i18n="function">Function</span>
|
|
849
|
+
<span class="count" id="count-function">0</span>
|
|
850
|
+
</div>
|
|
851
|
+
<div class="filter-btn" data-filter="module">
|
|
852
|
+
<span data-i18n="module">Module</span>
|
|
853
|
+
<span class="count" id="count-module">0</span>
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
|
|
858
|
+
<div class="panel-section">
|
|
859
|
+
<div class="section-title">
|
|
860
|
+
<i class="bi bi-grid-3x3"></i>
|
|
861
|
+
<span data-i18n="layoutMode">Layout Mode</span>
|
|
862
|
+
</div>
|
|
863
|
+
<div class="layout-grid">
|
|
864
|
+
<div class="layout-btn active" data-layout="cose">
|
|
865
|
+
<i class="bi bi-bounding-box"></i>
|
|
866
|
+
<div data-i18n="forceDirected">Force</div>
|
|
867
|
+
</div>
|
|
868
|
+
<div class="layout-btn" data-layout="circle">
|
|
869
|
+
<i class="bi bi-circle"></i>
|
|
870
|
+
<div data-i18n="circle">Circle</div>
|
|
871
|
+
</div>
|
|
872
|
+
<div class="layout-btn" data-layout="concentric">
|
|
873
|
+
<i class="bi bi-bullseye"></i>
|
|
874
|
+
<div data-i18n="concentric">Concentric</div>
|
|
875
|
+
</div>
|
|
876
|
+
<div class="layout-btn" data-layout="breadthfirst">
|
|
877
|
+
<i class="bi bi-list-nested"></i>
|
|
878
|
+
<div data-i18n="hierarchical">Hierarchical</div>
|
|
879
|
+
</div>
|
|
880
|
+
</div>
|
|
881
|
+
</div>
|
|
882
|
+
|
|
883
|
+
<div class="panel-section">
|
|
884
|
+
<div class="section-title">
|
|
885
|
+
<i class="bi bi-palette"></i>
|
|
886
|
+
<span data-i18n="legend">Legend</span>
|
|
887
|
+
</div>
|
|
888
|
+
<div class="legend-item">
|
|
889
|
+
<div class="legend-color" style="background: #4a90d9;">C</div>
|
|
890
|
+
<div class="legend-label" data-i18n="classLegend">Class</div>
|
|
891
|
+
</div>
|
|
892
|
+
<div class="legend-item">
|
|
893
|
+
<div class="legend-color" style="background: #28a745;">F</div>
|
|
894
|
+
<div class="legend-label" data-i18n="functionLegend">Function</div>
|
|
895
|
+
</div>
|
|
896
|
+
<div class="legend-item">
|
|
897
|
+
<div class="legend-color" style="background: #ffc107;">M</div>
|
|
898
|
+
<div class="legend-label" data-i18n="moduleLegend">Module</div>
|
|
899
|
+
</div>
|
|
900
|
+
</div>
|
|
901
|
+
|
|
902
|
+
<div class="panel-section">
|
|
903
|
+
<div class="section-title">
|
|
904
|
+
<i class="bi bi-tools"></i>
|
|
905
|
+
<span data-i18n="tools">Tools</span>
|
|
906
|
+
</div>
|
|
907
|
+
<div class="tool-buttons">
|
|
908
|
+
<button class="tool-btn" onclick="exportImage()">
|
|
909
|
+
<i class="bi bi-download"></i>
|
|
910
|
+
<span data-i18n="exportImage">Export</span>
|
|
911
|
+
</button>
|
|
912
|
+
<button class="tool-btn" onclick="resetView()">
|
|
913
|
+
<i class="bi bi-arrow-counterclockwise"></i>
|
|
914
|
+
<span data-i18n="resetView">Reset</span>
|
|
915
|
+
</button>
|
|
916
|
+
<button class="tool-btn" onclick="fitGraph()">
|
|
917
|
+
<i class="bi bi-aspect-ratio"></i>
|
|
918
|
+
<span data-i18n="fitWindow">Fit</span>
|
|
919
|
+
</button>
|
|
920
|
+
</div>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
|
|
924
|
+
<!-- 中间图形区域 -->
|
|
925
|
+
<div class="graph-container">
|
|
926
|
+
<div id="cy"></div>
|
|
927
|
+
|
|
928
|
+
<!-- 边缘恢复按钮 -->
|
|
929
|
+
<button class="edge-toggle-btn left-edge expanded" onclick="togglePanel('control-panel')" data-i18n-title="hideControlPanel" title="Hide control panel">
|
|
930
|
+
<i class="bi bi-chevron-left"></i>
|
|
931
|
+
</button>
|
|
932
|
+
<button class="edge-toggle-btn right-edge expanded" onclick="togglePanel('info-panel')" data-i18n-title="hideInfoPanel" title="Hide info panel">
|
|
933
|
+
<i class="bi bi-chevron-right"></i>
|
|
934
|
+
</button>
|
|
935
|
+
|
|
936
|
+
<!-- 控制按钮组 - 仅缩放按钮 -->
|
|
937
|
+
<div class="control-buttons">
|
|
938
|
+
<button class="ctrl-btn" onclick="zoomIn()" data-i18n-title="zoomIn" title="Zoom in">
|
|
939
|
+
<i class="bi bi-plus-lg"></i>
|
|
940
|
+
</button>
|
|
941
|
+
<button class="ctrl-btn" onclick="zoomOut()" data-i18n-title="zoomOut" title="Zoom out">
|
|
942
|
+
<i class="bi bi-dash-lg"></i>
|
|
943
|
+
</button>
|
|
944
|
+
</div>
|
|
945
|
+
|
|
946
|
+
<!-- 导航按钮组 - 右下角 -->
|
|
947
|
+
<div class="nav-buttons">
|
|
948
|
+
<button class="nav-btn" onclick="navigateBack()" data-i18n-title="back" title="Back" id="btn-back" disabled>
|
|
949
|
+
<i class="bi bi-arrow-left"></i>
|
|
950
|
+
</button>
|
|
951
|
+
<button class="nav-btn" onclick="navigateForward()" data-i18n-title="forward" title="Forward" id="btn-forward" disabled>
|
|
952
|
+
<i class="bi bi-arrow-right"></i>
|
|
953
|
+
</button>
|
|
954
|
+
</div>
|
|
955
|
+
</div>
|
|
956
|
+
|
|
957
|
+
<!-- 右侧信息面板 -->
|
|
958
|
+
<div class="info-panel" id="info-panel">
|
|
959
|
+
<div class="info-header">
|
|
960
|
+
<i class="bi bi-info-circle" style="font-size: 24px; color: var(--primary-color);"></i>
|
|
961
|
+
<h3 data-i18n="nodeDetails">Node Details</h3>
|
|
962
|
+
</div>
|
|
963
|
+
|
|
964
|
+
<div class="no-selection" id="no-selection">
|
|
965
|
+
<i class="bi bi-hand-index-thumb"></i>
|
|
966
|
+
<p data-i18n="clickNode">Click a node to view details</p>
|
|
967
|
+
</div>
|
|
968
|
+
|
|
969
|
+
<div class="info-content" id="info-content">
|
|
970
|
+
<div class="info-row">
|
|
971
|
+
<div class="info-label" data-i18n="idLabel">ID:</div>
|
|
972
|
+
<div class="info-value" id="info-id">-</div>
|
|
973
|
+
</div>
|
|
974
|
+
<div class="info-row">
|
|
975
|
+
<div class="info-label" data-i18n="nameLabel">Name:</div>
|
|
976
|
+
<div class="info-value" id="info-label">-</div>
|
|
977
|
+
</div>
|
|
978
|
+
<div class="info-row">
|
|
979
|
+
<div class="info-label" data-i18n="typeLabel">Type:</div>
|
|
980
|
+
<div class="info-value" id="info-type">-</div>
|
|
981
|
+
</div>
|
|
982
|
+
<div class="info-row">
|
|
983
|
+
<div class="info-label" data-i18n="fileLabel">File:</div>
|
|
984
|
+
<div class="info-value" id="info-file">-</div>
|
|
985
|
+
</div>
|
|
986
|
+
<div class="info-row">
|
|
987
|
+
<div class="info-label" data-i18n="startLineLabel">Start:</div>
|
|
988
|
+
<div class="info-value" id="info-start-line">-</div>
|
|
989
|
+
</div>
|
|
990
|
+
<div class="info-row">
|
|
991
|
+
<div class="info-label" data-i18n="endLineLabel">End:</div>
|
|
992
|
+
<div class="info-value" id="info-end-line">-</div>
|
|
993
|
+
</div>
|
|
994
|
+
<div class="info-row">
|
|
995
|
+
<div class="info-label" data-i18n="connectionsLabel">Connections:</div>
|
|
996
|
+
<div class="info-value" id="info-connections">-</div>
|
|
997
|
+
</div>
|
|
998
|
+
|
|
999
|
+
<!-- 连接节点列表 -->
|
|
1000
|
+
<div style="margin-top: 20px; padding-top: 15px; border-top: 2px solid #e9ecef;">
|
|
1001
|
+
<div style="font-weight: 600; color: #495057; margin-bottom: 10px; font-size: 18px;">
|
|
1002
|
+
<i class="bi bi-diagram-3" style="color: var(--primary-color);"></i>
|
|
1003
|
+
<span data-i18n="connectedNodes">Connected Nodes</span>
|
|
1004
|
+
</div>
|
|
1005
|
+
<div id="connections-container" style="max-height: 300px; overflow-y: auto;">
|
|
1006
|
+
<div style="text-align: center; padding: 20px; color: #adb5bd; font-size: 12px;">
|
|
1007
|
+
<span data-i18n="clickNodeConnections">Click a node to see connections</span>
|
|
1008
|
+
</div>
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
</div>
|
|
1012
|
+
</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
|
|
1015
|
+
<script>
|
|
1016
|
+
let cy;
|
|
1017
|
+
let currentFilter = 'all';
|
|
1018
|
+
let currentLayout = 'cose';
|
|
1019
|
+
|
|
1020
|
+
// 语言设置
|
|
1021
|
+
let currentLang = localStorage.getItem('app-language') || 'en'; // 默认英文
|
|
1022
|
+
|
|
1023
|
+
// 语言字典
|
|
1024
|
+
const translations = {
|
|
1025
|
+
en: {
|
|
1026
|
+
title: 'Code Dependency Visualization',
|
|
1027
|
+
selectFile: 'Select or drag JSON data file:',
|
|
1028
|
+
dragFile: 'Drag .json file here',
|
|
1029
|
+
fileTip: 'Tip: Generated via --output call-graph.json',
|
|
1030
|
+
nodes: 'Nodes',
|
|
1031
|
+
edges: 'Edges',
|
|
1032
|
+
depth: 'Depth',
|
|
1033
|
+
searchNodes: 'Search Nodes',
|
|
1034
|
+
searchPlaceholder: 'Enter node name (comma-separated for multiple)',
|
|
1035
|
+
clearSearch: 'Clear search',
|
|
1036
|
+
filterType: 'Filter Type',
|
|
1037
|
+
all: 'All',
|
|
1038
|
+
class: 'Class',
|
|
1039
|
+
function: 'Function',
|
|
1040
|
+
module: 'Module',
|
|
1041
|
+
layoutMode: 'Layout Mode',
|
|
1042
|
+
forceDirected: 'Force',
|
|
1043
|
+
circle: 'Circle',
|
|
1044
|
+
concentric: 'Concentric',
|
|
1045
|
+
hierarchical: 'Hierarchical',
|
|
1046
|
+
legend: 'Legend',
|
|
1047
|
+
classLegend: 'Class',
|
|
1048
|
+
functionLegend: 'Function',
|
|
1049
|
+
moduleLegend: 'Module',
|
|
1050
|
+
tools: 'Tools',
|
|
1051
|
+
exportImage: 'Export',
|
|
1052
|
+
resetView: 'Reset',
|
|
1053
|
+
fitWindow: 'Fit',
|
|
1054
|
+
hideControlPanel: 'Hide control panel',
|
|
1055
|
+
showControlPanel: 'Show control panel',
|
|
1056
|
+
hideInfoPanel: 'Hide info panel',
|
|
1057
|
+
showInfoPanel: 'Show info panel',
|
|
1058
|
+
zoomIn: 'Zoom in',
|
|
1059
|
+
zoomOut: 'Zoom out',
|
|
1060
|
+
back: 'Back',
|
|
1061
|
+
forward: 'Forward',
|
|
1062
|
+
nodeDetails: 'Node Details',
|
|
1063
|
+
clickNode: 'Click a node to view details',
|
|
1064
|
+
idLabel: 'ID:',
|
|
1065
|
+
nameLabel: 'Name:',
|
|
1066
|
+
typeLabel: 'Type:',
|
|
1067
|
+
fileLabel: 'File:',
|
|
1068
|
+
startLineLabel: 'Start:',
|
|
1069
|
+
endLineLabel: 'End:',
|
|
1070
|
+
connectionsLabel: 'Connections:',
|
|
1071
|
+
connectedNodes: 'Connected Nodes',
|
|
1072
|
+
clickNodeConnections: 'Click a node to see connections',
|
|
1073
|
+
loading: 'Loading data...',
|
|
1074
|
+
loadingFailed: 'Loading failed',
|
|
1075
|
+
reload: 'Reload',
|
|
1076
|
+
largeDataset: `Dataset is large ({count} nodes), loading may take a few seconds...`,
|
|
1077
|
+
veryLargeDataset: `Dataset is very large ({count} nodes), may cause browser lag. Continue? Suggestions:\n1. Reduce analysis scope\n2. Use filter function\n\nContinue loading?`,
|
|
1078
|
+
foundNodes: 'Found {count} matching nodes',
|
|
1079
|
+
foundNodesLimited: 'Found {count} matching nodes, showing first {max}',
|
|
1080
|
+
noMatchingNodes: 'No matching nodes found',
|
|
1081
|
+
typeFiltered: 'Showing {count} {type} nodes',
|
|
1082
|
+
viewReset: 'View reset',
|
|
1083
|
+
windowFitted: 'Fitted to window',
|
|
1084
|
+
imageExported: 'Image exported',
|
|
1085
|
+
panelHidden: '{panel} hidden',
|
|
1086
|
+
panelShown: '{panel} shown',
|
|
1087
|
+
selected: 'Selected: {label}',
|
|
1088
|
+
navigatingBack: 'Back ({index}/{total}): {label}',
|
|
1089
|
+
navigatingForward: 'Forward ({index}/{total}): {label}',
|
|
1090
|
+
layoutUpdated: 'Layout updated',
|
|
1091
|
+
applyingLayout: 'Applying new layout, please wait...',
|
|
1092
|
+
noConnections: 'No connections',
|
|
1093
|
+
dependents: 'Dependents',
|
|
1094
|
+
dependencies: 'Dependencies',
|
|
1095
|
+
connections: 'connections',
|
|
1096
|
+
initializeError: 'Initialization failed: {error}',
|
|
1097
|
+
showNodeInfoError: 'Failed to show node info: {error}',
|
|
1098
|
+
loadFileError: 'File loading failed: {error}',
|
|
1099
|
+
pleaseSelectJson: 'Please select .json file',
|
|
1100
|
+
searchCleared: 'Search cleared, showing all nodes',
|
|
1101
|
+
showAllNodes: 'Show all nodes',
|
|
1102
|
+
showClassNodes: 'Show class nodes',
|
|
1103
|
+
showFunctionNodes: 'Show function nodes',
|
|
1104
|
+
showModuleNodes: 'Show module nodes'
|
|
1105
|
+
},
|
|
1106
|
+
zh: {
|
|
1107
|
+
title: '代码依赖关系可视化',
|
|
1108
|
+
selectFile: '选择或拖拽 JSON 数据文件:',
|
|
1109
|
+
dragFile: '拖拽 .json 文件到这里',
|
|
1110
|
+
fileTip: '提示:通过 --output call-graph.json 生成',
|
|
1111
|
+
nodes: '节点',
|
|
1112
|
+
edges: '关系',
|
|
1113
|
+
depth: '层级',
|
|
1114
|
+
searchNodes: '搜索节点',
|
|
1115
|
+
searchPlaceholder: '输入节点名称搜索(支持逗号分隔多个词)',
|
|
1116
|
+
clearSearch: '清空搜索',
|
|
1117
|
+
filterType: '筛选类型',
|
|
1118
|
+
all: '全部',
|
|
1119
|
+
class: '类',
|
|
1120
|
+
function: '函数',
|
|
1121
|
+
module: '模块',
|
|
1122
|
+
layoutMode: '布局方式',
|
|
1123
|
+
forceDirected: '力导向',
|
|
1124
|
+
circle: '圆形',
|
|
1125
|
+
concentric: '同心圆',
|
|
1126
|
+
hierarchical: '层级',
|
|
1127
|
+
legend: '图例',
|
|
1128
|
+
classLegend: '类',
|
|
1129
|
+
functionLegend: '函数',
|
|
1130
|
+
moduleLegend: '模块',
|
|
1131
|
+
tools: '工具',
|
|
1132
|
+
exportImage: '导出图片',
|
|
1133
|
+
resetView: '重置视图',
|
|
1134
|
+
fitWindow: '适应窗口',
|
|
1135
|
+
hideControlPanel: '隐藏控制面板',
|
|
1136
|
+
showControlPanel: '显示控制面板',
|
|
1137
|
+
hideInfoPanel: '隐藏信息面板',
|
|
1138
|
+
showInfoPanel: '显示信息面板',
|
|
1139
|
+
zoomIn: '放大',
|
|
1140
|
+
zoomOut: '缩小',
|
|
1141
|
+
back: '后退',
|
|
1142
|
+
forward: '前进',
|
|
1143
|
+
nodeDetails: '节点详情',
|
|
1144
|
+
clickNode: '点击节点查看详细信息',
|
|
1145
|
+
idLabel: 'ID:',
|
|
1146
|
+
nameLabel: '名称:',
|
|
1147
|
+
typeLabel: '类型:',
|
|
1148
|
+
fileLabel: '文件:',
|
|
1149
|
+
startLineLabel: '起始行:',
|
|
1150
|
+
endLineLabel: '结束行:',
|
|
1151
|
+
connectionsLabel: '连接数:',
|
|
1152
|
+
connectedNodes: '连接节点',
|
|
1153
|
+
clickNodeConnections: '点击节点查看连接关系',
|
|
1154
|
+
loading: '正在加载数据...',
|
|
1155
|
+
loadingFailed: '加载失败',
|
|
1156
|
+
reload: '重新选择文件',
|
|
1157
|
+
largeDataset: '数据量较大({count}个节点),加载可能需要几秒钟...',
|
|
1158
|
+
veryLargeDataset: '数据量非常大({count}个节点),可能导致浏览器卡顿。是否继续?建议:\n1. 减少分析范围\n2. 使用筛选功能\n\n是否继续加载?',
|
|
1159
|
+
foundNodes: '找到 {count} 个匹配节点',
|
|
1160
|
+
foundNodesLimited: '找到 {count} 个匹配节点,仅显示前 {max} 个',
|
|
1161
|
+
noMatchingNodes: '未找到匹配节点',
|
|
1162
|
+
typeFiltered: '已显示 {count} 个{type}类型的节点',
|
|
1163
|
+
viewReset: '视图已重置',
|
|
1164
|
+
windowFitted: '已适应窗口',
|
|
1165
|
+
imageExported: '图片已导出',
|
|
1166
|
+
panelHidden: '{panel}已隐藏',
|
|
1167
|
+
panelShown: '{panel}已显示',
|
|
1168
|
+
selected: '已选中: {label}',
|
|
1169
|
+
navigatingBack: '后退 ({index}/{total}): {label}',
|
|
1170
|
+
navigatingForward: '前进 ({index}/{total}): {label}',
|
|
1171
|
+
layoutUpdated: '布局已更新',
|
|
1172
|
+
applyingLayout: '正在应用新布局,请稍候...',
|
|
1173
|
+
noConnections: '暂无连接节点',
|
|
1174
|
+
dependents: '被依赖',
|
|
1175
|
+
dependencies: '依赖',
|
|
1176
|
+
connections: ' 连接',
|
|
1177
|
+
initializeError: '初始化失败: {error}',
|
|
1178
|
+
showNodeInfoError: '显示节点信息失败: {error}',
|
|
1179
|
+
loadFileError: '文件加载失败:{error}',
|
|
1180
|
+
pleaseSelectJson: '请选择 .json 文件',
|
|
1181
|
+
searchCleared: '搜索已清空,恢复显示所有节点',
|
|
1182
|
+
showAllNodes: '显示所有节点',
|
|
1183
|
+
showClassNodes: '显示类节点',
|
|
1184
|
+
showFunctionNodes: '显示函数节点',
|
|
1185
|
+
showModuleNodes: '显示模块节点'
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
// 获取翻译文本
|
|
1190
|
+
function t(key, params = {}) {
|
|
1191
|
+
const text = translations[currentLang][key] || translations.en[key] || key;
|
|
1192
|
+
// 支持参数替换
|
|
1193
|
+
if (typeof text === 'string') {
|
|
1194
|
+
return text.replace(/\{(\w+)\}/g, (match, paramKey) => params[paramKey] || match);
|
|
1195
|
+
}
|
|
1196
|
+
return text;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// 切换语言
|
|
1200
|
+
function toggleLanguage() {
|
|
1201
|
+
currentLang = currentLang === 'en' ? 'zh' : 'en';
|
|
1202
|
+
localStorage.setItem('app-language', currentLang);
|
|
1203
|
+
updateLanguage();
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// 更新界面语言
|
|
1207
|
+
function updateLanguage() {
|
|
1208
|
+
// 更新语言按钮
|
|
1209
|
+
const langText = document.getElementById('lang-text');
|
|
1210
|
+
if (langText) {
|
|
1211
|
+
langText.textContent = currentLang === 'en' ? 'EN' : '中文';
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// 更新所有带有 data-i18n 属性的元素
|
|
1215
|
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
1216
|
+
const key = el.getAttribute('data-i18n');
|
|
1217
|
+
el.textContent = t(key);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// 更新所有带有 data-i18n-placeholder 属性的元素
|
|
1221
|
+
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
|
1222
|
+
const key = el.getAttribute('data-i18n-placeholder');
|
|
1223
|
+
el.placeholder = t(key);
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
// 更新所有带有 data-i18n-title 属性的元素
|
|
1227
|
+
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
|
1228
|
+
const key = el.getAttribute('data-i18n-title');
|
|
1229
|
+
el.title = t(key);
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
// 更新 HTML lang 属性
|
|
1233
|
+
document.documentElement.lang = currentLang;
|
|
1234
|
+
|
|
1235
|
+
// 如果有选中的节点,重新显示其信息(包括连接列表)
|
|
1236
|
+
if (cy && cy.$(':selected').length > 0) {
|
|
1237
|
+
const selectedNode = cy.$(':selected').first();
|
|
1238
|
+
if (selectedNode && selectedNode.isNode()) {
|
|
1239
|
+
displayConnectionList(selectedNode);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// 历史记录管理
|
|
1245
|
+
let nodeHistory = [];
|
|
1246
|
+
let historyIndex = -1;
|
|
1247
|
+
let isNavigatingHistory = false;
|
|
1248
|
+
|
|
1249
|
+
// 搜索过滤状态
|
|
1250
|
+
let searchFilteredNodes = null; // 存储搜索过滤的节点
|
|
1251
|
+
let isSearchActive = false; // 搜索是否激活
|
|
1252
|
+
let isClearing = false; // 是否正在执行清空操作(避免事件冲突)
|
|
1253
|
+
|
|
1254
|
+
// 初始化 - 等待用户选择文件或拖拽
|
|
1255
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1256
|
+
// 初始化语言
|
|
1257
|
+
updateLanguage();
|
|
1258
|
+
|
|
1259
|
+
const fileInput = document.getElementById('json-file-input');
|
|
1260
|
+
const dropZone = document.getElementById('drop-zone');
|
|
1261
|
+
|
|
1262
|
+
// 文件选择器
|
|
1263
|
+
fileInput.addEventListener('change', async function(e) {
|
|
1264
|
+
const file = e.target.files[0];
|
|
1265
|
+
if (!file) return;
|
|
1266
|
+
await loadFile(file);
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
// 拖拽事件处理
|
|
1270
|
+
dropZone.addEventListener('dragover', function(e) {
|
|
1271
|
+
e.preventDefault();
|
|
1272
|
+
e.stopPropagation();
|
|
1273
|
+
dropZone.style.borderColor = '#4a90d9';
|
|
1274
|
+
dropZone.style.backgroundColor = 'rgba(74, 144, 217, 0.05)';
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
dropZone.addEventListener('dragleave', function(e) {
|
|
1278
|
+
e.preventDefault();
|
|
1279
|
+
e.stopPropagation();
|
|
1280
|
+
dropZone.style.borderColor = '#e9ecef';
|
|
1281
|
+
dropZone.style.backgroundColor = 'transparent';
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
dropZone.addEventListener('drop', async function(e) {
|
|
1285
|
+
e.preventDefault();
|
|
1286
|
+
e.stopPropagation();
|
|
1287
|
+
dropZone.style.borderColor = '#e9ecef';
|
|
1288
|
+
dropZone.style.backgroundColor = 'transparent';
|
|
1289
|
+
|
|
1290
|
+
const files = e.dataTransfer.files;
|
|
1291
|
+
if (files.length === 0) return;
|
|
1292
|
+
|
|
1293
|
+
const file = files[0];
|
|
1294
|
+
if (!file.name.endsWith('.json')) {
|
|
1295
|
+
alert(t('pleaseSelectJson'));
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
await loadFile(file);
|
|
1300
|
+
});
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
// 加载文件函数
|
|
1304
|
+
async function loadFile(file) {
|
|
1305
|
+
try {
|
|
1306
|
+
// 显示加载动画
|
|
1307
|
+
document.getElementById('loading').querySelector('.loading-content').innerHTML =
|
|
1308
|
+
'<div class="loading-spinner"></div><p style="margin-top: 20px; color: #6c757d;">' + t('loading') + '</p>';
|
|
1309
|
+
|
|
1310
|
+
// 读取文件
|
|
1311
|
+
const text = await file.text();
|
|
1312
|
+
const elements = JSON.parse(text);
|
|
1313
|
+
|
|
1314
|
+
// 隐藏加载界面
|
|
1315
|
+
document.getElementById('loading').style.display = 'none';
|
|
1316
|
+
|
|
1317
|
+
// 初始化图表
|
|
1318
|
+
await initVisualization(elements);
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
alert(t('loadFileError', { error: error.message }));
|
|
1321
|
+
document.getElementById('loading').querySelector('.loading-content').innerHTML =
|
|
1322
|
+
'<h2 style="color: #dc3545;">' + t('loadingFailed') + '</h2>' +
|
|
1323
|
+
'<p>' + error.message + '</p>' +
|
|
1324
|
+
'<button onclick="location.reload()" style="margin-top: 20px; padding: 10px 20px; background: #4a90d9; color: white; border: none; border-radius: 8px; cursor: pointer;">' + t('reload') + '</button>';
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// 初始化可视化(接收 elements 参数)
|
|
1329
|
+
async function initVisualization(elements) {
|
|
1330
|
+
try {
|
|
1331
|
+
console.log('开始初始化...');
|
|
1332
|
+
console.log('数据元素数量:', elements.length);
|
|
1333
|
+
|
|
1334
|
+
// 检查数据量,对大数据量给出提示
|
|
1335
|
+
const nodeCount = elements.filter(e => e.group === 'nodes').length;
|
|
1336
|
+
if (nodeCount > 500) {
|
|
1337
|
+
showToast(t('largeDataset', { count: nodeCount }));
|
|
1338
|
+
}
|
|
1339
|
+
if (nodeCount > 1000) {
|
|
1340
|
+
const confirmed = confirm(t('veryLargeDataset', { count: nodeCount }));
|
|
1341
|
+
if (!confirmed) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// 使用 setTimeout 让浏览器先渲染加载动画
|
|
1347
|
+
setTimeout(() => {
|
|
1348
|
+
initCytoscape(elements);
|
|
1349
|
+
initFilters();
|
|
1350
|
+
initSearch();
|
|
1351
|
+
initLayouts();
|
|
1352
|
+
initConnectionListClickHandlers();
|
|
1353
|
+
console.log('初始化完成,节点数:', cy.nodes().length, '边数:', cy.edges().length);
|
|
1354
|
+
}, 100);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
console.error('初始化错误:', error);
|
|
1357
|
+
showToast(t('initializeError', { error: error.message }));
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// 初始化 Cytoscape
|
|
1362
|
+
function initCytoscape(elements) {
|
|
1363
|
+
// 检查数据
|
|
1364
|
+
console.log('元素数量:', elements.length);
|
|
1365
|
+
if (!elements || elements.length === 0) {
|
|
1366
|
+
throw new Error('没有可用的图数据');
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// 检查容器
|
|
1370
|
+
const container = document.getElementById('cy');
|
|
1371
|
+
if (!container) {
|
|
1372
|
+
throw new Error('找不到图形容器 #cy');
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
try {
|
|
1376
|
+
cy = cytoscape({
|
|
1377
|
+
container: container,
|
|
1378
|
+
elements: elements,
|
|
1379
|
+
style: [
|
|
1380
|
+
{
|
|
1381
|
+
selector: 'node',
|
|
1382
|
+
style: {
|
|
1383
|
+
'label': 'data(label)',
|
|
1384
|
+
'width': 60,
|
|
1385
|
+
'height': 60,
|
|
1386
|
+
'background-color': '#4a90d9',
|
|
1387
|
+
'color': '#333',
|
|
1388
|
+
'font-size': '11px',
|
|
1389
|
+
'text-valign': 'bottom',
|
|
1390
|
+
'text-margin-y': 5,
|
|
1391
|
+
'border-width': 3,
|
|
1392
|
+
'border-color': '#357abd',
|
|
1393
|
+
'text-wrap': 'wrap',
|
|
1394
|
+
'text-max-width': '80px'
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
selector: 'node[type="class"]',
|
|
1399
|
+
style: {
|
|
1400
|
+
'shape': 'round-rectangle',
|
|
1401
|
+
'width': 70,
|
|
1402
|
+
'height': 45,
|
|
1403
|
+
'background-color': '#4a90d9',
|
|
1404
|
+
'border-color': '#357abd'
|
|
1405
|
+
}
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
selector: 'node[type="function"]',
|
|
1409
|
+
style: {
|
|
1410
|
+
'shape': 'ellipse',
|
|
1411
|
+
'width': 50,
|
|
1412
|
+
'height': 50,
|
|
1413
|
+
'background-color': '#28a745',
|
|
1414
|
+
'border-color': '#218838'
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
selector: 'node[type="module"]',
|
|
1419
|
+
style: {
|
|
1420
|
+
'shape': 'round-rectangle',
|
|
1421
|
+
'width': 80,
|
|
1422
|
+
'height': 50,
|
|
1423
|
+
'background-color': '#ffc107',
|
|
1424
|
+
'border-color': '#e0a800'
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
selector: 'node:selected',
|
|
1429
|
+
style: {
|
|
1430
|
+
'border-width': 4,
|
|
1431
|
+
'border-color': '#dc3545'
|
|
1432
|
+
}
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
selector: 'node.highlighted',
|
|
1436
|
+
style: {
|
|
1437
|
+
'border-width': 4,
|
|
1438
|
+
'border-color': '#ffc107'
|
|
1439
|
+
}
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
selector: 'node.faded',
|
|
1443
|
+
style: {
|
|
1444
|
+
'opacity': 0.3
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
selector: 'edge',
|
|
1449
|
+
style: {
|
|
1450
|
+
'width': 2,
|
|
1451
|
+
'line-color': '#adb5bd',
|
|
1452
|
+
'target-arrow-color': '#adb5bd',
|
|
1453
|
+
'target-arrow-shape': 'triangle',
|
|
1454
|
+
'curve-style': 'bezier',
|
|
1455
|
+
'arrow-scale': 1.2
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
selector: 'edge:selected',
|
|
1460
|
+
style: {
|
|
1461
|
+
'line-color': '#dc3545',
|
|
1462
|
+
'target-arrow-color': '#dc3545'
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
],
|
|
1466
|
+
layout: getLayoutOptions('cose')
|
|
1467
|
+
});
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
console.error('Cytoscape 初始化失败:', error);
|
|
1470
|
+
throw new Error('图初始化失败: ' + error.message);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// 延迟更新统计信息,避免阻塞UI
|
|
1474
|
+
setTimeout(() => {
|
|
1475
|
+
updateStats();
|
|
1476
|
+
// 隐藏加载动画
|
|
1477
|
+
const loading = document.getElementById('loading');
|
|
1478
|
+
if (loading) {
|
|
1479
|
+
loading.style.display = 'none';
|
|
1480
|
+
}
|
|
1481
|
+
}, 100);
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
// 节点点击事件 - 使用 'click' 事件替代 'tap' 以确保兼容性
|
|
1485
|
+
cy.on('click', 'node', function(evt) {
|
|
1486
|
+
try {
|
|
1487
|
+
const node = evt.target;
|
|
1488
|
+
console.log('节点被点击:', node.data());
|
|
1489
|
+
|
|
1490
|
+
// 记录到历史(如果不是在导航中)
|
|
1491
|
+
if (!isNavigatingHistory) {
|
|
1492
|
+
addToHistory(node);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
showNodeInfo(node);
|
|
1496
|
+
highlightConnections(node);
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
console.error('显示节点信息时出错:', error);
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
// 点击空白处取消选择
|
|
1503
|
+
cy.on('click', function(evt) {
|
|
1504
|
+
if (evt.target === cy) {
|
|
1505
|
+
clearSelection();
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// 获取布局选项
|
|
1511
|
+
function getLayoutOptions(layoutName) {
|
|
1512
|
+
const options = {
|
|
1513
|
+
cose: {
|
|
1514
|
+
name: 'cose',
|
|
1515
|
+
padding: 50,
|
|
1516
|
+
nodeRepulsion: 8000,
|
|
1517
|
+
idealEdgeLength: 100,
|
|
1518
|
+
edgeElasticity: 100,
|
|
1519
|
+
animate: true,
|
|
1520
|
+
animationDuration: 500,
|
|
1521
|
+
// 随机化初始位置避免卡顿
|
|
1522
|
+
randomize: true,
|
|
1523
|
+
// 大数据量时禁用某些优化
|
|
1524
|
+
nodeDimensionsIncludeLabels: false,
|
|
1525
|
+
// 性能优化参数
|
|
1526
|
+
refresh: 30, // 每30帧刷新一次,提高响应速度
|
|
1527
|
+
fit: true,
|
|
1528
|
+
padding: 30,
|
|
1529
|
+
boundingBox: undefined,
|
|
1530
|
+
nodeOverlap: 20,
|
|
1531
|
+
// 停止条件优化
|
|
1532
|
+
stopThreshold: 0.1, // 降低停止阈值
|
|
1533
|
+
stopThresholdMaxIter: 500 // 最大迭代次数
|
|
1534
|
+
},
|
|
1535
|
+
circle: {
|
|
1536
|
+
name: 'circle',
|
|
1537
|
+
padding: 50,
|
|
1538
|
+
radius: 200,
|
|
1539
|
+
animate: true,
|
|
1540
|
+
animationDuration: 500
|
|
1541
|
+
},
|
|
1542
|
+
concentric: {
|
|
1543
|
+
name: 'concentric',
|
|
1544
|
+
padding: 50,
|
|
1545
|
+
concentric: function(node) {
|
|
1546
|
+
return node.degree();
|
|
1547
|
+
},
|
|
1548
|
+
minNodeSpacing: 50,
|
|
1549
|
+
animate: true,
|
|
1550
|
+
animationDuration: 500
|
|
1551
|
+
},
|
|
1552
|
+
breadthfirst: {
|
|
1553
|
+
name: 'breadthfirst',
|
|
1554
|
+
padding: 50,
|
|
1555
|
+
directed: true,
|
|
1556
|
+
animate: true,
|
|
1557
|
+
animationDuration: 500
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
return options[layoutName] || options.cose;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// 更新统计信息(优化版 - 移除深度计算)
|
|
1564
|
+
function updateStats() {
|
|
1565
|
+
const nodes = cy.nodes();
|
|
1566
|
+
const edges = cy.edges();
|
|
1567
|
+
|
|
1568
|
+
document.getElementById('node-count').textContent = nodes.length;
|
|
1569
|
+
document.getElementById('edge-count').textContent = edges.length;
|
|
1570
|
+
|
|
1571
|
+
// 移除深度计算以提升性能
|
|
1572
|
+
// 对于大图,深度计算非常耗时且意义不大
|
|
1573
|
+
document.getElementById('depth-count').textContent = 'N/A';
|
|
1574
|
+
|
|
1575
|
+
// 更新过滤器计数(使用缓存提升性能)
|
|
1576
|
+
document.getElementById('count-all').textContent = nodes.length;
|
|
1577
|
+
|
|
1578
|
+
// 使用reduce一次性计算,避免多次遍历
|
|
1579
|
+
const typeCounts = nodes.reduce((acc, node) => {
|
|
1580
|
+
const type = node.data('type');
|
|
1581
|
+
acc[type] = (acc[type] || 0) + 1;
|
|
1582
|
+
return acc;
|
|
1583
|
+
}, {});
|
|
1584
|
+
|
|
1585
|
+
document.getElementById('count-class').textContent = typeCounts['class'] || 0;
|
|
1586
|
+
document.getElementById('count-function').textContent = typeCounts['function'] || 0;
|
|
1587
|
+
document.getElementById('count-module').textContent = typeCounts['module'] || 0;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// 移除深度计算函数以提升性能
|
|
1591
|
+
// 该函数对大图性能影响极大,已移除
|
|
1592
|
+
|
|
1593
|
+
|
|
1594
|
+
// 显示节点信息
|
|
1595
|
+
function showNodeInfo(node) {
|
|
1596
|
+
try {
|
|
1597
|
+
console.log('显示节点信息:', node.data());
|
|
1598
|
+
|
|
1599
|
+
// 获取DOM元素
|
|
1600
|
+
const noSelection = document.getElementById('no-selection');
|
|
1601
|
+
const infoContent = document.getElementById('info-content');
|
|
1602
|
+
const infoPanel = document.getElementById('info-panel');
|
|
1603
|
+
|
|
1604
|
+
console.log('DOM元素状态:', {
|
|
1605
|
+
noSelection: noSelection ? 'found' : 'not found',
|
|
1606
|
+
infoContent: infoContent ? 'found' : 'not found',
|
|
1607
|
+
infoPanel: infoPanel ? 'found' : 'not found',
|
|
1608
|
+
infoPanelDisplay: infoPanel ? window.getComputedStyle(infoPanel).display : 'N/A',
|
|
1609
|
+
infoContentDisplay: infoContent ? window.getComputedStyle(infoContent).display : 'N/A'
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// 确保info-panel可见
|
|
1613
|
+
if (infoPanel) {
|
|
1614
|
+
infoPanel.style.display = 'block';
|
|
1615
|
+
infoPanel.classList.remove('collapsed');
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// 隐藏"无选择"提示
|
|
1619
|
+
if (noSelection) {
|
|
1620
|
+
noSelection.style.display = 'none';
|
|
1621
|
+
console.log('隐藏no-selection提示');
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// 显示信息内容
|
|
1625
|
+
if (infoContent) {
|
|
1626
|
+
infoContent.classList.add('active');
|
|
1627
|
+
infoContent.style.display = 'block';
|
|
1628
|
+
infoContent.style.visibility = 'visible';
|
|
1629
|
+
console.log('显示info-content, display:', infoContent.style.display);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// 获取并显示节点数据
|
|
1633
|
+
const nodeId = node.data('id') || '-';
|
|
1634
|
+
const nodeLabel = node.data('label') || '-';
|
|
1635
|
+
const nodeType = node.data('type') || '-';
|
|
1636
|
+
const nodeFile = node.data('file') || '-';
|
|
1637
|
+
const nodeStartLine = node.data('startLine') || '-';
|
|
1638
|
+
const nodeEndLine = node.data('endLine') || '-';
|
|
1639
|
+
const nodeConnections = node.degree() || 0;
|
|
1640
|
+
|
|
1641
|
+
console.log('节点数据:', { nodeId, nodeLabel, nodeType, nodeFile, nodeStartLine, nodeEndLine, nodeConnections });
|
|
1642
|
+
|
|
1643
|
+
// 更新各个字段
|
|
1644
|
+
const infoId = document.getElementById('info-id');
|
|
1645
|
+
const infoLabel = document.getElementById('info-label');
|
|
1646
|
+
const infoType = document.getElementById('info-type');
|
|
1647
|
+
const infoFile = document.getElementById('info-file');
|
|
1648
|
+
const infoStartLine = document.getElementById('info-start-line');
|
|
1649
|
+
const infoEndLine = document.getElementById('info-end-line');
|
|
1650
|
+
const infoConnections = document.getElementById('info-connections');
|
|
1651
|
+
|
|
1652
|
+
if (infoId) {
|
|
1653
|
+
infoId.textContent = nodeId;
|
|
1654
|
+
console.log('更新info-id:', nodeId);
|
|
1655
|
+
}
|
|
1656
|
+
if (infoLabel) {
|
|
1657
|
+
infoLabel.textContent = nodeLabel;
|
|
1658
|
+
console.log('更新info-label:', nodeLabel);
|
|
1659
|
+
}
|
|
1660
|
+
if (infoType) {
|
|
1661
|
+
infoType.textContent = nodeType;
|
|
1662
|
+
console.log('更新info-type:', nodeType);
|
|
1663
|
+
}
|
|
1664
|
+
if (infoFile) {
|
|
1665
|
+
infoFile.textContent = nodeFile;
|
|
1666
|
+
console.log('更新info-file:', nodeFile);
|
|
1667
|
+
}
|
|
1668
|
+
if (infoStartLine) {
|
|
1669
|
+
infoStartLine.textContent = nodeStartLine;
|
|
1670
|
+
console.log('更新info-start-line:', nodeStartLine);
|
|
1671
|
+
}
|
|
1672
|
+
if (infoEndLine) {
|
|
1673
|
+
infoEndLine.textContent = nodeEndLine;
|
|
1674
|
+
console.log('更新info-end-line:', nodeEndLine);
|
|
1675
|
+
}
|
|
1676
|
+
if (infoConnections) {
|
|
1677
|
+
infoConnections.textContent = nodeConnections;
|
|
1678
|
+
console.log('更新info-connections:', nodeConnections);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// 显示连接节点列表
|
|
1682
|
+
displayConnectionList(node);
|
|
1683
|
+
|
|
1684
|
+
console.log('节点信息已显示完成');
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
console.error('显示节点信息时出错:', error);
|
|
1687
|
+
console.error('错误堆栈:', error.stack);
|
|
1688
|
+
showToast(t('showNodeInfoError', { error: error.message }));
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// 显示连接节点列表
|
|
1693
|
+
function displayConnectionList(node) {
|
|
1694
|
+
try {
|
|
1695
|
+
const container = document.getElementById('connections-container');
|
|
1696
|
+
if (!container) return;
|
|
1697
|
+
|
|
1698
|
+
// 获取所有连接
|
|
1699
|
+
const neighborhood = node.neighborhood().nodes();
|
|
1700
|
+
const edges = node.connectedEdges();
|
|
1701
|
+
|
|
1702
|
+
if (neighborhood.length === 0) {
|
|
1703
|
+
container.innerHTML = '<div style="text-align: center; padding: 20px; color: #adb5bd; font-size: 12px;">' + t('noConnections') + '</div>';
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// 分类连接:incoming(指向当前节点)和outgoing(从当前节点指出)
|
|
1708
|
+
const incomingNodes = [];
|
|
1709
|
+
const outgoingNodes = [];
|
|
1710
|
+
|
|
1711
|
+
edges.forEach(edge => {
|
|
1712
|
+
const source = edge.source();
|
|
1713
|
+
const target = edge.target();
|
|
1714
|
+
|
|
1715
|
+
if (source.id() === node.id()) {
|
|
1716
|
+
// 当前节点是源,这是outgoing连接
|
|
1717
|
+
if (!outgoingNodes.find(n => n.id === target.id())) {
|
|
1718
|
+
outgoingNodes.push({
|
|
1719
|
+
node: target,
|
|
1720
|
+
edge: edge
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
} else {
|
|
1724
|
+
// 当前节点是目标,这是incoming连接
|
|
1725
|
+
if (!incomingNodes.find(n => n.id === source.id())) {
|
|
1726
|
+
incomingNodes.push({
|
|
1727
|
+
node: source,
|
|
1728
|
+
edge: edge
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
// 生成HTML
|
|
1735
|
+
let html = '';
|
|
1736
|
+
|
|
1737
|
+
// Incoming连接
|
|
1738
|
+
if (incomingNodes.length > 0) {
|
|
1739
|
+
html += '<div class="connections-header">';
|
|
1740
|
+
html += '<i class="bi bi-arrow-left"></i> ' + t('dependents');
|
|
1741
|
+
html += '<span class="count">' + incomingNodes.length + '</span>';
|
|
1742
|
+
html += '</div>';
|
|
1743
|
+
html += '<ul class="connection-list">';
|
|
1744
|
+
|
|
1745
|
+
incomingNodes.forEach(item => {
|
|
1746
|
+
const connNode = item.node;
|
|
1747
|
+
const nodeType = connNode.data('type') || 'unknown';
|
|
1748
|
+
const typeLabel = getTypeLabel(nodeType);
|
|
1749
|
+
|
|
1750
|
+
html += '<li class="connection-item incoming" data-node-id="' + connNode.id() + '">';
|
|
1751
|
+
html += '<div class="connection-main">';
|
|
1752
|
+
html += '<span class="connection-type-badge incoming">' + t('dependents') + '</span>';
|
|
1753
|
+
html += '<span class="connection-node-type ' + nodeType + '">' + typeLabel + '</span>';
|
|
1754
|
+
html += '<span class="connection-name">' + connNode.data('label') + '</span>';
|
|
1755
|
+
html += '<span class="connection-connections">' + connNode.degree() + t('connections') + '</span>';
|
|
1756
|
+
html += '</div>';
|
|
1757
|
+
html += '<div class="connection-id">ID: ' + connNode.id() + '</div>';
|
|
1758
|
+
html += '</li>';
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
html += '</ul>';
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// Outgoing连接
|
|
1765
|
+
if (outgoingNodes.length > 0) {
|
|
1766
|
+
html += '<div class="connections-header" style="margin-top: 15px;">';
|
|
1767
|
+
html += '<i class="bi bi-arrow-right"></i> ' + t('dependencies');
|
|
1768
|
+
html += '<span class="count">' + outgoingNodes.length + '</span>';
|
|
1769
|
+
html += '</div>';
|
|
1770
|
+
html += '<ul class="connection-list">';
|
|
1771
|
+
|
|
1772
|
+
outgoingNodes.forEach(item => {
|
|
1773
|
+
const connNode = item.node;
|
|
1774
|
+
const nodeType = connNode.data('type') || 'unknown';
|
|
1775
|
+
const typeLabel = getTypeLabel(nodeType);
|
|
1776
|
+
|
|
1777
|
+
html += '<li class="connection-item outgoing" data-node-id="' + connNode.id() + '">';
|
|
1778
|
+
html += '<div class="connection-main">';
|
|
1779
|
+
html += '<span class="connection-type-badge outgoing">' + t('dependencies') + '</span>';
|
|
1780
|
+
html += '<span class="connection-node-type ' + nodeType + '">' + typeLabel + '</span>';
|
|
1781
|
+
html += '<span class="connection-name">' + connNode.data('label') + '</span>';
|
|
1782
|
+
html += '<span class="connection-connections">' + connNode.degree() + t('connections') + '</span>';
|
|
1783
|
+
html += '</div>';
|
|
1784
|
+
html += '<div class="connection-id">ID: ' + connNode.id() + '</div>';
|
|
1785
|
+
html += '</li>';
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
html += '</ul>';
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
container.innerHTML = html;
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
console.error('显示连接列表时出错:', error);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// 获取类型标签
|
|
1798
|
+
function getTypeLabel(type) {
|
|
1799
|
+
const labels = {
|
|
1800
|
+
'class': currentLang === 'en' ? 'Class' : '类',
|
|
1801
|
+
'function': currentLang === 'en' ? 'Func' : '函数',
|
|
1802
|
+
'module': currentLang === 'en' ? 'Mod' : '模块'
|
|
1803
|
+
};
|
|
1804
|
+
return labels[type] || type.substring(0, 1).toUpperCase();
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// 选中指定节点
|
|
1808
|
+
function selectNode(nodeId) {
|
|
1809
|
+
try {
|
|
1810
|
+
const node = cy.getElementById(nodeId);
|
|
1811
|
+
if (node) {
|
|
1812
|
+
// 记录到历史(如果不是在导航中)
|
|
1813
|
+
if (!isNavigatingHistory) {
|
|
1814
|
+
addToHistory(node);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// 选中节点
|
|
1818
|
+
cy.nodes().unselect();
|
|
1819
|
+
node.select();
|
|
1820
|
+
|
|
1821
|
+
// 居中显示
|
|
1822
|
+
cy.animate({
|
|
1823
|
+
center: { eles: node },
|
|
1824
|
+
zoom: 1.5
|
|
1825
|
+
}, {
|
|
1826
|
+
duration: 300
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
// 显示节点信息
|
|
1830
|
+
showNodeInfo(node);
|
|
1831
|
+
highlightConnections(node);
|
|
1832
|
+
|
|
1833
|
+
showToast(t('selected', { label: node.data('label') }));
|
|
1834
|
+
}
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
console.error('选中节点时出错:', error);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// 添加到历史记录
|
|
1841
|
+
function addToHistory(node) {
|
|
1842
|
+
const nodeId = node.id();
|
|
1843
|
+
|
|
1844
|
+
// 如果当前位置不在历史末尾,删除当前位置之后的所有记录
|
|
1845
|
+
if (historyIndex < nodeHistory.length - 1) {
|
|
1846
|
+
nodeHistory = nodeHistory.slice(0, historyIndex + 1);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// 如果当前节点与历史中最后一个节点不同,才添加
|
|
1850
|
+
if (nodeHistory.length === 0 || nodeHistory[historyIndex].id !== nodeId) {
|
|
1851
|
+
nodeHistory.push({
|
|
1852
|
+
id: nodeId,
|
|
1853
|
+
label: node.data('label'),
|
|
1854
|
+
timestamp: new Date().getTime()
|
|
1855
|
+
});
|
|
1856
|
+
historyIndex = nodeHistory.length - 1;
|
|
1857
|
+
|
|
1858
|
+
// 更新按钮状态
|
|
1859
|
+
updateNavigationButtons();
|
|
1860
|
+
|
|
1861
|
+
console.log('添加到历史:', nodeId, '历史索引:', historyIndex);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// 更新导航按钮状态
|
|
1866
|
+
function updateNavigationButtons() {
|
|
1867
|
+
const btnBack = document.getElementById('btn-back');
|
|
1868
|
+
const btnForward = document.getElementById('btn-forward');
|
|
1869
|
+
|
|
1870
|
+
if (btnBack) {
|
|
1871
|
+
btnBack.disabled = historyIndex <= 0;
|
|
1872
|
+
}
|
|
1873
|
+
if (btnForward) {
|
|
1874
|
+
btnForward.disabled = historyIndex >= nodeHistory.length - 1;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// 后退
|
|
1879
|
+
function navigateBack() {
|
|
1880
|
+
if (historyIndex > 0) {
|
|
1881
|
+
isNavigatingHistory = true;
|
|
1882
|
+
historyIndex--;
|
|
1883
|
+
|
|
1884
|
+
const historyItem = nodeHistory[historyIndex];
|
|
1885
|
+
const node = cy.getElementById(historyItem.id);
|
|
1886
|
+
|
|
1887
|
+
if (node) {
|
|
1888
|
+
// 选中节点
|
|
1889
|
+
cy.nodes().unselect();
|
|
1890
|
+
node.select();
|
|
1891
|
+
|
|
1892
|
+
// 居中显示
|
|
1893
|
+
cy.animate({
|
|
1894
|
+
center: { eles: node },
|
|
1895
|
+
zoom: 1.5
|
|
1896
|
+
}, {
|
|
1897
|
+
duration: 300
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
// 显示节点信息
|
|
1901
|
+
showNodeInfo(node);
|
|
1902
|
+
highlightConnections(node);
|
|
1903
|
+
|
|
1904
|
+
showToast(t('navigatingBack', { index: historyIndex + 1, total: nodeHistory.length, label: historyItem.label }));
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
updateNavigationButtons();
|
|
1908
|
+
isNavigatingHistory = false;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// 前进
|
|
1913
|
+
function navigateForward() {
|
|
1914
|
+
if (historyIndex < nodeHistory.length - 1) {
|
|
1915
|
+
isNavigatingHistory = true;
|
|
1916
|
+
historyIndex++;
|
|
1917
|
+
|
|
1918
|
+
const historyItem = nodeHistory[historyIndex];
|
|
1919
|
+
const node = cy.getElementById(historyItem.id);
|
|
1920
|
+
|
|
1921
|
+
if (node) {
|
|
1922
|
+
// 选中节点
|
|
1923
|
+
cy.nodes().unselect();
|
|
1924
|
+
node.select();
|
|
1925
|
+
|
|
1926
|
+
// 居中显示
|
|
1927
|
+
cy.animate({
|
|
1928
|
+
center: { eles: node },
|
|
1929
|
+
zoom: 1.5
|
|
1930
|
+
}, {
|
|
1931
|
+
duration: 300
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
// 显示节点信息
|
|
1935
|
+
showNodeInfo(node);
|
|
1936
|
+
highlightConnections(node);
|
|
1937
|
+
|
|
1938
|
+
showToast(t('navigatingForward', { index: historyIndex + 1, total: nodeHistory.length, label: historyItem.label }));
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
updateNavigationButtons();
|
|
1942
|
+
isNavigatingHistory = false;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// 初始化连接节点点击事件委托
|
|
1947
|
+
function initConnectionListClickHandlers() {
|
|
1948
|
+
// 使用事件委托处理连接列表项的点击
|
|
1949
|
+
const container = document.getElementById('connections-container');
|
|
1950
|
+
if (container) {
|
|
1951
|
+
container.addEventListener('click', function(e) {
|
|
1952
|
+
const listItem = e.target.closest('.connection-item');
|
|
1953
|
+
if (listItem) {
|
|
1954
|
+
const nodeId = listItem.getAttribute('data-node-id');
|
|
1955
|
+
if (nodeId) {
|
|
1956
|
+
selectNode(nodeId);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// 高亮连接
|
|
1964
|
+
function highlightConnections(node) {
|
|
1965
|
+
cy.elements().removeClass('highlighted faded');
|
|
1966
|
+
|
|
1967
|
+
const neighborhood = node.closedNeighborhood();
|
|
1968
|
+
cy.elements().not(neighborhood).addClass('faded');
|
|
1969
|
+
neighborhood.addClass('highlighted');
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// 清除选择(不影响搜索过滤状态)
|
|
1973
|
+
function clearSelection() {
|
|
1974
|
+
try {
|
|
1975
|
+
cy.elements().removeClass('highlighted faded selected');
|
|
1976
|
+
|
|
1977
|
+
// 如果搜索激活状态,保持搜索过滤的可见性
|
|
1978
|
+
if (isSearchActive && searchFilteredNodes) {
|
|
1979
|
+
// 保持搜索过滤状态,只移除高亮
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
const noSelection = document.getElementById('no-selection');
|
|
1984
|
+
const infoContent = document.getElementById('info-content');
|
|
1985
|
+
|
|
1986
|
+
console.log('清除选择');
|
|
1987
|
+
|
|
1988
|
+
if (noSelection) {
|
|
1989
|
+
noSelection.style.display = 'block';
|
|
1990
|
+
console.log('显示no-selection提示');
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
if (infoContent) {
|
|
1994
|
+
infoContent.classList.remove('active');
|
|
1995
|
+
infoContent.style.display = 'none';
|
|
1996
|
+
infoContent.style.visibility = 'hidden';
|
|
1997
|
+
console.log('隐藏info-content');
|
|
1998
|
+
}
|
|
1999
|
+
} catch (error) {
|
|
2000
|
+
console.error('清除选择时出错:', error);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// 初始化筛选器
|
|
2005
|
+
function initFilters() {
|
|
2006
|
+
const filterButtons = document.querySelectorAll('.filter-btn');
|
|
2007
|
+
filterButtons.forEach(btn => {
|
|
2008
|
+
btn.addEventListener('click', function() {
|
|
2009
|
+
filterButtons.forEach(b => b.classList.remove('active'));
|
|
2010
|
+
this.classList.add('active');
|
|
2011
|
+
|
|
2012
|
+
currentFilter = this.dataset.filter;
|
|
2013
|
+
applyFilter(currentFilter);
|
|
2014
|
+
});
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// 应用筛选(优化版 - 批量操作)
|
|
2019
|
+
function applyFilter(filter) {
|
|
2020
|
+
// 清除搜索状态
|
|
2021
|
+
isSearchActive = false;
|
|
2022
|
+
searchFilteredNodes = null;
|
|
2023
|
+
document.getElementById('search-input').value = '';
|
|
2024
|
+
|
|
2025
|
+
cy.elements().removeClass('highlighted faded');
|
|
2026
|
+
|
|
2027
|
+
if (filter === 'all') {
|
|
2028
|
+
cy.elements().show();
|
|
2029
|
+
} else {
|
|
2030
|
+
// 使用批量操作提升性能
|
|
2031
|
+
const nodesToShow = cy.nodes().filter(n => n.data('type') === filter);
|
|
2032
|
+
const nodesToHide = cy.nodes().filter(n => n.data('type') !== filter);
|
|
2033
|
+
|
|
2034
|
+
nodesToShow.show();
|
|
2035
|
+
nodesToHide.connectedEdges().show();
|
|
2036
|
+
nodesToHide.hide();
|
|
2037
|
+
|
|
2038
|
+
const typeMap = { class: t('class'), function: t('function'), module: t('module') };
|
|
2039
|
+
showToast(t('typeFiltered', { count: nodesToShow.length, type: typeMap[filter] || filter }));
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// 初始化搜索(优化版 - 添加结果限制)
|
|
2044
|
+
function initSearch() {
|
|
2045
|
+
const searchInput = document.getElementById('search-input');
|
|
2046
|
+
const clearBtn = document.getElementById('search-clear-btn');
|
|
2047
|
+
let searchTimeout;
|
|
2048
|
+
|
|
2049
|
+
// 自动调整textarea高度
|
|
2050
|
+
function autoResizeTextarea() {
|
|
2051
|
+
searchInput.style.height = 'auto';
|
|
2052
|
+
const newHeight = Math.min(searchInput.scrollHeight, 120);
|
|
2053
|
+
searchInput.style.height = newHeight + 'px';
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// 更新清空按钮显示状态
|
|
2057
|
+
function updateClearButton() {
|
|
2058
|
+
if (searchInput.value.trim().length > 0) {
|
|
2059
|
+
clearBtn.classList.add('show');
|
|
2060
|
+
} else {
|
|
2061
|
+
clearBtn.classList.remove('show');
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// 清空搜索
|
|
2066
|
+
function clearSearch() {
|
|
2067
|
+
isClearing = true;
|
|
2068
|
+
isSearchActive = false;
|
|
2069
|
+
searchFilteredNodes = null;
|
|
2070
|
+
cy.elements().removeClass('highlighted faded');
|
|
2071
|
+
cy.elements().show();
|
|
2072
|
+
searchInput.value = '';
|
|
2073
|
+
autoResizeTextarea(); // 自动调整高度
|
|
2074
|
+
updateClearButton();
|
|
2075
|
+
console.log('搜索已清空,恢复显示所有节点');
|
|
2076
|
+
|
|
2077
|
+
// 延迟重置标志,确保 input 事件已经处理完毕
|
|
2078
|
+
setTimeout(() => {
|
|
2079
|
+
isClearing = false;
|
|
2080
|
+
}, 100);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// 清空按钮点击事件
|
|
2084
|
+
clearBtn.addEventListener('click', function(e) {
|
|
2085
|
+
e.preventDefault(); // 阻止默认行为
|
|
2086
|
+
e.stopPropagation(); // 阻止事件冒泡
|
|
2087
|
+
console.log('清空按钮被点击');
|
|
2088
|
+
clearSearch();
|
|
2089
|
+
searchInput.focus();
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
searchInput.addEventListener('input', function() {
|
|
2093
|
+
autoResizeTextarea(); // 自动调整高度
|
|
2094
|
+
updateClearButton();
|
|
2095
|
+
|
|
2096
|
+
// 如果正在执行清空操作,跳过处理
|
|
2097
|
+
if (isClearing) {
|
|
2098
|
+
console.log('正在清空,跳过 input 事件处理');
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
clearTimeout(searchTimeout);
|
|
2103
|
+
searchTimeout = setTimeout(() => {
|
|
2104
|
+
const searchTerm = this.value.toLowerCase().trim();
|
|
2105
|
+
|
|
2106
|
+
if (searchTerm === '') {
|
|
2107
|
+
// 清空搜索框,恢复显示所有节点
|
|
2108
|
+
clearSearch();
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// 支持逗号分割的多个搜索词(OR 关系)
|
|
2113
|
+
const searchTerms = searchTerm.split(',')
|
|
2114
|
+
.map(term => term.trim())
|
|
2115
|
+
.filter(term => term.length > 0);
|
|
2116
|
+
|
|
2117
|
+
if (searchTerms.length === 0) {
|
|
2118
|
+
clearSearch();
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// 搜索匹配的节点(任意一个搜索词匹配即可)
|
|
2123
|
+
const matchingNodes = cy.nodes().filter(node => {
|
|
2124
|
+
const nodeLabel = node.data('label').toLowerCase();
|
|
2125
|
+
const nodeId = node.data('id').toLowerCase();
|
|
2126
|
+
return searchTerms.some(term =>
|
|
2127
|
+
nodeLabel.includes(term) || nodeId.includes(term)
|
|
2128
|
+
);
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
if (matchingNodes.length > 0) {
|
|
2132
|
+
// 设置搜索激活状态
|
|
2133
|
+
isSearchActive = true;
|
|
2134
|
+
searchFilteredNodes = matchingNodes;
|
|
2135
|
+
|
|
2136
|
+
// 隐藏所有元素
|
|
2137
|
+
cy.elements().hide();
|
|
2138
|
+
|
|
2139
|
+
// 只显示匹配的节点
|
|
2140
|
+
matchingNodes.show();
|
|
2141
|
+
|
|
2142
|
+
// 显示匹配节点之间的边(两个端点都在匹配集合中)
|
|
2143
|
+
const edgesBetweenMatched = cy.edges().filter(edge => {
|
|
2144
|
+
const source = edge.source();
|
|
2145
|
+
const target = edge.target();
|
|
2146
|
+
return source.inside(matchingNodes) && target.inside(matchingNodes);
|
|
2147
|
+
});
|
|
2148
|
+
edgesBetweenMatched.show();
|
|
2149
|
+
|
|
2150
|
+
// 限制最多显示100个结果,避免性能问题
|
|
2151
|
+
const maxResults = 100;
|
|
2152
|
+
const nodesToHighlight = matchingNodes.slice(0, maxResults);
|
|
2153
|
+
|
|
2154
|
+
nodesToHighlight.addClass('highlighted');
|
|
2155
|
+
|
|
2156
|
+
// 选中第一个匹配项并居中
|
|
2157
|
+
const firstNode = matchingNodes[0];
|
|
2158
|
+
cy.animate({
|
|
2159
|
+
center: { eles: firstNode },
|
|
2160
|
+
zoom: 1.5
|
|
2161
|
+
}, {
|
|
2162
|
+
duration: 300
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
if (matchingNodes.length > maxResults) {
|
|
2166
|
+
showToast(t('foundNodesLimited', { count: matchingNodes.length, max: maxResults }));
|
|
2167
|
+
} else {
|
|
2168
|
+
showToast(t('foundNodes', { count: matchingNodes.length }));
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
console.log(`搜索完成:显示 ${matchingNodes.length} 个节点`);
|
|
2172
|
+
} else {
|
|
2173
|
+
isSearchActive = true;
|
|
2174
|
+
searchFilteredNodes = $();
|
|
2175
|
+
cy.elements().hide();
|
|
2176
|
+
showToast(t('noMatchingNodes'));
|
|
2177
|
+
}
|
|
2178
|
+
}, 300);
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// 初始化布局
|
|
2183
|
+
function initLayouts() {
|
|
2184
|
+
const layoutButtons = document.querySelectorAll('.layout-btn');
|
|
2185
|
+
layoutButtons.forEach(btn => {
|
|
2186
|
+
btn.addEventListener('click', function() {
|
|
2187
|
+
layoutButtons.forEach(b => b.classList.remove('active'));
|
|
2188
|
+
this.classList.add('active');
|
|
2189
|
+
|
|
2190
|
+
currentLayout = this.dataset.layout;
|
|
2191
|
+
applyLayout(currentLayout);
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// 应用布局(优化版 - 添加进度反馈)
|
|
2197
|
+
function applyLayout(layoutName) {
|
|
2198
|
+
const nodeCount = cy.nodes().length;
|
|
2199
|
+
|
|
2200
|
+
if (nodeCount > 500) {
|
|
2201
|
+
showToast(t('applyingLayout'));
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
const layout = cy.layout(getLayoutOptions(layoutName));
|
|
2205
|
+
|
|
2206
|
+
layout.on('layoutstart', function() {
|
|
2207
|
+
if (nodeCount > 300) {
|
|
2208
|
+
document.body.style.cursor = 'wait';
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2211
|
+
|
|
2212
|
+
layout.on('layoutstop', function() {
|
|
2213
|
+
document.body.style.cursor = 'default';
|
|
2214
|
+
if (nodeCount > 500) {
|
|
2215
|
+
showToast(t('layoutUpdated'));
|
|
2216
|
+
}
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
layout.run();
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// 放大
|
|
2223
|
+
function zoomIn() {
|
|
2224
|
+
cy.zoom({
|
|
2225
|
+
level: cy.zoom() * 1.2,
|
|
2226
|
+
renderedPosition: {
|
|
2227
|
+
x: cy.width() / 2,
|
|
2228
|
+
y: cy.height() / 2
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// 缩小
|
|
2234
|
+
function zoomOut() {
|
|
2235
|
+
cy.zoom({
|
|
2236
|
+
level: cy.zoom() * 0.8,
|
|
2237
|
+
renderedPosition: {
|
|
2238
|
+
x: cy.width() / 2,
|
|
2239
|
+
y: cy.height() / 2
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// 重置视图
|
|
2245
|
+
function resetView() {
|
|
2246
|
+
cy.fit();
|
|
2247
|
+
cy.center();
|
|
2248
|
+
showToast(t('viewReset'));
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// 适应窗口
|
|
2252
|
+
function fitGraph() {
|
|
2253
|
+
cy.fit();
|
|
2254
|
+
showToast(t('windowFitted'));
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// 导出图片
|
|
2258
|
+
function exportImage() {
|
|
2259
|
+
const png = cy.png({ full: true, scale: 2 });
|
|
2260
|
+
const link = document.createElement('a');
|
|
2261
|
+
link.href = png;
|
|
2262
|
+
link.download = 'dependency-graph.png';
|
|
2263
|
+
link.click();
|
|
2264
|
+
showToast(t('imageExported'));
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// 切换面板
|
|
2268
|
+
function togglePanel(panelId) {
|
|
2269
|
+
const panel = document.getElementById(panelId);
|
|
2270
|
+
const isCurrentlyCollapsed = panel.classList.contains('collapsed');
|
|
2271
|
+
|
|
2272
|
+
if (isCurrentlyCollapsed) {
|
|
2273
|
+
// 显示面板
|
|
2274
|
+
panel.classList.remove('collapsed');
|
|
2275
|
+
|
|
2276
|
+
// 更新按钮样式和图标
|
|
2277
|
+
const btnClass = panelId === 'control-panel' ? '.edge-toggle-btn.left-edge' : '.edge-toggle-btn.right-edge';
|
|
2278
|
+
const btn = document.querySelector(btnClass);
|
|
2279
|
+
if (btn) {
|
|
2280
|
+
btn.classList.remove('collapsed');
|
|
2281
|
+
btn.classList.add('expanded');
|
|
2282
|
+
// 反向图标:展开时图标指向外
|
|
2283
|
+
const icon = btn.querySelector('i');
|
|
2284
|
+
if (panelId === 'control-panel') {
|
|
2285
|
+
icon.classList.remove('bi-chevron-right');
|
|
2286
|
+
icon.classList.add('bi-chevron-left');
|
|
2287
|
+
} else {
|
|
2288
|
+
icon.classList.remove('bi-chevron-left');
|
|
2289
|
+
icon.classList.add('bi-chevron-right');
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
const panelName = panelId === 'control-panel' ? (currentLang === 'en' ? 'Control panel' : '控制面板') : (currentLang === 'en' ? 'Info panel' : '信息面板');
|
|
2294
|
+
showToast(t('panelShown', { panel: panelName }));
|
|
2295
|
+
} else {
|
|
2296
|
+
// 隐藏面板
|
|
2297
|
+
panel.classList.add('collapsed');
|
|
2298
|
+
|
|
2299
|
+
// 更新按钮样式和图标
|
|
2300
|
+
const btnClass = panelId === 'control-panel' ? '.edge-toggle-btn.left-edge' : '.edge-toggle-btn.right-edge';
|
|
2301
|
+
const btn = document.querySelector(btnClass);
|
|
2302
|
+
if (btn) {
|
|
2303
|
+
btn.classList.remove('expanded');
|
|
2304
|
+
btn.classList.add('collapsed');
|
|
2305
|
+
// 反向图标:折叠时图标指向内
|
|
2306
|
+
const icon = btn.querySelector('i');
|
|
2307
|
+
if (panelId === 'control-panel') {
|
|
2308
|
+
icon.classList.remove('bi-chevron-left');
|
|
2309
|
+
icon.classList.add('bi-chevron-right');
|
|
2310
|
+
} else {
|
|
2311
|
+
icon.classList.remove('bi-chevron-right');
|
|
2312
|
+
icon.classList.add('bi-chevron-left');
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
const panelName = panelId === 'control-panel' ? (currentLang === 'en' ? 'Control panel' : '控制面板') : (currentLang === 'en' ? 'Info panel' : '信息面板');
|
|
2317
|
+
showToast(t('panelHidden', { panel: panelName }));
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// 显示提示
|
|
2322
|
+
function showToast(message) {
|
|
2323
|
+
const toast = document.getElementById('toast');
|
|
2324
|
+
toast.textContent = message;
|
|
2325
|
+
toast.classList.add('show');
|
|
2326
|
+
|
|
2327
|
+
setTimeout(() => {
|
|
2328
|
+
toast.classList.remove('show');
|
|
2329
|
+
}, 2000);
|
|
2330
|
+
}
|
|
2331
|
+
</script>
|
|
2332
|
+
</body>
|
|
2333
|
+
</html>
|