@mx-sose-front/mx-sose-graph 1.1.2 → 1.1.4
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 +178 -10
- package/dist/index.esm.js +4944 -63227
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -38
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +10 -1
- package/src/components/ContextMenu/ContextMenu.vue +27 -13
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +7 -12
- package/src/components/InteractionLayer.vue +656 -496
- package/src/components/LineStyle/LineStyleMarker.vue +1 -1
- package/src/components/NameEditor/NameEditor.vue +212 -0
- package/src/components/SelectionBox/SelectionBox.vue +189 -0
- package/src/components/Shape/Block.vue +1 -1
- package/src/constants/edgeShapeKeys.ts +43 -3
- package/src/constants/index.ts +21 -4
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useHighlight.ts +223 -0
- package/src/hooks/useNameEdit.ts +234 -0
- package/src/{utils/resizeUtils.ts → hooks/useResize.ts} +55 -155
- package/src/index.ts +4 -1
- package/src/render/shape-renderer.ts +59 -46
- package/src/statics/icons/createMenu/show.png +0 -0
- package/src/statics/icons/createMenu/tree.png +0 -0
- package/src/statics/icons/createMenu//345/261/225/347/244/272/347/253/257/345/217/243/345/261/236/346/200/247@3x.png +0 -0
- package/src/statics/icons/createMenu//345/261/225/347/244/272/350/277/236/347/272/277@3x.png +0 -0
- package/src/statics/icons/createMenu//346/211/200/345/234/250/345/233/276/350/241/250@3x.png +0 -0
- package/src/store/graphStore.ts +185 -65
- package/src/types/index.ts +4 -2
- package/src/types/interactionLayer.ts +1 -0
- package/src/utils/batchAutoExpand.ts +65 -0
- package/src/utils/compartment.ts +78 -4
- package/src/utils/containers.ts +24 -10
- package/src/utils/contextMenuUtils.ts +126 -110
- package/src/utils/diagram.ts +19 -15
- package/src/utils/drag.ts +10 -5
- package/src/utils/edgeUtils.ts +3 -4
- package/src/utils/graphDragService.ts +27 -23
- package/src/utils/iconLoader.ts +7 -7
- package/src/utils/keyboardUtils.ts +221 -30
- package/src/utils/pinUtils.ts +1 -2
- package/src/utils/shapeOps/shapeOps.ts +168 -0
- package/src/utils/viewportCulling.ts +193 -0
- package/src/view/graph.vue +115 -60
- package/src/utils/highlightUtils.ts +0 -162
- package/src/utils/nameEditUtils.ts +0 -132
- package/src/utils/packgeMap.ts +0 -1
- /package/src/statics/icons/createMenu/{scissors.png → cut.png} +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
2
|
+
import type { Ref } from 'vue';
|
|
3
|
+
import type { Shape } from '../types';
|
|
4
|
+
import { eventBus } from '../store';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 高亮覆盖层的边界信息
|
|
8
|
+
*/
|
|
9
|
+
export interface HighlightOverlayBounds {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 高亮颜色类型
|
|
18
|
+
*/
|
|
19
|
+
export type HighlightColor = 'blue' | 'red';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 高亮工具接口 - 用于 EdgeUtils.cancelConnection 等方法
|
|
23
|
+
*/
|
|
24
|
+
export interface IHighlightUtils {
|
|
25
|
+
highlightShape: (shape: Shape | null, isHighlight: boolean, isValidSource?: boolean) => void;
|
|
26
|
+
clearHighlightTimeout: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 外部高亮控制事件的 payload 类型
|
|
31
|
+
*/
|
|
32
|
+
export interface HighlightShapePayload {
|
|
33
|
+
shape: Shape;
|
|
34
|
+
isHighlight: boolean;
|
|
35
|
+
isValidSource?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* useHighlight 配置选项
|
|
40
|
+
*/
|
|
41
|
+
export interface UseHighlightOptions {
|
|
42
|
+
/** 是否监听 eventBus 事件,默认 true */
|
|
43
|
+
listenEvents?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* useHighlight 返回类型
|
|
48
|
+
*/
|
|
49
|
+
export interface UseHighlightReturn extends IHighlightUtils {
|
|
50
|
+
// 状态(用于模板绑定)
|
|
51
|
+
overlayBounds: Ref<HighlightOverlayBounds | null>;
|
|
52
|
+
overlayColor: Ref<HighlightColor>;
|
|
53
|
+
highlightedShapeId: Ref<string | null>;
|
|
54
|
+
// 方法
|
|
55
|
+
setHighlightTimeout: (callback: () => void, delay?: number) => void;
|
|
56
|
+
getHighlightedShapeId: () => string | null;
|
|
57
|
+
// 清理
|
|
58
|
+
dispose: () => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 图元高亮 Composable
|
|
63
|
+
* 使用覆盖层方式实现高亮(性能优化:不修改 graphStore)
|
|
64
|
+
*
|
|
65
|
+
* @param options 配置选项
|
|
66
|
+
* @returns 高亮相关的状态和方法
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```vue
|
|
70
|
+
* <script setup>
|
|
71
|
+
* const { overlayBounds, overlayColor, highlightShape } = useHighlight();
|
|
72
|
+
* </script>
|
|
73
|
+
*
|
|
74
|
+
* <template>
|
|
75
|
+
* <div v-if="overlayBounds" class="highlight-overlay" :class="overlayColor" :style="{
|
|
76
|
+
* left: overlayBounds.x + 'px',
|
|
77
|
+
* top: overlayBounds.y + 'px',
|
|
78
|
+
* width: overlayBounds.width + 'px',
|
|
79
|
+
* height: overlayBounds.height + 'px',
|
|
80
|
+
* }" />
|
|
81
|
+
* </template>
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @example 外部控制高亮
|
|
85
|
+
* ```ts
|
|
86
|
+
* // 触发高亮
|
|
87
|
+
* eventBus.emit('highlight-shape-overlay', { shape, isHighlight: true, isValidSource: true });
|
|
88
|
+
* // 清除高亮
|
|
89
|
+
* eventBus.emit('clear-highlight-overlay');
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function useHighlight(options: UseHighlightOptions = {}): UseHighlightReturn {
|
|
93
|
+
const { listenEvents = true } = options;
|
|
94
|
+
|
|
95
|
+
// 内部状态
|
|
96
|
+
const overlayBounds = ref<HighlightOverlayBounds | null>(null);
|
|
97
|
+
const overlayColor = ref<HighlightColor>('blue');
|
|
98
|
+
const highlightedShapeId = ref<string | null>(null);
|
|
99
|
+
const highlightTimeout = ref<ReturnType<typeof setTimeout> | null>(null);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 清除高亮定时器
|
|
103
|
+
*/
|
|
104
|
+
const clearHighlightTimeout = () => {
|
|
105
|
+
if (highlightTimeout.value) {
|
|
106
|
+
clearTimeout(highlightTimeout.value);
|
|
107
|
+
highlightTimeout.value = null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 设置高亮定时器
|
|
113
|
+
* @param callback 回调函数
|
|
114
|
+
* @param delay 延迟时间(毫秒),默认 60ms
|
|
115
|
+
*/
|
|
116
|
+
const setHighlightTimeout = (callback: () => void, delay: number = 60) => {
|
|
117
|
+
clearHighlightTimeout();
|
|
118
|
+
highlightTimeout.value = setTimeout(callback, delay);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 高亮或取消高亮图元(使用覆盖层方式)
|
|
123
|
+
* @param shape 要高亮的图元,null 表示取消高亮
|
|
124
|
+
* @param isHighlight 是否高亮
|
|
125
|
+
* @param isValidSource 是否是有效的连接源(影响高亮颜色:蓝色=有效,红色=无效)
|
|
126
|
+
*/
|
|
127
|
+
const highlightShape = (
|
|
128
|
+
shape: Shape | null,
|
|
129
|
+
isHighlight: boolean,
|
|
130
|
+
isValidSource: boolean = true
|
|
131
|
+
) => {
|
|
132
|
+
// 取消高亮
|
|
133
|
+
if (!shape || !isHighlight) {
|
|
134
|
+
overlayBounds.value = null;
|
|
135
|
+
highlightedShapeId.value = null;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 不高亮 diagram 和 edge 类型
|
|
140
|
+
if (shape.shapeType === 'diagram' || shape.shapeType === 'edge') {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 不高亮没有有效 bounds 的图元
|
|
145
|
+
if (!shape.bounds || (shape.bounds.width ?? 0) <= 0 || (shape.bounds.height ?? 0) <= 0) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 不高亮没有 shapeKey 的图元
|
|
150
|
+
if (!shape.shapeKey) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 设置高亮覆盖层(向外扩展 3px,完全覆盖图元边框)
|
|
155
|
+
overlayBounds.value = {
|
|
156
|
+
x: shape.bounds.x ?? 0,
|
|
157
|
+
y: shape.bounds.y ?? 0,
|
|
158
|
+
width: shape.bounds.width ?? 0,
|
|
159
|
+
height: shape.bounds.height ?? 0,
|
|
160
|
+
};
|
|
161
|
+
overlayColor.value = isValidSource ? 'blue' : 'red';
|
|
162
|
+
highlightedShapeId.value = shape.id;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 获取当前高亮的图元 ID
|
|
167
|
+
*/
|
|
168
|
+
const getHighlightedShapeId = () => highlightedShapeId.value;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 清理所有状态
|
|
172
|
+
*/
|
|
173
|
+
const dispose = () => {
|
|
174
|
+
clearHighlightTimeout();
|
|
175
|
+
overlayBounds.value = null;
|
|
176
|
+
highlightedShapeId.value = null;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// eventBus 事件处理
|
|
180
|
+
const handleHighlightShapeOverlay = (payload: HighlightShapePayload | null) => {
|
|
181
|
+
if (!payload) {
|
|
182
|
+
highlightShape(null, false);
|
|
183
|
+
} else {
|
|
184
|
+
highlightShape(payload.shape, payload.isHighlight, payload.isValidSource ?? true);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleClearHighlightOverlay = () => {
|
|
189
|
+
highlightShape(null, false);
|
|
190
|
+
clearHighlightTimeout();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// 生命周期
|
|
194
|
+
if (listenEvents) {
|
|
195
|
+
onMounted(() => {
|
|
196
|
+
eventBus.on('highlight-shape-overlay', handleHighlightShapeOverlay);
|
|
197
|
+
eventBus.on('clear-highlight-overlay', handleClearHighlightOverlay);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
onUnmounted(() => {
|
|
201
|
+
eventBus.off('highlight-shape-overlay', handleHighlightShapeOverlay);
|
|
202
|
+
eventBus.off('clear-highlight-overlay', handleClearHighlightOverlay);
|
|
203
|
+
dispose();
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
onUnmounted(() => {
|
|
207
|
+
dispose();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
// 状态
|
|
213
|
+
overlayBounds,
|
|
214
|
+
overlayColor,
|
|
215
|
+
highlightedShapeId,
|
|
216
|
+
// 方法
|
|
217
|
+
highlightShape,
|
|
218
|
+
setHighlightTimeout,
|
|
219
|
+
clearHighlightTimeout,
|
|
220
|
+
getHighlightedShapeId,
|
|
221
|
+
dispose,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { ref, nextTick, onUnmounted, type Ref } from 'vue';
|
|
2
|
+
import type { Shape } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 名称编辑配置选项
|
|
6
|
+
*/
|
|
7
|
+
export interface UseNameEditOptions {
|
|
8
|
+
/** 获取当前选中的 shape(用于 onNameChange 回调) */
|
|
9
|
+
getSelectedShape?: () => Shape | null;
|
|
10
|
+
/** 名称变更时的回调,包含 shape 信息 */
|
|
11
|
+
onNameChange?: (shape: Shape, oldName: string, newName: string) => void;
|
|
12
|
+
/** 名称验证函数,返回错误信息,null 表示验证通过 */
|
|
13
|
+
validateName?: (name: string) => string | null;
|
|
14
|
+
/** 开始编辑时的回调 */
|
|
15
|
+
onEditStart?: () => void;
|
|
16
|
+
/** 结束编辑时的回调 */
|
|
17
|
+
onEditEnd?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 名称编辑管理器接口
|
|
22
|
+
* 用于 NameEditor 组件的 props 类型定义
|
|
23
|
+
*/
|
|
24
|
+
export interface INameEditManager {
|
|
25
|
+
// 响应式状态(通过 editingState getter 访问)
|
|
26
|
+
editingState: {
|
|
27
|
+
isEditingName: Ref<boolean>;
|
|
28
|
+
editingName: Ref<string>;
|
|
29
|
+
nameInput: Ref<HTMLInputElement | null>;
|
|
30
|
+
};
|
|
31
|
+
// 方法
|
|
32
|
+
setNameInput: (input: HTMLInputElement | null) => void;
|
|
33
|
+
startEdit: (shape: Shape | null) => Promise<void>;
|
|
34
|
+
finishEdit: (shape: Shape | null) => void;
|
|
35
|
+
cancelEdit: () => void;
|
|
36
|
+
handleKeyUp: (event: KeyboardEvent, shape: Shape | null) => void;
|
|
37
|
+
handleBlur: (shape: Shape | null) => void;
|
|
38
|
+
canEdit: (shape: Shape | null) => boolean;
|
|
39
|
+
getDisplayName: (shape: Shape | null) => string;
|
|
40
|
+
reset: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* useNameEdit 返回类型
|
|
45
|
+
*/
|
|
46
|
+
export interface UseNameEditReturn extends INameEditManager {
|
|
47
|
+
// 直接暴露的响应式状态(方便解构使用)
|
|
48
|
+
isEditingName: Ref<boolean>;
|
|
49
|
+
editingName: Ref<string>;
|
|
50
|
+
nameInput: Ref<HTMLInputElement | null>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 名称编辑 Composable
|
|
55
|
+
*
|
|
56
|
+
* 用于管理图元名称的编辑状态和行为
|
|
57
|
+
*
|
|
58
|
+
* @param options 配置选项
|
|
59
|
+
* @returns 名称编辑相关的状态和方法
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```vue
|
|
63
|
+
* <script setup>
|
|
64
|
+
* // 方式1:解构使用
|
|
65
|
+
* const { isEditingName, editingName, startEdit, handleBlur } = useNameEdit({
|
|
66
|
+
* onNameChange: (oldName, newName) => {
|
|
67
|
+
* emit('editName', selectedShape, newName, oldName);
|
|
68
|
+
* }
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // 方式2:作为对象传递给子组件(兼容 NameEditor 组件)
|
|
72
|
+
* const nameEditManager = useNameEdit({ ... });
|
|
73
|
+
* // <NameEditor :name-edit-manager="nameEditManager" />
|
|
74
|
+
* </script>
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function useNameEdit(options: UseNameEditOptions = {}): UseNameEditReturn {
|
|
78
|
+
// 响应式状态
|
|
79
|
+
const isEditingName = ref(false);
|
|
80
|
+
const editingName = ref('');
|
|
81
|
+
const nameInput = ref<HTMLInputElement | null>(null);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 设置名称输入框引用
|
|
85
|
+
*/
|
|
86
|
+
const setNameInput = (input: HTMLInputElement | null) => {
|
|
87
|
+
nameInput.value = input;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 开始编辑名称
|
|
92
|
+
*/
|
|
93
|
+
const startEdit = async (shape: Shape | null): Promise<void> => {
|
|
94
|
+
if (!shape || isEditingName.value) return;
|
|
95
|
+
|
|
96
|
+
isEditingName.value = true;
|
|
97
|
+
editingName.value = shape.name || '';
|
|
98
|
+
|
|
99
|
+
// 触发开始编辑回调
|
|
100
|
+
options.onEditStart?.();
|
|
101
|
+
|
|
102
|
+
await nextTick();
|
|
103
|
+
|
|
104
|
+
// 聚焦并选中文本
|
|
105
|
+
nameInput.value?.focus();
|
|
106
|
+
nameInput.value?.select();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 完成编辑
|
|
111
|
+
*/
|
|
112
|
+
const finishEdit = (shape: Shape | null): void => {
|
|
113
|
+
if (!shape || !isEditingName.value) return;
|
|
114
|
+
|
|
115
|
+
const newName = editingName.value.trim();
|
|
116
|
+
const oldName = shape.name || '';
|
|
117
|
+
|
|
118
|
+
if (newName && newName !== oldName) {
|
|
119
|
+
// 验证名称
|
|
120
|
+
const validationError = options.validateName?.(newName);
|
|
121
|
+
if (validationError) {
|
|
122
|
+
console.warn('名称验证失败:', validationError);
|
|
123
|
+
cancelEdit();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 调用名称变更回调(优先使用 getSelectedShape 获取最新的 shape)
|
|
128
|
+
const targetShape = options.getSelectedShape?.() ?? shape;
|
|
129
|
+
if (targetShape) {
|
|
130
|
+
options.onNameChange?.(targetShape, oldName, newName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
isEditingName.value = false;
|
|
135
|
+
editingName.value = '';
|
|
136
|
+
options.onEditEnd?.();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 取消编辑
|
|
141
|
+
*/
|
|
142
|
+
const cancelEdit = (): void => {
|
|
143
|
+
isEditingName.value = false;
|
|
144
|
+
editingName.value = '';
|
|
145
|
+
options.onEditEnd?.();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 处理键盘事件
|
|
150
|
+
*/
|
|
151
|
+
const handleKeyUp = (event: KeyboardEvent, shape: Shape | null): void => {
|
|
152
|
+
if (event.key === 'Enter') {
|
|
153
|
+
finishEdit(shape);
|
|
154
|
+
} else if (event.key === 'Escape') {
|
|
155
|
+
cancelEdit();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 处理失焦事件
|
|
161
|
+
*/
|
|
162
|
+
const handleBlur = (shape: Shape | null): void => {
|
|
163
|
+
finishEdit(shape);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 判断是否可以编辑
|
|
168
|
+
*/
|
|
169
|
+
const canEdit = (shape: Shape | null): boolean => {
|
|
170
|
+
return !!shape &&
|
|
171
|
+
shape.shapeKey !== 'ConceptRole' &&
|
|
172
|
+
!isEditingName.value;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 获取名称显示文本
|
|
177
|
+
*/
|
|
178
|
+
const getDisplayName = (shape: Shape | null): string => {
|
|
179
|
+
return shape?.name || '';
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 重置状态
|
|
184
|
+
*/
|
|
185
|
+
const reset = (): void => {
|
|
186
|
+
isEditingName.value = false;
|
|
187
|
+
editingName.value = '';
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// 组件卸载时自动重置
|
|
191
|
+
onUnmounted(() => {
|
|
192
|
+
reset();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
// 直接暴露的状态(方便解构)
|
|
197
|
+
isEditingName,
|
|
198
|
+
editingName,
|
|
199
|
+
nameInput,
|
|
200
|
+
// editingState getter(兼容 NameEditor 组件)
|
|
201
|
+
get editingState() {
|
|
202
|
+
return {
|
|
203
|
+
isEditingName,
|
|
204
|
+
editingName,
|
|
205
|
+
nameInput,
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
// 方法
|
|
209
|
+
setNameInput,
|
|
210
|
+
startEdit,
|
|
211
|
+
finishEdit,
|
|
212
|
+
cancelEdit,
|
|
213
|
+
handleKeyUp,
|
|
214
|
+
handleBlur,
|
|
215
|
+
canEdit,
|
|
216
|
+
getDisplayName,
|
|
217
|
+
reset,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 默认名称验证函数
|
|
223
|
+
*/
|
|
224
|
+
export function defaultNameValidator(name: string): string | null {
|
|
225
|
+
if (!name.trim()) {
|
|
226
|
+
return '名称不能为空';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (name.length > 100) {
|
|
230
|
+
return '名称长度不能超过100个字符';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|