@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,114 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onUnmounted, ref, watch } from "vue";
|
|
3
|
+
|
|
4
|
+
export type BlueprintContextMenuState =
|
|
5
|
+
| {
|
|
6
|
+
kind: "pane";
|
|
7
|
+
clientX: number;
|
|
8
|
+
clientY: number;
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
kind: "node";
|
|
12
|
+
clientX: number;
|
|
13
|
+
clientY: number;
|
|
14
|
+
nodeId: string;
|
|
15
|
+
role: "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "clock";
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
kind: "edge";
|
|
19
|
+
clientX: number;
|
|
20
|
+
clientY: number;
|
|
21
|
+
edgeId: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{
|
|
25
|
+
menu: BlueprintContextMenuState | null;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
close: [];
|
|
30
|
+
addBlueprintNode: [clientX: number, clientY: number];
|
|
31
|
+
deleteNode: [nodeId: string];
|
|
32
|
+
deleteEdge: [edgeId: string];
|
|
33
|
+
}>();
|
|
34
|
+
|
|
35
|
+
const menuRef = ref<HTMLDivElement | null>(null);
|
|
36
|
+
let cleanupListeners: (() => void) | null = null;
|
|
37
|
+
|
|
38
|
+
function closeOnPointerDown(event: Event) {
|
|
39
|
+
const target = event.target as globalThis.Node | null;
|
|
40
|
+
if (target && menuRef.value?.contains(target)) return;
|
|
41
|
+
emit("close");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
watch(
|
|
45
|
+
() => props.menu,
|
|
46
|
+
(menu) => {
|
|
47
|
+
cleanupListeners?.();
|
|
48
|
+
cleanupListeners = null;
|
|
49
|
+
if (!menu) return;
|
|
50
|
+
|
|
51
|
+
const close = () => emit("close");
|
|
52
|
+
window.addEventListener("pointerdown", closeOnPointerDown);
|
|
53
|
+
window.addEventListener("scroll", close, true);
|
|
54
|
+
window.addEventListener("resize", close);
|
|
55
|
+
cleanupListeners = () => {
|
|
56
|
+
window.removeEventListener("pointerdown", closeOnPointerDown);
|
|
57
|
+
window.removeEventListener("scroll", close, true);
|
|
58
|
+
window.removeEventListener("resize", close);
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
{ immediate: true }
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
onUnmounted(() => {
|
|
65
|
+
cleanupListeners?.();
|
|
66
|
+
});
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div
|
|
71
|
+
v-if="menu"
|
|
72
|
+
ref="menuRef"
|
|
73
|
+
class="bp-context-menu"
|
|
74
|
+
:style="{ left: `${menu.clientX}px`, top: `${menu.clientY}px` }"
|
|
75
|
+
@mousedown.stop
|
|
76
|
+
@contextmenu.prevent
|
|
77
|
+
>
|
|
78
|
+
<button
|
|
79
|
+
v-if="menu.kind === 'pane'"
|
|
80
|
+
type="button"
|
|
81
|
+
class="bp-context-menu__item"
|
|
82
|
+
@click="
|
|
83
|
+
emit('addBlueprintNode', menu.clientX, menu.clientY);
|
|
84
|
+
emit('close');
|
|
85
|
+
"
|
|
86
|
+
>
|
|
87
|
+
新增节点
|
|
88
|
+
</button>
|
|
89
|
+
|
|
90
|
+
<button
|
|
91
|
+
v-else-if="menu.kind === 'node'"
|
|
92
|
+
type="button"
|
|
93
|
+
class="bp-context-menu__item bp-context-menu__item--danger"
|
|
94
|
+
@click="
|
|
95
|
+
emit('deleteNode', menu.nodeId);
|
|
96
|
+
emit('close');
|
|
97
|
+
"
|
|
98
|
+
>
|
|
99
|
+
删除节点
|
|
100
|
+
</button>
|
|
101
|
+
|
|
102
|
+
<button
|
|
103
|
+
v-else-if="menu.kind === 'edge'"
|
|
104
|
+
type="button"
|
|
105
|
+
class="bp-context-menu__item bp-context-menu__item--danger"
|
|
106
|
+
@click="
|
|
107
|
+
emit('deleteEdge', menu.edgeId);
|
|
108
|
+
emit('close');
|
|
109
|
+
"
|
|
110
|
+
>
|
|
111
|
+
删除连线
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, ref, watch } from "vue";
|
|
3
|
+
import { Button, Checkbox, Input, Tooltip } from "ant-design-vue";
|
|
4
|
+
import {
|
|
5
|
+
PAGE_LIFECYCLE_LABELS,
|
|
6
|
+
type ExecutionTraceEntry,
|
|
7
|
+
type PageLifecyclePhase,
|
|
8
|
+
} from "@arronqzy/blueprint-dsl";
|
|
9
|
+
|
|
10
|
+
import type { ExecutionLogSettings } from "../library/execution-log-settings";
|
|
11
|
+
import { cn } from "../utils/cn";
|
|
12
|
+
|
|
13
|
+
export type BlueprintExecutionLogPanelProps = {
|
|
14
|
+
entries: ExecutionTraceEntry[];
|
|
15
|
+
settings: ExecutionLogSettings;
|
|
16
|
+
onUpdateSettings: (patch: Partial<ExecutionLogSettings>) => void;
|
|
17
|
+
onSave: () => void;
|
|
18
|
+
onExport: () => void;
|
|
19
|
+
onClear: () => void;
|
|
20
|
+
onClearAllSaved?: () => void | Promise<void>;
|
|
21
|
+
onApplyRetention: () => void;
|
|
22
|
+
hasSavedRuns?: boolean;
|
|
23
|
+
lifecyclePhase?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<BlueprintExecutionLogPanelProps>(), {
|
|
27
|
+
hasSavedRuns: false,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const scrollRef = ref<HTMLDivElement | null>(null);
|
|
31
|
+
const bottomRef = ref<HTMLDivElement | null>(null);
|
|
32
|
+
const prevEntryCount = ref(props.entries.length);
|
|
33
|
+
const stickToBottom = ref(true);
|
|
34
|
+
const hasNewBelow = ref(false);
|
|
35
|
+
|
|
36
|
+
const SCROLL_BOTTOM_THRESHOLD = 48;
|
|
37
|
+
|
|
38
|
+
function isScrollNearBottom(element: HTMLElement) {
|
|
39
|
+
return (
|
|
40
|
+
element.scrollHeight - element.scrollTop - element.clientHeight <=
|
|
41
|
+
SCROLL_BOTTOM_THRESHOLD
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatJson(value: unknown) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.stringify(value, null, 2);
|
|
48
|
+
} catch {
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const phaseLabel = computed(() => {
|
|
54
|
+
if (!props.lifecyclePhase) return null;
|
|
55
|
+
return (
|
|
56
|
+
PAGE_LIFECYCLE_LABELS[props.lifecyclePhase as PageLifecyclePhase] ??
|
|
57
|
+
props.lifecyclePhase
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const latestEntryPreview = computed(() => {
|
|
62
|
+
const latest = props.entries[props.entries.length - 1];
|
|
63
|
+
if (!latest) return "";
|
|
64
|
+
return latest.nodeLabel ?? latest.nodeId;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
async function scrollToBottom(behavior: ScrollBehavior = "smooth") {
|
|
68
|
+
await nextTick();
|
|
69
|
+
bottomRef.value?.scrollIntoView({ behavior, block: "end" });
|
|
70
|
+
stickToBottom.value = true;
|
|
71
|
+
hasNewBelow.value = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleScroll() {
|
|
75
|
+
const el = scrollRef.value;
|
|
76
|
+
if (!el) return;
|
|
77
|
+
const nearBottom = isScrollNearBottom(el);
|
|
78
|
+
stickToBottom.value = nearBottom;
|
|
79
|
+
if (nearBottom) {
|
|
80
|
+
hasNewBelow.value = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
watch(
|
|
85
|
+
() => props.entries,
|
|
86
|
+
(entries) => {
|
|
87
|
+
const prevCount = prevEntryCount.value;
|
|
88
|
+
prevEntryCount.value = entries.length;
|
|
89
|
+
|
|
90
|
+
if (entries.length === 0) {
|
|
91
|
+
hasNewBelow.value = false;
|
|
92
|
+
stickToBottom.value = true;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (entries.length <= prevCount) return;
|
|
97
|
+
|
|
98
|
+
if (stickToBottom.value) {
|
|
99
|
+
void scrollToBottom(prevCount === 0 ? "auto" : "smooth");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
hasNewBelow.value = true;
|
|
104
|
+
},
|
|
105
|
+
{ deep: true }
|
|
106
|
+
);
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<div class="flex h-full min-h-0 flex-col overflow-hidden bg-background text-foreground">
|
|
111
|
+
<div class="shrink-0 border-b border-border px-3 py-2">
|
|
112
|
+
<div class="flex items-start justify-between gap-2">
|
|
113
|
+
<div class="min-w-0">
|
|
114
|
+
<div class="text-xs font-semibold">蓝图任务输出日志</div>
|
|
115
|
+
<div class="mt-0.5 text-[11px] text-muted-foreground">
|
|
116
|
+
{{ phaseLabel ? `模拟场景:${phaseLabel}` : "选择生命周期节点并开始调试" }}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
120
|
+
<Tooltip
|
|
121
|
+
v-if="onClearAllSaved"
|
|
122
|
+
title="立即清空 IndexedDB 中所有已保存的蓝图执行日志"
|
|
123
|
+
>
|
|
124
|
+
<Button
|
|
125
|
+
size="small"
|
|
126
|
+
class="h-7 w-7 shrink-0"
|
|
127
|
+
aria-label="清空 IndexedDB 日志"
|
|
128
|
+
:disabled="!hasSavedRuns"
|
|
129
|
+
@click="() => void onClearAllSaved?.()"
|
|
130
|
+
>
|
|
131
|
+
🗄✕
|
|
132
|
+
</Button>
|
|
133
|
+
</Tooltip>
|
|
134
|
+
<Tooltip title="清空当前日志,并重置画布执行高亮">
|
|
135
|
+
<Button
|
|
136
|
+
size="small"
|
|
137
|
+
class="h-7 w-7 shrink-0"
|
|
138
|
+
aria-label="清空日志"
|
|
139
|
+
:disabled="entries.length === 0"
|
|
140
|
+
@click="onClear"
|
|
141
|
+
>
|
|
142
|
+
🗑
|
|
143
|
+
</Button>
|
|
144
|
+
</Tooltip>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div class="relative min-h-0 flex-1">
|
|
150
|
+
<div ref="scrollRef" class="h-full overflow-auto p-3" @scroll="handleScroll">
|
|
151
|
+
<p v-if="entries.length === 0" class="text-[11px] text-muted-foreground">
|
|
152
|
+
暂无执行记录。使用工具栏「走完全流程」或「下一步」开始模拟。
|
|
153
|
+
</p>
|
|
154
|
+
<div v-else class="space-y-0">
|
|
155
|
+
<div v-for="(entry, index) in entries" :key="entry.id">
|
|
156
|
+
<div class="rounded-md border border-border/70 bg-muted/20 p-2.5">
|
|
157
|
+
<div class="flex items-start justify-between gap-2">
|
|
158
|
+
<div>
|
|
159
|
+
<div class="text-xs font-medium text-foreground">
|
|
160
|
+
{{ entry.nodeLabel ?? entry.nodeId }}
|
|
161
|
+
</div>
|
|
162
|
+
<div class="mt-0.5 font-mono text-[10px] text-muted-foreground">
|
|
163
|
+
{{ entry.nodeType }} · {{ entry.nodeId }}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<time class="shrink-0 text-[10px] text-muted-foreground">
|
|
167
|
+
{{ entry.isoTime }}
|
|
168
|
+
</time>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div class="mt-2 grid gap-2">
|
|
172
|
+
<div>
|
|
173
|
+
<div class="mb-1 text-[10px] font-medium text-muted-foreground">输入</div>
|
|
174
|
+
<pre
|
|
175
|
+
class="max-h-32 overflow-auto rounded border border-border/60 bg-background p-2 font-mono text-[10px] leading-relaxed text-foreground"
|
|
176
|
+
>{{ formatJson(entry.inputs) }}</pre>
|
|
177
|
+
</div>
|
|
178
|
+
<div>
|
|
179
|
+
<div class="mb-1 text-[10px] font-medium text-muted-foreground">输出</div>
|
|
180
|
+
<pre
|
|
181
|
+
class="max-h-32 overflow-auto rounded border p-2 font-mono text-[10px] leading-relaxed"
|
|
182
|
+
:class="
|
|
183
|
+
cn(
|
|
184
|
+
entry.error
|
|
185
|
+
? 'border-destructive/40 bg-destructive/5 text-destructive'
|
|
186
|
+
: 'border-border/60 bg-background text-foreground'
|
|
187
|
+
)
|
|
188
|
+
"
|
|
189
|
+
>{{ entry.error ?? formatJson(entry.outputs) }}</pre>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<div
|
|
194
|
+
v-if="index < entries.length - 1"
|
|
195
|
+
class="flex justify-center py-1 text-muted-foreground"
|
|
196
|
+
>
|
|
197
|
+
↓
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
<div ref="bottomRef" class="h-px shrink-0" aria-hidden="true" />
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div
|
|
205
|
+
v-if="hasNewBelow"
|
|
206
|
+
class="pointer-events-none absolute inset-x-0 bottom-3 flex justify-center px-3"
|
|
207
|
+
>
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
class="pointer-events-auto inline-flex max-w-full items-center gap-1.5 rounded-full border border-primary/30 bg-primary px-3 py-1.5 text-[11px] font-medium text-primary-foreground shadow-md transition hover:bg-primary/90"
|
|
211
|
+
@click="() => void scrollToBottom('smooth')"
|
|
212
|
+
>
|
|
213
|
+
<span class="shrink-0">↓</span>
|
|
214
|
+
<span class="truncate">有新日志</span>
|
|
215
|
+
<span v-if="latestEntryPreview" class="truncate opacity-90">
|
|
216
|
+
· {{ latestEntryPreview }}
|
|
217
|
+
</span>
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="shrink-0 space-y-2 border-t border-border p-3 text-xs">
|
|
223
|
+
<div class="grid grid-cols-2 gap-2">
|
|
224
|
+
<Tooltip title="将当前调试日志保存到 IndexedDB">
|
|
225
|
+
<Button size="small" block :disabled="entries.length === 0" @click="onSave">
|
|
226
|
+
保存日志
|
|
227
|
+
</Button>
|
|
228
|
+
</Tooltip>
|
|
229
|
+
<Tooltip title="将当前调试日志导出为 JSON 文件">
|
|
230
|
+
<Button size="small" block :disabled="entries.length === 0" @click="onExport">
|
|
231
|
+
导出 JSON
|
|
232
|
+
</Button>
|
|
233
|
+
</Tooltip>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<Tooltip title="调试运行完成后,自动将日志写入 IndexedDB">
|
|
237
|
+
<label class="flex cursor-default items-center gap-2">
|
|
238
|
+
<Checkbox
|
|
239
|
+
:checked="settings.autoSave"
|
|
240
|
+
@update:checked="(v) => onUpdateSettings({ autoSave: Boolean(v) })"
|
|
241
|
+
/>
|
|
242
|
+
<span class="text-[11px] text-muted-foreground">
|
|
243
|
+
运行完成后自动保存到 IndexedDB
|
|
244
|
+
</span>
|
|
245
|
+
</label>
|
|
246
|
+
</Tooltip>
|
|
247
|
+
|
|
248
|
+
<div class="grid grid-cols-2 gap-2">
|
|
249
|
+
<Tooltip title="IndexedDB 中最多保留的日志条数,超出后自动删除最旧记录">
|
|
250
|
+
<div class="space-y-1">
|
|
251
|
+
<span class="text-[11px] text-muted-foreground">最多保存条数</span>
|
|
252
|
+
<Input
|
|
253
|
+
type="number"
|
|
254
|
+
size="small"
|
|
255
|
+
:min="1"
|
|
256
|
+
:step="1"
|
|
257
|
+
:value="settings.maxSavedRuns"
|
|
258
|
+
aria-label="IndexedDB 最多保存日志条数"
|
|
259
|
+
@update:value="
|
|
260
|
+
(v) =>
|
|
261
|
+
onUpdateSettings({
|
|
262
|
+
maxSavedRuns: Math.max(1, Number(v) || 1),
|
|
263
|
+
})
|
|
264
|
+
"
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
</Tooltip>
|
|
268
|
+
<Tooltip title="超过保留天数的已保存日志将在清理时被删除">
|
|
269
|
+
<div class="space-y-1">
|
|
270
|
+
<span class="text-[11px] text-muted-foreground">保留天数</span>
|
|
271
|
+
<Input
|
|
272
|
+
type="number"
|
|
273
|
+
size="small"
|
|
274
|
+
:min="1"
|
|
275
|
+
:step="1"
|
|
276
|
+
:value="settings.retentionDays"
|
|
277
|
+
aria-label="日志保留天数"
|
|
278
|
+
@update:value="
|
|
279
|
+
(v) =>
|
|
280
|
+
onUpdateSettings({
|
|
281
|
+
retentionDays: Math.max(1, Number(v) || 1),
|
|
282
|
+
})
|
|
283
|
+
"
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
</Tooltip>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<Tooltip title="按保留天数与条数上限,清理 IndexedDB 中的日志记录">
|
|
290
|
+
<Button size="small" block @click="onApplyRetention">清理过期与超额</Button>
|
|
291
|
+
</Tooltip>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</template>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from "vue";
|
|
3
|
+
import { Button, Input, Modal } from "ant-design-vue";
|
|
4
|
+
|
|
5
|
+
import type { BlueprintMetaDraft } from "../library/types";
|
|
6
|
+
|
|
7
|
+
export type BlueprintMetaDialogProps = {
|
|
8
|
+
open: boolean;
|
|
9
|
+
mode: "export" | "save";
|
|
10
|
+
initialMeta: BlueprintMetaDraft;
|
|
11
|
+
onOpenChange: (open: boolean) => void;
|
|
12
|
+
onConfirm: (meta: BlueprintMetaDraft) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const props = defineProps<BlueprintMetaDialogProps>();
|
|
16
|
+
|
|
17
|
+
const name = ref(props.initialMeta.name);
|
|
18
|
+
const remark = ref(props.initialMeta.remark);
|
|
19
|
+
|
|
20
|
+
watch(
|
|
21
|
+
() => [props.open, props.initialMeta.name, props.initialMeta.remark] as const,
|
|
22
|
+
([open, nextName, nextRemark]) => {
|
|
23
|
+
if (!open) return;
|
|
24
|
+
name.value = nextName;
|
|
25
|
+
remark.value = nextRemark;
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const title = () => (props.mode === "export" ? "导出蓝图" : "保存蓝图");
|
|
30
|
+
const description = () =>
|
|
31
|
+
props.mode === "export"
|
|
32
|
+
? "填写蓝图名称与备注,将当前蓝图导出为 JSON 文件。"
|
|
33
|
+
: "填写蓝图名称与备注,将当前蓝图保存到本地蓝图库。";
|
|
34
|
+
const confirmLabel = () => (props.mode === "export" ? "导出" : "保存");
|
|
35
|
+
|
|
36
|
+
function handleConfirm() {
|
|
37
|
+
props.onConfirm({
|
|
38
|
+
name: name.value.trim() || "未命名蓝图",
|
|
39
|
+
remark: remark.value.trim(),
|
|
40
|
+
});
|
|
41
|
+
props.onOpenChange(false);
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<Modal
|
|
47
|
+
:open="open"
|
|
48
|
+
:title="title()"
|
|
49
|
+
:ok-text="confirmLabel()"
|
|
50
|
+
cancel-text="取消"
|
|
51
|
+
@update:open="onOpenChange"
|
|
52
|
+
@ok="handleConfirm"
|
|
53
|
+
>
|
|
54
|
+
<p class="mb-3 text-xs text-muted-foreground">{{ description() }}</p>
|
|
55
|
+
<div class="space-y-3">
|
|
56
|
+
<label class="block space-y-1">
|
|
57
|
+
<span class="text-sm">蓝图名称</span>
|
|
58
|
+
<Input
|
|
59
|
+
id="blueprint-meta-name"
|
|
60
|
+
v-model:value="name"
|
|
61
|
+
placeholder="例如:首页初始化流程"
|
|
62
|
+
autofocus
|
|
63
|
+
/>
|
|
64
|
+
</label>
|
|
65
|
+
<label class="block space-y-1">
|
|
66
|
+
<span class="text-sm">蓝图备注</span>
|
|
67
|
+
<Input.TextArea
|
|
68
|
+
id="blueprint-meta-remark"
|
|
69
|
+
v-model:value="remark"
|
|
70
|
+
placeholder="可选:描述蓝图用途、触发条件等"
|
|
71
|
+
:rows="3"
|
|
72
|
+
/>
|
|
73
|
+
</label>
|
|
74
|
+
</div>
|
|
75
|
+
<template #footer>
|
|
76
|
+
<Button @click="onOpenChange(false)">取消</Button>
|
|
77
|
+
<Button type="primary" @click="handleConfirm">{{ confirmLabel() }}</Button>
|
|
78
|
+
</template>
|
|
79
|
+
</Modal>
|
|
80
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { Button, Modal } from "ant-design-vue";
|
|
4
|
+
|
|
5
|
+
export type BlueprintNodeSwitchTaskDialogProps = {
|
|
6
|
+
open: boolean;
|
|
7
|
+
fromNodeId: string;
|
|
8
|
+
toNodeId: string | null;
|
|
9
|
+
onOpenChange: (open: boolean) => void;
|
|
10
|
+
onKeepTaskAndSwitch: () => void;
|
|
11
|
+
onCancelTaskAndSwitch: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const props = defineProps<BlueprintNodeSwitchTaskDialogProps>();
|
|
15
|
+
|
|
16
|
+
const targetLabel = computed(() =>
|
|
17
|
+
props.toNodeId === null ? "取消选中" : `节点 ${props.toNodeId}`
|
|
18
|
+
);
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<Modal
|
|
23
|
+
:open="open"
|
|
24
|
+
title="节点正在执行任务"
|
|
25
|
+
:footer="null"
|
|
26
|
+
@update:open="onOpenChange"
|
|
27
|
+
>
|
|
28
|
+
<p class="text-sm text-muted-foreground">
|
|
29
|
+
节点 <span class="font-mono text-foreground">{{ fromNodeId }}</span>
|
|
30
|
+
正在执行任务(Swagger 解析或请求调试)。是否切换到 {{ targetLabel }}?
|
|
31
|
+
</p>
|
|
32
|
+
<p class="mt-2 text-xs text-muted-foreground">
|
|
33
|
+
选择「保留并切换」将在后台继续执行,回到该节点时仍能看到进度;选择「取消并切换」会中止当前任务。
|
|
34
|
+
</p>
|
|
35
|
+
<div class="mt-4 flex flex-wrap justify-end gap-2">
|
|
36
|
+
<Button @click="onOpenChange(false)">留在此节点</Button>
|
|
37
|
+
<Button @click="onCancelTaskAndSwitch">取消并切换</Button>
|
|
38
|
+
<Button type="primary" @click="onKeepTaskAndSwitch">保留并切换</Button>
|
|
39
|
+
</div>
|
|
40
|
+
</Modal>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { Checkbox, Input } from "ant-design-vue";
|
|
4
|
+
import type { ClockNodeConfig } from "@arronqzy/blueprint-dsl";
|
|
5
|
+
|
|
6
|
+
import type { BlueprintGraphNode } from "../graph/document";
|
|
7
|
+
import { resolveNodeClockConfig } from "../graph/document";
|
|
8
|
+
|
|
9
|
+
export type ClockNodeConfigPanelProps = {
|
|
10
|
+
node: BlueprintGraphNode;
|
|
11
|
+
onUpdateNode: (
|
|
12
|
+
nodeId: string,
|
|
13
|
+
patch: Partial<Pick<BlueprintGraphNode, "clockConfig" | "configSource">>
|
|
14
|
+
) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const props = defineProps<ClockNodeConfigPanelProps>();
|
|
18
|
+
|
|
19
|
+
function patchClockConfig(node: BlueprintGraphNode, patch: Partial<ClockNodeConfig>) {
|
|
20
|
+
return {
|
|
21
|
+
clockConfig: { ...resolveNodeClockConfig(node), ...patch },
|
|
22
|
+
configSource: "clock" as const,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const clockConfig = computed(() => resolveNodeClockConfig(props.node));
|
|
27
|
+
|
|
28
|
+
function handleIntervalChange(value: string | number) {
|
|
29
|
+
const raw = Number(value);
|
|
30
|
+
const intervalSeconds =
|
|
31
|
+
Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;
|
|
32
|
+
props.onUpdateNode(props.node.id, patchClockConfig(props.node, { intervalSeconds }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function handleOutputCountChange(value: string | number) {
|
|
36
|
+
const raw = Number(value);
|
|
37
|
+
const outputCount = Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 1;
|
|
38
|
+
props.onUpdateNode(props.node.id, patchClockConfig(props.node, { outputCount }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleFormatChange(event: Event) {
|
|
42
|
+
props.onUpdateNode(
|
|
43
|
+
props.node.id,
|
|
44
|
+
patchClockConfig(props.node, {
|
|
45
|
+
timeFormat: (event.target as HTMLInputElement).value,
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleEmitImmediatelyChange(checked: boolean) {
|
|
51
|
+
props.onUpdateNode(
|
|
52
|
+
props.node.id,
|
|
53
|
+
patchClockConfig(props.node, { emitImmediately: checked })
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<div class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
|
|
60
|
+
<div class="font-medium text-foreground">时钟节点</div>
|
|
61
|
+
<p class="text-[11px] text-muted-foreground">
|
|
62
|
+
收到<strong>真信号</strong>后才开始计时输出;每次输出向下游发出
|
|
63
|
+
<strong>真信号</strong>,值包含当前时间(formatted / timestamp / isoTime)。
|
|
64
|
+
假信号会原样向下游传递。
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
<label class="block space-y-1">
|
|
68
|
+
<span class="text-muted-foreground">时钟信号间隔(秒)</span>
|
|
69
|
+
<Input
|
|
70
|
+
type="number"
|
|
71
|
+
:min="0"
|
|
72
|
+
:step="1"
|
|
73
|
+
size="small"
|
|
74
|
+
:value="clockConfig.intervalSeconds"
|
|
75
|
+
@update:value="handleIntervalChange"
|
|
76
|
+
/>
|
|
77
|
+
<p class="text-[11px] text-muted-foreground">
|
|
78
|
+
两次输出之间的间隔 n 秒;多次输出或未开启「立即发送」时须大于 0。
|
|
79
|
+
</p>
|
|
80
|
+
</label>
|
|
81
|
+
|
|
82
|
+
<label class="block space-y-1">
|
|
83
|
+
<span class="text-muted-foreground">输出次数</span>
|
|
84
|
+
<Input
|
|
85
|
+
type="number"
|
|
86
|
+
:min="1"
|
|
87
|
+
:step="1"
|
|
88
|
+
size="small"
|
|
89
|
+
:value="clockConfig.outputCount"
|
|
90
|
+
@update:value="handleOutputCountChange"
|
|
91
|
+
/>
|
|
92
|
+
<p class="text-[11px] text-muted-foreground">
|
|
93
|
+
收到真信号后总共输出的次数,默认 1。
|
|
94
|
+
</p>
|
|
95
|
+
</label>
|
|
96
|
+
|
|
97
|
+
<label class="flex items-start gap-2">
|
|
98
|
+
<Checkbox
|
|
99
|
+
:checked="clockConfig.emitImmediately"
|
|
100
|
+
class="mt-0.5"
|
|
101
|
+
@update:checked="handleEmitImmediatelyChange"
|
|
102
|
+
/>
|
|
103
|
+
<span class="text-[11px] leading-relaxed text-muted-foreground">
|
|
104
|
+
收到信号立即发送:开启且输出次数大于 1 时,会立刻执行第 1 次,剩余次数按间隔 m
|
|
105
|
+
秒依次执行;仅输出 1 次时也会立刻执行。关闭则每次(含首次)都先等待 m 秒。
|
|
106
|
+
</span>
|
|
107
|
+
</label>
|
|
108
|
+
|
|
109
|
+
<label class="block space-y-1">
|
|
110
|
+
<span class="text-muted-foreground">时间格式</span>
|
|
111
|
+
<Input
|
|
112
|
+
size="small"
|
|
113
|
+
:value="clockConfig.timeFormat"
|
|
114
|
+
spellcheck="false"
|
|
115
|
+
class="font-mono text-[11px]"
|
|
116
|
+
placeholder="YYYY-MM-DD HH:mm:ss"
|
|
117
|
+
@input="handleFormatChange"
|
|
118
|
+
/>
|
|
119
|
+
<p class="text-[11px] text-muted-foreground">
|
|
120
|
+
支持 YYYY、MM、DD、HH、mm、ss,默认 YYYY-MM-DD HH:mm:ss。
|
|
121
|
+
</p>
|
|
122
|
+
</label>
|
|
123
|
+
</div>
|
|
124
|
+
</template>
|