@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.
- package/README.md +50 -0
- package/package.json +44 -0
- package/src/BlueprintCanvasContext.ts +71 -0
- package/src/BlueprintNodeConfigSidebar.vue +338 -0
- package/src/blueprint.css +327 -0
- package/src/blueprintNodeTypes.ts +20 -0
- package/src/components/BluePrintVueRoot.vue +73 -0
- package/src/components/BlueprintCanvas.vue +220 -0
- package/src/components/BlueprintContextMenu.vue +114 -0
- package/src/components/BlueprintExecutionLogPanel.vue +294 -0
- package/src/components/BlueprintMetaDialog.vue +80 -0
- package/src/components/BlueprintNodeSwitchTaskDialog.vue +41 -0
- package/src/components/ClockNodeConfigPanel.vue +124 -0
- package/src/components/FetchNodeConfigPanel.vue +559 -0
- package/src/components/FetchUrlAutocomplete.vue +174 -0
- package/src/components/JsonNodeConfigPanel.vue +73 -0
- package/src/components/LogicNodeConfigPanel.vue +73 -0
- package/src/components/ViewElementMultiSelect.vue +50 -0
- package/src/composables/useBlueprintDebugSession.ts +441 -0
- package/src/composables/useBlueprintFlowState.ts +486 -0
- package/src/composables/useBlueprintFlowViewport.ts +65 -0
- package/src/composables/useBlueprintNodeSelectionGuard.ts +41 -0
- package/src/composables/useBlueprintPageLifecycle.ts +244 -0
- package/src/createBlueprintEdgeTypes.ts +10 -0
- package/src/edges/BlueprintSmoothEdge.vue +31 -0
- package/src/env.d.ts +7 -0
- package/src/fetch-config-task-store.ts +206 -0
- package/src/flowCoordinates.ts +19 -0
- package/src/flowDefaults.ts +9 -0
- package/src/graph/blueprint-graph.ts +265 -0
- package/src/graph/document.ts +422 -0
- package/src/graph/index.ts +7 -0
- package/src/graph/node-summary.ts +88 -0
- package/src/graph/node-types.ts +9 -0
- package/src/graph/sync-edges.ts +69 -0
- package/src/graph/sync-nodes.ts +110 -0
- package/src/graph/vue-flow-adapter.ts +127 -0
- package/src/index.ts +37 -0
- package/src/library/blueprint-io.ts +108 -0
- package/src/library/blueprint-library-db.ts +112 -0
- package/src/library/execution-log-db.ts +171 -0
- package/src/library/execution-log-settings.ts +50 -0
- package/src/library/swagger-docs.ts +56 -0
- package/src/library/types.ts +35 -0
- package/src/nodes/AndFlowNode.vue +60 -0
- package/src/nodes/BlueprintFlowNode.vue +26 -0
- package/src/nodes/BlueprintNodeCard.vue +155 -0
- package/src/nodes/BlueprintNodeShell.vue +70 -0
- package/src/nodes/ClockFlowNode.vue +60 -0
- package/src/nodes/FetchFlowNode.vue +26 -0
- package/src/nodes/JsonFlowNode.vue +26 -0
- package/src/nodes/LifecycleFlowNode.vue +45 -0
- package/src/nodes/LogicFlowNode.vue +26 -0
- package/src/runtime/document-to-runnable-graph.ts +51 -0
- package/src/runtime/execution-overlay.ts +169 -0
- package/src/types.ts +1 -0
- 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>
|