@arronqzy/vue-blueprint 0.1.13 → 0.1.16

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.
@@ -66,17 +66,13 @@ function closeDropdown() {
66
66
  }
67
67
 
68
68
  function refreshForInput(input: HTMLInputElement | HTMLTextAreaElement) {
69
- if (props.scope === undefined) {
70
- closeDropdown();
71
- return;
72
- }
73
69
  const cursor = input.selectionStart ?? input.value.length;
74
- const parsed = parseScopeAutocomplete(input.value, cursor, props.scope);
70
+ const parsed = parseScopeAutocomplete(input.value, cursor, props.scope ?? {});
75
71
  if (!parsed) {
76
72
  closeDropdown();
77
73
  return;
78
74
  }
79
- const suggestions = getScopeAutocompleteSuggestions(props.scope, parsed);
75
+ const suggestions = getScopeAutocompleteSuggestions(props.scope ?? {}, parsed);
80
76
  if (suggestions.length === 0) {
81
77
  closeDropdown();
82
78
  return;
@@ -170,7 +166,7 @@ function onPointerDown(event: MouseEvent) {
170
166
  watch(
171
167
  () => toValue(props.containerRef),
172
168
  (container, _prev, onCleanup) => {
173
- if (!container || props.scope === undefined) return;
169
+ if (!container) return;
174
170
 
175
171
  container.addEventListener("focusin", onFocusIn);
176
172
  container.addEventListener("input", onInput, true);
@@ -194,13 +190,15 @@ watch(
194
190
  watch(
195
191
  () => props.scope,
196
192
  () => {
197
- if (props.scope === undefined) closeDropdown();
193
+ // scope 变化时刷新;system 联想不依赖 scope,勿因 undefined 关掉
194
+ const current = dropdown.value;
195
+ if (current) refreshForInput(current.input);
198
196
  }
199
197
  );
200
198
  </script>
201
199
 
202
200
  <template>
203
- <Teleport v-if="dropdown && scope !== undefined" to="body">
201
+ <Teleport v-if="dropdown" to="body">
204
202
  <div
205
203
  data-scope-ac-list
206
204
  class="fixed z-[10120] max-h-44 min-w-[140px] overflow-auto rounded-md border border-border bg-white py-1 text-gray-800 shadow-md"
@@ -0,0 +1,157 @@
1
+ <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
3
+ import {
4
+ filterStorageKeySuggestions,
5
+ listBrowserStorageKeys,
6
+ type StorageKind,
7
+ } from "@arronqzy/blueprint-dsl";
8
+ import { computed, onUnmounted, ref, watch } from "vue";
9
+
10
+ import { cn } from "../utils/cn";
11
+
12
+ const { t } = useI18n();
13
+
14
+ const props = withDefaults(
15
+ defineProps<{
16
+ value: string;
17
+ storages: StorageKind | readonly StorageKind[];
18
+ placeholder?: string;
19
+ }>(),
20
+ {
21
+ placeholder: "",
22
+ }
23
+ );
24
+
25
+ const emit = defineEmits<{
26
+ change: [value: string];
27
+ }>();
28
+
29
+ const open = ref(false);
30
+ const activeIndex = ref(0);
31
+ const keys = ref<string[]>([]);
32
+ const containerRef = ref<HTMLDivElement | null>(null);
33
+
34
+ const storageList = computed(() =>
35
+ Array.isArray(props.storages) ? [...props.storages] : [props.storages]
36
+ );
37
+
38
+ function refreshKeys() {
39
+ keys.value = listBrowserStorageKeys(storageList.value);
40
+ }
41
+
42
+ watch(
43
+ storageList,
44
+ () => {
45
+ refreshKeys();
46
+ },
47
+ { immediate: true, deep: true }
48
+ );
49
+
50
+ const filtered = computed(() =>
51
+ filterStorageKeySuggestions(keys.value, props.value)
52
+ );
53
+
54
+ watch([() => props.value, () => filtered.value.length], () => {
55
+ activeIndex.value = 0;
56
+ });
57
+
58
+ let pointerCleanup: (() => void) | null = null;
59
+
60
+ watch(open, (isOpen) => {
61
+ pointerCleanup?.();
62
+ pointerCleanup = null;
63
+ if (!isOpen) return;
64
+ const onPointerDown = (event: MouseEvent) => {
65
+ if (!containerRef.value?.contains(event.target as globalThis.Node)) {
66
+ open.value = false;
67
+ }
68
+ };
69
+ window.addEventListener("pointerdown", onPointerDown);
70
+ pointerCleanup = () => window.removeEventListener("pointerdown", onPointerDown);
71
+ });
72
+
73
+ onUnmounted(() => {
74
+ pointerCleanup?.();
75
+ });
76
+
77
+ const showSuggestions = computed(() => open.value && keys.value.length > 0);
78
+
79
+ function selectSuggestion(index: number) {
80
+ const item = filtered.value[index];
81
+ if (!item) return;
82
+ emit("change", item);
83
+ open.value = false;
84
+ }
85
+
86
+ function openDropdown() {
87
+ refreshKeys();
88
+ open.value = true;
89
+ }
90
+
91
+ function handleInput(event: Event) {
92
+ emit("change", (event.target as HTMLInputElement).value);
93
+ open.value = true;
94
+ }
95
+
96
+ function handleKeyDown(event: KeyboardEvent) {
97
+ if (!showSuggestions.value || filtered.value.length === 0) {
98
+ if (event.key === "Escape") open.value = false;
99
+ return;
100
+ }
101
+ if (event.key === "ArrowDown") {
102
+ event.preventDefault();
103
+ activeIndex.value = Math.min(activeIndex.value + 1, filtered.value.length - 1);
104
+ } else if (event.key === "ArrowUp") {
105
+ event.preventDefault();
106
+ activeIndex.value = Math.max(activeIndex.value - 1, 0);
107
+ } else if (event.key === "Enter") {
108
+ event.preventDefault();
109
+ selectSuggestion(activeIndex.value);
110
+ } else if (event.key === "Escape") {
111
+ open.value = false;
112
+ }
113
+ }
114
+ </script>
115
+
116
+ <template>
117
+ <div ref="containerRef" class="relative">
118
+ <input
119
+ :value="value"
120
+ :placeholder="placeholder"
121
+ spellcheck="false"
122
+ class="flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 font-mono text-[11px] text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
123
+ @input="handleInput"
124
+ @focus="openDropdown"
125
+ @click="openDropdown"
126
+ @keydown="handleKeyDown"
127
+ />
128
+ <div
129
+ v-if="showSuggestions"
130
+ class="absolute z-[10150] mt-1 max-h-52 w-full overflow-auto rounded-md border border-border bg-popover py-1 text-popover-foreground shadow-md"
131
+ >
132
+ <div
133
+ v-if="filtered.length === 0"
134
+ class="px-2 py-1.5 text-[11px] text-muted-foreground"
135
+ >
136
+ {{ t("blueprint.config.noMatchingStorageKey") }}
137
+ </div>
138
+ <button
139
+ v-for="(key, index) in filtered"
140
+ :key="key"
141
+ type="button"
142
+ class="flex w-full items-center px-2 py-1.5 text-left font-mono text-[11px] hover:bg-accent"
143
+ :class="cn(index === activeIndex && 'bg-accent')"
144
+ @mousedown.prevent
145
+ @click="selectSuggestion(index)"
146
+ >
147
+ {{ key }}
148
+ </button>
149
+ </div>
150
+ <div
151
+ v-else-if="open && keys.length === 0"
152
+ class="absolute z-[10150] mt-1 w-full rounded-md border border-border bg-popover px-2 py-1.5 text-[11px] text-muted-foreground shadow-md"
153
+ >
154
+ {{ t("blueprint.config.noStorageKeys") }}
155
+ </div>
156
+ </div>
157
+ </template>
@@ -19,6 +19,7 @@ import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
19
19
  import { resolveNodeStorageConfig } from "../graph/document";
20
20
  import ConfigHintIcon from "./ConfigHintIcon.vue";
21
21
  import ScopeTemplateAutocompleteHost from "./ScopeTemplateAutocompleteHost.vue";
22
+ import StorageKeyAutocomplete from "./StorageKeyAutocomplete.vue";
22
23
 
23
24
  const { t } = useI18n();
24
25
 
@@ -112,6 +113,12 @@ function handleSetValueChange(value: string) {
112
113
  patchStorageConfig(props.node, { set: { value: String(value ?? "") } })
113
114
  );
114
115
  }
116
+
117
+ const setKeySuggestionStorages = computed(() =>
118
+ storageConfig.value.set.storages.length > 0
119
+ ? storageConfig.value.set.storages
120
+ : (["session", "local"] as const)
121
+ );
115
122
  </script>
116
123
 
117
124
  <template>
@@ -148,13 +155,11 @@ function handleSetValueChange(value: string) {
148
155
  </label>
149
156
  <label class="block space-y-1">
150
157
  <span class="text-muted-foreground">{{ t("blueprint.config.storageKey") }}</span>
151
- <Input
152
- size="small"
158
+ <StorageKeyAutocomplete
153
159
  :value="storageConfig.read.key"
154
- spellcheck="false"
155
- class="font-mono text-[11px]"
160
+ :storages="storageConfig.read.storage"
156
161
  placeholder="key"
157
- @update:value="(v) => handleReadKeyChange(String(v ?? ''))"
162
+ @change="handleReadKeyChange"
158
163
  />
159
164
  </label>
160
165
  </div>
@@ -187,13 +192,11 @@ function handleSetValueChange(value: string) {
187
192
  </div>
188
193
  <label class="block space-y-1">
189
194
  <span class="text-muted-foreground">{{ t("blueprint.config.storageKey") }}</span>
190
- <Input
191
- size="small"
195
+ <StorageKeyAutocomplete
192
196
  :value="storageConfig.set.key"
193
- spellcheck="false"
194
- class="font-mono text-[11px]"
197
+ :storages="setKeySuggestionStorages"
195
198
  placeholder="key"
196
- @update:value="(v) => handleSetKeyChange(String(v ?? ''))"
199
+ @change="handleSetKeyChange"
197
200
  />
198
201
  </label>
199
202
  <label class="block space-y-1">
@@ -2,8 +2,8 @@
2
2
  import { BaseEdge, getSmoothStepPath, type EdgeProps } from "@vue-flow/core";
3
3
  import { computed } from "vue";
4
4
 
5
- const EDGE_STROKE = "#2563eb";
6
- const EDGE_WIDTH = 2.5;
5
+ const EDGE_STROKE = "#94a3b8";
6
+ const EDGE_WIDTH = 1.75;
7
7
 
8
8
  const props = defineProps<EdgeProps>();
9
9
 
@@ -3,6 +3,7 @@ import {
3
3
  createNodeId,
4
4
  defaultBlueprintNodeLabel,
5
5
  filterInvalidBlueprintEdges,
6
+ isDefaultBlueprintNodeLabel,
6
7
  patchNodeConfigSource,
7
8
  resolveNodeClockConfig,
8
9
  resolveNodeEventConfig,
@@ -218,6 +219,14 @@ export class BlueprintGraph {
218
219
  nodes: this.document.nodes.map((n) => {
219
220
  if (n.id !== nodeId) return n;
220
221
  const nextNode = { ...n, ...configPatch, ...patch };
222
+ if (
223
+ patch.configSource &&
224
+ patch.configSource !== exists.configSource &&
225
+ patch.label === undefined &&
226
+ isDefaultBlueprintNodeLabel(exists.label)
227
+ ) {
228
+ nextNode.label = defaultBlueprintNodeLabel(patch.configSource);
229
+ }
221
230
  if (
222
231
  patch.viewElementIds !== undefined ||
223
232
  patch.viewElementId !== undefined
@@ -289,6 +289,18 @@ export function defaultBlueprintNodeLabel(
289
289
  return getBlueprintNodeTypeLabel(t, configSource);
290
290
  }
291
291
 
292
+ /** 当前名称是否仍是「类型默认名」(空或等于任一类型文案) */
293
+ export function isDefaultBlueprintNodeLabel(
294
+ label: string | undefined | null,
295
+ t: TranslateFn = defaultTranslate()
296
+ ): boolean {
297
+ const trimmed = (label ?? "").trim();
298
+ if (!trimmed) return true;
299
+ return (
300
+ Object.keys(BLUEPRINT_CONFIG_TYPE_LABEL_KEYS) as BlueprintConfigSource[]
301
+ ).some((src) => getBlueprintNodeTypeLabel(t, src) === trimmed);
302
+ }
303
+
292
304
  export function resolveNodeFetchConfig(
293
305
  node: Pick<BlueprintGraphNode, "fetchConfig">
294
306
  ): FetchRequestConfig {
@@ -6,8 +6,8 @@ import type { BlueprintGraph } from "./blueprint-graph";
6
6
 
7
7
  /** 边线描边(写入 edge.style;使用 hex 避免 SVG style 解析 hsl 异常) */
8
8
  export const BP_EDGE_STYLE = {
9
- stroke: "#2563eb",
10
- strokeWidth: 2.5,
9
+ stroke: "#94a3b8",
10
+ strokeWidth: 1.75,
11
11
  } as const;
12
12
 
13
13
  export const BP_FLOW_EDGE_TYPE = "blueprintSmooth" as const;
package/src/index.ts CHANGED
@@ -15,6 +15,33 @@ export { default as BlueprintExecutionLogPanel } from "./components/BlueprintExe
15
15
  export type { BlueprintExecutionLogPanelProps } from "./components/BlueprintExecutionLogPanel.vue";
16
16
  export { default as BlueprintMetaDialog } from "./components/BlueprintMetaDialog.vue";
17
17
  export type { BlueprintMetaDialogProps } from "./components/BlueprintMetaDialog.vue";
18
+ export { default as ApiCollectionManagerDialog } from "./components/ApiCollectionManagerDialog.vue";
19
+ export type {
20
+ ApiCollectionManagerDialogProps,
21
+ ApiCollectionUploadStartInfo,
22
+ ApiCollectionUploadSuccessInfo,
23
+ } from "./components/ApiCollectionManagerDialog.vue";
24
+ export {
25
+ hydrateApiCollections,
26
+ getApiCollectionRecords,
27
+ getApiCollectionById,
28
+ getApiCollectionListItems,
29
+ upsertApiCollectionFromJson,
30
+ removeApiCollection,
31
+ subscribeApiCollections,
32
+ useApiCollections,
33
+ useApiCollectionListItems,
34
+ } from "./library/api-collection-store";
35
+ export type {
36
+ UpsertApiCollectionInput,
37
+ UpsertApiCollectionResult,
38
+ } from "./library/api-collection-store";
39
+ export {
40
+ listApiCollectionRecords,
41
+ getApiCollectionRecord,
42
+ putApiCollectionRecord,
43
+ deleteApiCollectionRecord,
44
+ } from "./library/api-collection-db";
18
45
  export { default as BlueprintNodeSwitchTaskDialog } from "./components/BlueprintNodeSwitchTaskDialog.vue";
19
46
  export type { BlueprintNodeSwitchTaskDialogProps } from "./components/BlueprintNodeSwitchTaskDialog.vue";
20
47
  export type { ExecutionLogSettings } from "./library/execution-log-settings";
@@ -0,0 +1,98 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
2
+ import type { ApiCollectionRecord } from "@arronqzy/blueprint-dsl";
3
+
4
+ const DB_NAME = "arronqzy-api-collections";
5
+ const DB_VERSION = 1;
6
+ const STORE_NAME = "collections";
7
+
8
+ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
9
+ return new Promise((resolve, reject) => {
10
+ const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
11
+ request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
12
+ request.onupgradeneeded = () => {
13
+ const db = request.result;
14
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
15
+ const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
16
+ store.createIndex("updatedAt", "updatedAt", { 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
+ nameSpace?: string | null
27
+ ): Promise<T> {
28
+ return openDb(nameSpace).then(
29
+ (db) =>
30
+ new Promise<T>((resolve, reject) => {
31
+ const tx = db.transaction(STORE_NAME, mode);
32
+ const store = tx.objectStore(STORE_NAME);
33
+ const request = runner(store);
34
+ let result!: T;
35
+
36
+ request.onerror = () =>
37
+ reject(request.error ?? new Error("indexeddb-request-failed"));
38
+ request.onsuccess = () => {
39
+ result = request.result as T;
40
+ };
41
+
42
+ tx.oncomplete = () => {
43
+ db.close();
44
+ resolve(result);
45
+ };
46
+ tx.onerror = () => {
47
+ db.close();
48
+ reject(tx.error ?? new Error("indexeddb-transaction-failed"));
49
+ };
50
+ })
51
+ );
52
+ }
53
+
54
+ export async function listApiCollectionRecords(
55
+ nameSpace?: string | null
56
+ ): Promise<ApiCollectionRecord[]> {
57
+ const records = await runTransaction<ApiCollectionRecord[]>(
58
+ "readonly",
59
+ (store) => store.getAll(),
60
+ nameSpace
61
+ );
62
+ return records.sort((a, b) => b.updatedAt - a.updatedAt);
63
+ }
64
+
65
+ export async function getApiCollectionRecord(
66
+ id: string,
67
+ nameSpace?: string | null
68
+ ): Promise<ApiCollectionRecord | null> {
69
+ const record = await runTransaction<ApiCollectionRecord | undefined>(
70
+ "readonly",
71
+ (store) => store.get(id),
72
+ nameSpace
73
+ );
74
+ return record ?? null;
75
+ }
76
+
77
+ export async function putApiCollectionRecord(
78
+ record: ApiCollectionRecord,
79
+ nameSpace?: string | null
80
+ ): Promise<ApiCollectionRecord> {
81
+ await runTransaction<IDBValidKey>(
82
+ "readwrite",
83
+ (store) => store.put(record),
84
+ nameSpace
85
+ );
86
+ return record;
87
+ }
88
+
89
+ export async function deleteApiCollectionRecord(
90
+ id: string,
91
+ nameSpace?: string | null
92
+ ): Promise<void> {
93
+ await runTransaction<undefined>(
94
+ "readwrite",
95
+ (store) => store.delete(id),
96
+ nameSpace
97
+ );
98
+ }
@@ -0,0 +1,143 @@
1
+ import { computed, onScopeDispose, ref, type ComputedRef, type Ref } from "vue";
2
+ import {
3
+ apiCollectionToListItem,
4
+ createApiCollectionId,
5
+ parseApiCollectionJson,
6
+ type ApiCollectionDocument,
7
+ type ApiCollectionListItem,
8
+ type ApiCollectionRecord,
9
+ } from "@arronqzy/blueprint-dsl";
10
+
11
+ import {
12
+ deleteApiCollectionRecord,
13
+ listApiCollectionRecords,
14
+ putApiCollectionRecord,
15
+ } from "./api-collection-db";
16
+
17
+ type Listener = () => void;
18
+
19
+ let nameSpaceRef: string | null | undefined = null;
20
+ let records: ApiCollectionRecord[] = [];
21
+ let hydrated = false;
22
+ const listeners = new Set<Listener>();
23
+
24
+ function emit() {
25
+ for (const listener of listeners) listener();
26
+ }
27
+
28
+ function sortRecords(list: ApiCollectionRecord[]): ApiCollectionRecord[] {
29
+ return [...list].sort((a, b) => b.updatedAt - a.updatedAt);
30
+ }
31
+
32
+ export function getApiCollectionRecords(): ApiCollectionRecord[] {
33
+ return records;
34
+ }
35
+
36
+ export function getApiCollectionListItems(): ApiCollectionListItem[] {
37
+ return records.map(apiCollectionToListItem);
38
+ }
39
+
40
+ export function getApiCollectionById(id: string): ApiCollectionRecord | null {
41
+ return records.find((item) => item.id === id) ?? null;
42
+ }
43
+
44
+ export function subscribeApiCollections(listener: Listener): () => void {
45
+ listeners.add(listener);
46
+ return () => listeners.delete(listener);
47
+ }
48
+
49
+ export async function hydrateApiCollections(
50
+ nameSpace?: string | null
51
+ ): Promise<ApiCollectionRecord[]> {
52
+ nameSpaceRef = nameSpace;
53
+ const persisted = await listApiCollectionRecords(nameSpace);
54
+ const memoryOnly = records.filter((item) => !item.persistLocal);
55
+ const persistedIds = new Set(persisted.map((item) => item.id));
56
+ records = sortRecords([
57
+ ...persisted,
58
+ ...memoryOnly.filter((item) => !persistedIds.has(item.id)),
59
+ ]);
60
+ hydrated = true;
61
+ emit();
62
+ return records;
63
+ }
64
+
65
+ export type UpsertApiCollectionInput = {
66
+ id?: string;
67
+ rawJson: string;
68
+ persistLocal: boolean;
69
+ /** 未写 document.name 时用文件名 */
70
+ fallbackName?: string;
71
+ };
72
+
73
+ export type UpsertApiCollectionResult =
74
+ | { ok: true; record: ApiCollectionRecord }
75
+ | { ok: false; error: string };
76
+
77
+ export async function upsertApiCollectionFromJson(
78
+ input: UpsertApiCollectionInput
79
+ ): Promise<UpsertApiCollectionResult> {
80
+ const parsed = parseApiCollectionJson(input.rawJson);
81
+ if (!parsed.ok) return parsed;
82
+
83
+ const document: ApiCollectionDocument = {
84
+ ...parsed.document,
85
+ name:
86
+ parsed.document.name.trim() ||
87
+ input.fallbackName?.replace(/\.json$/i, "").trim() ||
88
+ parsed.document.name,
89
+ };
90
+ const now = Date.now();
91
+ const existing = input.id
92
+ ? records.find((item) => item.id === input.id) ?? null
93
+ : null;
94
+ const record: ApiCollectionRecord = {
95
+ id: existing?.id ?? createApiCollectionId(),
96
+ name: document.name,
97
+ persistLocal: input.persistLocal,
98
+ document,
99
+ rawJson: JSON.stringify(document, null, 2),
100
+ createdAt: existing?.createdAt ?? now,
101
+ updatedAt: now,
102
+ };
103
+
104
+ if (record.persistLocal) {
105
+ await putApiCollectionRecord(record, nameSpaceRef);
106
+ } else if (existing?.persistLocal) {
107
+ await deleteApiCollectionRecord(record.id, nameSpaceRef);
108
+ }
109
+
110
+ const next = records.filter((item) => item.id !== record.id);
111
+ next.push(record);
112
+ records = sortRecords(next);
113
+ emit();
114
+ return { ok: true, record };
115
+ }
116
+
117
+ export async function removeApiCollection(id: string): Promise<void> {
118
+ const existing = records.find((item) => item.id === id);
119
+ if (existing?.persistLocal || hydrated) {
120
+ try {
121
+ await deleteApiCollectionRecord(id, nameSpaceRef);
122
+ } catch {
123
+ // 内存-only 记录在库里可能不存在
124
+ }
125
+ }
126
+ records = records.filter((item) => item.id !== id);
127
+ emit();
128
+ }
129
+
130
+ /** Vue composable:返回响应式集合列表 */
131
+ export function useApiCollections(): Ref<ApiCollectionRecord[]> {
132
+ const list = ref<ApiCollectionRecord[]>([...records]);
133
+ const unsubscribe = subscribeApiCollections(() => {
134
+ list.value = [...records];
135
+ });
136
+ onScopeDispose(unsubscribe);
137
+ return list;
138
+ }
139
+
140
+ export function useApiCollectionListItems(): ComputedRef<ApiCollectionListItem[]> {
141
+ const list = useApiCollections();
142
+ return computed(() => list.value.map(apiCollectionToListItem));
143
+ }
@@ -22,6 +22,7 @@ const executionTone = resolveBlueprintNodeExecutionTone(nodeData);
22
22
  :class="
23
23
  cn(
24
24
  'bp-node bp-node--and',
25
+ nodeData.isSelected && 'bp-node--selected',
25
26
  executionTone === 'success' && 'bp-node--execution-true',
26
27
  executionTone === 'error' && 'bp-node--execution-false'
27
28
  )
@@ -32,7 +33,7 @@ const executionTone = resolveBlueprintNodeExecutionTone(nodeData);
32
33
  :position="Position.Left"
33
34
  id="inA"
34
35
  class="bp-flow-handle bp-flow-handle--target"
35
- :style="{ top: '35%' }"
36
+ :style="{ top: '32%' }"
36
37
  :title="t('blueprint.node.andInA')"
37
38
  />
38
39
  <Handle
@@ -40,7 +41,7 @@ const executionTone = resolveBlueprintNodeExecutionTone(nodeData);
40
41
  :position="Position.Left"
41
42
  id="inB"
42
43
  class="bp-flow-handle bp-flow-handle--target"
43
- :style="{ top: '65%' }"
44
+ :style="{ top: '68%' }"
44
45
  :title="t('blueprint.node.andInB')"
45
46
  />
46
47
  <BlueprintNodeCard
@@ -1,7 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import { useI18n } from "@arronqzy/i18n/vue";
3
3
  import type { NodeProps } from "@vue-flow/core";
4
- import { resolveBlueprintNodeTypeLabel } from "../graph/document";
4
+ import { computed } from "vue";
5
+ import {
6
+ resolveBlueprintConfigSource,
7
+ resolveBlueprintNodeTypeLabel,
8
+ } from "../graph/document";
5
9
  import { resolveBlueprintNodeSummary } from "../graph/node-summary";
6
10
  import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
7
11
  import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
@@ -13,6 +17,9 @@ const { t } = useI18n();
13
17
  const props = defineProps<NodeProps<BlueprintFlowNodeData>>();
14
18
  const onSelect = useBlueprintNodeSelect();
15
19
  const nodeData = props.data;
20
+ const variant = computed(() =>
21
+ resolveBlueprintConfigSource(nodeData) === "view" ? "view" : "blueprint"
22
+ );
16
23
  </script>
17
24
 
18
25
  <template>
@@ -21,7 +28,7 @@ const nodeData = props.data;
21
28
  :label="nodeData.label"
22
29
  :meta="resolveBlueprintNodeTypeLabel(nodeData, t)"
23
30
  :subtitle="resolveBlueprintNodeSummary(nodeData, t)"
24
- variant="blueprint"
31
+ :variant="variant"
25
32
  :selected="Boolean(nodeData.isSelected)"
26
33
  :execution-tone="resolveBlueprintNodeExecutionTone(nodeData)"
27
34
  @select="onSelect"