-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.js.backup
More file actions
3982 lines (3425 loc) · 124 KB
/
main.js.backup
File metadata and controls
3982 lines (3425 loc) · 124 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
if (typeof joint === 'undefined') {
alert('JointJS未加载成功,请检查CDN或网络!');
throw new Error('JointJS未加载成功');
}
// 获取节点相关的所有连接线(包括内部节点之间的连接线)
function getAllRelatedLinks(element) {
const graph = element.graph;
if (!graph) return [];
let links = [];
// 如果是容器节点,获取所有嵌套节点
if (element.isContainer) {
const embeddedCells = element.getEmbeddedCells();
const embeddedElements = embeddedCells.filter(cell => !cell.isLink());
// 获取容器内每个节点的连接线
embeddedElements.forEach(cell => {
// 获取与该节点相连的所有连接线
const cellLinks = graph.getConnectedLinks(cell, { includeEnclosed: true });
links = links.concat(cellLinks);
});
// 特别处理:获取容器内部节点之间的连接线
// 这是关键部分,确保容器内部节点之间的连接线也被包含
const internalLinks = [];
embeddedElements.forEach(sourceCell => {
embeddedElements.forEach(targetCell => {
if (sourceCell.id !== targetCell.id) {
// 查找从sourceCell到targetCell的连接线
const directLinks = graph.getConnectedLinks(sourceCell, {
outbound: true,
inbound: false,
includeEnclosed: true
}).filter(link => {
const target = link.getTargetCell();
return target && target.id === targetCell.id;
});
internalLinks.push(...directLinks);
}
});
});
links = links.concat(internalLinks);
}
// 获取与元素本身相连的连接线
const elementLinks = graph.getConnectedLinks(element);
links = links.concat(elementLinks);
// 去重
return Array.from(new Set(links));
}
const graph = new joint.dia.Graph();
// 计算初始画布尺寸
const paperContainer = document.getElementById('paper-container');
const initialWidth = window.innerWidth - 140; // 减去左侧面板宽度
const initialHeight = window.innerHeight;
const paper = new joint.dia.Paper({
el: paperContainer,
model: graph,
width: initialWidth,
height: initialHeight,
gridSize: 10,
drawGrid: true,
background: { color: '#f8f9fa' },
defaultLink: () => new joint.shapes.standard.Link({
attrs: {
line: {
stroke: '#333',
strokeWidth: 2,
targetMarker: {
type: 'path',
d: 'M 10 -5 0 0 10 5 z',
fill: '#333'
}
}
}
}),
interactive: {
magnet: true,
elementMove: true,
linkMove: true,
addLinkFromMagnet: true,
validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
// Allow connections between any elements
return true;
}
},
highlighting: {
'default': {
name: 'stroke',
options: {
padding: 6,
attrs: {
'stroke-width': 3,
stroke: '#1976d2'
}
}
}
},
// 设置画布可平移
async: true,
frozen: false
});
// 添加全局层级管理 - 监听节点添加事件
graph.on('add', function(cell) {
// 如果添加的是节点(不是连接线)
if (!cell.isLink()) {
// 检查是否放置在容器节点上
const containers = graph.getElements().filter(e => e.isContainer);
for (const container of containers) {
const bbox = container.getBBox();
const cellBBox = cell.getBBox();
// 判断节点中心是否在容器内
const center = cellBBox.center();
if (
center.x > bbox.x &&
center.x < bbox.x + bbox.width &&
center.y > bbox.y &&
center.y < bbox.y + bbox.height
) {
// 检查节点类型,开始和结束节点不能被嵌套
const nodeType = cell.get('type');
const isStartOrEnd = nodeType === 'standard.Circle' &&
(cell.attr('label/text') === '开始' || cell.attr('label/text') === '结束');
if (isStartOrEnd) {
// 开始或结束节点不嵌套,但确保它们在最上层
cell.toFront();
continue;
}
// 如果节点不是容器节点本身,则嵌套
if (!cell.isContainer) {
// 确保节点在容器上方显示
cell.toFront();
// 嵌套节点到容器中
container.embed(cell);
// 确保嵌套后节点仍然可见
setTimeout(() => {
cell.toFront();
}, 50);
}
}
}
}
});
// Ensure all elements are visible and interactive
paper.on('cell:pointerdown', function(cellView) {
const model = cellView.model;
// 如果是连接线,检查它是否连接了容器内的节点
if (model.isLink()) {
const sourceCell = model.getSourceCell();
const targetCell = model.getTargetCell();
// 检查源节点或目标节点是否在容器内
const sourceParent = sourceCell ? sourceCell.getParentCell() : null;
const targetParent = targetCell ? targetCell.getParentCell() : null;
// 如果连接线连接的是容器内的节点,确保它在最上层
if (sourceParent && sourceParent.isContainer || targetParent && targetParent.isContainer) {
// 找到所有相关的容器
const containers = [];
if (sourceParent && sourceParent.isContainer) containers.push(sourceParent);
if (targetParent && targetParent.isContainer) containers.push(targetParent);
// 确保容器、容器内节点和连接线都在最上层
containers.forEach(container => {
// 先将容器节点移到前面
container.toFront({ deep: false });
// 然后将嵌套节点移到前面
const embeds = container.getEmbeddedCells();
if (embeds.length > 0) {
embeds.forEach(embed => {
embed.toFront();
});
}
});
// 最后将连接线移到最前面
model.toFront();
return;
}
}
// 对于其他元素,正常处理
model.toFront();
});
// ========== 左侧面板拖放功能 ========== //
// 获取左侧面板元素
const sidebar = document.getElementById('sidebar');
const nodeTypes = sidebar.querySelectorAll('.node-type');
// 移除旧的palette元素(如果存在)
const oldPalette = document.querySelector('.palette-item');
if (oldPalette && oldPalette.parentElement) {
oldPalette.parentElement.remove();
}
// 为每个节点类型添加draggable属性
nodeTypes.forEach(nodeType => {
nodeType.setAttribute('draggable', 'true');
});
// 添加一些额外的样式
const style = document.createElement('style');
style.innerHTML = `
/* 拖拽时的样式 */
.node-type.dragging {
opacity: 0.5;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* 拖拽时的光标样式 */
.node-type:active {
cursor: grabbing;
}
/* 确保所有元素可见 */
.joint-element {
z-index: 1;
}
/* 确保开始和结束节点始终可见 */
.joint-element circle {
z-index: 2;
}
/* 确保文本标签可见 */
.joint-element text {
z-index: 3;
}
/* 平移模式样式 */
.panning-cursor * {
cursor: grab !important;
}
.panning-cursor:active * {
cursor: grabbing !important;
}
`;
document.head.appendChild(style);
// ========== 锚点分组配置 ========== //
const portGroups = {
bottom: {
position: { name: 'bottom' }, // 使用name: 'bottom'确保位置正确
attrs: {
circle: {
r: 4, // 锚点半径设置为4px,直径8px
magnet: true,
stroke: '#333', // 边框颜色
strokeWidth: 1,
fill: '#fff', // 填充颜色
cursor: 'crosshair',
opacity: 0 // 默认隐藏
}
},
markup: [
{
tagName: 'circle',
selector: 'circle',
attributes: {
'r': 4,
'magnet': 'true',
'fill': '#fff',
'stroke': '#333',
'stroke-width': 1
}
}
]
},
// 为 Switch 节点添加自定义锚点分组
switchPorts: {
position: function(ports, elBBox) {
// 计算每个锚点的位置
if (ports.length === 1) {
// 如果只有一个锚点,放在中间
return [{ x: elBBox.width / 2, y: elBBox.height }];
} else if (ports.length > 1) {
// 如果有多个锚点,均匀分布
const spacing = elBBox.width / (ports.length + 1);
return ports.map((port, index) => {
return {
x: spacing * (index + 1),
y: elBBox.height
};
});
}
return [];
},
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#333',
strokeWidth: 1,
fill: '#fff',
cursor: 'crosshair',
opacity: 0
}
},
markup: [
{
tagName: 'circle',
selector: 'circle',
attributes: {
'r': 4,
'magnet': 'true',
'fill': '#fff',
'stroke': '#333',
'stroke-width': 1
}
}
]
}
};
// ========== 拖拽创建节点 ========== //
let dragType = null;
let draggedElement = null;
// 为每个节点类型添加拖拽事件
nodeTypes.forEach(nodeType => {
// 拖拽开始
nodeType.addEventListener('dragstart', e => {
dragType = e.target.dataset.type;
draggedElement = e.target;
// 添加拖拽中的样式
e.target.classList.add('dragging');
// 设置拖拽数据
e.dataTransfer.setData('text/plain', dragType);
e.dataTransfer.effectAllowed = 'copy'; // 允许复制效果
// 使用默认拖拽图像,不需要自定义
});
// 拖拽结束
nodeType.addEventListener('dragend', e => {
e.target.classList.remove('dragging');
dragType = null;
draggedElement = null;
});
});
// 画布上的拖拽事件
paper.el.addEventListener('dragover', e => {
// 只有从侧边栏拖拽时才允许放置
if (dragType) {
e.preventDefault(); // 允许放置
e.dataTransfer.dropEffect = 'copy'; // 设置放置效果为复制
}
});
paper.el.addEventListener('drop', e => {
e.preventDefault(); // 阻止默认行为,处理自定义放置逻辑
if (!dragType) return; // 如果不是从侧边栏拖拽来的,不处理
const { left, top } = paper.el.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
let node;
// 根据 dragType 创建不同类型的节点
if (dragType === 'start') {
node = new joint.shapes.standard.Circle();
node.position(x - 40, y - 40);
node.resize(80, 80);
node.attr({
body: {
fill: '#4caf50',
stroke: '#388e3c',
strokeWidth: 3,
pointerEvents: 'auto' // Ensure pointer events work
},
label: {
text: '开始',
fill: '#fff',
fontWeight: 'bold',
fontSize: 20,
pointerEvents: 'auto' // Ensure pointer events work
}
});
// Set z-index to ensure visibility
node.set('z', 10);
} else if (dragType === 'end') {
node = new joint.shapes.standard.Circle();
node.position(x - 40, y - 40);
node.resize(80, 80);
node.attr({
body: {
fill: '#f44336',
stroke: '#b71c1c',
strokeWidth: 3,
pointerEvents: 'auto' // Ensure pointer events work
},
label: {
text: '结束',
fill: '#fff',
fontWeight: 'bold',
fontSize: 20,
pointerEvents: 'auto' // Ensure pointer events work
}
});
// Set z-index to ensure visibility
node.set('z', 10);
} else if (dragType === 'process') {
node = new joint.shapes.standard.Rectangle();
node.position(x - 60, y - 40);
node.resize(120, 80);
node.attr({
body: {
fill: '#2196f3',
stroke: '#1565c0',
strokeWidth: 3,
rx: 10,
ry: 10,
pointerEvents: 'auto'
},
label: {
text: 'Grouping',
fill: '#fff',
fontWeight: 'bold',
fontSize: 18,
pointerEvents: 'auto'
}
});
// Set z-index to ensure visibility
node.set('z', 5);
} else if (dragType === 'decision') {
node = new joint.shapes.standard.Polygon();
node.position(x - 60, y - 60);
node.resize(120, 120);
node.attr({
body: {
fill: '#ffeb3b',
stroke: '#fbc02d',
strokeWidth: 3,
refPoints: '50,0 100,60 50,120 0,60',
pointerEvents: 'auto'
},
label: {
text: '决策',
fill: '#333',
fontWeight: 'bold',
fontSize: 18,
pointerEvents: 'auto'
}
});
// Set z-index to ensure visibility
node.set('z', 5);
} else if (dragType === 'switch') {
node = new joint.shapes.standard.Polygon();
node.position(x - 70, y - 40);
node.resize(140, 80);
// 定义八边形的点坐标 (矩形四角切角)
const octagonPoints = '15,0 85,0 100,15 100,85 85,100 15,100 0,85 0,15';
node.attr({
body: {
fill: '#9c27b0', // 紫色背景
stroke: '#6a1b9a',
strokeWidth: 3,
refPoints: octagonPoints,
pointerEvents: 'auto'
},
label: {
text: 'Switch',
fill: '#fff',
fontWeight: 'bold',
fontSize: 18,
pointerEvents: 'auto'
}
});
// 设置初始 cases 属性,包含一个不可删除的 Default case
node.prop('properties', {
name: 'Switch',
description: '评估多个条件并根据结果继续执行',
cases: [
{ name: 'Default', expression: '', isDefault: true },
{ name: 'Case 1', expression: '' }
]
});
// 标记为 Switch 节点
node.isSwitch = true;
// Set z-index to ensure visibility
node.set('z', 5);
} else if (dragType === 'container') {
node = new joint.shapes.standard.Rectangle();
node.position(x - 150, y - 120);
node.resize(300, 240);
node.attr({
body: {
fill: '#FFFFFF', // 纯白色背景
stroke: '#CCCCCC', // 灰色边框
strokeWidth: 1, // 细边框
rx: 2, // 轻微圆角
ry: 2,
pointerEvents: 'auto'
},
label: {
text: '容器',
fill: '#666666', // 深灰色文字
fontWeight: 'bold',
fontSize: 14,
refX: 10, // 左对齐
refY: 10, // 顶部对齐
textAnchor: 'start',
textVerticalAnchor: 'top',
pointerEvents: 'auto'
}
});
// 标记为容器节点
node.isContainer = true;
node.isResizable = true; // 标记为可调整大小
// 为容器节点添加锚点
node.set('ports', { groups: portGroups });
node.addPort({ group: 'bottom' }); // 添加底部锚点
}
// 添加节点和端口
if (node) {
// 如果是 Switch 节点,添加多个锚点
if (node.isSwitch) {
// 获取 cases 数量
const cases = node.prop('properties').cases || [];
const casesCount = cases.length;
console.log('创建 Switch 节点,Cases 数量:', casesCount);
// 设置端口分组配置
node.set('ports', { groups: portGroups });
// 为每个 case 添加一个锚点
for (let i = 0; i < casesCount; i++) {
const portId = `case_${i}`;
console.log(`添加锚点 ${portId} 对应 Case: ${cases[i].name}`);
node.addPort({
id: portId,
group: 'switchPorts', // 使用自定义的 switchPorts 分组
attrs: {
text: {
text: cases[i].name,
fill: '#333',
fontSize: 10,
textAnchor: 'middle',
yAlignment: 'bottom',
refY: 20
},
circle: {
fill: '#fff',
stroke: '#333',
r: 5, // 稍微增大锚点半径,使其更容易看到
opacity: 0
}
}
});
}
}
// 如果不是容器节点和Switch节点,添加锚点
else if (!node.isContainer) {
// 设置端口分组配置并添加底部锚点
node.set('ports', { groups: portGroups }); // 使用 set 方法更新 ports 属性
node.addPort({ group: 'bottom' }); // 添加底部锚点
}
// 添加节点到图表
node.addTo(graph);
// 检查是否放置在容器节点上
const containers = graph.getElements().filter(e => e.isContainer);
for (const container of containers) {
const bbox = container.getBBox();
const nodeBBox = node.getBBox();
// 判断节点中心是否在容器内
const center = nodeBBox.center();
if (
center.x > bbox.x &&
center.x < bbox.x + bbox.width &&
center.y > bbox.y &&
center.y < bbox.y + bbox.height
) {
// 检查节点类型,开始和结束节点不能被嵌套
const nodeType = node.get('type');
const isStartOrEnd = nodeType === 'standard.Circle' &&
(node.attr('label/text') === '开始' || node.attr('label/text') === '结束');
if (isStartOrEnd) {
// 开始或结束节点不嵌套,但确保它们在最上层
node.toFront();
continue;
}
// 确保节点在容器上方显示
node.toFront();
// 嵌套节点到容器中
container.embed(node);
// 自动调整节点到容器内部
const minX = bbox.x + 10; // 添加内边距
const maxX = bbox.x + bbox.width - nodeBBox.width - 10;
const minY = bbox.y + 30; // 顶部留出更多空间给标题
const maxY = bbox.y + bbox.height - nodeBBox.height - 10;
node.position(
Math.max(minX, Math.min(nodeBBox.x, maxX)),
Math.max(minY, Math.min(nodeBBox.y, maxY))
);
// 确保嵌套后节点仍然可见
setTimeout(() => {
node.toFront();
// 获取所有相关连接线并确保它们在最上层
const relatedLinks = graph.getConnectedLinks(node);
if (relatedLinks.length > 0) {
relatedLinks.forEach(link => {
link.toFront();
});
}
}, 50);
break; // 找到一个容器后就退出循环
}
}
}
// 节点已添加,不需要显示任何成功提示
// 重置拖拽状态
dragType = null;
});
// 当拖拽离开画布时的处理
paper.el.addEventListener('dragleave', e => {
// 这里不重置dragType,避免拖拽经过子元素时误触发
// dragType会在drop或dragend事件中重置
});
// ========== 画布自适应和平移功能 ========== //
// 获取平移模式指示器
const panningIndicator = document.querySelector('.panning-mode-indicator');
// 平移状态变量
let isPanningMode = false;
let isPanning = false;
let lastClientX = 0;
let lastClientY = 0;
// 调整画布大小函数
function resizePaper() {
// 获取当前窗口尺寸
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 计算画布尺寸(减去左侧面板宽度)
const paperWidth = windowWidth - 140; // 140px是左侧面板宽度
const paperHeight = windowHeight;
// 获取当前的平移和缩放
const currentTranslate = paper.translate();
const currentScale = paper.scale();
// 设置画布尺寸
paper.setDimensions(paperWidth, paperHeight);
// 触发自定义事件,通知其他组件画布尺寸已更改
paper.trigger('paper:resize', paperWidth, paperHeight);
// 更新小地图视口
if (typeof updateMinimapViewport === 'function') {
updateMinimapViewport();
}
// 更新小地图尺寸
if (typeof updateMinimapSize === 'function') {
updateMinimapSize();
}
// 如果有元素,确保它们在视图内
const elements = graph.getElements();
if (elements.length > 0) {
// 获取所有元素的边界框
const bbox = graph.getBBox();
if (bbox) {
// 检查是否需要调整视图以显示所有元素
const visibleRect = paper.getArea();
const needsAdjustment =
bbox.x < visibleRect.x ||
bbox.y < visibleRect.y ||
bbox.x + bbox.width > visibleRect.x + visibleRect.width ||
bbox.y + bbox.height > visibleRect.y + visibleRect.height;
if (needsAdjustment) {
// 计算适当的平移以显示所有元素
const tx = -bbox.x + 50; // 添加一些边距
const ty = -bbox.y + 50;
// 应用平移
paper.translate(tx, ty);
}
}
}
}
// 初始调整大小 - 使用requestAnimationFrame确保DOM已完全加载
requestAnimationFrame(function() {
resizePaper();
});
// 监听窗口大小变化 - 使用防抖处理以提高性能
let resizeTimeout;
window.addEventListener('resize', function() {
// 清除之前的定时器
clearTimeout(resizeTimeout);
// 设置新的定时器,延迟执行以防止频繁调用
resizeTimeout = setTimeout(function() {
resizePaper();
}, 100);
});
// 监听空格键按下事件 - 进入平移模式
document.addEventListener('keydown', function(evt) {
if (evt.code === 'Space' && !isPanningMode) {
// 进入平移模式
isPanningMode = true;
document.body.classList.add('panning-cursor');
panningIndicator.style.display = 'block';
// 暂时禁用其他交互
paper.setInteractivity(false);
}
});
// 监听空格键释放事件 - 退出平移模式
document.addEventListener('keyup', function(evt) {
if (evt.code === 'Space' && isPanningMode) {
// 退出平移模式
isPanningMode = false;
isPanning = false;
document.body.classList.remove('panning-cursor');
panningIndicator.style.display = 'none';
// 恢复交互
paper.setInteractivity(true);
}
});
// 监听鼠标按下事件 - 开始平移
paper.el.addEventListener('mousedown', function(evt) {
if (isPanningMode) {
isPanning = true;
lastClientX = evt.clientX;
lastClientY = evt.clientY;
// 阻止默认行为和事件冒泡
evt.preventDefault();
evt.stopPropagation();
}
});
// 监听鼠标移动事件 - 执行平移
document.addEventListener('mousemove', function(evt) {
if (isPanning) {
// 计算鼠标移动距离
const dx = evt.clientX - lastClientX;
const dy = evt.clientY - lastClientY;
// 更新最后位置
lastClientX = evt.clientX;
lastClientY = evt.clientY;
// 获取当前视图转换
const currentTranslate = paper.translate();
// 应用平移
paper.translate(currentTranslate.tx + dx, currentTranslate.ty + dy);
// 阻止默认行为和事件冒泡
evt.preventDefault();
evt.stopPropagation();
}
});
// 监听鼠标释放事件 - 结束平移
document.addEventListener('mouseup', function(evt) {
if (isPanning) {
isPanning = false;
// 阻止默认行为和事件冒泡
evt.preventDefault();
evt.stopPropagation();
}
});
// 监听鼠标离开窗口事件 - 结束平移
document.addEventListener('mouseleave', function() {
if (isPanning) {
isPanning = false;
}
});
// ========== 小地图功能 ========== //
// 创建小地图
const minimapContainer = document.getElementById('minimap-container');
const minimapScale = 0.15; // 小地图缩放比例
// 创建小地图的 paper 实例
const minimap = new joint.dia.Paper({
el: minimapContainer,
model: graph, // 使用相同的 graph 模型
width: 220,
height: 180,
gridSize: 1,
interactive: false, // 禁用交互
background: { color: '#f8f9fa' },
async: true,
frozen: false,
sorting: joint.dia.Paper.sorting.NONE, // 提高渲染性能
viewport: function(view) {
// 简化小地图上的元素显示
return true; // 显示所有元素
},
// 缩放小地图
scale: { x: minimapScale, y: minimapScale }
});
// 创建小地图标题
const minimapTitle = document.createElement('div');
minimapTitle.className = 'minimap-title';
minimapTitle.textContent = '工作流概览';
minimapContainer.appendChild(minimapTitle);
// 创建视口指示器
const minimapViewport = document.createElement('div');
minimapViewport.className = 'minimap-viewport';
minimapContainer.appendChild(minimapViewport);
// 设置小地图初始透明度
minimapContainer.style.opacity = '0.7';
// 更新视口指示器位置和大小
function updateMinimapViewport() {
// 获取主画布的可视区域和内容区域
const paperRect = paper.getContentBBox();
const visibleRect = paper.getArea();
// 获取当前的平移和缩放
const currentTranslate = paper.translate();
const currentScale = paper.scale();
// 获取当前画布尺寸
const paperWidth = paper.options.width;
const paperHeight = paper.options.height;
// 计算实际可见区域(考虑平移和缩放)
const actualVisibleRect = {
x: -currentTranslate.tx / currentScale.sx,
y: -currentTranslate.ty / currentScale.sy,
width: paperWidth / currentScale.sx,
height: paperHeight / currentScale.sy
};
// 确保小地图能显示所有内容
let contentWidth = 0;
let contentHeight = 0;
// 如果有元素,使用元素的边界框
if (paperRect && paperRect.width && paperRect.height) {
contentWidth = Math.max(paperRect.width, actualVisibleRect.width);
contentHeight = Math.max(paperRect.height, actualVisibleRect.height);
} else {
// 如果没有元素,使用可视区域
contentWidth = actualVisibleRect.width;
contentHeight = actualVisibleRect.height;
}
// 获取小地图的当前缩放比例
const minimapCurrentScale = minimap.scale();
const effectiveMinimapScale = minimapCurrentScale.sx;
// 计算视口指示器的位置和大小
const minimapX = actualVisibleRect.x * effectiveMinimapScale;
const minimapY = actualVisibleRect.y * effectiveMinimapScale;
const minimapWidth = actualVisibleRect.width * effectiveMinimapScale;
const minimapHeight = actualVisibleRect.height * effectiveMinimapScale;
// 设置视口指示器的位置和大小
minimapViewport.style.left = minimapX + 'px';
minimapViewport.style.top = minimapY + 'px';
minimapViewport.style.width = minimapWidth + 'px';
minimapViewport.style.height = minimapHeight + 'px';
// 确保小地图视口指示器不超出小地图边界
const vpLeft = parseFloat(minimapViewport.style.left);
const vpTop = parseFloat(minimapViewport.style.top);
const vpWidth = parseFloat(minimapViewport.style.width);
const vpHeight = parseFloat(minimapViewport.style.height);
if (vpLeft < 0) minimapViewport.style.left = '0px';
if (vpTop < 0) minimapViewport.style.top = '0px';
if (vpLeft + vpWidth > minimap.options.width) {
minimapViewport.style.width = (minimap.options.width - vpLeft) + 'px';
}
if (vpTop + vpHeight > minimap.options.height) {
minimapViewport.style.height = (minimap.options.height - vpTop) + 'px';
}
}
// 初始更新视口指示器
updateMinimapViewport();
// 监听主画布的变化,更新视口指示器
paper.on('translate', updateMinimapViewport);
paper.on('scale', updateMinimapViewport);
paper.on('resize', updateMinimapViewport);
// 监听小地图上的视口指示器拖动
let isDraggingViewport = false;
let lastMinimapX = 0;
let lastMinimapY = 0;
// 视口指示器拖动开始
minimapViewport.addEventListener('mousedown', function(evt) {
isDraggingViewport = true;
lastMinimapX = evt.clientX;
lastMinimapY = evt.clientY;
// 阻止事件冒泡和默认行为
evt.stopPropagation();
evt.preventDefault();
});
// 视口指示器拖动
document.addEventListener('mousemove', function(evt) {
if (!isDraggingViewport) return;
// 计算鼠标移动距离
const dx = evt.clientX - lastMinimapX;
const dy = evt.clientY - lastMinimapY;
// 更新最后位置
lastMinimapX = evt.clientX;
lastMinimapY = evt.clientY;
// 计算主画布应该平移的距离(考虑缩放比例)
const paperDx = dx / minimapScale;
const paperDy = dy / minimapScale;
// 获取当前主画布的平移位置
const currentTranslate = paper.translate();
// 应用平移到主画布
paper.translate(currentTranslate.tx - paperDx, currentTranslate.ty - paperDy);
// 阻止事件冒泡和默认行为
evt.stopPropagation();
evt.preventDefault();
});
// 视口指示器拖动结束
document.addEventListener('mouseup', function() {
isDraggingViewport = false;
});
// 调整小地图内容以适应所有元素
function fitMinimapContent() {
// 获取所有元素的边界框
const elements = graph.getElements();
// 如果没有元素,使用默认缩放
if (elements.length === 0) {
minimap.scale(minimapScale, minimapScale);
updateMinimapViewport();
return;
}