@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,171 @@
1
+ import type { ExecutionRunRecord } from "@arronqzy/blueprint-dsl";
2
+
3
+ const DB_NAME = "arronqzy-blueprint-execution-log";
4
+ const DB_VERSION = 1;
5
+ const STORE_NAME = "runs";
6
+
7
+ function openDb(): Promise<IDBDatabase> {
8
+ return new Promise((resolve, reject) => {
9
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
10
+ request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
11
+ request.onupgradeneeded = () => {
12
+ const db = request.result;
13
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
14
+ const store = db.createObjectStore(STORE_NAME, { keyPath: "runId" });
15
+ store.createIndex("startedAt", "startedAt", { unique: false });
16
+ store.createIndex("blueprintId", "blueprintId", { unique: false });
17
+ }
18
+ };
19
+ request.onsuccess = () => resolve(request.result);
20
+ });
21
+ }
22
+
23
+ function runTransaction<T>(
24
+ mode: IDBTransactionMode,
25
+ runner: (store: IDBObjectStore) => IDBRequest<T>
26
+ ): Promise<T> {
27
+ return openDb().then(
28
+ (db) =>
29
+ new Promise<T>((resolve, reject) => {
30
+ const tx = db.transaction(STORE_NAME, mode);
31
+ const store = tx.objectStore(STORE_NAME);
32
+ const request = runner(store);
33
+ let result!: T;
34
+
35
+ request.onerror = () =>
36
+ reject(request.error ?? new Error("indexeddb-request-failed"));
37
+ request.onsuccess = () => {
38
+ result = request.result as T;
39
+ };
40
+
41
+ tx.oncomplete = () => {
42
+ db.close();
43
+ resolve(result);
44
+ };
45
+ tx.onerror = () => {
46
+ db.close();
47
+ reject(tx.error ?? new Error("indexeddb-transaction-failed"));
48
+ };
49
+ })
50
+ );
51
+ }
52
+
53
+ export async function putExecutionRunRecord(
54
+ record: ExecutionRunRecord
55
+ ): Promise<ExecutionRunRecord> {
56
+ await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record));
57
+ return record;
58
+ }
59
+
60
+ export async function getExecutionRunRecord(
61
+ runId: string
62
+ ): Promise<ExecutionRunRecord | null> {
63
+ const record = await runTransaction<ExecutionRunRecord | undefined>(
64
+ "readonly",
65
+ (store) => store.get(runId)
66
+ );
67
+ return record ?? null;
68
+ }
69
+
70
+ export async function listExecutionRunRecords(
71
+ blueprintId?: string | null
72
+ ): Promise<ExecutionRunRecord[]> {
73
+ const records = await runTransaction<ExecutionRunRecord[]>("readonly", (store) =>
74
+ store.getAll()
75
+ );
76
+ const filtered = blueprintId
77
+ ? records.filter((item) => item.blueprintId === blueprintId)
78
+ : records;
79
+ return filtered.sort((a, b) => b.startedAt - a.startedAt);
80
+ }
81
+
82
+ export async function deleteExecutionRunRecord(runId: string): Promise<void> {
83
+ await runTransaction<undefined>("readwrite", (store) => store.delete(runId));
84
+ }
85
+
86
+ export async function clearAllExecutionRunRecords(): Promise<number> {
87
+ const records = await runTransaction<ExecutionRunRecord[]>("readonly", (store) =>
88
+ store.getAll()
89
+ );
90
+ if (records.length === 0) return 0;
91
+
92
+ await runTransaction<undefined>("readwrite", (store) => store.clear());
93
+ return records.length;
94
+ }
95
+
96
+ export async function trimExecutionRunRecordsToMax(
97
+ maxCount: number
98
+ ): Promise<number> {
99
+ if (!Number.isFinite(maxCount) || maxCount < 1) return 0;
100
+
101
+ const records = await runTransaction<ExecutionRunRecord[]>("readonly", (store) =>
102
+ store.getAll()
103
+ );
104
+ if (records.length <= maxCount) return 0;
105
+
106
+ const sorted = [...records].sort((a, b) => b.startedAt - a.startedAt);
107
+ const overflow = sorted.slice(maxCount);
108
+ if (overflow.length === 0) return 0;
109
+
110
+ await openDb().then(
111
+ (db) =>
112
+ new Promise<void>((resolve, reject) => {
113
+ const tx = db.transaction(STORE_NAME, "readwrite");
114
+ const store = tx.objectStore(STORE_NAME);
115
+ for (const item of overflow) {
116
+ store.delete(item.runId);
117
+ }
118
+ tx.oncomplete = () => {
119
+ db.close();
120
+ resolve();
121
+ };
122
+ tx.onerror = () => {
123
+ db.close();
124
+ reject(tx.error ?? new Error("indexeddb-transaction-failed"));
125
+ };
126
+ })
127
+ );
128
+
129
+ return overflow.length;
130
+ }
131
+
132
+ export async function purgeExecutionRunsOlderThan(cutoffMs: number): Promise<number> {
133
+ const records = await runTransaction<ExecutionRunRecord[]>("readonly", (store) =>
134
+ store.getAll()
135
+ );
136
+ const stale = records.filter((item) => item.startedAt < cutoffMs);
137
+ if (stale.length === 0) return 0;
138
+
139
+ await openDb().then(
140
+ (db) =>
141
+ new Promise<void>((resolve, reject) => {
142
+ const tx = db.transaction(STORE_NAME, "readwrite");
143
+ const store = tx.objectStore(STORE_NAME);
144
+ for (const item of stale) {
145
+ store.delete(item.runId);
146
+ }
147
+ tx.oncomplete = () => {
148
+ db.close();
149
+ resolve();
150
+ };
151
+ tx.onerror = () => {
152
+ db.close();
153
+ reject(tx.error ?? new Error("indexeddb-transaction-failed"));
154
+ };
155
+ })
156
+ );
157
+
158
+ return stale.length;
159
+ }
160
+
161
+ export function downloadExecutionRunExport(record: ExecutionRunRecord) {
162
+ const blob = new Blob([JSON.stringify(record, null, 2)], {
163
+ type: "application/json",
164
+ });
165
+ const url = URL.createObjectURL(blob);
166
+ const anchor = document.createElement("a");
167
+ anchor.href = url;
168
+ anchor.download = `blueprint-run-${record.runId}.json`;
169
+ anchor.click();
170
+ URL.revokeObjectURL(url);
171
+ }
@@ -0,0 +1,50 @@
1
+ export type ExecutionLogSettings = {
2
+ retentionDays: number;
3
+ autoSave: boolean;
4
+ /** IndexedDB 中最多保留的日志条数 */
5
+ maxSavedRuns: number;
6
+ };
7
+
8
+ const SETTINGS_KEY = "arronqzy-blueprint-execution-log-settings";
9
+
10
+ export const DEFAULT_EXECUTION_LOG_SETTINGS: ExecutionLogSettings = {
11
+ retentionDays: 7,
12
+ autoSave: true,
13
+ maxSavedRuns: 80,
14
+ };
15
+
16
+ export function readExecutionLogSettings(): ExecutionLogSettings {
17
+ try {
18
+ const raw = localStorage.getItem(SETTINGS_KEY);
19
+ if (!raw) return { ...DEFAULT_EXECUTION_LOG_SETTINGS };
20
+ const parsed = JSON.parse(raw) as Partial<ExecutionLogSettings>;
21
+ return {
22
+ retentionDays:
23
+ typeof parsed.retentionDays === "number" && parsed.retentionDays > 0
24
+ ? parsed.retentionDays
25
+ : DEFAULT_EXECUTION_LOG_SETTINGS.retentionDays,
26
+ autoSave:
27
+ typeof parsed.autoSave === "boolean"
28
+ ? parsed.autoSave
29
+ : DEFAULT_EXECUTION_LOG_SETTINGS.autoSave,
30
+ maxSavedRuns:
31
+ typeof parsed.maxSavedRuns === "number" && parsed.maxSavedRuns > 0
32
+ ? Math.floor(parsed.maxSavedRuns)
33
+ : DEFAULT_EXECUTION_LOG_SETTINGS.maxSavedRuns,
34
+ };
35
+ } catch {
36
+ return { ...DEFAULT_EXECUTION_LOG_SETTINGS };
37
+ }
38
+ }
39
+
40
+ export function writeExecutionLogSettings(settings: ExecutionLogSettings) {
41
+ try {
42
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
43
+ } catch {
44
+ // ignore storage errors
45
+ }
46
+ }
47
+
48
+ export function retentionCutoffMs(retentionDays: number) {
49
+ return Date.now() - retentionDays * 24 * 60 * 60 * 1000;
50
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ parseSwaggerDocument,
3
+ type ParsedSwaggerDocument,
4
+ } from "@arronqzy/blueprint-dsl";
5
+
6
+ export async function loadSwaggerDocument(
7
+ docsUrl: string,
8
+ signal?: AbortSignal
9
+ ): Promise<ParsedSwaggerDocument> {
10
+ const trimmed = docsUrl.trim();
11
+ if (!trimmed) {
12
+ throw new Error("请输入 Swagger 文档 URL");
13
+ }
14
+
15
+ if (signal?.aborted) {
16
+ throw new DOMException("Aborted", "AbortError");
17
+ }
18
+
19
+ let response: Response;
20
+ try {
21
+ response = await fetch(trimmed, {
22
+ method: "GET",
23
+ credentials: "same-origin",
24
+ signal,
25
+ });
26
+ } catch (error) {
27
+ if (signal?.aborted || (error instanceof DOMException && error.name === "AbortError")) {
28
+ throw new DOMException("Aborted", "AbortError");
29
+ }
30
+ throw new Error(
31
+ error instanceof Error ? error.message : "无法请求 Swagger 文档"
32
+ );
33
+ }
34
+
35
+ if (signal?.aborted) {
36
+ throw new DOMException("Aborted", "AbortError");
37
+ }
38
+
39
+ if (!response.ok) {
40
+ throw new Error(`Swagger 文档请求失败: HTTP ${response.status}`);
41
+ }
42
+
43
+ const contentType = response.headers.get("content-type") ?? "";
44
+ if (contentType.includes("text/html")) {
45
+ throw new Error("返回内容为 HTML,请填写 OpenAPI/Swagger 的 JSON 地址");
46
+ }
47
+
48
+ let spec: unknown;
49
+ try {
50
+ spec = await response.json();
51
+ } catch {
52
+ throw new Error("Swagger 文档不是有效的 JSON");
53
+ }
54
+
55
+ return parseSwaggerDocument(spec, trimmed);
56
+ }
@@ -0,0 +1,35 @@
1
+ import type { BlueprintDocument } from "../graph/document";
2
+
3
+ export type BlueprintLibrarySource = "saved" | "imported";
4
+
5
+ export type BlueprintLibraryRecord = {
6
+ id: string;
7
+ name: string;
8
+ remark?: string;
9
+ source: BlueprintLibrarySource;
10
+ createdAt: number;
11
+ updatedAt: number;
12
+ document: BlueprintDocument;
13
+ };
14
+
15
+ export type BlueprintLibraryListItem = Pick<
16
+ BlueprintLibraryRecord,
17
+ "id" | "name" | "remark" | "source" | "updatedAt"
18
+ >;
19
+
20
+ export const BLUEPRINT_EXPORT_KIND = "arronqzy-blueprint" as const;
21
+ export const BLUEPRINT_EXPORT_VERSION = 1 as const;
22
+
23
+ export type BlueprintExportPayload = {
24
+ kind: typeof BLUEPRINT_EXPORT_KIND;
25
+ version: typeof BLUEPRINT_EXPORT_VERSION;
26
+ name: string;
27
+ remark?: string;
28
+ exportedAt: number;
29
+ document: BlueprintDocument;
30
+ };
31
+
32
+ export type BlueprintMetaDraft = {
33
+ name: string;
34
+ remark: string;
35
+ };
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ import { Handle, Position, type NodeProps } from "@vue-flow/core";
3
+ import { resolveBlueprintNodeTypeLabel } from "../graph/document";
4
+ import { resolveBlueprintNodeSummary } from "../graph/node-summary";
5
+ import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
6
+ import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
7
+ import type { BlueprintFlowNodeData } from "../types";
8
+ import BlueprintNodeCard from "./BlueprintNodeCard.vue";
9
+ import { cn } from "../utils/cn";
10
+
11
+ const props = defineProps<NodeProps<BlueprintFlowNodeData>>();
12
+ const onSelect = useBlueprintNodeSelect();
13
+ const nodeData = props.data;
14
+ const executionTone = resolveBlueprintNodeExecutionTone(nodeData);
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ :class="
20
+ cn(
21
+ 'bp-node bp-node--and',
22
+ executionTone === 'success' && 'bp-node--execution-true',
23
+ executionTone === 'error' && 'bp-node--execution-false'
24
+ )
25
+ "
26
+ >
27
+ <Handle
28
+ type="target"
29
+ :position="Position.Left"
30
+ id="inA"
31
+ class="bp-flow-handle bp-flow-handle--target"
32
+ :style="{ top: '35%' }"
33
+ title="输入 A(多连线为或)"
34
+ />
35
+ <Handle
36
+ type="target"
37
+ :position="Position.Left"
38
+ id="inB"
39
+ class="bp-flow-handle bp-flow-handle--target"
40
+ :style="{ top: '65%' }"
41
+ title="输入 B(多连线为或)"
42
+ />
43
+ <BlueprintNodeCard
44
+ :node-id="props.id"
45
+ :label="nodeData.label"
46
+ :meta="resolveBlueprintNodeTypeLabel(nodeData)"
47
+ :subtitle="resolveBlueprintNodeSummary(nodeData)"
48
+ variant="and"
49
+ :selected="Boolean(nodeData.isSelected)"
50
+ @select="onSelect"
51
+ />
52
+ <Handle
53
+ type="source"
54
+ :position="Position.Right"
55
+ id="out"
56
+ class="bp-flow-handle bp-flow-handle--source"
57
+ title="两路均为真时输出真信号"
58
+ />
59
+ </div>
60
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import type { NodeProps } from "@vue-flow/core";
3
+ import { resolveBlueprintNodeTypeLabel } from "../graph/document";
4
+ import { resolveBlueprintNodeSummary } from "../graph/node-summary";
5
+ import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
6
+ import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
7
+ import type { BlueprintFlowNodeData } from "../types";
8
+ import BlueprintNodeShell from "./BlueprintNodeShell.vue";
9
+
10
+ const props = defineProps<NodeProps<BlueprintFlowNodeData>>();
11
+ const onSelect = useBlueprintNodeSelect();
12
+ const nodeData = props.data;
13
+ </script>
14
+
15
+ <template>
16
+ <BlueprintNodeShell
17
+ :node-id="props.id"
18
+ :label="nodeData.label"
19
+ :meta="resolveBlueprintNodeTypeLabel(nodeData)"
20
+ :subtitle="resolveBlueprintNodeSummary(nodeData)"
21
+ variant="blueprint"
22
+ :selected="Boolean(nodeData.isSelected)"
23
+ :execution-tone="resolveBlueprintNodeExecutionTone(nodeData)"
24
+ @select="onSelect"
25
+ />
26
+ </template>
@@ -0,0 +1,155 @@
1
+ <script setup lang="ts">
2
+ import { cn } from "../utils/cn";
3
+
4
+ export type BlueprintNodeCardProps = {
5
+ nodeId: string;
6
+ label: string;
7
+ meta?: string;
8
+ subtitle?: string;
9
+ progressLabel?: string;
10
+ variant?: "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "clock";
11
+ selected?: boolean;
12
+ hideLeadingDot?: boolean;
13
+ };
14
+
15
+ const props = withDefaults(defineProps<BlueprintNodeCardProps>(), {
16
+ variant: "blueprint",
17
+ selected: false,
18
+ hideLeadingDot: false,
19
+ });
20
+
21
+ const emit = defineEmits<{
22
+ select: [nodeId: string];
23
+ }>();
24
+
25
+ const variantStyle = {
26
+ blueprint: {
27
+ accent: "border-l-primary",
28
+ badge: "bg-primary/10 text-primary",
29
+ dot: "bg-primary",
30
+ },
31
+ logic: {
32
+ accent: "border-l-sky-500 dark:border-l-sky-400",
33
+ badge: "bg-sky-500/10 text-sky-700 dark:text-sky-300",
34
+ dot: "bg-sky-500",
35
+ },
36
+ and: {
37
+ accent: "border-l-indigo-500 dark:border-l-indigo-400",
38
+ badge: "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300",
39
+ dot: "bg-indigo-500",
40
+ },
41
+ lifecycle: {
42
+ accent: "border-l-amber-500 dark:border-l-amber-400",
43
+ badge: "bg-amber-500/10 text-amber-700 dark:text-amber-300",
44
+ dot: "bg-amber-500",
45
+ },
46
+ fetch: {
47
+ accent: "border-l-violet-500 dark:border-l-violet-400",
48
+ badge: "bg-violet-500/10 text-violet-700 dark:text-violet-300",
49
+ dot: "bg-violet-500",
50
+ },
51
+ json: {
52
+ accent: "border-l-teal-500 dark:border-l-teal-400",
53
+ badge: "bg-teal-500/10 text-teal-700 dark:text-teal-300",
54
+ dot: "bg-teal-500",
55
+ },
56
+ clock: {
57
+ accent: "border-l-rose-500 dark:border-l-rose-400",
58
+ badge: "bg-rose-500/10 text-rose-700 dark:text-rose-300",
59
+ dot: "bg-rose-500",
60
+ },
61
+ } as const;
62
+
63
+ const v = variantStyle[props.variant];
64
+ </script>
65
+
66
+ <template>
67
+ <div
68
+ data-blueprint-node-card
69
+ :class="
70
+ cn(
71
+ 'bp-node-card w-[168px] overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-sm',
72
+ 'transition-[box-shadow,border-color] duration-150',
73
+ hideLeadingDot
74
+ ? 'border-t-[3px] border-t-amber-500 dark:border-t-amber-400'
75
+ : cn('border-l-[3px]', v.accent),
76
+ selected &&
77
+ 'border-primary/50 shadow-[0_0_0_1px_hsl(var(--primary)/0.35),0_4px_12px_hsl(var(--primary)/0.12)]'
78
+ )
79
+ "
80
+ >
81
+ <div
82
+ class="bp-flow-drag-handle flex cursor-grab items-center gap-2 border-b border-border/50 px-2 py-1.5 bg-muted/25 text-muted-foreground active:cursor-grabbing"
83
+ title="拖拽移动"
84
+ >
85
+ <div class="flex shrink-0 flex-col gap-[3px] opacity-35" aria-hidden="true">
86
+ <span class="flex gap-[3px]">
87
+ <span class="h-[3px] w-[3px] rounded-full bg-current" />
88
+ <span class="h-[3px] w-[3px] rounded-full bg-current" />
89
+ </span>
90
+ <span class="flex gap-[3px]">
91
+ <span class="h-[3px] w-[3px] rounded-full bg-current" />
92
+ <span class="h-[3px] w-[3px] rounded-full bg-current" />
93
+ </span>
94
+ </div>
95
+ <span
96
+ v-if="meta"
97
+ :class="
98
+ cn(
99
+ 'min-w-0 flex-1 truncate rounded px-1.5 py-0.5 text-[10px] font-medium leading-none tracking-wide',
100
+ v.badge
101
+ )
102
+ "
103
+ :title="meta"
104
+ >
105
+ {{ meta }}
106
+ </span>
107
+ </div>
108
+
109
+ <button
110
+ type="button"
111
+ :class="
112
+ cn(
113
+ 'nodrag flex w-full text-left outline-none transition-colors hover:bg-accent/30',
114
+ 'focus-visible:ring-1 focus-visible:ring-primary focus-visible:ring-inset',
115
+ hideLeadingDot
116
+ ? cn('px-2.5 py-2', subtitle ? 'items-start' : 'items-center')
117
+ : 'items-start gap-2 px-2.5 py-2'
118
+ )
119
+ "
120
+ @click.stop="emit('select', nodeId)"
121
+ >
122
+ <span
123
+ v-if="!hideLeadingDot"
124
+ :class="cn('mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full', v.dot)"
125
+ aria-hidden="true"
126
+ />
127
+ <div class="min-w-0 flex-1">
128
+ <div class="flex items-start gap-1.5">
129
+ <div class="min-w-0 flex-1 truncate text-[13px] font-medium leading-snug text-foreground">
130
+ {{ label }}
131
+ </div>
132
+ <span
133
+ v-if="progressLabel"
134
+ :class="
135
+ cn(
136
+ 'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold leading-none',
137
+ v.badge
138
+ )
139
+ "
140
+ title="已发送信号次数"
141
+ >
142
+ {{ progressLabel }}
143
+ </span>
144
+ </div>
145
+ <div
146
+ v-if="subtitle"
147
+ class="mt-0.5 truncate font-mono text-[10px] leading-tight text-muted-foreground"
148
+ :title="subtitle"
149
+ >
150
+ {{ subtitle }}
151
+ </div>
152
+ </div>
153
+ </button>
154
+ </div>
155
+ </template>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import { Handle, Position } from "@vue-flow/core";
3
+ import BlueprintNodeCard, { type BlueprintNodeCardProps } from "./BlueprintNodeCard.vue";
4
+ import type { BlueprintNodeExecutionTone } from "../runtime/execution-overlay";
5
+ import { cn } from "../utils/cn";
6
+
7
+ export type BlueprintNodeShellProps = BlueprintNodeCardProps & {
8
+ selected?: boolean;
9
+ executionTone?: BlueprintNodeExecutionTone | null;
10
+ };
11
+
12
+ const props = withDefaults(defineProps<BlueprintNodeShellProps>(), {
13
+ selected: false,
14
+ executionTone: null,
15
+ });
16
+
17
+ const {
18
+ nodeId,
19
+ label,
20
+ meta,
21
+ subtitle,
22
+ progressLabel,
23
+ variant,
24
+ selected,
25
+ hideLeadingDot,
26
+ executionTone,
27
+ } = props;
28
+
29
+ const emit = defineEmits<{
30
+ select: [nodeId: string];
31
+ }>();
32
+ </script>
33
+
34
+ <template>
35
+ <div
36
+ :class="
37
+ cn(
38
+ 'bp-node',
39
+ executionTone === 'success' && 'bp-node--execution-true',
40
+ executionTone === 'error' && 'bp-node--execution-false'
41
+ )
42
+ "
43
+ >
44
+ <Handle
45
+ type="target"
46
+ :position="Position.Left"
47
+ id="in"
48
+ class="bp-flow-handle bp-flow-handle--target"
49
+ title="真/假信号输入"
50
+ />
51
+ <BlueprintNodeCard
52
+ :node-id="nodeId"
53
+ :label="label"
54
+ :meta="meta"
55
+ :subtitle="subtitle"
56
+ :progress-label="progressLabel"
57
+ :variant="variant"
58
+ :selected="selected"
59
+ :hide-leading-dot="hideLeadingDot"
60
+ @select="emit('select', $event)"
61
+ />
62
+ <Handle
63
+ type="source"
64
+ :position="Position.Right"
65
+ id="out"
66
+ class="bp-flow-handle bp-flow-handle--source"
67
+ title="真/假信号输出"
68
+ />
69
+ </div>
70
+ </template>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ import type { NodeProps } from "@vue-flow/core";
3
+ import { computed } from "vue";
4
+ import { resolveBlueprintNodeTypeLabel } from "../graph/document";
5
+ import { resolveBlueprintNodeSummary } from "../graph/node-summary";
6
+ import {
7
+ useBlueprintClockAbort,
8
+ useBlueprintNodeSelect,
9
+ useClockNodeCanAbort,
10
+ } from "../BlueprintCanvasContext";
11
+ import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
12
+ import type { BlueprintFlowNodeData } from "../types";
13
+ import BlueprintNodeShell from "./BlueprintNodeShell.vue";
14
+ import { cn } from "../utils/cn";
15
+
16
+ const props = defineProps<NodeProps<BlueprintFlowNodeData>>();
17
+ const onSelect = useBlueprintNodeSelect();
18
+ const onAbortClock = useBlueprintClockAbort();
19
+ const canAbort = useClockNodeCanAbort(props.id);
20
+ const nodeData = props.data;
21
+
22
+ const progressLabel = computed(() => {
23
+ const progress = nodeData.clockEmitProgress;
24
+ return progress ? `${progress.current}/${progress.total}` : undefined;
25
+ });
26
+ </script>
27
+
28
+ <template>
29
+ <div class="relative">
30
+ <BlueprintNodeShell
31
+ :node-id="props.id"
32
+ :label="nodeData.label"
33
+ :meta="resolveBlueprintNodeTypeLabel(nodeData)"
34
+ :subtitle="resolveBlueprintNodeSummary(nodeData)"
35
+ :progress-label="progressLabel"
36
+ variant="clock"
37
+ :selected="Boolean(nodeData.isSelected)"
38
+ :execution-tone="resolveBlueprintNodeExecutionTone(nodeData)"
39
+ @select="onSelect"
40
+ />
41
+ <button
42
+ v-if="canAbort"
43
+ type="button"
44
+ :class="
45
+ cn(
46
+ 'nodrag nopan absolute right-1.5 top-1.5 z-10 flex h-5 w-5 items-center justify-center',
47
+ 'rounded border border-rose-500/50 bg-rose-500/15 text-rose-600 shadow-sm',
48
+ 'hover:bg-rose-500/25 dark:text-rose-300'
49
+ )
50
+ "
51
+ title="中止时钟:停止剩余次数与正在执行的下游任务"
52
+ @pointerdown.stop
53
+ @click.stop="onAbortClock(props.id)"
54
+ >
55
+ <svg viewBox="0 0 24 24" class="h-3 w-3" aria-hidden="true">
56
+ <rect x="6" y="6" width="12" height="12" rx="1.5" fill="currentColor" />
57
+ </svg>
58
+ </button>
59
+ </div>
60
+ </template>