@arronqzy/vue-blueprint 0.1.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.
Files changed (57) hide show
  1. package/README.md +50 -0
  2. package/package.json +44 -0
  3. package/src/BlueprintCanvasContext.ts +71 -0
  4. package/src/BlueprintNodeConfigSidebar.vue +338 -0
  5. package/src/blueprint.css +327 -0
  6. package/src/blueprintNodeTypes.ts +20 -0
  7. package/src/components/BluePrintVueRoot.vue +73 -0
  8. package/src/components/BlueprintCanvas.vue +220 -0
  9. package/src/components/BlueprintContextMenu.vue +114 -0
  10. package/src/components/BlueprintExecutionLogPanel.vue +294 -0
  11. package/src/components/BlueprintMetaDialog.vue +80 -0
  12. package/src/components/BlueprintNodeSwitchTaskDialog.vue +41 -0
  13. package/src/components/ClockNodeConfigPanel.vue +124 -0
  14. package/src/components/FetchNodeConfigPanel.vue +559 -0
  15. package/src/components/FetchUrlAutocomplete.vue +174 -0
  16. package/src/components/JsonNodeConfigPanel.vue +73 -0
  17. package/src/components/LogicNodeConfigPanel.vue +73 -0
  18. package/src/components/ViewElementMultiSelect.vue +50 -0
  19. package/src/composables/useBlueprintDebugSession.ts +441 -0
  20. package/src/composables/useBlueprintFlowState.ts +486 -0
  21. package/src/composables/useBlueprintFlowViewport.ts +65 -0
  22. package/src/composables/useBlueprintNodeSelectionGuard.ts +41 -0
  23. package/src/composables/useBlueprintPageLifecycle.ts +244 -0
  24. package/src/createBlueprintEdgeTypes.ts +10 -0
  25. package/src/edges/BlueprintSmoothEdge.vue +31 -0
  26. package/src/env.d.ts +7 -0
  27. package/src/fetch-config-task-store.ts +206 -0
  28. package/src/flowCoordinates.ts +19 -0
  29. package/src/flowDefaults.ts +9 -0
  30. package/src/graph/blueprint-graph.ts +265 -0
  31. package/src/graph/document.ts +422 -0
  32. package/src/graph/index.ts +7 -0
  33. package/src/graph/node-summary.ts +88 -0
  34. package/src/graph/node-types.ts +9 -0
  35. package/src/graph/sync-edges.ts +69 -0
  36. package/src/graph/sync-nodes.ts +110 -0
  37. package/src/graph/vue-flow-adapter.ts +127 -0
  38. package/src/index.ts +37 -0
  39. package/src/library/blueprint-io.ts +108 -0
  40. package/src/library/blueprint-library-db.ts +112 -0
  41. package/src/library/execution-log-db.ts +171 -0
  42. package/src/library/execution-log-settings.ts +50 -0
  43. package/src/library/swagger-docs.ts +56 -0
  44. package/src/library/types.ts +35 -0
  45. package/src/nodes/AndFlowNode.vue +60 -0
  46. package/src/nodes/BlueprintFlowNode.vue +26 -0
  47. package/src/nodes/BlueprintNodeCard.vue +155 -0
  48. package/src/nodes/BlueprintNodeShell.vue +70 -0
  49. package/src/nodes/ClockFlowNode.vue +60 -0
  50. package/src/nodes/FetchFlowNode.vue +26 -0
  51. package/src/nodes/JsonFlowNode.vue +26 -0
  52. package/src/nodes/LifecycleFlowNode.vue +45 -0
  53. package/src/nodes/LogicFlowNode.vue +26 -0
  54. package/src/runtime/document-to-runnable-graph.ts +51 -0
  55. package/src/runtime/execution-overlay.ts +169 -0
  56. package/src/types.ts +1 -0
  57. package/src/utils/cn.ts +3 -0
@@ -0,0 +1,244 @@
1
+ import { onUnmounted, ref, shallowRef, toValue, watch, type MaybeRefOrGetter, type Ref } from "vue";
2
+ import {
3
+ BlueprintGraphRunner,
4
+ detectBlueprintReferenceCycle,
5
+ type LibraryBlueprintResolver,
6
+ type PageLifecyclePhase,
7
+ } from "@arronqzy/blueprint-dsl";
8
+
9
+ import type { BlueprintGraph } from "../graph/blueprint-graph";
10
+ import { documentToRunnableGraph } from "../runtime/document-to-runnable-graph";
11
+
12
+ const PAGE_BOOT_PHASES: PageLifecyclePhase[] = [
13
+ "created",
14
+ "beforeMount",
15
+ "mounted",
16
+ ];
17
+
18
+ const PAGE_TEARDOWN_PHASES: PageLifecyclePhase[] = [
19
+ "beforeDestroy",
20
+ "destroy",
21
+ ];
22
+
23
+ async function waitForPagePaint() {
24
+ if (document.readyState !== "complete") {
25
+ await new Promise<void>((resolve) => {
26
+ window.addEventListener("load", () => resolve(), { once: true });
27
+ });
28
+ }
29
+ await new Promise<void>((resolve) => {
30
+ requestAnimationFrame(() => {
31
+ requestAnimationFrame(() => resolve());
32
+ });
33
+ });
34
+ }
35
+
36
+ export type UseBlueprintPageLifecycleOptions = {
37
+ graph: Ref<BlueprintGraph> | BlueprintGraph;
38
+ active?: Ref<boolean> | boolean;
39
+ enabled?: Ref<boolean> | boolean;
40
+ bootPhases?: PageLifecyclePhase[];
41
+ bootKey?: Ref<unknown> | unknown;
42
+ waitForPageReady?: boolean;
43
+ onUpdated?: Ref<unknown> | unknown;
44
+ resolveLibraryBlueprint?: LibraryBlueprintResolver;
45
+ libraryNameById?: MaybeRefOrGetter<ReadonlyMap<string, string>>;
46
+ rootLibraryBlueprintId?: MaybeRefOrGetter<string | null>;
47
+ onExecutionBlocked?: (message: string) => void;
48
+ onViewScopeUpdate?: (viewElementIds: string[], scope: unknown) => void;
49
+ };
50
+
51
+ type RunnerOptions = Pick<
52
+ UseBlueprintPageLifecycleOptions,
53
+ | "resolveLibraryBlueprint"
54
+ | "libraryNameById"
55
+ | "rootLibraryBlueprintId"
56
+ | "onExecutionBlocked"
57
+ | "onViewScopeUpdate"
58
+ >;
59
+
60
+ function resolveValue<T>(value: Ref<T> | T): T {
61
+ return value && typeof value === "object" && "value" in value
62
+ ? (value as Ref<T>).value
63
+ : (value as T);
64
+ }
65
+
66
+ function resolveBlueprintName(
67
+ libraryNameById: ReadonlyMap<string, string> | undefined,
68
+ id: string
69
+ ) {
70
+ return libraryNameById?.get(id) ?? id;
71
+ }
72
+
73
+ function createRunner(graph: BlueprintGraph, options: RunnerOptions) {
74
+ const rootLibraryBlueprintId = toValue(options.rootLibraryBlueprintId ?? null);
75
+ const libraryNameById = toValue(options.libraryNameById);
76
+ return new BlueprintGraphRunner(
77
+ documentToRunnableGraph(graph.document, {
78
+ libraryNameById,
79
+ }),
80
+ {
81
+ resolveLibraryBlueprint: options.resolveLibraryBlueprint,
82
+ rootLibraryBlueprintId,
83
+ resolveBlueprintName: (id) =>
84
+ resolveBlueprintName(libraryNameById, id),
85
+ onViewScopeUpdate: options.onViewScopeUpdate,
86
+ }
87
+ );
88
+ }
89
+
90
+ async function emitPhases(
91
+ graph: BlueprintGraph,
92
+ phases: PageLifecyclePhase[],
93
+ options: RunnerOptions
94
+ ) {
95
+ if (!phases.length) return;
96
+
97
+ const rootLibraryBlueprintId = toValue(options.rootLibraryBlueprintId ?? null);
98
+ const libraryNameById = toValue(options.libraryNameById);
99
+
100
+ if (options.resolveLibraryBlueprint) {
101
+ const result = await detectBlueprintReferenceCycle({
102
+ rootGraph: documentToRunnableGraph(graph.document, {
103
+ libraryNameById,
104
+ }),
105
+ rootLibraryBlueprintId,
106
+ resolveLibraryBlueprint: options.resolveLibraryBlueprint,
107
+ resolveBlueprintName: (id) =>
108
+ resolveBlueprintName(libraryNameById, id),
109
+ });
110
+ if (!result.ok) {
111
+ options.onExecutionBlocked?.(result.message);
112
+ return;
113
+ }
114
+ }
115
+
116
+ const runner = createRunner(graph, options);
117
+ for (const phase of phases) {
118
+ await runner.emitLifecycle(phase);
119
+ }
120
+ }
121
+
122
+ export function useBlueprintPageLifecycle(options: UseBlueprintPageLifecycleOptions) {
123
+ const graphRef = shallowRef(resolveValue(options.graph));
124
+ const runnerOptionsRef = shallowRef<RunnerOptions>({
125
+ resolveLibraryBlueprint: options.resolveLibraryBlueprint,
126
+ libraryNameById: options.libraryNameById,
127
+ rootLibraryBlueprintId: options.rootLibraryBlueprintId,
128
+ onExecutionBlocked: options.onExecutionBlocked,
129
+ onViewScopeUpdate: options.onViewScopeUpdate,
130
+ });
131
+
132
+ const bootPhases = options.bootPhases ?? PAGE_BOOT_PHASES;
133
+ const bootPhasesKey = bootPhases.join("|");
134
+ const bootTrigger = options.bootKey ?? "__initial__";
135
+ const bootCompleted = ref(false);
136
+ const hadBoot = ref(false);
137
+ const prevActive = ref<boolean | undefined>(undefined);
138
+ let bootCancelled = false;
139
+
140
+ watch(
141
+ () => resolveValue(options.graph),
142
+ (next) => {
143
+ graphRef.value = next;
144
+ },
145
+ { deep: true }
146
+ );
147
+
148
+ watch(
149
+ () => ({
150
+ resolveLibraryBlueprint: options.resolveLibraryBlueprint,
151
+ libraryNameById: options.libraryNameById,
152
+ rootLibraryBlueprintId: options.rootLibraryBlueprintId,
153
+ onExecutionBlocked: options.onExecutionBlocked,
154
+ onViewScopeUpdate: options.onViewScopeUpdate,
155
+ }),
156
+ (next) => {
157
+ runnerOptionsRef.value = next;
158
+ },
159
+ { deep: true }
160
+ );
161
+
162
+ const runBootCycle = async () => {
163
+ const enabled = resolveValue(options.enabled ?? true);
164
+ if (!enabled) {
165
+ bootCompleted.value = false;
166
+ return;
167
+ }
168
+
169
+ bootCancelled = false;
170
+ const waitForPageReady = options.waitForPageReady ?? false;
171
+
172
+ if (waitForPageReady) {
173
+ await waitForPagePaint();
174
+ }
175
+ if (bootCancelled) return;
176
+
177
+ await emitPhases(graphRef.value, bootPhases, runnerOptionsRef.value);
178
+ if (bootCancelled) return;
179
+
180
+ const active = resolveValue(options.active ?? true);
181
+ if (active) {
182
+ await emitPhases(graphRef.value, ["activated"], runnerOptionsRef.value);
183
+ }
184
+ if (bootCancelled) return;
185
+
186
+ hadBoot.value = true;
187
+ prevActive.value = active;
188
+ bootCompleted.value = true;
189
+ };
190
+
191
+ const teardownBoot = () => {
192
+ bootCancelled = true;
193
+ bootCompleted.value = false;
194
+ prevActive.value = undefined;
195
+ if (hadBoot.value) {
196
+ hadBoot.value = false;
197
+ void emitPhases(graphRef.value, PAGE_TEARDOWN_PHASES, runnerOptionsRef.value);
198
+ }
199
+ };
200
+
201
+ watch(
202
+ () => [
203
+ bootPhasesKey,
204
+ resolveValue(bootTrigger),
205
+ resolveValue(options.enabled ?? true),
206
+ options.waitForPageReady ?? false,
207
+ ],
208
+ () => {
209
+ teardownBoot();
210
+ void runBootCycle();
211
+ },
212
+ { immediate: true }
213
+ );
214
+
215
+ watch(
216
+ () => [resolveValue(options.active ?? true), bootCompleted.value, resolveValue(options.enabled ?? true)],
217
+ ([active, completed, enabled]) => {
218
+ if (!enabled || !completed) return;
219
+ const prev = prevActive.value;
220
+ if (prev === undefined || prev === active) {
221
+ prevActive.value = active;
222
+ return;
223
+ }
224
+ prevActive.value = active;
225
+ void emitPhases(
226
+ graphRef.value,
227
+ [active ? "activated" : "deactivated"],
228
+ runnerOptionsRef.value
229
+ );
230
+ }
231
+ );
232
+
233
+ watch(
234
+ () => [bootCompleted.value, resolveValue(options.enabled ?? true), resolveValue(options.onUpdated)],
235
+ ([completed, enabled]) => {
236
+ if (!enabled || !completed) return;
237
+ void emitPhases(graphRef.value, ["updated"], runnerOptionsRef.value);
238
+ }
239
+ );
240
+
241
+ onUnmounted(() => {
242
+ teardownBoot();
243
+ });
244
+ }
@@ -0,0 +1,10 @@
1
+ import { markRaw, type Component } from "vue";
2
+
3
+ import BlueprintSmoothEdge from "./edges/BlueprintSmoothEdge.vue";
4
+ import { BP_FLOW_EDGE_TYPE } from "./graph/sync-edges";
5
+
6
+ export function createBlueprintEdgeTypes(): Record<string, Component> {
7
+ return {
8
+ [BP_FLOW_EDGE_TYPE]: markRaw(BlueprintSmoothEdge),
9
+ };
10
+ }
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ import { BaseEdge, getSmoothStepPath, type EdgeProps } from "@vue-flow/core";
3
+ import { computed } from "vue";
4
+
5
+ const EDGE_STROKE = "#2563eb";
6
+ const EDGE_WIDTH = 2.5;
7
+
8
+ const props = defineProps<EdgeProps>();
9
+
10
+ const path = computed(() => {
11
+ const [edgePath] = getSmoothStepPath({
12
+ sourceX: props.sourceX,
13
+ sourceY: props.sourceY,
14
+ sourcePosition: props.sourcePosition,
15
+ targetX: props.targetX,
16
+ targetY: props.targetY,
17
+ targetPosition: props.targetPosition,
18
+ });
19
+ return edgePath;
20
+ });
21
+
22
+ const edgeStyle = computed(() => ({
23
+ stroke: EDGE_STROKE,
24
+ strokeWidth: EDGE_WIDTH,
25
+ ...(props.style as Record<string, unknown> | undefined),
26
+ }));
27
+ </script>
28
+
29
+ <template>
30
+ <BaseEdge :path="path" class="bp-edge-visible" :style="edgeStyle" />
31
+ </template>
package/src/env.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module "*.vue" {
4
+ import type { DefineComponent } from "vue";
5
+ const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
6
+ export default component;
7
+ }
@@ -0,0 +1,206 @@
1
+ import { onUnmounted, ref, type Ref } from "vue";
2
+ import type {
3
+ FetchRequestConfig,
4
+ FetchResultValue,
5
+ ParsedSwaggerDocument,
6
+ } from "@arronqzy/blueprint-dsl";
7
+ import { executeFetch } from "@arronqzy/blueprint-dsl";
8
+
9
+ import { loadSwaggerDocument } from "./library/swagger-docs";
10
+
11
+ export type SwaggerLoadTaskStatus = "idle" | "loading" | "error";
12
+
13
+ export type SwaggerLoadTaskRecord = {
14
+ status: SwaggerLoadTaskStatus;
15
+ error?: string | null;
16
+ };
17
+
18
+ type TaskEntry = {
19
+ record: SwaggerLoadTaskRecord;
20
+ abortController: AbortController;
21
+ };
22
+
23
+ const IDLE_SWAGGER_LOAD_TASK: SwaggerLoadTaskRecord = { status: "idle" };
24
+ const LOADING_SWAGGER_LOAD_TASK: SwaggerLoadTaskRecord = { status: "loading" };
25
+
26
+ const swaggerTasks = new Map<string, TaskEntry>();
27
+ const fetchDebugTasks = new Map<string, FetchDebugTaskEntry>();
28
+ const listeners = new Set<() => void>();
29
+
30
+ export type FetchDebugTaskStatus = "idle" | "loading" | "success" | "error";
31
+
32
+ export type FetchDebugTaskRecord = {
33
+ status: FetchDebugTaskStatus;
34
+ result?: FetchResultValue;
35
+ error?: string | null;
36
+ };
37
+
38
+ type FetchDebugTaskEntry = {
39
+ record: FetchDebugTaskRecord;
40
+ abortController: AbortController;
41
+ };
42
+
43
+ const IDLE_FETCH_DEBUG_TASK: FetchDebugTaskRecord = { status: "idle" };
44
+ const LOADING_FETCH_DEBUG_TASK: FetchDebugTaskRecord = { status: "loading" };
45
+
46
+ function emit() {
47
+ listeners.forEach((listener) => listener());
48
+ }
49
+
50
+ export function subscribeFetchConfigTasks(listener: () => void) {
51
+ listeners.add(listener);
52
+ return () => listeners.delete(listener);
53
+ }
54
+
55
+ export function getSwaggerLoadTask(nodeId: string): SwaggerLoadTaskRecord {
56
+ return swaggerTasks.get(nodeId)?.record ?? IDLE_SWAGGER_LOAD_TASK;
57
+ }
58
+
59
+ export function getFetchDebugTask(nodeId: string): FetchDebugTaskRecord {
60
+ return fetchDebugTasks.get(nodeId)?.record ?? IDLE_FETCH_DEBUG_TASK;
61
+ }
62
+
63
+ export function findActiveSwaggerLoadTaskNodeId(): string | null {
64
+ for (const [nodeId, task] of swaggerTasks) {
65
+ if (task.record.status === "loading") return nodeId;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ export function findActiveFetchDebugTaskNodeId(): string | null {
71
+ for (const [nodeId, task] of fetchDebugTasks) {
72
+ if (task.record.status === "loading") return nodeId;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ export function findActiveFetchConfigTaskNodeId(): string | null {
78
+ return findActiveSwaggerLoadTaskNodeId() ?? findActiveFetchDebugTaskNodeId();
79
+ }
80
+
81
+ export function isSwaggerLoadTaskActive(nodeId: string): boolean {
82
+ return getSwaggerLoadTask(nodeId).status === "loading";
83
+ }
84
+
85
+ export function isFetchDebugTaskActive(nodeId: string): boolean {
86
+ return getFetchDebugTask(nodeId).status === "loading";
87
+ }
88
+
89
+ export function cancelSwaggerLoadTask(nodeId: string) {
90
+ const task = swaggerTasks.get(nodeId);
91
+ if (!task) return;
92
+ task.abortController.abort();
93
+ swaggerTasks.delete(nodeId);
94
+ emit();
95
+ }
96
+
97
+ export function cancelFetchDebugTask(nodeId: string) {
98
+ const task = fetchDebugTasks.get(nodeId);
99
+ if (!task) return;
100
+ task.abortController.abort();
101
+ fetchDebugTasks.delete(nodeId);
102
+ emit();
103
+ }
104
+
105
+ function useFetchConfigTaskSnapshot<T>(
106
+ getSnapshot: () => T
107
+ ): Ref<T> {
108
+ const snapshot = ref(getSnapshot()) as Ref<T>;
109
+ const update = () => {
110
+ snapshot.value = getSnapshot();
111
+ };
112
+ const unsubscribe = subscribeFetchConfigTasks(update);
113
+ onUnmounted(unsubscribe);
114
+ return snapshot;
115
+ }
116
+
117
+ export function useSwaggerLoadTask(nodeId: string) {
118
+ return useFetchConfigTaskSnapshot(() => getSwaggerLoadTask(nodeId));
119
+ }
120
+
121
+ export function useFetchDebugTask(nodeId: string) {
122
+ return useFetchConfigTaskSnapshot(() => getFetchDebugTask(nodeId));
123
+ }
124
+
125
+ export function startSwaggerLoadTask(params: {
126
+ nodeId: string;
127
+ docsUrl: string;
128
+ onSuccess: (parsed: ParsedSwaggerDocument, docsUrl: string) => void;
129
+ }) {
130
+ cancelSwaggerLoadTask(params.nodeId);
131
+
132
+ const abortController = new AbortController();
133
+ swaggerTasks.set(params.nodeId, {
134
+ record: LOADING_SWAGGER_LOAD_TASK,
135
+ abortController,
136
+ });
137
+ emit();
138
+
139
+ void (async () => {
140
+ try {
141
+ const parsed = await loadSwaggerDocument(params.docsUrl, abortController.signal);
142
+ if (abortController.signal.aborted) return;
143
+
144
+ swaggerTasks.delete(params.nodeId);
145
+ emit();
146
+ params.onSuccess(parsed, params.docsUrl);
147
+ } catch (error) {
148
+ if (abortController.signal.aborted) {
149
+ swaggerTasks.delete(params.nodeId);
150
+ emit();
151
+ return;
152
+ }
153
+
154
+ const message =
155
+ error instanceof Error ? error.message : "Swagger 文档解析失败";
156
+ swaggerTasks.set(params.nodeId, {
157
+ record: { status: "error", error: message },
158
+ abortController,
159
+ });
160
+ emit();
161
+ }
162
+ })();
163
+ }
164
+
165
+ export function startFetchDebugTask(params: {
166
+ nodeId: string;
167
+ config: FetchRequestConfig;
168
+ }) {
169
+ cancelFetchDebugTask(params.nodeId);
170
+
171
+ const abortController = new AbortController();
172
+ fetchDebugTasks.set(params.nodeId, {
173
+ record: LOADING_FETCH_DEBUG_TASK,
174
+ abortController,
175
+ });
176
+ emit();
177
+
178
+ void (async () => {
179
+ try {
180
+ const result = await executeFetch(params.config, {
181
+ signal: abortController.signal,
182
+ allowHttpError: true,
183
+ });
184
+ if (abortController.signal.aborted) return;
185
+
186
+ fetchDebugTasks.set(params.nodeId, {
187
+ record: { status: "success", result },
188
+ abortController,
189
+ });
190
+ emit();
191
+ } catch (error) {
192
+ if (abortController.signal.aborted) {
193
+ fetchDebugTasks.delete(params.nodeId);
194
+ emit();
195
+ return;
196
+ }
197
+
198
+ const message = error instanceof Error ? error.message : "请求失败";
199
+ fetchDebugTasks.set(params.nodeId, {
200
+ record: { status: "error", error: message },
201
+ abortController,
202
+ });
203
+ emit();
204
+ }
205
+ })();
206
+ }
@@ -0,0 +1,19 @@
1
+ import type { XYPosition } from "@vue-flow/core";
2
+
3
+ import {
4
+ BP_FLOW_NODE_HEIGHT,
5
+ BP_FLOW_NODE_WIDTH,
6
+ } from "./graph/vue-flow-adapter";
7
+
8
+ /** 屏幕坐标 → 流程坐标,落点以节点中心对齐光标 */
9
+ export function clientToFlowNodePosition(
10
+ screenToFlowPosition: (position: XYPosition) => XYPosition,
11
+ clientX: number,
12
+ clientY: number
13
+ ): XYPosition {
14
+ const flow = screenToFlowPosition({ x: clientX, y: clientY });
15
+ return {
16
+ x: flow.x - BP_FLOW_NODE_WIDTH / 2,
17
+ y: flow.y - BP_FLOW_NODE_HEIGHT / 2,
18
+ };
19
+ }
@@ -0,0 +1,9 @@
1
+ import type { DefaultEdgeOptions } from "@vue-flow/core";
2
+
3
+ import { BP_EDGE_STYLE, BP_FLOW_EDGE_TYPE } from "./graph/sync-edges";
4
+
5
+ export const BLUEPRINT_DEFAULT_EDGE_OPTIONS = {
6
+ type: BP_FLOW_EDGE_TYPE,
7
+ zIndex: 1000,
8
+ style: { ...BP_EDGE_STYLE },
9
+ } satisfies DefaultEdgeOptions;