@arronqzy/vue-blueprint 0.1.3 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-blueprint",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Vue 3 blueprint editor for Abuilder (Vue Flow + Ant Design Vue)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,9 +19,9 @@
19
19
  "ant-design-vue": "^4.2.6",
20
20
  "dayjs": "^1.11.13",
21
21
  "vue": "^3.5.13",
22
- "@arronqzy/blueprint-dsl": "1.0.5",
23
- "@arronqzy/i18n": "0.1.3",
24
- "@arronqzy/vue-rx-store": "0.1.2"
22
+ "@arronqzy/blueprint-dsl": "1.0.7",
23
+ "@arronqzy/vue-rx-store": "0.1.2",
24
+ "@arronqzy/i18n": "0.1.6"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "vue": "^3.4.0"
@@ -5,7 +5,10 @@ import { Checkbox, Input, Select } from "ant-design-vue";
5
5
  import {
6
6
  LIFECYCLE_NODE_TYPE,
7
7
  PAGE_LIFECYCLE_PHASES,
8
+ VIEW_EVENT_TYPES,
9
+ type ExecutionTraceEntry,
8
10
  type PageLifecyclePhase,
11
+ type ViewEventType,
9
12
  } from "@arronqzy/blueprint-dsl";
10
13
 
11
14
  import FetchNodeConfigPanel from "./components/FetchNodeConfigPanel.vue";
@@ -13,13 +16,18 @@ import ClockNodeConfigPanel from "./components/ClockNodeConfigPanel.vue";
13
16
  import JsonNodeConfigPanel from "./components/JsonNodeConfigPanel.vue";
14
17
  import LogicNodeConfigPanel from "./components/LogicNodeConfigPanel.vue";
15
18
  import ViewElementMultiSelect from "./components/ViewElementMultiSelect.vue";
19
+ import ConfigHintIcon from "./components/ConfigHintIcon.vue";
16
20
  import {
17
21
  getLifecyclePhaseLabel,
22
+ getViewEventTypeLabel,
23
+ inspectEventNodeInputs,
18
24
  patchNodeConfigSource,
19
25
  pruneViewElementIds,
20
26
  resolveBlueprintConfigSource,
27
+ resolveNodeEventConfig,
21
28
  resolveViewElementIds,
22
29
  type BlueprintConfigSource,
30
+ type BlueprintGraphEdge,
23
31
  type BlueprintGraphNode,
24
32
  type BlueprintNodeRole,
25
33
  } from "./graph/document";
@@ -38,6 +46,9 @@ export type BlueprintLibraryOption = {
38
46
 
39
47
  export type BlueprintNodeConfigSidebarProps = {
40
48
  node: BlueprintGraphNode;
49
+ graphNodes?: BlueprintGraphNode[];
50
+ graphEdges?: BlueprintGraphEdge[];
51
+ traceEntries?: ExecutionTraceEntry[];
41
52
  viewElementOptions?: BlueprintViewElementOption[];
42
53
  blueprintLibraryOptions?: BlueprintLibraryOption[];
43
54
  allowFalseSignalPropagation?: boolean;
@@ -60,6 +71,7 @@ export type BlueprintNodeConfigSidebarProps = {
60
71
  | "jsonConfig"
61
72
  | "logicConfig"
62
73
  | "clockConfig"
74
+ | "eventConfig"
63
75
  >
64
76
  >
65
77
  ) => void;
@@ -68,11 +80,15 @@ export type BlueprintNodeConfigSidebarProps = {
68
80
  const props = withDefaults(defineProps<BlueprintNodeConfigSidebarProps>(), {
69
81
  viewElementOptions: () => [],
70
82
  blueprintLibraryOptions: () => [],
83
+ graphNodes: () => [],
84
+ graphEdges: () => [],
85
+ traceEntries: () => [],
71
86
  allowFalseSignalPropagation: false,
72
87
  });
73
88
 
74
89
  const configSource = computed(() => resolveBlueprintConfigSource(props.node));
75
90
  const linkedViewElementIds = computed(() => resolveViewElementIds(props.node));
91
+ const selectedEventTypes = computed(() => resolveNodeEventConfig(props.node).eventTypes);
76
92
  const existingViewElementIdSet = computed(
77
93
  () => new Set(props.viewElementOptions.map((opt) => opt.id))
78
94
  );
@@ -88,12 +104,17 @@ const ROLE_LABEL_KEYS: Record<BlueprintNodeRole, string> = {
88
104
  json: "blueprint.config.roleJson",
89
105
  logic: "blueprint.config.roleLogic",
90
106
  clock: "blueprint.config.roleClock",
107
+ event: "blueprint.config.roleEvent",
91
108
  };
92
109
 
93
110
  const roleLabel = computed(() =>
94
111
  t(ROLE_LABEL_KEYS[props.node.role] ?? "blueprint.config.roleLogic")
95
112
  );
96
113
 
114
+ const eventInputs = computed(() =>
115
+ inspectEventNodeInputs(props.node.id, props.graphNodes, props.graphEdges)
116
+ );
117
+
97
118
  watch(
98
119
  [
99
120
  configSource,
@@ -104,7 +125,7 @@ watch(
104
125
  () => props.viewElementOptions.length,
105
126
  ],
106
127
  () => {
107
- if (configSource.value !== "view") return;
128
+ if (configSource.value !== "view" && configSource.value !== "event") return;
108
129
  if (props.viewElementOptions.length === 0) return;
109
130
  const linked = resolveViewElementIds(props.node);
110
131
  if (linked.length === 0) return;
@@ -113,7 +134,7 @@ watch(
113
134
  props.onUpdateNode(props.node.id, {
114
135
  viewElementIds: pruned.length > 0 ? pruned : undefined,
115
136
  viewElementId: undefined,
116
- configSource: "view",
137
+ configSource: configSource.value,
117
138
  });
118
139
  }
119
140
  );
@@ -122,7 +143,7 @@ function handleConfigSourceChange(value: string) {
122
143
  const nextSource = value as BlueprintConfigSource;
123
144
  props.onUpdateNode(props.node.id, {
124
145
  ...patchNodeConfigSource(props.node, nextSource),
125
- ...(nextSource === "view"
146
+ ...(nextSource === "view" || nextSource === "event"
126
147
  ? {
127
148
  viewElementIds: linkedViewElementIds.value,
128
149
  viewElementId: undefined,
@@ -133,6 +154,19 @@ function handleConfigSourceChange(value: string) {
133
154
  }),
134
155
  });
135
156
  }
157
+
158
+ function toggleEventType(eventType: ViewEventType, checked: boolean) {
159
+ const current = selectedEventTypes.value;
160
+ const nextTypes: ViewEventType[] = checked
161
+ ? [...new Set([...current, eventType])]
162
+ : current.filter((type) => type !== eventType);
163
+ props.onUpdateNode(props.node.id, {
164
+ eventConfig: {
165
+ eventTypes: nextTypes.length > 0 ? nextTypes : ["click"],
166
+ },
167
+ configSource: "event",
168
+ });
169
+ }
136
170
  </script>
137
171
 
138
172
  <template>
@@ -168,12 +202,18 @@ function handleConfigSourceChange(value: string) {
168
202
  <Select.Option value="fetch">{{ t("blueprint.config.configFetch") }}</Select.Option>
169
203
  <Select.Option value="json">{{ t("blueprint.config.configJson") }}</Select.Option>
170
204
  <Select.Option value="clock">{{ t("blueprint.config.configClock") }}</Select.Option>
205
+ <Select.Option value="event">{{ t("blueprint.config.configEvent") }}</Select.Option>
171
206
  <Select.Option value="view">{{ t("blueprint.config.configView") }}</Select.Option>
172
207
  </Select>
173
208
  </label>
174
209
 
175
- <div v-if="configSource === 'view'" class="block space-y-1">
176
- <span class="text-muted-foreground">{{ t("blueprint.config.linkedViewNodes") }}</span>
210
+ <div v-if="configSource === 'view' || configSource === 'event'" class="block space-y-1">
211
+ <span class="inline-flex items-center gap-1 text-muted-foreground">
212
+ {{ t("blueprint.config.linkedViewNodes") }}
213
+ <ConfigHintIcon :label="t('blueprint.config.linkedViewNodes')">
214
+ {{ configSource === 'event' ? t("blueprint.config.eventViewHint") : t("blueprint.config.viewMultiHint") }}
215
+ </ConfigHintIcon>
216
+ </span>
177
217
  <ViewElementMultiSelect
178
218
  :options="viewElementOptions"
179
219
  :value="linkedViewElementIds"
@@ -183,14 +223,11 @@ function handleConfigSourceChange(value: string) {
183
223
  onUpdateNode(node.id, {
184
224
  viewElementIds: next,
185
225
  viewElementId: undefined,
186
- configSource: 'view',
226
+ configSource,
187
227
  })
188
228
  "
189
229
  />
190
- <p v-if="linkedViewElementIds.length === 0" class="text-[11px] text-muted-foreground">
191
- {{ t("blueprint.config.viewMultiHint") }}
192
- </p>
193
- <p v-else class="text-[11px] text-muted-foreground">
230
+ <p v-if="linkedViewElementIds.length > 0" class="text-[11px] text-muted-foreground">
194
231
  {{
195
232
  t("blueprint.config.linkedViewCount", {
196
233
  count: linkedViewElementIds.length,
@@ -206,8 +243,12 @@ function handleConfigSourceChange(value: string) {
206
243
  v-if="configSource === 'blueprint'"
207
244
  class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5"
208
245
  >
209
- <div class="font-medium text-foreground">{{ t("blueprint.config.blueprintAttrs") }}</div>
210
- <p class="text-[11px] text-muted-foreground">{{ t("blueprint.config.blueprintAttrsHint") }}</p>
246
+ <div class="flex items-center gap-1.5">
247
+ <div class="font-medium text-foreground">{{ t("blueprint.config.blueprintAttrs") }}</div>
248
+ <ConfigHintIcon :label="t('blueprint.config.blueprintAttrs')">
249
+ {{ t("blueprint.config.blueprintAttrsHint") }}
250
+ </ConfigHintIcon>
251
+ </div>
211
252
  <label class="block space-y-1">
212
253
  <span class="text-muted-foreground">{{ t("blueprint.config.refLibrary") }}</span>
213
254
  <Select
@@ -249,18 +290,58 @@ function handleConfigSourceChange(value: string) {
249
290
  </div>
250
291
 
251
292
  <div
252
- v-if="configSource === 'lifecycle'"
293
+ v-if="configSource === 'event'"
253
294
  class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5"
254
295
  >
255
- <div class="font-medium text-foreground">{{ t("blueprint.config.lifecycleHook") }}</div>
256
- <p class="text-[11px] text-muted-foreground">
257
- {{ t("blueprint.config.lifecycleNoInput") }}
258
- {{
259
- node.lifecyclePhase === "blueprintActivated"
260
- ? t("blueprint.config.lifecycleBlueprintActivatedHint")
261
- : t("blueprint.config.lifecyclePageHint")
262
- }}
296
+ <div class="flex items-center gap-1.5">
297
+ <div class="font-medium text-foreground">{{ t("blueprint.config.eventListen") }}</div>
298
+ <ConfigHintIcon :label="t('blueprint.config.eventListen')">
299
+ <p>{{ t("blueprint.config.eventNoInput") }}</p>
300
+ <p>{{ t("blueprint.config.eventHint") }}</p>
301
+ </ConfigHintIcon>
302
+ </div>
303
+ <p
304
+ v-if="eventInputs.nonLifecycleCount > 0"
305
+ class="rounded border border-destructive/40 bg-destructive/10 px-2 py-1.5 text-[11px] leading-relaxed text-destructive"
306
+ >
307
+ {{ t("blueprint.config.eventInputNotLifecycle") }}
263
308
  </p>
309
+ <p
310
+ v-else-if="eventInputs.lifecycleCount === 0"
311
+ class="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] leading-relaxed text-amber-700"
312
+ >
313
+ {{ t("blueprint.config.eventInputMissing") }}
314
+ </p>
315
+ <label
316
+ v-for="eventType in VIEW_EVENT_TYPES"
317
+ :key="eventType"
318
+ class="flex items-center gap-2"
319
+ >
320
+ <Checkbox
321
+ :checked="selectedEventTypes.includes(eventType)"
322
+ @update:checked="(v) => toggleEventType(eventType, Boolean(v))"
323
+ />
324
+ <span>{{ getViewEventTypeLabel(t, eventType) }}</span>
325
+ </label>
326
+ </div>
327
+
328
+ <div
329
+ v-if="configSource === 'lifecycle'"
330
+ class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5"
331
+ >
332
+ <div class="flex items-center gap-1.5">
333
+ <div class="font-medium text-foreground">{{ t("blueprint.config.lifecycleHook") }}</div>
334
+ <ConfigHintIcon :label="t('blueprint.config.lifecycleHook')">
335
+ <p>{{ t("blueprint.config.lifecycleNoInput") }}</p>
336
+ <p>
337
+ {{
338
+ node.lifecyclePhase === "blueprintActivated"
339
+ ? t("blueprint.config.lifecycleBlueprintActivatedHint")
340
+ : t("blueprint.config.lifecyclePageHint")
341
+ }}
342
+ </p>
343
+ </ConfigHintIcon>
344
+ </div>
264
345
  <label class="block space-y-1">
265
346
  <span class="text-muted-foreground">{{ t("blueprint.config.listenPhase") }}</span>
266
347
  <Select
@@ -290,6 +371,9 @@ function handleConfigSourceChange(value: string) {
290
371
  <FetchNodeConfigPanel
291
372
  v-if="configSource === 'fetch'"
292
373
  :node="node"
374
+ :graph-nodes="graphNodes"
375
+ :graph-edges="graphEdges"
376
+ :trace-entries="traceEntries"
293
377
  :on-update-node="onUpdateNode"
294
378
  />
295
379
  <JsonNodeConfigPanel
@@ -307,8 +391,12 @@ function handleConfigSourceChange(value: string) {
307
391
  v-if="configSource === 'and'"
308
392
  class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5"
309
393
  >
310
- <div class="font-medium text-foreground">{{ t("blueprint.config.andTitle") }}</div>
311
- <p class="text-[11px] text-muted-foreground">{{ t("blueprint.config.andHint") }}</p>
394
+ <div class="flex items-center gap-1.5">
395
+ <div class="font-medium text-foreground">{{ t("blueprint.config.andTitle") }}</div>
396
+ <ConfigHintIcon :label="t('blueprint.config.andTitle')">
397
+ {{ t("blueprint.config.andHint") }}
398
+ </ConfigHintIcon>
399
+ </div>
312
400
  </div>
313
401
 
314
402
  <template v-if="configSource === 'logic'">
@@ -6,6 +6,7 @@ import ClockFlowNode from "./nodes/ClockFlowNode.vue";
6
6
  import FetchFlowNode from "./nodes/FetchFlowNode.vue";
7
7
  import JsonFlowNode from "./nodes/JsonFlowNode.vue";
8
8
  import LifecycleFlowNode from "./nodes/LifecycleFlowNode.vue";
9
+ import EventFlowNode from "./nodes/EventFlowNode.vue";
9
10
  import LogicFlowNode from "./nodes/LogicFlowNode.vue";
10
11
 
11
12
  /** 稳定引用,避免 Vue Flow 因 nodeTypes 变化反复卸载节点 */
@@ -17,4 +18,5 @@ export const blueprintNodeTypes: Record<string, Component> = {
17
18
  json: markRaw(JsonFlowNode),
18
19
  logic: markRaw(LogicFlowNode),
19
20
  lifecycle: markRaw(LifecycleFlowNode),
21
+ event: markRaw(EventFlowNode),
20
22
  };
@@ -105,6 +105,7 @@ const {
105
105
  onEdgesChange,
106
106
  onNodeDragStop,
107
107
  onConnect,
108
+ onConnectEnd,
108
109
  isValidConnection,
109
110
  } = flowState;
110
111
 
@@ -201,6 +202,7 @@ function handleDeleteEdge(edgeId: string) {
201
202
  @nodes-change="onNodesChange"
202
203
  @edges-change="onEdgesChange"
203
204
  @connect="onConnect"
205
+ @connect-end="onConnectEnd"
204
206
  :is-valid-connection="isValidConnection"
205
207
  @node-drag-stop="onNodeDragStop"
206
208
  @pane-context-menu="onPaneContextMenu"
@@ -14,7 +14,7 @@ export type BlueprintContextMenuState =
14
14
  clientX: number;
15
15
  clientY: number;
16
16
  nodeId: string;
17
- role: "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "clock";
17
+ role: "blueprint" | "logic" | "and" | "lifecycle" | "event" | "fetch" | "json" | "clock";
18
18
  }
19
19
  | {
20
20
  kind: "edge";
@@ -4,6 +4,7 @@ import { computed } from "vue";
4
4
  import { Checkbox, Input } from "ant-design-vue";
5
5
  import type { ClockNodeConfig } from "@arronqzy/blueprint-dsl";
6
6
 
7
+ import ConfigHintIcon from "./ConfigHintIcon.vue";
7
8
  import type { BlueprintGraphNode } from "../graph/document";
8
9
  import { resolveNodeClockConfig } from "../graph/document";
9
10
 
@@ -60,11 +61,20 @@ function handleEmitImmediatelyChange(checked: boolean) {
60
61
 
61
62
  <template>
62
63
  <div class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
63
- <div class="font-medium text-foreground">{{ t("blueprint.config.clockTitle") }}</div>
64
- <p class="text-[11px] text-muted-foreground">{{ t("blueprint.config.clockHint") }}</p>
64
+ <div class="flex items-center gap-1.5">
65
+ <div class="font-medium text-foreground">{{ t("blueprint.config.clockTitle") }}</div>
66
+ <ConfigHintIcon :label="t('blueprint.config.clockTitle')">
67
+ {{ t("blueprint.config.clockHint") }}
68
+ </ConfigHintIcon>
69
+ </div>
65
70
 
66
71
  <label class="block space-y-1">
67
- <span class="text-muted-foreground">{{ t("blueprint.config.clockIntervalSeconds") }}</span>
72
+ <span class="inline-flex items-center gap-1 text-muted-foreground">
73
+ {{ t("blueprint.config.clockIntervalSeconds") }}
74
+ <ConfigHintIcon :label="t('blueprint.config.clockIntervalSeconds')">
75
+ {{ t("blueprint.config.clockIntervalHint") }}
76
+ </ConfigHintIcon>
77
+ </span>
68
78
  <Input
69
79
  type="number"
70
80
  :min="0"
@@ -73,13 +83,15 @@ function handleEmitImmediatelyChange(checked: boolean) {
73
83
  :value="clockConfig.intervalSeconds"
74
84
  @update:value="handleIntervalChange"
75
85
  />
76
- <p class="text-[11px] text-muted-foreground">
77
- {{ t("blueprint.config.clockIntervalHint") }}
78
- </p>
79
86
  </label>
80
87
 
81
88
  <label class="block space-y-1">
82
- <span class="text-muted-foreground">{{ t("blueprint.config.outputCount") }}</span>
89
+ <span class="inline-flex items-center gap-1 text-muted-foreground">
90
+ {{ t("blueprint.config.outputCount") }}
91
+ <ConfigHintIcon :label="t('blueprint.config.outputCount')">
92
+ {{ t("blueprint.config.outputCountHint") }}
93
+ </ConfigHintIcon>
94
+ </span>
83
95
  <Input
84
96
  type="number"
85
97
  :min="1"
@@ -88,9 +100,6 @@ function handleEmitImmediatelyChange(checked: boolean) {
88
100
  :value="clockConfig.outputCount"
89
101
  @update:value="handleOutputCountChange"
90
102
  />
91
- <p class="text-[11px] text-muted-foreground">
92
- {{ t("blueprint.config.outputCountHint") }}
93
- </p>
94
103
  </label>
95
104
 
96
105
  <label class="flex items-start gap-2">
@@ -105,7 +114,12 @@ function handleEmitImmediatelyChange(checked: boolean) {
105
114
  </label>
106
115
 
107
116
  <label class="block space-y-1">
108
- <span class="text-muted-foreground">{{ t("blueprint.config.timeFormat") }}</span>
117
+ <span class="inline-flex items-center gap-1 text-muted-foreground">
118
+ {{ t("blueprint.config.timeFormat") }}
119
+ <ConfigHintIcon :label="t('blueprint.config.timeFormat')">
120
+ {{ t("blueprint.config.timeFormatHint") }}
121
+ </ConfigHintIcon>
122
+ </span>
109
123
  <Input
110
124
  size="small"
111
125
  :value="clockConfig.timeFormat"
@@ -114,9 +128,6 @@ function handleEmitImmediatelyChange(checked: boolean) {
114
128
  placeholder="YYYY-MM-DD HH:mm:ss"
115
129
  @input="handleFormatChange"
116
130
  />
117
- <p class="text-[11px] text-muted-foreground">
118
- {{ t("blueprint.config.timeFormatHint") }}
119
- </p>
120
131
  </label>
121
132
  </div>
122
133
  </template>
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { useI18n } from "@arronqzy/i18n/vue";
4
+ import { Tooltip } from "ant-design-vue";
5
+
6
+ const { t } = useI18n();
7
+ const props = defineProps<{
8
+ label?: string;
9
+ contentClass?: string;
10
+ buttonClass?: string;
11
+ }>();
12
+
13
+ const resolvedLabel = computed(() => props.label || t("common.hint"));
14
+ </script>
15
+
16
+ <template>
17
+ <Tooltip
18
+ placement="top"
19
+ :overlay-class-name="`z-[10120] max-w-[360px] text-[11px] leading-5 ${contentClass ?? ''}`"
20
+ :mouse-enter-delay="0.12"
21
+ >
22
+ <template #title>
23
+ <div class="space-y-1.5">
24
+ <slot />
25
+ </div>
26
+ </template>
27
+ <button
28
+ type="button"
29
+ :class="
30
+ buttonClass ??
31
+ 'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] leading-none text-muted-foreground hover:bg-accent/50'
32
+ "
33
+ :aria-label="t('common.hintAria', { label: resolvedLabel })"
34
+ >
35
+ ?
36
+ </button>
37
+ </Tooltip>
38
+ </template>