@mx-sose-front/mx-sose-graph 1.1.1 → 1.1.2

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.
@@ -252,11 +252,12 @@ export const collectAffectedShapeIds = (shapes: Shape[], changedIds: string[], c
252
252
 
253
253
  for (const id of allChanged) {
254
254
  affected.add(id)
255
+ // 只算自己子树(组拖动需要)
256
+ collectDescendantIds(shapes, id).forEach(cid => affected.add(cid))
257
+
258
+ // 父节点本身可能受影响(容器/连线)
255
259
  const parentId = shapes.find(s => s.id === id)?.parenShapeId ?? null
256
- if (parentId) {
257
- affected.add(parentId)
258
- collectDescendantIds(shapes, parentId).forEach(cid => affected.add(cid))
259
- }
260
+ if (parentId) affected.add(parentId)
260
261
  }
261
262
 
262
263
  for (const id of Array.from(affected)) {
@@ -305,7 +306,9 @@ export const createOnNestDoneCallback = (options: {
305
306
 
306
307
  const parent = shapes.find(s => s.id === childShape.parenShapeId)
307
308
  if (!parent) return
308
- const { expanded, affectedIds } = expandParentToFitChildBounds(
309
+ // 只记录 parent 的前后 bounds
310
+ const beforeParent = JSON.stringify(parent.bounds ?? {})
311
+ const { expanded } = expandParentToFitChildBounds(
309
312
  shapes,
310
313
  parent,
311
314
  childShape.id,
@@ -313,17 +316,11 @@ export const createOnNestDoneCallback = (options: {
313
316
  updateShape,
314
317
  )
315
318
  if (!expanded) return
316
-
317
- const sizePayload: ShapeSizeUpdatePayload[] = affectedIds
318
- .map(id => shapes.find(s => s.id === id))
319
- .filter((s): s is Shape => !!s)
320
- .filter(s => s.id !== childId) // 不用更新子,子已经是最新的 bounds
321
- .map(s => ({
322
- id: s.id,
323
- bounds: JSON.stringify(s.bounds ?? {}),
324
- }))
325
-
326
- // 交给外面单独的监听去调 updateShapeSize 接口
327
- eventBus.emit('shape-size-update', sizePayload)
319
+ const afterParent = JSON.stringify(parent.bounds ?? {})
320
+ if (afterParent == beforeParent) return
321
+ // 只更新当前嵌套的父图元(parent)
322
+ eventBus.emit('shape-size-update', [
323
+ { id: parent.id, bounds: afterParent },
324
+ ])
328
325
  }
329
326
  }
@@ -0,0 +1,229 @@
1
+ import { useGraphStore } from '../store/graphStore';
2
+ import { eventBus } from '../store';
3
+ // 定义快捷键处理函数类型
4
+ type KeyboardEventHandler = (e: KeyboardEvent) => void;
5
+
6
+ // 定义快捷键配置类型
7
+ interface KeyboardConfig {
8
+ onDelete?: () => void;
9
+ onEditProperty?: () => void;
10
+ onCancelConnection?: () => void;
11
+ onShapesRemove?: (items: Array<{ modelId: string; shapeId: string; shapeType: string; isRemoveModelTree: boolean }>) => void;
12
+ isEditingName?: () => boolean;
13
+ }
14
+
15
+ /**
16
+ * 创建键盘事件处理器
17
+ * @param config 快捷键配置
18
+ * @returns 键盘事件处理函数
19
+ */
20
+ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHandler => {
21
+ const graphStore = useGraphStore();
22
+
23
+ return (e: KeyboardEvent) => {
24
+ // 按下Esc且正在连线时,取消连线
25
+ if (e.key === 'Escape' && config.onCancelConnection) {
26
+ e.preventDefault();
27
+ config.onCancelConnection();
28
+ }
29
+
30
+ // 处理Delete键移除图元
31
+ if (e.key === 'Delete') {
32
+ const target = e.target as HTMLElement | null;
33
+ if (target) {
34
+ const tag = target.tagName;
35
+ // 如果在原生输入类控件里(input / textarea)
36
+ if (tag === 'INPUT' || tag === 'TEXTAREA') {
37
+ return; // 让浏览器自己处理 Delete,删文字,不动图元
38
+ }
39
+ }
40
+
41
+ // 如果鼠标不在图元区域,不处理删除
42
+ if (!graphStore.museInGraphView) return;
43
+ e.preventDefault(); // 避免浏览器默认行为
44
+
45
+ // 选中的图元中如果有画布(diagram),则不允许删除
46
+ const selectedShapes = graphStore.marqueeShapes ?? [];
47
+ // 判断是否包含画布
48
+ const hasDiagram = selectedShapes.some(s =>
49
+ s.shapeType?.toLowerCase?.() === 'diagram'
50
+ );
51
+ if (hasDiagram) {
52
+ // 选中了画布,直接不处理删除
53
+ return;
54
+ }
55
+
56
+ const items = selectedShapes
57
+ .map(s => ({
58
+ modelId: s.modelId,
59
+ shapeId: s.id,
60
+ shapeType: s.shapeType,
61
+ isRemoveModelTree: false
62
+ }));
63
+
64
+ if (items.length) {
65
+ eventBus.emit('shapes-remove', items);
66
+ if (config.onShapesRemove) {
67
+ config.onShapesRemove(items);
68
+ }
69
+ }
70
+ }
71
+
72
+ // 按下Alt+B时触发树上高亮功能
73
+ if (e.altKey && e.key === 'b') {
74
+ e.preventDefault();
75
+ if (graphStore.selectedShape) {
76
+ eventBus.emit('highlight-shape', graphStore.selectedShape);
77
+ }
78
+ }
79
+
80
+ // 按下Ctrl+A时选中所有图元
81
+ if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
82
+ // 如果正在编辑名称,不执行全选功能,让输入框正常处理Ctrl+A
83
+ if (config.isEditingName && config.isEditingName()) {
84
+ return;
85
+ }
86
+
87
+ e.preventDefault();
88
+ if (graphStore.museInGraphView) {
89
+ graphStore.selectAll();
90
+ }
91
+ }
92
+
93
+ // 检查是否在输入元素中
94
+ const activeElement = document.activeElement;
95
+ const isInputElement = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.hasAttribute('contenteditable'));
96
+
97
+ // 按下Enter键时触发属性配置功能(不在输入框或文本域中时)
98
+ if (e.key === 'Enter' && graphStore.selectedShape && graphStore.museInGraphView && !isInputElement) {
99
+ // 如果正在编辑名称,不执行属性配置功能
100
+ if (config.isEditingName && config.isEditingName()) {
101
+ return;
102
+ }
103
+
104
+ e.preventDefault();
105
+ if (graphStore.museInGraphView && config.onEditProperty) {
106
+ config.onEditProperty();
107
+ }
108
+ }
109
+
110
+ // 按下Ctrl+D时触发删除功能(不在输入框或文本域中时)
111
+ if ((e.ctrlKey || e.metaKey) && e.key === 'd' && graphStore.museInGraphView && !isInputElement) {
112
+ // 如果正在编辑名称,不执行删除功能
113
+ if (config.isEditingName && config.isEditingName()) {
114
+ return;
115
+ }
116
+
117
+ e.preventDefault();
118
+ if (graphStore.selectedIds.length === 1 && graphStore.selectedShape && graphStore.selectedShape.shapeType?.toLowerCase?.() !== 'diagram' && graphStore.museInGraphView) {
119
+ // 单选情况下发送删除事件
120
+ eventBus.emit('shapes-delete', graphStore.selectedShape);
121
+ if (config.onDelete) {
122
+ config.onDelete();
123
+ }
124
+ }
125
+ }
126
+
127
+ // 按下方向键时移动选中的图元(不在输入框或文本域中时)
128
+ if ((e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') && graphStore.museInGraphView && !isInputElement) {
129
+ // 如果正在编辑名称,不执行移动功能
130
+ if (config.isEditingName && config.isEditingName()) {
131
+ return;
132
+ }
133
+
134
+ e.preventDefault();
135
+ const step = 1; // 移动距离
136
+
137
+ // 获取画布信息(diagram类型的shape)
138
+ const canvas = graphStore.shapes.find(shape => shape.shapeType === 'diagram');
139
+ const canvasX = canvas?.bounds?.x || 0;
140
+ const canvasY = canvas?.bounds?.y || 0;
141
+ const canvasWidth = canvas?.bounds?.width || 300; // 最小画布宽度
142
+ const canvasHeight = canvas?.bounds?.height || 200; // 最小画布高度
143
+
144
+ // 计算画布的边界约束
145
+ const canvasMinX = canvasX;
146
+ const canvasMinY = canvasY;
147
+ const canvasMaxX = canvasWidth + canvasX;
148
+ const canvasMaxY = canvasHeight + canvasY;
149
+
150
+ // 获取所有选中的图元
151
+ const selectedShapes = graphStore.selectedIds.map(id => graphStore.shapes.find(s => s.id === id)).filter(Boolean);
152
+
153
+ selectedShapes.forEach(shape => {
154
+ if (shape && shape.bounds) {
155
+ const { x = 0, y = 0, width = 0, height = 0 } = shape.bounds;
156
+ let newX = x;
157
+ let newY = y;
158
+
159
+ // 根据按键方向调整位置
160
+ switch (e.key) {
161
+ case 'ArrowUp':
162
+ newY -= step;
163
+ break;
164
+ case 'ArrowDown':
165
+ newY += step;
166
+ break;
167
+ case 'ArrowLeft':
168
+ newX -= step;
169
+ break;
170
+ case 'ArrowRight':
171
+ newX += step;
172
+ break;
173
+ }
174
+
175
+ // 添加边界约束:确保图元不会超出画布的边界
176
+ newX = Math.max(newX, canvasMinX);
177
+ newY = Math.max(newY, canvasMinY);
178
+ newX = Math.min(newX, canvasMaxX - width);
179
+ newY = Math.min(newY, canvasMaxY - height);
180
+
181
+ // 计算位置变化量
182
+ const deltaX = newX - x;
183
+ const deltaY = newY - y;
184
+
185
+ // 更新父图元位置
186
+ graphStore.updateShape(shape.id, {
187
+ bounds: {
188
+ x: newX,
189
+ y: newY,
190
+ width,
191
+ height
192
+ }
193
+ });
194
+
195
+ // 查找并移动所有子图元
196
+ const childShapes = graphStore.shapes.filter(child => child.parenShapeId === shape.id);
197
+ childShapes.forEach(child => {
198
+ if (child && child.bounds) {
199
+ const childX = child.bounds.x || 0;
200
+ const childY = child.bounds.y || 0;
201
+ const childWidth = child.bounds.width || 0;
202
+ const childHeight = child.bounds.height || 0;
203
+
204
+ // 计算子图元的新位置
205
+ const childNewX = childX + deltaX;
206
+ const childNewY = childY + deltaY;
207
+
208
+ // 为子图元添加边界约束
209
+ const constrainedChildX = Math.max(childNewX, canvasMinX);
210
+ const constrainedChildY = Math.max(childNewY, canvasMinY);
211
+ const constrainedChildXWithWidth = Math.min(constrainedChildX, canvasMaxX - childWidth);
212
+ const constrainedChildYWithHeight = Math.min(constrainedChildY, canvasMaxY - childHeight);
213
+
214
+ // 更新子图元位置
215
+ graphStore.updateShape(child.id, {
216
+ bounds: {
217
+ x: constrainedChildXWithWidth,
218
+ y: constrainedChildYWithHeight,
219
+ width: childWidth,
220
+ height: childHeight
221
+ }
222
+ });
223
+ }
224
+ });
225
+ }
226
+ });
227
+ }
228
+ };
229
+ };
@@ -0,0 +1,50 @@
1
+ import { ElMessage, type MessageHandler } from "element-plus"
2
+ import { useGraphStore } from "../store/graphStore"
3
+
4
+ /**
5
+ * ElMessage.warning 互斥锁:同一时间只显示一个 warning
6
+ * - warning 显示期间:不会重复弹
7
+ * - warning 关闭后:允许下一次再弹
8
+ */
9
+ let warningLock: Promise<void> | null = null
10
+ let warningHandler: MessageHandler | null = null
11
+
12
+ function showWarningOnce(message?: string) {
13
+ // 如果上一次 warning 还在显示,就直接复用“锁”,不再重复弹
14
+ if (warningLock) return warningLock
15
+
16
+ warningLock = new Promise<void>((resolve) => {
17
+ warningHandler = ElMessage.warning({
18
+ message,
19
+ onClose: () => {
20
+ warningHandler = null
21
+ warningLock = null
22
+ resolve()
23
+ },
24
+ })
25
+ })
26
+
27
+ return warningLock
28
+ }
29
+
30
+ /**
31
+ * 操作拦截器:
32
+ * - canOperate=false 时:提示并阻止执行(warning 同一时间只显示一个)
33
+ * - canOperate=true 时:执行传入的函数
34
+ */
35
+ export async function guardOperate<T>(
36
+ fn: () => T | Promise<T>,
37
+ options?: { message?: string; silent?: boolean }
38
+ ): Promise<T | undefined> {
39
+ const licenseStore = useGraphStore()
40
+
41
+ if (!licenseStore.canOperate) {
42
+ return
43
+ if (!options?.silent) {
44
+ // showWarningOnce(options?.message ?? "当前软件未激活License,请激活后使用!")
45
+ }
46
+ return
47
+ }
48
+
49
+ return await fn()
50
+ }
@@ -0,0 +1,132 @@
1
+ import { ref, nextTick, type Ref } from 'vue';
2
+ import type { Shape } from '../types';
3
+
4
+ export interface NameEditOptions {
5
+ onNameChange?: (oldName: string, newName: string) => void;
6
+ validateName?: (name: string) => string | null; // 返回错误信息,null表示验证通过
7
+ onEditStart?: () => void;
8
+ onEditEnd?: () => void;
9
+ }
10
+
11
+ export class NameEditManager {
12
+ private isEditing: Ref<boolean>;
13
+ private editingName: Ref<string>;
14
+ private nameInput: Ref<HTMLInputElement | undefined>;
15
+ private options: NameEditOptions;
16
+
17
+ constructor(options: NameEditOptions = {}) {
18
+ this.isEditing = ref(false);
19
+ this.editingName = ref('');
20
+ this.nameInput = ref<HTMLInputElement>();
21
+ this.options = options;
22
+ }
23
+
24
+ // 获取响应式状态
25
+ get editingState() {
26
+ return {
27
+ isEditingName: this.isEditing,
28
+ editingName: this.editingName,
29
+ nameInput: this.nameInput
30
+ };
31
+ }
32
+
33
+ // 开始编辑名称
34
+ async startEdit(shape: Shape | null): Promise<void> {
35
+ if (!shape || this.isEditing.value) return;
36
+
37
+ this.isEditing.value = true;
38
+ this.editingName.value = shape.name;
39
+
40
+ // 触发开始编辑回调
41
+ this.options.onEditStart?.();
42
+
43
+ await nextTick();
44
+
45
+ // 聚焦并选中文本
46
+ this.nameInput.value?.focus();
47
+ this.nameInput.value?.select();
48
+ }
49
+
50
+ // 完成编辑
51
+ finishEdit(shape: Shape | null): void {
52
+ if (!shape || !this.isEditing.value) return;
53
+
54
+ const newName = this.editingName.value.trim();
55
+ const oldName = shape.name;
56
+
57
+ if (newName && newName !== oldName) {
58
+ // 验证名称
59
+ const validationError = this.options.validateName?.(newName);
60
+ if (validationError) {
61
+ // 如果验证失败,可以显示错误提示
62
+ console.warn('名称验证失败:', validationError);
63
+ this.cancelEdit();
64
+ return;
65
+ }
66
+
67
+ // 调用名称变更回调
68
+ this.options.onNameChange?.(oldName, newName);
69
+ }
70
+
71
+ this.isEditing.value = false;
72
+ this.editingName.value = '';
73
+ this.options.onEditEnd?.();
74
+ }
75
+
76
+ // 取消编辑
77
+ cancelEdit(): void {
78
+ this.isEditing.value = false;
79
+ this.editingName.value = '';
80
+ this.options.onEditEnd?.();
81
+ }
82
+
83
+ // 处理键盘事件
84
+ handleKeyUp(event: KeyboardEvent, shape: Shape | null): void {
85
+ if (event.key === 'Enter') {
86
+ this.finishEdit(shape);
87
+ } else if (event.key === 'Escape') {
88
+ this.cancelEdit();
89
+ }
90
+ }
91
+
92
+ // 处理失焦事件
93
+ handleBlur(shape: Shape | null): void {
94
+ this.finishEdit(shape);
95
+ }
96
+
97
+ // 判断是否可以编辑
98
+ canEdit(shape: Shape | null): boolean {
99
+ return !!shape &&
100
+ shape.shapeKey !== 'ConceptRole' &&
101
+ !this.isEditing.value;
102
+ }
103
+
104
+ // 获取名称显示文本
105
+ getDisplayName(shape: Shape | null): string {
106
+ return shape?.name || '';
107
+ }
108
+
109
+ // 重置状态
110
+ reset(): void {
111
+ this.isEditing.value = false;
112
+ this.editingName.value = '';
113
+ }
114
+ }
115
+
116
+ // 便捷创建函数
117
+ export function createNameEditManager(options?: NameEditOptions): NameEditManager {
118
+ return new NameEditManager(options);
119
+ }
120
+
121
+ // 默认验证函数
122
+ export function defaultNameValidator(name: string): string | null {
123
+ if (!name.trim()) {
124
+ return '名称不能为空';
125
+ }
126
+
127
+ if (name.length > 100) {
128
+ return '名称长度不能超过100个字符';
129
+ }
130
+
131
+ return null;
132
+ }