@flowgram-vue/free-container-plugin 0.2.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/LICENSE +22 -0
- package/dist/index.cjs +0 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
- package/src/env.d.ts +10 -0
- package/src/index.ts +8 -0
- package/src/node-into-container/constant.ts +9 -0
- package/src/node-into-container/index.ts +13 -0
- package/src/node-into-container/plugin.ts +26 -0
- package/src/node-into-container/service.ts +370 -0
- package/src/node-into-container/type.ts +29 -0
- package/src/sub-canvas/components/background/index.ts +6 -0
- package/src/sub-canvas/components/background/sub-canvas-background.vue +64 -0
- package/src/sub-canvas/components/border/index.ts +6 -0
- package/src/sub-canvas/components/border/sub-canvas-border.vue +47 -0
- package/src/sub-canvas/components/index.ts +9 -0
- package/src/sub-canvas/components/render/index.ts +6 -0
- package/src/sub-canvas/components/render/sub-canvas-render.vue +54 -0
- package/src/sub-canvas/components/tips/global-store.ts +38 -0
- package/src/sub-canvas/components/tips/index.ts +6 -0
- package/src/sub-canvas/components/tips/is-mac-os.ts +6 -0
- package/src/sub-canvas/components/tips/sub-canvas-tips.vue +157 -0
- package/src/sub-canvas/components/tips/use-control.ts +65 -0
- package/src/sub-canvas/hooks/index.ts +8 -0
- package/src/sub-canvas/hooks/use-node-size.ts +69 -0
- package/src/sub-canvas/hooks/use-sync-node-render-size.ts +26 -0
- package/src/sub-canvas/index.ts +7 -0
- package/src/utils/adjust-sub-node-position.ts +41 -0
- package/src/utils/get-collision-transform.ts +41 -0
- package/src/utils/get-container-transforms.ts +27 -0
- package/src/utils/index.ts +18 -0
- package/src/utils/is-container.ts +9 -0
- package/src/utils/is-point-in-rect.ts +9 -0
- package/src/utils/is-rect-intersects.ts +15 -0
- package/src/utils/next-frame.ts +8 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- no need */
|
|
7
|
+
import { throttle } from 'lodash-es';
|
|
8
|
+
import { inject, injectable } from 'inversify';
|
|
9
|
+
import { type Disposable, DisposableCollection, Emitter } from '@flowgram-vue/utils';
|
|
10
|
+
import {
|
|
11
|
+
type NodesDragEvent,
|
|
12
|
+
WorkflowDocument,
|
|
13
|
+
WorkflowDragService,
|
|
14
|
+
WorkflowLinesManager,
|
|
15
|
+
type WorkflowNodeEntity,
|
|
16
|
+
WorkflowNodeMeta,
|
|
17
|
+
WorkflowOperationBaseService,
|
|
18
|
+
WorkflowSelectService,
|
|
19
|
+
} from '@flowgram-vue/free-layout-core';
|
|
20
|
+
import { HistoryService } from '@flowgram-vue/free-history-plugin';
|
|
21
|
+
import { FlowNodeRenderData, FlowNodeBaseType } from '@flowgram-vue/document';
|
|
22
|
+
import { PlaygroundConfigEntity, TransformData } from '@flowgram-vue/core';
|
|
23
|
+
|
|
24
|
+
import type { NodeIntoContainerEvent, NodeIntoContainerState } from './type';
|
|
25
|
+
import { NodeIntoContainerType } from './constant';
|
|
26
|
+
import { ContainerUtils } from '../utils';
|
|
27
|
+
|
|
28
|
+
@injectable()
|
|
29
|
+
export class NodeIntoContainerService {
|
|
30
|
+
public state: NodeIntoContainerState;
|
|
31
|
+
|
|
32
|
+
@inject(WorkflowDragService)
|
|
33
|
+
private dragService: WorkflowDragService;
|
|
34
|
+
|
|
35
|
+
@inject(WorkflowDocument)
|
|
36
|
+
private document: WorkflowDocument;
|
|
37
|
+
|
|
38
|
+
@inject(PlaygroundConfigEntity)
|
|
39
|
+
private playgroundConfig: PlaygroundConfigEntity;
|
|
40
|
+
|
|
41
|
+
@inject(WorkflowOperationBaseService)
|
|
42
|
+
private operationService: WorkflowOperationBaseService;
|
|
43
|
+
|
|
44
|
+
@inject(WorkflowLinesManager)
|
|
45
|
+
private linesManager: WorkflowLinesManager;
|
|
46
|
+
|
|
47
|
+
@inject(HistoryService) private historyService: HistoryService;
|
|
48
|
+
|
|
49
|
+
@inject(WorkflowSelectService) private selectService: WorkflowSelectService;
|
|
50
|
+
|
|
51
|
+
private emitter = new Emitter<NodeIntoContainerEvent>();
|
|
52
|
+
|
|
53
|
+
private toDispose = new DisposableCollection();
|
|
54
|
+
|
|
55
|
+
public readonly on = this.emitter.event;
|
|
56
|
+
|
|
57
|
+
public init(): void {
|
|
58
|
+
this.initState();
|
|
59
|
+
this.toDispose.push(this.emitter);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public ready(): void {
|
|
63
|
+
this.toDispose.push(this.listenDragToContainer());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public dispose(): void {
|
|
67
|
+
this.initState();
|
|
68
|
+
this.toDispose.dispose();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 将节点移出容器 */
|
|
72
|
+
public async moveOutContainer(params: { node: WorkflowNodeEntity }): Promise<void> {
|
|
73
|
+
const { node } = params;
|
|
74
|
+
const parentNode = node.parent;
|
|
75
|
+
const containerNode = parentNode?.parent;
|
|
76
|
+
const nodeJSON = this.document.toNodeJSON(node);
|
|
77
|
+
if (
|
|
78
|
+
!parentNode ||
|
|
79
|
+
!containerNode ||
|
|
80
|
+
!ContainerUtils.isContainer(parentNode) ||
|
|
81
|
+
!nodeJSON.meta?.position
|
|
82
|
+
) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const parentTransform = parentNode.getData<TransformData>(TransformData);
|
|
86
|
+
this.operationService.moveNode(node, {
|
|
87
|
+
parent: containerNode,
|
|
88
|
+
});
|
|
89
|
+
await ContainerUtils.nextFrame();
|
|
90
|
+
parentTransform.fireChange();
|
|
91
|
+
this.operationService.updateNodePosition(node, {
|
|
92
|
+
x: parentTransform.position.x + nodeJSON.meta!.position!.x,
|
|
93
|
+
y: parentTransform.position.y + nodeJSON.meta!.position!.y,
|
|
94
|
+
});
|
|
95
|
+
this.emitter.fire({
|
|
96
|
+
type: NodeIntoContainerType.Out,
|
|
97
|
+
node,
|
|
98
|
+
sourceContainer: parentNode,
|
|
99
|
+
targetContainer: containerNode,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 能否将节点移出容器 */
|
|
104
|
+
public canMoveOutContainer(node: WorkflowNodeEntity): boolean {
|
|
105
|
+
const parentNode = node.parent;
|
|
106
|
+
const containerNode = parentNode?.parent;
|
|
107
|
+
if (!parentNode || !containerNode || !ContainerUtils.isContainer(parentNode)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const canDrop = this.dragService.canDropToNode({
|
|
111
|
+
dragNodeType: node.flowNodeType,
|
|
112
|
+
dragNode: node,
|
|
113
|
+
dropNodeType: containerNode?.flowNodeType,
|
|
114
|
+
dropNode: containerNode,
|
|
115
|
+
});
|
|
116
|
+
if (!canDrop.allowDrop) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 移除节点所有非法连线 */
|
|
123
|
+
public async clearInvalidLines(params: {
|
|
124
|
+
dragNode?: WorkflowNodeEntity;
|
|
125
|
+
sourceParent?: WorkflowNodeEntity;
|
|
126
|
+
}): Promise<void> {
|
|
127
|
+
const { dragNode, sourceParent } = params;
|
|
128
|
+
if (!dragNode) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (dragNode.parent === sourceParent) {
|
|
132
|
+
// 容器节点未改变
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
dragNode.parent?.flowNodeType === FlowNodeBaseType.GROUP ||
|
|
137
|
+
sourceParent?.flowNodeType === FlowNodeBaseType.GROUP
|
|
138
|
+
) {
|
|
139
|
+
// 移入移出 group 节点无需删除节点
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await this.removeNodeLines(dragNode);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 移除节点连线 */
|
|
146
|
+
public async removeNodeLines(node: WorkflowNodeEntity): Promise<void> {
|
|
147
|
+
const lines = this.linesManager.getAllLines();
|
|
148
|
+
lines.forEach((line) => {
|
|
149
|
+
if (line.from?.id !== node.id && line.to?.id !== node.id) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
line.dispose();
|
|
153
|
+
});
|
|
154
|
+
await ContainerUtils.nextFrame();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 初始化状态 */
|
|
158
|
+
private initState(): void {
|
|
159
|
+
this.state = {
|
|
160
|
+
isDraggingNode: false,
|
|
161
|
+
isSkipEvent: false,
|
|
162
|
+
transforms: undefined,
|
|
163
|
+
dragNode: undefined,
|
|
164
|
+
dropNode: undefined,
|
|
165
|
+
sourceParent: undefined,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 监听节点拖拽 */
|
|
170
|
+
private listenDragToContainer(): Disposable {
|
|
171
|
+
const draggingNode = (e: NodesDragEvent) => this.draggingNode(e);
|
|
172
|
+
const throttledDraggingNode = throttle(draggingNode, 200); // 200ms触发一次计算
|
|
173
|
+
return this.dragService.onNodesDrag(async (event) => {
|
|
174
|
+
if (this.selectService.selectedNodes.length !== 1) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (event.type === 'onDragStart') {
|
|
178
|
+
if (this.state.isSkipEvent) {
|
|
179
|
+
// 拖出容器后重新进入
|
|
180
|
+
this.state.isSkipEvent = false;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.historyService.startTransaction(); // 开始合并历史记录
|
|
184
|
+
this.state.isDraggingNode = true;
|
|
185
|
+
this.state.transforms = ContainerUtils.getContainerTransforms(this.document.getAllNodes());
|
|
186
|
+
this.state.dragNode = this.selectService.selectedNodes[0];
|
|
187
|
+
this.state.dropNode = undefined;
|
|
188
|
+
this.state.sourceParent = this.state.dragNode?.parent;
|
|
189
|
+
await this.dragOutContainer(event); // 检查是否需拖出容器
|
|
190
|
+
}
|
|
191
|
+
if (event.type === 'onDragging') {
|
|
192
|
+
throttledDraggingNode(event);
|
|
193
|
+
}
|
|
194
|
+
if (event.type === 'onDragEnd') {
|
|
195
|
+
if (this.state.isSkipEvent) {
|
|
196
|
+
// 拖出容器情况下需跳过本次事件
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
throttledDraggingNode.cancel();
|
|
200
|
+
draggingNode(event); // 直接触发一次计算,防止延迟
|
|
201
|
+
await this.dropNodeToContainer(); // 放置节点
|
|
202
|
+
await this.clearInvalidLines({
|
|
203
|
+
dragNode: this.state.dragNode,
|
|
204
|
+
sourceParent: this.state.sourceParent,
|
|
205
|
+
}); // 清除非法线条
|
|
206
|
+
this.setDropNode(undefined);
|
|
207
|
+
this.initState(); // 重置状态
|
|
208
|
+
this.historyService.endTransaction(); // 结束合并历史记录
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 监听节点拖拽出容器 */
|
|
214
|
+
private async dragOutContainer(event: NodesDragEvent): Promise<void> {
|
|
215
|
+
const { dragNode } = this.state;
|
|
216
|
+
const activated = event.triggerEvent.metaKey || event.triggerEvent.ctrlKey;
|
|
217
|
+
if (
|
|
218
|
+
!activated || // 必须按住指定按键
|
|
219
|
+
!dragNode || // 必须有一个节点
|
|
220
|
+
!this.canMoveOutContainer(dragNode) // 需要能被移出容器
|
|
221
|
+
) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.moveOutContainer({ node: dragNode });
|
|
225
|
+
this.state.isSkipEvent = true;
|
|
226
|
+
event.dragger.stop(event.dragEvent.clientX, event.dragEvent.clientY);
|
|
227
|
+
await ContainerUtils.nextFrame();
|
|
228
|
+
this.dragService.startDragSelectedNodes(event.triggerEvent);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** 设置放置节点高亮 */
|
|
232
|
+
private setDropNode(dropNode?: WorkflowNodeEntity) {
|
|
233
|
+
if (this.state.dropNode === dropNode) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (this.state.dropNode) {
|
|
237
|
+
// 清除上一个节点高亮
|
|
238
|
+
const renderData = this.state.dropNode.getData(FlowNodeRenderData);
|
|
239
|
+
const renderDom = renderData.node?.children?.[0] as HTMLElement;
|
|
240
|
+
if (renderDom) {
|
|
241
|
+
renderDom.classList.remove('selected');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
this.state.dropNode = dropNode;
|
|
245
|
+
if (!dropNode) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// 设置当前节点高亮
|
|
249
|
+
const renderData = dropNode.getData(FlowNodeRenderData);
|
|
250
|
+
const renderDom = renderData.node?.children?.[0] as HTMLElement;
|
|
251
|
+
if (renderDom) {
|
|
252
|
+
renderDom.classList.add('selected');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 放置节点到容器 */
|
|
257
|
+
private async dropNodeToContainer(): Promise<void> {
|
|
258
|
+
const { dropNode, dragNode, isDraggingNode } = this.state;
|
|
259
|
+
if (!isDraggingNode || !dragNode || !dropNode) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
return await this.moveIntoContainer({
|
|
263
|
+
node: dragNode,
|
|
264
|
+
containerNode: dropNode,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 拖拽节点 */
|
|
269
|
+
private draggingNode(nodeDragEvent: NodesDragEvent): void {
|
|
270
|
+
const { dragNode, isDraggingNode, transforms = [] } = this.state;
|
|
271
|
+
if (!isDraggingNode || !dragNode || !transforms?.length) {
|
|
272
|
+
return this.setDropNode(undefined);
|
|
273
|
+
}
|
|
274
|
+
const mousePos = this.playgroundConfig.getPosFromMouseEvent(nodeDragEvent.dragEvent);
|
|
275
|
+
const availableTransforms = transforms.filter(
|
|
276
|
+
(transform) => transform.entity.id !== dragNode.id
|
|
277
|
+
);
|
|
278
|
+
const collisionTransform = ContainerUtils.getCollisionTransform({
|
|
279
|
+
targetPoint: mousePos,
|
|
280
|
+
transforms: availableTransforms,
|
|
281
|
+
document: this.document,
|
|
282
|
+
});
|
|
283
|
+
const dropNode = collisionTransform?.entity;
|
|
284
|
+
const canDrop = this.canDropToContainer({
|
|
285
|
+
dragNode,
|
|
286
|
+
dropNode,
|
|
287
|
+
});
|
|
288
|
+
if (!canDrop) {
|
|
289
|
+
return this.setDropNode(undefined);
|
|
290
|
+
}
|
|
291
|
+
return this.setDropNode(dropNode);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** 判断能否将节点拖入容器 */
|
|
295
|
+
protected canDropToContainer(params: {
|
|
296
|
+
dragNode: WorkflowNodeEntity;
|
|
297
|
+
dropNode?: WorkflowNodeEntity;
|
|
298
|
+
}): boolean {
|
|
299
|
+
const { dragNode, dropNode } = params;
|
|
300
|
+
const isDropContainer = dropNode?.getNodeMeta<WorkflowNodeMeta>().isContainer;
|
|
301
|
+
if (
|
|
302
|
+
!dropNode ||
|
|
303
|
+
!isDropContainer ||
|
|
304
|
+
this.isParent(dragNode, dropNode) ||
|
|
305
|
+
this.isParent(dropNode, dragNode)
|
|
306
|
+
) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
if (
|
|
310
|
+
dragNode.flowNodeType === FlowNodeBaseType.GROUP &&
|
|
311
|
+
dropNode.flowNodeType !== FlowNodeBaseType.GROUP
|
|
312
|
+
) {
|
|
313
|
+
// 禁止将 group 节点拖入非 group 节点(由于目前不支持多节点拖入容器,无法计算有效线条,因此进行屏蔽)
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
const canDrop = this.dragService.canDropToNode({
|
|
317
|
+
dragNodeType: dragNode.flowNodeType,
|
|
318
|
+
dropNodeType: dropNode?.flowNodeType,
|
|
319
|
+
dragNode,
|
|
320
|
+
dropNode,
|
|
321
|
+
});
|
|
322
|
+
if (!canDrop.allowDrop) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** 判断一个节点是否为另一个节点的父节点(向上查找直到根节点) */
|
|
329
|
+
private isParent(node: WorkflowNodeEntity, parent: WorkflowNodeEntity): boolean {
|
|
330
|
+
let current = node.parent;
|
|
331
|
+
while (current) {
|
|
332
|
+
if (current.id === parent.id) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
current = current.parent;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** 将节点移入容器 */
|
|
341
|
+
private async moveIntoContainer(params: {
|
|
342
|
+
node: WorkflowNodeEntity;
|
|
343
|
+
containerNode: WorkflowNodeEntity;
|
|
344
|
+
}): Promise<void> {
|
|
345
|
+
const { node, containerNode } = params;
|
|
346
|
+
const parentNode = node.parent;
|
|
347
|
+
|
|
348
|
+
this.operationService.moveNode(node, {
|
|
349
|
+
parent: containerNode,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const containerPadding = this.document.layout.getPadding(containerNode);
|
|
353
|
+
const position = ContainerUtils.adjustSubNodePosition({
|
|
354
|
+
targetNode: node,
|
|
355
|
+
containerNode,
|
|
356
|
+
containerPadding,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
this.operationService.updateNodePosition(node, position);
|
|
360
|
+
|
|
361
|
+
await ContainerUtils.nextFrame();
|
|
362
|
+
|
|
363
|
+
this.emitter.fire({
|
|
364
|
+
type: NodeIntoContainerType.In,
|
|
365
|
+
node,
|
|
366
|
+
sourceContainer: parentNode,
|
|
367
|
+
targetContainer: containerNode,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WorkflowNodeEntity } from '@flowgram-vue/free-layout-core';
|
|
7
|
+
import type { FlowNodeTransformData } from '@flowgram-vue/document';
|
|
8
|
+
|
|
9
|
+
import type { NodeIntoContainerType } from './constant';
|
|
10
|
+
|
|
11
|
+
export interface NodeIntoContainerState {
|
|
12
|
+
isDraggingNode: boolean;
|
|
13
|
+
isSkipEvent: boolean;
|
|
14
|
+
transforms?: FlowNodeTransformData[];
|
|
15
|
+
dragNode?: WorkflowNodeEntity;
|
|
16
|
+
dropNode?: WorkflowNodeEntity;
|
|
17
|
+
sourceParent?: WorkflowNodeEntity;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NodeIntoContainerEvent {
|
|
21
|
+
type: NodeIntoContainerType;
|
|
22
|
+
node: WorkflowNodeEntity;
|
|
23
|
+
sourceContainer?: WorkflowNodeEntity;
|
|
24
|
+
targetContainer: WorkflowNodeEntity;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WorkflowContainerPluginOptions {
|
|
28
|
+
disableNodeIntoContainer?: boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCurrentEntity } from '@flowgram-vue/free-layout-core';
|
|
8
|
+
import { useService } from '@flowgram-vue/core';
|
|
9
|
+
import { BackgroundConfig, type BackgroundLayerOptions } from '@flowgram-vue/background-plugin';
|
|
10
|
+
|
|
11
|
+
const node = useCurrentEntity();
|
|
12
|
+
|
|
13
|
+
let backgroundConfig: BackgroundLayerOptions = {};
|
|
14
|
+
try {
|
|
15
|
+
backgroundConfig = useService<BackgroundLayerOptions>(BackgroundConfig);
|
|
16
|
+
} catch {
|
|
17
|
+
// 如果 BackgroundConfig 没有注册,使用默认配置
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const gridSize = backgroundConfig.gridSize ?? 20;
|
|
21
|
+
const dotSize = backgroundConfig.dotSize ?? 1;
|
|
22
|
+
const dotColor = backgroundConfig.dotColor ?? '#eceeef';
|
|
23
|
+
const dotOpacity = backgroundConfig.dotOpacity ?? 0.5;
|
|
24
|
+
const backgroundColor = backgroundConfig.backgroundColor ?? '#f2f3f5';
|
|
25
|
+
const dotFillColor =
|
|
26
|
+
backgroundConfig.dotFillColor === dotColor ? '' : backgroundConfig.dotFillColor;
|
|
27
|
+
|
|
28
|
+
const patternId = `sub-canvas-dot-pattern-${node.id}`;
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div
|
|
33
|
+
class="sub-canvas-background"
|
|
34
|
+
data-flow-editor-selectable="true"
|
|
35
|
+
:style="{ backgroundColor }"
|
|
36
|
+
>
|
|
37
|
+
<svg width="100%" height="100%">
|
|
38
|
+
<pattern :id="patternId" :width="gridSize" :height="gridSize" patternUnits="userSpaceOnUse">
|
|
39
|
+
<circle
|
|
40
|
+
:cx="dotSize"
|
|
41
|
+
:cy="dotSize"
|
|
42
|
+
:r="dotSize"
|
|
43
|
+
:stroke="dotColor"
|
|
44
|
+
:fill="dotFillColor"
|
|
45
|
+
:fill-opacity="dotOpacity"
|
|
46
|
+
/>
|
|
47
|
+
</pattern>
|
|
48
|
+
<rect
|
|
49
|
+
width="100%"
|
|
50
|
+
height="100%"
|
|
51
|
+
:fill="`url(#${patternId})`"
|
|
52
|
+
:data-node-panel-container="node.id"
|
|
53
|
+
/>
|
|
54
|
+
</svg>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<style scoped>
|
|
59
|
+
.sub-canvas-background {
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 100%;
|
|
62
|
+
inset: 56px 18px 18px;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CSSProperties } from 'vue';
|
|
8
|
+
|
|
9
|
+
defineProps<{
|
|
10
|
+
style?: CSSProperties;
|
|
11
|
+
}>();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="sub-canvas-border" :style="style">
|
|
16
|
+
<slot />
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<style scoped>
|
|
21
|
+
.sub-canvas-border {
|
|
22
|
+
pointer-events: none;
|
|
23
|
+
position: relative;
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 100%;
|
|
28
|
+
background-color: transparent;
|
|
29
|
+
border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));
|
|
30
|
+
border-color: var(--coz-bg-plus, rgb(249, 249, 249));
|
|
31
|
+
border-style: solid;
|
|
32
|
+
border-width: 8px;
|
|
33
|
+
border-radius: 8px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sub-canvas-border::before {
|
|
37
|
+
content: '';
|
|
38
|
+
position: absolute;
|
|
39
|
+
z-index: 0;
|
|
40
|
+
inset: -4px;
|
|
41
|
+
background-color: transparent;
|
|
42
|
+
border-color: var(--coz-bg-plus, rgb(249, 249, 249));
|
|
43
|
+
border-style: solid;
|
|
44
|
+
border-width: 4px;
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { SubCanvasBackground } from './background';
|
|
7
|
+
export { SubCanvasBorder } from './border';
|
|
8
|
+
export { SubCanvasRender } from './render';
|
|
9
|
+
export { SubCanvasTips } from './tips';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { computed, type CSSProperties, type VNode } from 'vue';
|
|
8
|
+
|
|
9
|
+
import { SubCanvasTips } from '../tips';
|
|
10
|
+
import { SubCanvasBorder } from '../border';
|
|
11
|
+
import { SubCanvasBackground } from '../background';
|
|
12
|
+
import { useNodeSize, useSyncNodeRenderSize } from '../../hooks';
|
|
13
|
+
|
|
14
|
+
defineProps<{
|
|
15
|
+
offsetY?: number;
|
|
16
|
+
className?: string;
|
|
17
|
+
style?: CSSProperties;
|
|
18
|
+
tipText?: string | VNode;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const nodeSize = useNodeSize();
|
|
22
|
+
const nodeHeight = computed(() => nodeSize?.height ?? 0);
|
|
23
|
+
|
|
24
|
+
useSyncNodeRenderSize(nodeSize);
|
|
25
|
+
|
|
26
|
+
const handleDragStart = (e: DragEvent) => {
|
|
27
|
+
e.stopPropagation();
|
|
28
|
+
};
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div
|
|
33
|
+
class="sub-canvas-render"
|
|
34
|
+
:class="className"
|
|
35
|
+
:style="{
|
|
36
|
+
height: `${nodeHeight + (offsetY ?? 0)}px`,
|
|
37
|
+
...style,
|
|
38
|
+
}"
|
|
39
|
+
data-flow-editor-selectable="true"
|
|
40
|
+
@dragstart="handleDragStart"
|
|
41
|
+
>
|
|
42
|
+
<SubCanvasBorder>
|
|
43
|
+
<SubCanvasBackground />
|
|
44
|
+
<SubCanvasTips :tip-text="tipText" />
|
|
45
|
+
</SubCanvasBorder>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<style scoped>
|
|
50
|
+
.sub-canvas-render {
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* eslint-disable @typescript-eslint/naming-convention -- no need */
|
|
7
|
+
|
|
8
|
+
const STORAGE_KEY = 'workflow-move-into-sub-canvas-tip-visible';
|
|
9
|
+
const STORAGE_VALUE = 'false';
|
|
10
|
+
|
|
11
|
+
export class TipsGlobalStore {
|
|
12
|
+
private static _instance?: TipsGlobalStore;
|
|
13
|
+
|
|
14
|
+
public static get instance(): TipsGlobalStore {
|
|
15
|
+
if (!this._instance) {
|
|
16
|
+
this._instance = new TipsGlobalStore();
|
|
17
|
+
}
|
|
18
|
+
return this._instance;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private closed = false;
|
|
22
|
+
|
|
23
|
+
public isClosed(): boolean {
|
|
24
|
+
return this.isCloseForever() || this.closed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public close(): void {
|
|
28
|
+
this.closed = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public isCloseForever(): boolean {
|
|
32
|
+
return localStorage.getItem(STORAGE_KEY) === STORAGE_VALUE;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public closeForever(): void {
|
|
36
|
+
localStorage.setItem(STORAGE_KEY, STORAGE_VALUE);
|
|
37
|
+
}
|
|
38
|
+
}
|