@arronqzy/vue-view 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +50 -0
  2. package/package.json +49 -0
  3. package/src/env.d.ts +62 -0
  4. package/src/index.ts +4 -0
  5. package/src/panel/VueViewOnlinePreview.vue +276 -0
  6. package/src/panel/VueViewPanel.vue +871 -0
  7. package/src/panel/components/ConfigHintIcon.vue +34 -0
  8. package/src/panel/components/ElementsLayer.vue +165 -0
  9. package/src/panel/components/MaterialPreview.vue +135 -0
  10. package/src/panel/components/MaterialSidebar.vue +526 -0
  11. package/src/panel/components/MaterialSidebarTreeNode.vue +305 -0
  12. package/src/panel/components/MoveableLayer.vue +859 -0
  13. package/src/panel/components/PanelCanvas.vue +630 -0
  14. package/src/panel/components/PanelConfigSidebar.vue +397 -0
  15. package/src/panel/components/PanelRulers.vue +177 -0
  16. package/src/panel/components/SelectLayer.vue +115 -0
  17. package/src/panel/components/ViewElementScopePanel.vue +76 -0
  18. package/src/panel/components/WorkspaceConfigSidebar.vue +147 -0
  19. package/src/panel/components/WorkspaceProjectNav.vue +192 -0
  20. package/src/panel/components/WorkspaceStageSplit.vue +258 -0
  21. package/src/panel/components/config/ConfigColorField.vue +52 -0
  22. package/src/panel/components/config/ConfigFieldGroup.vue +20 -0
  23. package/src/panel/components/config/ConfigSection.vue +50 -0
  24. package/src/panel/components/config/PanelConfigAudioSection.vue +256 -0
  25. package/src/panel/components/config/PanelConfigChartSection.vue +650 -0
  26. package/src/panel/components/config/PanelConfigGeometrySection.vue +209 -0
  27. package/src/panel/components/config/PanelConfigGridChildSpan.vue +68 -0
  28. package/src/panel/components/config/PanelConfigGridSection.vue +103 -0
  29. package/src/panel/components/config/PanelConfigImageSection.vue +136 -0
  30. package/src/panel/components/config/PanelConfigMultiSelect.vue +434 -0
  31. package/src/panel/components/config/PanelConfigNodeInfo.vue +165 -0
  32. package/src/panel/components/config/PanelConfigReferenceSection.vue +77 -0
  33. package/src/panel/components/config/PanelConfigStyleSections.vue +208 -0
  34. package/src/panel/components/config/PanelConfigTextSection.vue +195 -0
  35. package/src/panel/components/config/PanelConfigVideoSection.vue +107 -0
  36. package/src/panel/components/config/shared.ts +74 -0
  37. package/src/panel/components/elementsLayerNodes.ts +830 -0
  38. package/src/panel/components/materialSidebarData.ts +85 -0
  39. package/src/panel/components/scope-config/ScopeConfigProvider.vue +153 -0
  40. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +234 -0
  41. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +192 -0
  42. package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +42 -0
  43. package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +20 -0
  44. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +63 -0
  45. package/src/panel/components/scope-config/scopeConfigContext.ts +17 -0
  46. package/src/panel/components/scope-config/useScopeConfig.ts +11 -0
  47. package/src/panel/constants/messages.ts +34 -0
  48. package/src/panel/constants/zIndex.ts +6 -0
  49. package/src/panel/hooks/usePanelElements.ts +1075 -0
  50. package/src/panel/hooks/useRafThrottledScroll.ts +25 -0
  51. package/src/panel/hooks/useWorkspaceProjects.ts +240 -0
  52. package/src/panel/lib/panel-ruler-canvas.ts +139 -0
  53. package/src/panel/library/workspace-project-cache.ts +23 -0
  54. package/src/panel/library/workspace-project-db.ts +111 -0
  55. package/src/panel/library/workspace-project-sync.ts +41 -0
  56. package/src/panel/library/workspace-snapshot.ts +30 -0
  57. package/src/panel/parseOnlinePreviewSearchParams.ts +13 -0
  58. package/src/panel/scope/view-scope-store.ts +82 -0
  59. package/src/panel/types.ts +127 -0
  60. package/src/panel/utils/chartOptionBuilder.ts +327 -0
  61. package/src/panel/utils/gridPlacement.ts +189 -0
  62. package/src/panel/utils/mappingLayerOps.ts +142 -0
  63. package/src/panel/utils/panelElementDefaults.ts +161 -0
  64. package/src/panel/utils/panelElementNodes.ts +35 -0
  65. package/src/panel/utils/panelStateIO.ts +124 -0
  66. package/src/panel/utils/scope-autocomplete.ts +114 -0
  67. package/src/panel/utils/scope-field-labels.ts +46 -0
  68. package/src/panel/utils/scope-template-chart.ts +92 -0
  69. package/src/panel/utils/scope-template-preview.ts +124 -0
  70. package/src/panel/utils/scope-template-spread.ts +229 -0
  71. package/src/panel/utils/scope-template-warnings.ts +243 -0
  72. package/src/panel/utils/scope-template.ts +97 -0
  73. package/src/panel/utils/updateElementDraft.ts +221 -0
  74. package/src/panel/viewportZoom.ts +26 -0
  75. package/src/tailwind.css +43 -0
@@ -0,0 +1,76 @@
1
+ <script setup lang="ts">
2
+ import { formatViewElementScope } from "../utils/scope-template";
3
+ import ConfigHintIcon from "./ConfigHintIcon.vue";
4
+ import ScopeTemplateUsageHint from "./scope-config/ScopeTemplateUsageHint.vue";
5
+ import { onMounted, ref, watch } from "vue";
6
+
7
+ const SCOPE_COLLAPSE_STORAGE_KEY = "panel:config-scope-collapsed";
8
+
9
+ const props = defineProps<{
10
+ scope: unknown;
11
+ }>();
12
+
13
+ const isCollapsed = ref(false);
14
+ const scopeUpdated = ref(false);
15
+ const prevScopeSerialized = ref<string | null>(null);
16
+
17
+ const themedScrollbarClass =
18
+ "scrollbar-thin [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-muted/40 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-thumb]:hover:bg-border";
19
+
20
+ const formattedScope = ref(formatViewElementScope(props.scope));
21
+
22
+ onMounted(() => {
23
+ const stored = window.localStorage.getItem(SCOPE_COLLAPSE_STORAGE_KEY);
24
+ if (stored === "1") isCollapsed.value = true;
25
+ });
26
+
27
+ watch(isCollapsed, (next) => {
28
+ window.localStorage.setItem(SCOPE_COLLAPSE_STORAGE_KEY, next ? "1" : "0");
29
+ });
30
+
31
+ watch(
32
+ () => props.scope,
33
+ (next) => {
34
+ const serialized = formatViewElementScope(next);
35
+ if (
36
+ prevScopeSerialized.value !== null &&
37
+ prevScopeSerialized.value !== serialized
38
+ ) {
39
+ scopeUpdated.value = true;
40
+ }
41
+ prevScopeSerialized.value = serialized;
42
+ formattedScope.value = serialized;
43
+ },
44
+ { immediate: true }
45
+ );
46
+ </script>
47
+
48
+ <template>
49
+ <div class="mt-2 border-t border-border/50 pt-2">
50
+ <div class="flex items-center justify-between gap-2">
51
+ <div class="flex min-w-0 items-center gap-1.5">
52
+ <div class="text-[11px] font-medium text-foreground">Scope 数据</div>
53
+ <ConfigHintIcon label="Scope 模版" content-class="max-w-[380px]">
54
+ <ScopeTemplateUsageHint />
55
+ </ConfigHintIcon>
56
+ <span
57
+ v-if="scopeUpdated"
58
+ class="shrink-0 rounded bg-sky-500/15 px-1.5 py-0.5 text-[10px] font-medium text-sky-700 dark:text-sky-300"
59
+ >
60
+ 已更新
61
+ </span>
62
+ </div>
63
+ <button
64
+ type="button"
65
+ class="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
66
+ @click="isCollapsed = !isCollapsed"
67
+ >
68
+ {{ isCollapsed ? "展开 Scope" : "收起 Scope" }}
69
+ </button>
70
+ </div>
71
+ <pre
72
+ v-if="!isCollapsed"
73
+ :class="`mt-2 max-h-[200px] overflow-auto rounded-md border border-border/70 bg-muted/30 p-2 font-mono text-[10px] leading-relaxed text-foreground ${themedScrollbarClass}`"
74
+ >{{ formattedScope }}</pre>
75
+ </div>
76
+ </template>
@@ -0,0 +1,147 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { Empty } from "ant-design-vue";
4
+ import {
5
+ BlueprintExecutionLogPanel,
6
+ BlueprintNodeConfigSidebar,
7
+ type BlueprintGraphNode,
8
+ type ExecutionLogSettings,
9
+ type ExecutionTraceEntry,
10
+ } from "@arronqzy/vue-blueprint";
11
+ import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../types";
12
+ import PanelConfigSidebar from "./PanelConfigSidebar.vue";
13
+
14
+ export type WorkspaceConfigFocus = "view" | "blueprint" | "blueprint-log";
15
+
16
+ export type PanelConfigSidebarProps = {
17
+ selectedElement: PanelElement | null;
18
+ selectedElements?: PanelElement[];
19
+ layers: PanelLayer[];
20
+ updateElement: (
21
+ id: string,
22
+ patch: Partial<PanelElement>,
23
+ options?: { batchId?: string; meta?: Record<string, unknown> }
24
+ ) => void;
25
+ setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
26
+ nodeZOrderLabel?: string;
27
+ onExcludeSelectedNode?: (nodeId: string) => void;
28
+ onAdjustNodeZOrder?: (
29
+ nodeId: string,
30
+ action: "bringForward" | "sendBackward" | "bringToFront" | "sendToBack"
31
+ ) => void;
32
+ viewElementScope?: unknown;
33
+ };
34
+
35
+ export type BlueprintExecutionLogViewProps = {
36
+ entries: ExecutionTraceEntry[];
37
+ settings: ExecutionLogSettings;
38
+ onUpdateSettings: (patch: Partial<ExecutionLogSettings>) => void;
39
+ onSave: () => void;
40
+ onExport: () => void;
41
+ onClear: () => void;
42
+ onClearAllSaved?: () => void | Promise<void>;
43
+ onApplyRetention: () => void;
44
+ hasSavedRuns?: boolean;
45
+ lifecyclePhase?: string;
46
+ };
47
+
48
+ export type WorkspaceConfigSidebarProps = Omit<
49
+ PanelConfigSidebarProps,
50
+ "selectedElement" | "selectedElements"
51
+ > & {
52
+ configFocus: WorkspaceConfigFocus;
53
+ executionLog?: BlueprintExecutionLogViewProps;
54
+ selectedBlueprintNode: BlueprintGraphNode | null;
55
+ allowFalseSignalPropagation?: boolean;
56
+ onUpdateAllowFalseSignalPropagation?: (value: boolean) => void;
57
+ onUpdateBlueprintNode: (
58
+ nodeId: string,
59
+ patch: Partial<
60
+ Pick<
61
+ BlueprintGraphNode,
62
+ | "label"
63
+ | "role"
64
+ | "nodeType"
65
+ | "configSource"
66
+ | "viewElementId"
67
+ | "viewElementIds"
68
+ | "nestedBlueprintId"
69
+ | "libraryBlueprintId"
70
+ | "lifecyclePhase"
71
+ | "fetchConfig"
72
+ | "jsonConfig"
73
+ | "logicConfig"
74
+ | "clockConfig"
75
+ >
76
+ >
77
+ ) => void;
78
+ blueprintLibraryOptions?: { id: string; label: string }[];
79
+ selectedElement: PanelElement | null;
80
+ selectedElements?: PanelElement[];
81
+ allViewElements: PanelElement[];
82
+ };
83
+
84
+ const props = withDefaults(
85
+ defineProps<WorkspaceConfigSidebarProps>(),
86
+ {
87
+ allowFalseSignalPropagation: false,
88
+ blueprintLibraryOptions: () => [],
89
+ selectedElements: () => [],
90
+ }
91
+ );
92
+
93
+ const viewElementOptions = computed(() =>
94
+ props.allViewElements.map((el) => ({
95
+ id: el.id,
96
+ label: el.name?.trim() || el.chart?.title || el.materialType || el.id,
97
+ }))
98
+ );
99
+ </script>
100
+
101
+ <template>
102
+ <div class="flex h-full flex-col overflow-hidden bg-white">
103
+ <PanelConfigSidebar
104
+ v-if="configFocus === 'view'"
105
+ :selected-element="selectedElement"
106
+ :selected-elements="selectedElements"
107
+ :layers="layers"
108
+ :update-element="updateElement"
109
+ :view-element-scope="viewElementScope"
110
+ :set-reference-copy-mode="setReferenceCopyMode"
111
+ :node-z-order-label="nodeZOrderLabel"
112
+ :on-exclude-selected-node="onExcludeSelectedNode"
113
+ :on-adjust-node-z-order="onAdjustNodeZOrder"
114
+ />
115
+
116
+ <BlueprintExecutionLogPanel
117
+ v-else-if="configFocus === 'blueprint-log' && executionLog"
118
+ :entries="executionLog.entries"
119
+ :settings="executionLog.settings"
120
+ :on-update-settings="executionLog.onUpdateSettings"
121
+ :on-save="() => { if (executionLog) void executionLog.onSave(); }"
122
+ :on-export="executionLog.onExport"
123
+ :on-clear="executionLog.onClear"
124
+ :on-clear-all-saved="executionLog.onClearAllSaved"
125
+ :has-saved-runs="executionLog.hasSavedRuns"
126
+ :on-apply-retention="() => { if (executionLog) void executionLog.onApplyRetention(); }"
127
+ :lifecycle-phase="executionLog.lifecyclePhase"
128
+ />
129
+
130
+ <div
131
+ v-else-if="!selectedBlueprintNode"
132
+ class="flex h-full flex-col overflow-hidden bg-background text-foreground"
133
+ >
134
+ <Empty class="py-10" description="在蓝图面板中选中一个节点后,这里会显示对应的配置。" />
135
+ </div>
136
+
137
+ <BlueprintNodeConfigSidebar
138
+ v-else
139
+ :node="selectedBlueprintNode"
140
+ :allow-false-signal-propagation="allowFalseSignalPropagation"
141
+ :on-update-allow-false-signal-propagation="onUpdateAllowFalseSignalPropagation"
142
+ :view-element-options="viewElementOptions"
143
+ :blueprint-library-options="blueprintLibraryOptions"
144
+ :on-update-node="onUpdateBlueprintNode"
145
+ />
146
+ </div>
147
+ </template>
@@ -0,0 +1,192 @@
1
+ <script setup lang="ts">
2
+ import { Button, Dropdown, Menu, Modal, Tooltip } from "ant-design-vue";
3
+ import { computed, h, ref } from "vue";
4
+ import type { WorkspaceProjectListItem } from "../library/workspace-project-db";
5
+
6
+ function formatUpdatedAt(ts: number): string {
7
+ try {
8
+ return new Date(ts).toLocaleString();
9
+ } catch {
10
+ return String(ts);
11
+ }
12
+ }
13
+
14
+ const props = defineProps<{
15
+ projects: WorkspaceProjectListItem[];
16
+ activeProjectId: string | null;
17
+ activeProjectName: string | null;
18
+ dirty: boolean;
19
+ }>();
20
+
21
+ const emit = defineEmits<{
22
+ createProject: [];
23
+ openProject: [id: string];
24
+ syncProject: [];
25
+ deleteProject: [id: string];
26
+ previewProject: [id: string, options?: { syncFirst?: boolean }];
27
+ }>();
28
+
29
+ const pendingDeleteId = ref<string | null>(null);
30
+ const busy = ref(false);
31
+
32
+ const pendingDeleteProject = computed(
33
+ () => props.projects.find((p) => p.id === pendingDeleteId.value) ?? null
34
+ );
35
+
36
+ const deleteModalOpen = computed({
37
+ get: () => pendingDeleteId.value !== null,
38
+ set: (open: boolean) => {
39
+ if (!open) pendingDeleteId.value = null;
40
+ },
41
+ });
42
+
43
+ async function runAction(action: () => Promise<unknown> | unknown) {
44
+ if (busy.value) return;
45
+ busy.value = true;
46
+ try {
47
+ await action();
48
+ } finally {
49
+ busy.value = false;
50
+ }
51
+ }
52
+
53
+ function confirmDelete() {
54
+ const id = pendingDeleteId.value;
55
+ pendingDeleteId.value = null;
56
+ if (!id) return;
57
+ void runAction(() => emit("deleteProject", id));
58
+ }
59
+
60
+ const projectMenu = computed(() =>
61
+ h(
62
+ Menu,
63
+ {},
64
+ {
65
+ default: () =>
66
+ props.projects.length === 0
67
+ ? [h(Menu.Item, { key: "empty", disabled: true }, () => "暂无已保存工作区")]
68
+ : props.projects.map((project) =>
69
+ h(
70
+ "div",
71
+ { key: project.id, class: "px-1 py-0.5" },
72
+ h("div", { class: "flex items-center gap-1" }, [
73
+ h(
74
+ "button",
75
+ {
76
+ type: "button",
77
+ class: [
78
+ "min-w-0 flex-1 rounded px-2 py-1.5 text-left text-xs hover:bg-accent",
79
+ props.activeProjectId === project.id
80
+ ? "bg-accent/60 font-medium"
81
+ : "",
82
+ ].join(" "),
83
+ onClick: () =>
84
+ void runAction(() => emit("openProject", project.id)),
85
+ },
86
+ [
87
+ h("div", { class: "truncate" }, project.name),
88
+ h(
89
+ "div",
90
+ { class: "truncate text-[10px] text-muted-foreground" },
91
+ formatUpdatedAt(project.updatedAt)
92
+ ),
93
+ ]
94
+ ),
95
+ h(
96
+ Button,
97
+ {
98
+ type: "text",
99
+ size: "small",
100
+ class: "h-7 shrink-0 px-2 text-[10px]",
101
+ onClick: () =>
102
+ void runAction(() =>
103
+ emit("previewProject", project.id, {
104
+ syncFirst: props.activeProjectId === project.id,
105
+ })
106
+ ),
107
+ },
108
+ () => "预览"
109
+ ),
110
+ h(
111
+ Button,
112
+ {
113
+ type: "text",
114
+ size: "small",
115
+ danger: true,
116
+ class: "h-7 shrink-0 px-2 text-[10px]",
117
+ onClick: () => {
118
+ pendingDeleteId.value = project.id;
119
+ },
120
+ },
121
+ () => "删除"
122
+ ),
123
+ ])
124
+ )
125
+ ),
126
+ }
127
+ )
128
+ );
129
+ </script>
130
+
131
+ <template>
132
+ <div class="flex items-center gap-1.5 border-l border-border pl-2">
133
+ <Tooltip
134
+ title="以当前产物名称新建一条 IndexedDB 工作区记录,不会覆盖已有工作区"
135
+ :overlay-style="{ zIndex: 10100 }"
136
+ :mouse-enter-delay="0.15"
137
+ >
138
+ <Button
139
+ size="small"
140
+ class="h-7 px-2 text-xs"
141
+ :disabled="busy"
142
+ @click="runAction(() => emit('createProject'))"
143
+ >
144
+ 创建工作区
145
+ </Button>
146
+ </Tooltip>
147
+
148
+ <Tooltip
149
+ v-if="activeProjectId"
150
+ :title="
151
+ dirty
152
+ ? `同步更新「${activeProjectName ?? '当前工作区'}」到 IndexedDB`
153
+ : '当前工作区已与 IndexedDB 同步'
154
+ "
155
+ :overlay-style="{ zIndex: 10100 }"
156
+ :mouse-enter-delay="0.15"
157
+ >
158
+ <Button
159
+ size="small"
160
+ class="h-7 px-2 text-xs"
161
+ :type="dirty ? 'primary' : 'default'"
162
+ :disabled="busy || !dirty"
163
+ @click="runAction(() => emit('syncProject'))"
164
+ >
165
+ 同步{{ dirty ? " *" : "" }}
166
+ </Button>
167
+ </Tooltip>
168
+
169
+ <Dropdown :trigger="['click']" :disabled="busy" :overlay-style="{ zIndex: 10100 }">
170
+ <Button size="small" class="h-7 max-w-[200px] truncate px-2 text-xs">
171
+ {{ activeProjectName ? `工作区:${activeProjectName}` : "已保存工作区" }}
172
+ </Button>
173
+ <template #overlay>
174
+ <component :is="projectMenu" />
175
+ </template>
176
+ </Dropdown>
177
+ </div>
178
+
179
+ <Modal
180
+ v-model:open="deleteModalOpen"
181
+ title="删除工作区?"
182
+ ok-text="删除"
183
+ cancel-text="取消"
184
+ ok-type="danger"
185
+ :z-index="10150"
186
+ @ok="confirmDelete"
187
+ >
188
+ <p class="text-sm text-muted-foreground">
189
+ 将永久删除 IndexedDB 中的「{{ pendingDeleteProject?.name ?? "" }}」,此操作不可恢复。
190
+ </p>
191
+ </Modal>
192
+ </template>
@@ -0,0 +1,258 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ onMounted,
5
+ onUnmounted,
6
+ ref,
7
+ watch,
8
+ } from "vue";
9
+ import { Button, Select, Space, Tooltip } from "ant-design-vue";
10
+ import {
11
+ BluePrintVueRoot,
12
+ type BlueprintGraph,
13
+ type BlueprintExecutionOverlay,
14
+ } from "@arronqzy/vue-blueprint";
15
+
16
+ export type BlueprintLibraryListItem = {
17
+ id: string;
18
+ name: string;
19
+ remark?: string;
20
+ source: "saved" | "imported";
21
+ updatedAt: number;
22
+ };
23
+
24
+ export type BlueprintVueRootProps = {
25
+ graph: BlueprintGraph;
26
+ selectedNodeId?: string | null;
27
+ executionOverlay?: BlueprintExecutionOverlay | null;
28
+ libraryNameById?: ReadonlyMap<string, string>;
29
+ onSelectNode?: (nodeId: string | null) => void;
30
+ onAbortClock?: (nodeId: string) => void;
31
+ style?: Record<string, string | number>;
32
+ };
33
+
34
+ export type BlueprintDebugToolbarProps = {
35
+ lifecyclePhase?: string;
36
+ lifecycleOptions?: { value: string; label: string }[];
37
+ onLifecyclePhaseChange?: (phase: string) => void;
38
+ };
39
+
40
+ const MIN_BLUEPRINT_HEIGHT = 48;
41
+ const MIN_VIEW_RATIO = 0.1;
42
+ const MIN_BLUEPRINT_RATIO = 0.15;
43
+
44
+ const props = withDefaults(
45
+ defineProps<{
46
+ blueprintOpen: boolean;
47
+ blueprintProps: BlueprintVueRootProps;
48
+ blueprintLibraryItems?: BlueprintLibraryListItem[];
49
+ activeBlueprintLibraryId?: string | null;
50
+ currentBlueprintLabel?: string;
51
+ onSelectBlueprintLibraryItem?: (id: string) => void;
52
+ onRenameBlueprintLibraryItem?: (id: string, name: string) => void;
53
+ onDeleteBlueprintLibraryItem?: (id: string) => void;
54
+ onSaveBlueprint?: () => void;
55
+ onSyncBlueprint?: () => void;
56
+ canSyncBlueprint?: boolean;
57
+ blueprintDebug?: BlueprintDebugToolbarProps;
58
+ }>(),
59
+ {
60
+ blueprintLibraryItems: () => [],
61
+ activeBlueprintLibraryId: null,
62
+ canSyncBlueprint: false,
63
+ }
64
+ );
65
+
66
+ const emit = defineEmits<{
67
+ graphChange: [graph: BlueprintGraph];
68
+ }>();
69
+
70
+ const splitRef = ref<HTMLElement | null>(null);
71
+ const blueprintWrapRef = ref<HTMLElement | null>(null);
72
+ const layoutReady = ref(false);
73
+ const viewRatio = ref(1);
74
+ const dragging = ref(false);
75
+
76
+ const libraryOptions = computed(() =>
77
+ props.blueprintLibraryItems.map((item) => ({
78
+ value: item.id,
79
+ label: item.name,
80
+ }))
81
+ );
82
+
83
+ let resizeObserver: ResizeObserver | null = null;
84
+
85
+ function checkBlueprintLayout() {
86
+ const el = blueprintWrapRef.value;
87
+ if (!el) {
88
+ layoutReady.value = false;
89
+ return;
90
+ }
91
+ layoutReady.value = el.clientHeight >= MIN_BLUEPRINT_HEIGHT;
92
+ }
93
+
94
+ function applyBlueprintOpenState() {
95
+ if (props.blueprintOpen) {
96
+ viewRatio.value = 0.55;
97
+ } else {
98
+ viewRatio.value = 1;
99
+ }
100
+ }
101
+
102
+ function onPointerDownHandle(event: PointerEvent) {
103
+ if (!props.blueprintOpen) return;
104
+ dragging.value = true;
105
+ (event.target as HTMLElement).setPointerCapture(event.pointerId);
106
+ }
107
+
108
+ function onPointerMove(event: PointerEvent) {
109
+ if (!dragging.value || !splitRef.value) return;
110
+ const rect = splitRef.value.getBoundingClientRect();
111
+ const next = (event.clientY - rect.top) / rect.height;
112
+ viewRatio.value = Math.min(1 - MIN_BLUEPRINT_RATIO, Math.max(MIN_VIEW_RATIO, next));
113
+ }
114
+
115
+ function onPointerUp(event: PointerEvent) {
116
+ if (!dragging.value) return;
117
+ dragging.value = false;
118
+ (event.target as HTMLElement).releasePointerCapture(event.pointerId);
119
+ }
120
+
121
+ function onGraphChange(graph: BlueprintGraph) {
122
+ emit("graphChange", graph);
123
+ }
124
+
125
+ function onSelectNode(nodeId: string | null) {
126
+ props.blueprintProps.onSelectNode?.(nodeId);
127
+ }
128
+
129
+ function onAbortClock(nodeId: string) {
130
+ props.blueprintProps.onAbortClock?.(nodeId);
131
+ }
132
+
133
+ watch(
134
+ () => props.blueprintOpen,
135
+ () => {
136
+ applyBlueprintOpenState();
137
+ },
138
+ { immediate: true }
139
+ );
140
+
141
+ onMounted(() => {
142
+ applyBlueprintOpenState();
143
+ const el = blueprintWrapRef.value;
144
+ if (el) {
145
+ resizeObserver = new ResizeObserver(checkBlueprintLayout);
146
+ resizeObserver.observe(el);
147
+ checkBlueprintLayout();
148
+ }
149
+ });
150
+
151
+ onUnmounted(() => {
152
+ resizeObserver?.disconnect();
153
+ });
154
+
155
+ watch(blueprintWrapRef, (el) => {
156
+ resizeObserver?.disconnect();
157
+ if (!el) return;
158
+ resizeObserver = new ResizeObserver(checkBlueprintLayout);
159
+ resizeObserver.observe(el);
160
+ checkBlueprintLayout();
161
+ });
162
+ </script>
163
+
164
+ <template>
165
+ <div
166
+ ref="splitRef"
167
+ class="relative flex min-h-0 flex-1 flex-col"
168
+ @pointermove="onPointerMove"
169
+ @pointerup="onPointerUp"
170
+ @pointercancel="onPointerUp"
171
+ >
172
+ <div
173
+ class="min-h-0 overflow-hidden"
174
+ :style="{ flex: `${viewRatio} 1 0%` }"
175
+ >
176
+ <slot />
177
+ </div>
178
+
179
+ <div
180
+ v-show="blueprintOpen"
181
+ class="group relative z-10 flex h-2 shrink-0 cursor-row-resize items-center justify-center border-y border-border bg-muted/30"
182
+ @pointerdown="onPointerDownHandle"
183
+ >
184
+ <div
185
+ class="h-1 w-10 rounded-full bg-border transition-colors group-hover:bg-primary/50"
186
+ />
187
+ </div>
188
+
189
+ <div
190
+ v-show="blueprintOpen"
191
+ class="flex min-h-0 flex-col overflow-hidden bg-background"
192
+ :style="{ flex: `${1 - viewRatio} 1 0%` }"
193
+ >
194
+ <div
195
+ class="flex shrink-0 flex-wrap items-center gap-2 border-b border-border bg-background/90 px-2 py-1"
196
+ >
197
+ <Space size="small" wrap>
198
+ <Select
199
+ size="small"
200
+ class="min-w-[140px]"
201
+ :value="activeBlueprintLibraryId ?? undefined"
202
+ :options="libraryOptions"
203
+ placeholder="蓝图库"
204
+ @change="(id) => id && onSelectBlueprintLibraryItem?.(String(id))"
205
+ />
206
+ <span
207
+ v-if="currentBlueprintLabel"
208
+ class="max-w-[160px] truncate text-[11px] text-muted-foreground"
209
+ >
210
+ {{ currentBlueprintLabel }}
211
+ </span>
212
+ <Tooltip title="保存蓝图到库">
213
+ <Button size="small" @click="onSaveBlueprint?.()">保存</Button>
214
+ </Tooltip>
215
+ <Tooltip :title="canSyncBlueprint ? '同步蓝图' : '无可同步变更'">
216
+ <Button
217
+ size="small"
218
+ type="primary"
219
+ :disabled="!canSyncBlueprint"
220
+ @click="onSyncBlueprint?.()"
221
+ >
222
+ 同步
223
+ </Button>
224
+ </Tooltip>
225
+ <Select
226
+ v-if="blueprintDebug?.lifecycleOptions?.length"
227
+ size="small"
228
+ class="min-w-[120px]"
229
+ :value="blueprintDebug.lifecyclePhase"
230
+ :options="blueprintDebug.lifecycleOptions"
231
+ placeholder="生命周期"
232
+ @change="(v) => v && blueprintDebug?.onLifecyclePhaseChange?.(String(v))"
233
+ />
234
+ </Space>
235
+ </div>
236
+ <div
237
+ ref="blueprintWrapRef"
238
+ data-workspace-region="blueprint"
239
+ class="relative min-h-0 flex-1"
240
+ >
241
+ <BluePrintVueRoot
242
+ v-if="layoutReady"
243
+ v-bind="blueprintProps"
244
+ :style="{ width: '100%', height: '100%' }"
245
+ @graph-change="onGraphChange"
246
+ @select-node="onSelectNode"
247
+ @abort-clock="onAbortClock"
248
+ />
249
+ <div
250
+ v-else
251
+ class="flex h-full items-center justify-center text-[11px] text-muted-foreground"
252
+ >
253
+ {{ blueprintOpen ? "画布加载中…" : "" }}
254
+ </div>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </template>