@mx-sose-front/mx-sose-graph 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +721 -454
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +7 -6
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ContextMenu/ContextMenu.vue +18 -4
- package/src/components/InteractionLayer.vue +408 -414
- package/src/components/NameEditor.vue +212 -0
- package/src/components/SelectionBox.vue +189 -0
- package/src/constants/index.ts +2 -0
- package/src/utils/contextMenuUtils.ts +63 -6
- package/src/utils/diagram.ts +19 -15
- package/src/utils/keyboardUtils.ts +28 -0
- package/src/utils/nameEditUtils.ts +6 -1
- package/src/utils/packgeMap.ts +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- 名称虚线框(选中时显示) -->
|
|
3
|
+
<div
|
|
4
|
+
v-if="
|
|
5
|
+
selectedShape &&
|
|
6
|
+
selectedShape.nameBounds &&
|
|
7
|
+
!isEditingName &&
|
|
8
|
+
selectedShape.shapeKey !== 'ConceptRole'
|
|
9
|
+
"
|
|
10
|
+
class="name-text-box-container"
|
|
11
|
+
:style="computedNameTextBoxContainerStyle"
|
|
12
|
+
>
|
|
13
|
+
<div
|
|
14
|
+
class="name-text-box"
|
|
15
|
+
:style="computedNameTextBoxStyle"
|
|
16
|
+
title="点击编辑名称"
|
|
17
|
+
@click="handleNameTextBoxClick"
|
|
18
|
+
></div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- 名称编辑输入框 -->
|
|
22
|
+
<div
|
|
23
|
+
v-if="
|
|
24
|
+
isEditingName &&
|
|
25
|
+
selectedShape &&
|
|
26
|
+
selectedShape.shapeKey !== 'ConceptRole'
|
|
27
|
+
"
|
|
28
|
+
class="name-editor-container"
|
|
29
|
+
:style="computedNameEditorContainerStyle"
|
|
30
|
+
>
|
|
31
|
+
<input
|
|
32
|
+
ref="nameInput"
|
|
33
|
+
v-model="localEditingName"
|
|
34
|
+
class="name-input"
|
|
35
|
+
:style="computedNameInputStyle"
|
|
36
|
+
@blur="handleBlur"
|
|
37
|
+
@keyup.enter="handleKeyUp($event)"
|
|
38
|
+
@keyup.escape="handleKeyUp($event)"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
import { ref, watch, computed, nextTick, type CSSProperties } from 'vue';
|
|
45
|
+
import type { Shape } from '../types';
|
|
46
|
+
import { nameTextBoxContainerStyle, nameTextBoxStyle, nameEditorContainerStyle, nameInputStyle } from '../utils/diagram';
|
|
47
|
+
import type { NameEditManager } from '../utils/nameEditUtils';
|
|
48
|
+
|
|
49
|
+
// 定义组件的props
|
|
50
|
+
interface NameEditorProps {
|
|
51
|
+
selectedShape: Shape | null;
|
|
52
|
+
canEdit: boolean;
|
|
53
|
+
isEditingName: boolean;
|
|
54
|
+
editingName: string;
|
|
55
|
+
nameEditManager: NameEditManager;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 定义组件的事件
|
|
59
|
+
const emit = defineEmits<{
|
|
60
|
+
(e: 'editName', shape: Shape, newName: string, oldName: string): void;
|
|
61
|
+
}>();
|
|
62
|
+
|
|
63
|
+
// 获取props
|
|
64
|
+
const props = defineProps<NameEditorProps>();
|
|
65
|
+
|
|
66
|
+
// 名称输入框引用
|
|
67
|
+
const nameInput = ref<HTMLInputElement | null>(null);
|
|
68
|
+
|
|
69
|
+
// 创建本地ref存储输入值
|
|
70
|
+
const localEditingName = ref(props.editingName);
|
|
71
|
+
|
|
72
|
+
// 监听props.editingName的变化,同步到本地ref
|
|
73
|
+
watch(() => props.editingName, (newValue) => {
|
|
74
|
+
localEditingName.value = newValue;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 监听nameInput ref变化,将其传递给NameEditManager
|
|
78
|
+
watch(() => nameInput.value, (newValue) => {
|
|
79
|
+
if (newValue) {
|
|
80
|
+
props.nameEditManager.setNameInput(newValue);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 监听本地输入值的变化,同步到nameEditManager
|
|
85
|
+
watch(() => localEditingName.value, (newValue) => {
|
|
86
|
+
// 更新NameEditManager中的editingName状态
|
|
87
|
+
(props.nameEditManager as any).editingName.value = newValue;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 监听selectedShape变化,如果编辑中的形状不是当前选中的形状,取消编辑
|
|
91
|
+
watch(() => props.selectedShape, (newShape) => {
|
|
92
|
+
if (props.isEditingName && (!newShape || newShape.id !== props.selectedShape?.id)) {
|
|
93
|
+
props.nameEditManager.cancelEdit();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 计算名称虚线框容器样式
|
|
98
|
+
const computedNameTextBoxContainerStyle = computed<CSSProperties>(() => {
|
|
99
|
+
if (!props.selectedShape) return {};
|
|
100
|
+
return nameTextBoxContainerStyle(props.selectedShape.id);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 计算名称虚线框样式
|
|
104
|
+
const computedNameTextBoxStyle = computed<CSSProperties>(() => {
|
|
105
|
+
if (!props.selectedShape) return {};
|
|
106
|
+
return nameTextBoxStyle(props.selectedShape);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 计算名称编辑器容器样式
|
|
110
|
+
const computedNameEditorContainerStyle = computed<CSSProperties>(() => {
|
|
111
|
+
if (!props.selectedShape) return {};
|
|
112
|
+
return nameEditorContainerStyle(props.selectedShape);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 计算名称输入框样式
|
|
116
|
+
const computedNameInputStyle = computed<CSSProperties>(() => {
|
|
117
|
+
if (!props.selectedShape) return {};
|
|
118
|
+
return nameInputStyle(props.selectedShape);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// 处理名称虚线框点击事件
|
|
122
|
+
const handleNameTextBoxClick = () => {
|
|
123
|
+
if (props.canEdit && props.nameEditManager.canEdit(props.selectedShape)) {
|
|
124
|
+
startEditName();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// 开始编辑名称
|
|
129
|
+
const startEditName = async () => {
|
|
130
|
+
await props.nameEditManager.startEdit(props.selectedShape);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 处理失焦事件
|
|
134
|
+
const handleBlur = () => {
|
|
135
|
+
props.nameEditManager.handleBlur(props.selectedShape);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// 处理键盘事件
|
|
139
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
140
|
+
props.nameEditManager.handleKeyUp(event, props.selectedShape);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// 暴露方法给父组件
|
|
144
|
+
defineExpose({
|
|
145
|
+
startEditName,
|
|
146
|
+
canEdit: (shape: Shape | null) => props.nameEditManager.canEdit(shape),
|
|
147
|
+
cancelEdit: () => props.nameEditManager.cancelEdit(),
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<style scoped>
|
|
152
|
+
/* 名称虚线框容器 */
|
|
153
|
+
.name-text-box-container {
|
|
154
|
+
position: absolute;
|
|
155
|
+
z-index: 1001;
|
|
156
|
+
pointer-events: all;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* 名称虚线框 */
|
|
160
|
+
.name-text-box {
|
|
161
|
+
border: 1px dashed #007bff;
|
|
162
|
+
background: rgba(255, 255, 255, 0.2);
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
pointer-events: all;
|
|
165
|
+
transition: all 0.2s ease;
|
|
166
|
+
border-radius: 4px;
|
|
167
|
+
box-shadow: 0 0 0 1px rgba(0, 123, 255, 0.1);
|
|
168
|
+
height: 100%;
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.name-text-box:hover {
|
|
175
|
+
border-color: #0056b3;
|
|
176
|
+
background: rgba(0, 123, 255, 0.05);
|
|
177
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2);
|
|
178
|
+
transform: scale(1.02);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* 名称编辑器容器 */
|
|
182
|
+
.name-editor-container {
|
|
183
|
+
position: absolute;
|
|
184
|
+
z-index: 1002;
|
|
185
|
+
display: flex;
|
|
186
|
+
justify-content: center;
|
|
187
|
+
align-items: center;
|
|
188
|
+
pointer-events: all;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* 名称输入框 */
|
|
192
|
+
.name-input {
|
|
193
|
+
width: calc(100% - 20px);
|
|
194
|
+
padding: 2px 4px;
|
|
195
|
+
border: 2px solid #007bff;
|
|
196
|
+
border-radius: 6px;
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
font-weight: 600;
|
|
199
|
+
background: #fff;
|
|
200
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
201
|
+
outline: none;
|
|
202
|
+
text-align: center;
|
|
203
|
+
font-family: inherit;
|
|
204
|
+
resize: none;
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.name-input:focus {
|
|
209
|
+
border-color: #0056b3;
|
|
210
|
+
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="selection-box" :style="getSelectionBoxStyle(shape)">
|
|
3
|
+
<!-- 只有当shapeType不是edge且不是conceptualRole时才渲染四个角手柄 -->
|
|
4
|
+
<div class="resize-handles" v-show="!isBusy && shape.shapeType !== 'edge'">
|
|
5
|
+
<div
|
|
6
|
+
v-for="h in resizeHandles"
|
|
7
|
+
:key="h.position"
|
|
8
|
+
class="resize-handle"
|
|
9
|
+
:class="[`resize-${h.position}`, { 'is-disabled': isMultiSelected }]"
|
|
10
|
+
:style="getHandleStyle(h, shape)"
|
|
11
|
+
@mousedown.stop.prevent="onHandleMouseDown($event, h.position)"
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
<div
|
|
15
|
+
class="action-buttons"
|
|
16
|
+
v-show="shouldShowActionButtons"
|
|
17
|
+
:style="actionButtonsStyle(shape)"
|
|
18
|
+
>
|
|
19
|
+
<div v-show="shape.modelTypePropertyId" class="border-btn">
|
|
20
|
+
<button
|
|
21
|
+
class="action-btn edit-btn"
|
|
22
|
+
@mousedown.stop.prevent="onModelTypePropertyIdClick"
|
|
23
|
+
title="设置类型"
|
|
24
|
+
>
|
|
25
|
+
<img src="../statics/icons/childIcons/设置类型.png" alt="设置类型" />
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
<button
|
|
29
|
+
v-for="value in shape.scenarioMenus"
|
|
30
|
+
:key="value.code"
|
|
31
|
+
class="action-btn edit-btn"
|
|
32
|
+
@mousedown.stop.prevent="onActionButtonClick(value.code)"
|
|
33
|
+
@click.stop.prevent
|
|
34
|
+
:title="value.name"
|
|
35
|
+
>
|
|
36
|
+
<img :src="getIcon('childIcons', value.icon || '')" />
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { computed } from "vue";
|
|
44
|
+
import type { Shape } from "../types";
|
|
45
|
+
import { resizeHandles } from "../constants/index";
|
|
46
|
+
import {
|
|
47
|
+
selectionBoxStyle,
|
|
48
|
+
handleStyle,
|
|
49
|
+
actionButtonsStyle,
|
|
50
|
+
ShapeConfig,
|
|
51
|
+
} from "../utils/diagram";
|
|
52
|
+
import { getIcon } from "../utils/iconLoader";
|
|
53
|
+
|
|
54
|
+
// Props
|
|
55
|
+
const props = defineProps<{
|
|
56
|
+
shape: Shape;
|
|
57
|
+
isBusy: boolean;
|
|
58
|
+
isMultiSelected: boolean;
|
|
59
|
+
}>();
|
|
60
|
+
|
|
61
|
+
// 计算属性:是否显示操作按钮
|
|
62
|
+
const shouldShowActionButtons = computed(() => {
|
|
63
|
+
return !props.isMultiSelected &&
|
|
64
|
+
props.shape.scenarioMenus &&
|
|
65
|
+
props.shape.scenarioMenus.length > 0 &&
|
|
66
|
+
props.shape.shapeType != ShapeConfig.SHAPE_TYPE;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Emits
|
|
70
|
+
const emit = defineEmits<{
|
|
71
|
+
(
|
|
72
|
+
e: "resize-start",
|
|
73
|
+
event: MouseEvent,
|
|
74
|
+
position: "nw" | "ne" | "sw" | "se",
|
|
75
|
+
shape: Shape
|
|
76
|
+
): void;
|
|
77
|
+
(e: "action-button-click", code: string, shape: Shape): void;
|
|
78
|
+
(e: "model-type-property-id-click", value: string, shape: Shape): void;
|
|
79
|
+
}>();
|
|
80
|
+
|
|
81
|
+
// Methods
|
|
82
|
+
const getSelectionBoxStyle = (shape: Shape) => selectionBoxStyle(shape);
|
|
83
|
+
const getHandleStyle = (h: any, shape: Shape) => handleStyle(h.position, shape);
|
|
84
|
+
|
|
85
|
+
// Event handlers
|
|
86
|
+
const onHandleMouseDown = (
|
|
87
|
+
event: MouseEvent,
|
|
88
|
+
position: "nw" | "ne" | "sw" | "se"
|
|
89
|
+
) => {
|
|
90
|
+
emit("resize-start", event, position, props.shape);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const onActionButtonClick = (code: string) => {
|
|
94
|
+
emit("action-button-click", code, props.shape);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const onModelTypePropertyIdClick = () => {
|
|
98
|
+
if (props.shape.modelTypePropertyId) {
|
|
99
|
+
emit(
|
|
100
|
+
"model-type-property-id-click",
|
|
101
|
+
props.shape.modelTypePropertyId,
|
|
102
|
+
props.shape
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<style scoped>
|
|
109
|
+
.selection-box {
|
|
110
|
+
pointer-events: none;
|
|
111
|
+
background: transparent;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.resize-handles {
|
|
115
|
+
position: relative;
|
|
116
|
+
width: 100%;
|
|
117
|
+
height: 100%;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.resize-handle {
|
|
121
|
+
position: absolute;
|
|
122
|
+
width: 10px;
|
|
123
|
+
height: 10px;
|
|
124
|
+
background-color: #007bff;
|
|
125
|
+
border: 2px solid #fff;
|
|
126
|
+
border-radius: 50%;
|
|
127
|
+
pointer-events: all;
|
|
128
|
+
transition: all 0.2s ease;
|
|
129
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
130
|
+
z-index: 999;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.resize-handle.is-disabled {
|
|
134
|
+
cursor: default !important;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.resize-handle:hover {
|
|
138
|
+
background-color: #0056b3;
|
|
139
|
+
transform: scale(1.2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.action-buttons {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
gap: 4px;
|
|
146
|
+
pointer-events: all;
|
|
147
|
+
background: rgba(255, 255, 255, 0.95);
|
|
148
|
+
padding: 6px;
|
|
149
|
+
border-radius: 4px;
|
|
150
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
151
|
+
border: 1px solid #e0e0e0;
|
|
152
|
+
backdrop-filter: blur(2px);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.action-btn {
|
|
156
|
+
width: 28px;
|
|
157
|
+
height: 28px;
|
|
158
|
+
border: 1px solid #d0d0d0;
|
|
159
|
+
border-radius: 3px;
|
|
160
|
+
background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
font-size: 12px;
|
|
166
|
+
font-weight: bold;
|
|
167
|
+
font-family: "Arial", sans-serif;
|
|
168
|
+
color: #495057;
|
|
169
|
+
transition: all 0.2s ease;
|
|
170
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.action-btn:hover {
|
|
174
|
+
background: linear-gradient(to bottom, #e3f2fd, #bbdefb);
|
|
175
|
+
border-color: #90caf9;
|
|
176
|
+
transform: translateY(-1px);
|
|
177
|
+
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.3);
|
|
178
|
+
color: #1976d2;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.edit-btn:hover {
|
|
182
|
+
background: #e3f2fd;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.border-btn {
|
|
186
|
+
padding-bottom: 4px;
|
|
187
|
+
border-bottom: 1px solid #e0e0e0;
|
|
188
|
+
}
|
|
189
|
+
</style>
|
package/src/constants/index.ts
CHANGED
|
@@ -228,7 +228,9 @@ export const BlockKeyMap = {
|
|
|
228
228
|
'ArbitraryRelationship':'Edge',//任意关系
|
|
229
229
|
'DirectedRelationship':'Edge',//定向关系
|
|
230
230
|
'OperationalControlFlow':'Edge',//业务控制流
|
|
231
|
+
'OperationalObjectFlow':'Edge',//业务对象流
|
|
231
232
|
'FunctionControlFlow':'Edge',//功能控制流
|
|
233
|
+
'FunctionObjectFlow': 'Edge', // 功能对象流
|
|
232
234
|
'ServiceConnector':'Edge',//服务连接器
|
|
233
235
|
'ServiceControlFlow':'Edge',//服务控制流
|
|
234
236
|
"ServiceObjectFlow": 'Edge', // 服务对象流
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ref, type Ref } from 'vue';
|
|
2
2
|
import { eventBus } from '../store';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import { getUuid } from '../utils/index';
|
|
3
5
|
|
|
4
6
|
// 菜单位置接口
|
|
5
7
|
export interface MenuPosition {
|
|
@@ -40,6 +42,9 @@ const DEFAULT_MENU_CONFIG: Required<MenuConfig> = {
|
|
|
40
42
|
|
|
41
43
|
// 右键菜单工具函数
|
|
42
44
|
export class ContextMenuUtils {
|
|
45
|
+
// 用于存储复制的图元
|
|
46
|
+
private static copiedShapes: any[] = [];
|
|
47
|
+
|
|
43
48
|
/**
|
|
44
49
|
* 处理右键菜单点击事件
|
|
45
50
|
* @param event 鼠标事件
|
|
@@ -73,7 +78,7 @@ export class ContextMenuUtils {
|
|
|
73
78
|
// 使用命中测试找到点击的图元
|
|
74
79
|
const hit = pickTarget(shapes, point);
|
|
75
80
|
|
|
76
|
-
if (hit.kind === "shape" && hit.shape) {
|
|
81
|
+
if ((hit.kind === "shape" || hit.kind === "pin") && hit.shape) {
|
|
77
82
|
// 选中图元
|
|
78
83
|
if (selectShape) {
|
|
79
84
|
selectShape(hit.shape);
|
|
@@ -174,11 +179,27 @@ export class ContextMenuUtils {
|
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
|
|
182
|
+
/**
|
|
183
|
+
* 设置复制的图元
|
|
184
|
+
*/
|
|
185
|
+
static setCopiedShapes(shapes: any[]) {
|
|
186
|
+
this.copiedShapes = _.cloneDeep(shapes);
|
|
187
|
+
console.log('已复制的图元:', this.copiedShapes);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 获取复制的图元
|
|
192
|
+
*/
|
|
193
|
+
static getCopiedShapes(): any[] {
|
|
194
|
+
return this.copiedShapes;
|
|
195
|
+
}
|
|
196
|
+
|
|
177
197
|
/**
|
|
178
198
|
* 处理复制
|
|
179
199
|
*/
|
|
180
200
|
static handleCopy(target: any) {
|
|
181
201
|
if (target) {
|
|
202
|
+
this.setCopiedShapes([target]);
|
|
182
203
|
eventBus.emit('copy-shape', target);
|
|
183
204
|
}
|
|
184
205
|
}
|
|
@@ -187,9 +208,40 @@ export class ContextMenuUtils {
|
|
|
187
208
|
* 处理粘贴
|
|
188
209
|
*/
|
|
189
210
|
static handlePaste(target: any) {
|
|
190
|
-
if (
|
|
191
|
-
|
|
211
|
+
if (this.copiedShapes.length === 0) {
|
|
212
|
+
return;
|
|
192
213
|
}
|
|
214
|
+
|
|
215
|
+
// 设置粘贴位置(画布左上角50像素处)
|
|
216
|
+
const pasteX = 50;
|
|
217
|
+
const pasteY = 50;
|
|
218
|
+
|
|
219
|
+
// 为每个复制的图元创建新的副本
|
|
220
|
+
const pastedShapes: any[] = [];
|
|
221
|
+
|
|
222
|
+
this.copiedShapes.forEach(shape => {
|
|
223
|
+
// 深拷贝图元
|
|
224
|
+
const newShape = _.cloneDeep(shape);
|
|
225
|
+
|
|
226
|
+
// 生成新的ID
|
|
227
|
+
newShape.id = getUuid();
|
|
228
|
+
|
|
229
|
+
// 更新位置到画布左上角50像素处
|
|
230
|
+
if (newShape.bounds) {
|
|
231
|
+
newShape.bounds.x = pasteX;
|
|
232
|
+
newShape.bounds.y = pasteY;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
pastedShapes.push(newShape);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 通过事件总线通知添加图元
|
|
239
|
+
eventBus.emit('paste-shapes', pastedShapes);
|
|
240
|
+
|
|
241
|
+
// 通过事件总线通知选中新粘贴的图元
|
|
242
|
+
eventBus.emit('select-shapes', pastedShapes.map(s => s.id));
|
|
243
|
+
|
|
244
|
+
console.log('已粘贴的图元:', pastedShapes);
|
|
193
245
|
}
|
|
194
246
|
|
|
195
247
|
/**
|
|
@@ -259,6 +311,11 @@ export const defaultMenuItems: MenuItemConfig[] = [
|
|
|
259
311
|
];
|
|
260
312
|
|
|
261
313
|
// 标准右键菜单配置
|
|
262
|
-
export const standardContextMenuConfig: MenuConfig = {
|
|
263
|
-
|
|
264
|
-
|
|
314
|
+
// export const standardContextMenuConfig: MenuConfig = {
|
|
315
|
+
// items: defaultMenuItems.map(item => {
|
|
316
|
+
// if (item.id === 'copy' || item.id === 'paste') {
|
|
317
|
+
// return { ...item, disabled: false };
|
|
318
|
+
// }
|
|
319
|
+
// return item;
|
|
320
|
+
// })
|
|
321
|
+
// };
|
package/src/utils/diagram.ts
CHANGED
|
@@ -375,13 +375,17 @@ export const nameInputStyle = (shape: Shape): Record<string, string> => {
|
|
|
375
375
|
* 名称编辑容器样式 - 确保在父组件内水平居中
|
|
376
376
|
*/
|
|
377
377
|
export const nameEditorContainerStyle = (shape: Shape): Record<string, string> => {
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
const
|
|
378
|
+
// 使用shape.id重新从store获取最新的图元信息,确保位置是最新的
|
|
379
|
+
const graphStore = useGraphStore();
|
|
380
|
+
const latestShape = graphStore.shapes.find(s => s.id === shape.id) || shape;
|
|
381
|
+
|
|
382
|
+
const b = latestShape.bounds ?? {} as any
|
|
383
|
+
const nb = (latestShape as any).nameBounds ?? {} as any
|
|
384
|
+
const ns = (latestShape as any).nameStyle ?? {} as any
|
|
381
385
|
|
|
382
|
-
if ((
|
|
386
|
+
if ((latestShape as any).shapeType === 'pin' && (nb.x != null) && (nb.y != null)) {
|
|
383
387
|
const fontSize = Number(ns.fontSize || nb.height || 12)
|
|
384
|
-
const textLen = (
|
|
388
|
+
const textLen = (latestShape.name?.length || 0)
|
|
385
389
|
const textWidth = Math.max(10, textLen * fontSize * 0.6)
|
|
386
390
|
const boxW = Math.min(300, Math.max(40, textWidth + 14))
|
|
387
391
|
const boxH = Math.max(22, fontSize + 10)
|
|
@@ -403,18 +407,18 @@ export const nameEditorContainerStyle = (shape: Shape): Record<string, string> =
|
|
|
403
407
|
else top = yAbs - fontSize
|
|
404
408
|
|
|
405
409
|
return {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
410
|
+
position: 'absolute',
|
|
411
|
+
left: `${Math.round(left)}px`,
|
|
412
|
+
top: `${Math.round(top)}px`,
|
|
413
|
+
width: `${Math.round(boxW)}px`,
|
|
414
|
+
height: `${Math.round(boxH)}px`,
|
|
415
|
+
display: 'block',
|
|
416
|
+
zIndex: '1001',
|
|
414
417
|
}
|
|
418
|
+
}
|
|
415
419
|
|
|
416
|
-
const shapeBounds =
|
|
417
|
-
const nameBounds =
|
|
420
|
+
const shapeBounds = latestShape.bounds ?? {};
|
|
421
|
+
const nameBounds = latestShape.nameBounds ?? {};
|
|
418
422
|
|
|
419
423
|
// 计算text name在画布中的绝对位置
|
|
420
424
|
const absoluteY = (shapeBounds.y ?? 0) + (nameBounds.y ?? 0);
|
|
@@ -10,6 +10,8 @@ interface KeyboardConfig {
|
|
|
10
10
|
onCancelConnection?: () => void;
|
|
11
11
|
onShapesRemove?: (items: Array<{ modelId: string; shapeId: string; shapeType: string; isRemoveModelTree: boolean }>) => void;
|
|
12
12
|
isEditingName?: () => boolean;
|
|
13
|
+
onCopy?: () => void;
|
|
14
|
+
onPaste?: () => void;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -124,6 +126,32 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
// 按下Ctrl+C时复制选中的图元
|
|
130
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'c' && graphStore.museInGraphView && !isInputElement) {
|
|
131
|
+
// 如果正在编辑名称,不执行复制功能
|
|
132
|
+
if (config.isEditingName && config.isEditingName()) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
if (config.onCopy) {
|
|
138
|
+
config.onCopy();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 按下Ctrl+V时粘贴选中的图元
|
|
143
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'v' && graphStore.museInGraphView && !isInputElement) {
|
|
144
|
+
// 如果正在编辑名称,不执行粘贴功能
|
|
145
|
+
if (config.isEditingName && config.isEditingName()) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
if (config.onPaste) {
|
|
151
|
+
config.onPaste();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
127
155
|
// 按下方向键时移动选中的图元(不在输入框或文本域中时)
|
|
128
156
|
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') && graphStore.museInGraphView && !isInputElement) {
|
|
129
157
|
// 如果正在编辑名称,不执行移动功能
|
|
@@ -11,7 +11,7 @@ export interface NameEditOptions {
|
|
|
11
11
|
export class NameEditManager {
|
|
12
12
|
private isEditing: Ref<boolean>;
|
|
13
13
|
private editingName: Ref<string>;
|
|
14
|
-
private nameInput: Ref<HTMLInputElement | undefined>;
|
|
14
|
+
private nameInput: Ref<HTMLInputElement | undefined | null>;
|
|
15
15
|
private options: NameEditOptions;
|
|
16
16
|
|
|
17
17
|
constructor(options: NameEditOptions = {}) {
|
|
@@ -30,6 +30,11 @@ export class NameEditManager {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// 设置名称输入框引用
|
|
34
|
+
setNameInput(input: HTMLInputElement | null) {
|
|
35
|
+
this.nameInput.value = input;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
// 开始编辑名称
|
|
34
39
|
async startEdit(shape: Shape | null): Promise<void> {
|
|
35
40
|
if (!shape || this.isEditing.value) return;
|
package/src/utils/packgeMap.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const packageMap = ['Strategic']
|