@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.
- package/README.md +50 -0
- package/package.json +49 -0
- package/src/env.d.ts +62 -0
- package/src/index.ts +4 -0
- package/src/panel/VueViewOnlinePreview.vue +276 -0
- package/src/panel/VueViewPanel.vue +871 -0
- package/src/panel/components/ConfigHintIcon.vue +34 -0
- package/src/panel/components/ElementsLayer.vue +165 -0
- package/src/panel/components/MaterialPreview.vue +135 -0
- package/src/panel/components/MaterialSidebar.vue +526 -0
- package/src/panel/components/MaterialSidebarTreeNode.vue +305 -0
- package/src/panel/components/MoveableLayer.vue +859 -0
- package/src/panel/components/PanelCanvas.vue +630 -0
- package/src/panel/components/PanelConfigSidebar.vue +397 -0
- package/src/panel/components/PanelRulers.vue +177 -0
- package/src/panel/components/SelectLayer.vue +115 -0
- package/src/panel/components/ViewElementScopePanel.vue +76 -0
- package/src/panel/components/WorkspaceConfigSidebar.vue +147 -0
- package/src/panel/components/WorkspaceProjectNav.vue +192 -0
- package/src/panel/components/WorkspaceStageSplit.vue +258 -0
- package/src/panel/components/config/ConfigColorField.vue +52 -0
- package/src/panel/components/config/ConfigFieldGroup.vue +20 -0
- package/src/panel/components/config/ConfigSection.vue +50 -0
- package/src/panel/components/config/PanelConfigAudioSection.vue +256 -0
- package/src/panel/components/config/PanelConfigChartSection.vue +650 -0
- package/src/panel/components/config/PanelConfigGeometrySection.vue +209 -0
- package/src/panel/components/config/PanelConfigGridChildSpan.vue +68 -0
- package/src/panel/components/config/PanelConfigGridSection.vue +103 -0
- package/src/panel/components/config/PanelConfigImageSection.vue +136 -0
- package/src/panel/components/config/PanelConfigMultiSelect.vue +434 -0
- package/src/panel/components/config/PanelConfigNodeInfo.vue +165 -0
- package/src/panel/components/config/PanelConfigReferenceSection.vue +77 -0
- package/src/panel/components/config/PanelConfigStyleSections.vue +208 -0
- package/src/panel/components/config/PanelConfigTextSection.vue +195 -0
- package/src/panel/components/config/PanelConfigVideoSection.vue +107 -0
- package/src/panel/components/config/shared.ts +74 -0
- package/src/panel/components/elementsLayerNodes.ts +830 -0
- package/src/panel/components/materialSidebarData.ts +85 -0
- package/src/panel/components/scope-config/ScopeConfigProvider.vue +153 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +234 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +192 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +42 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +20 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +63 -0
- package/src/panel/components/scope-config/scopeConfigContext.ts +17 -0
- package/src/panel/components/scope-config/useScopeConfig.ts +11 -0
- package/src/panel/constants/messages.ts +34 -0
- package/src/panel/constants/zIndex.ts +6 -0
- package/src/panel/hooks/usePanelElements.ts +1075 -0
- package/src/panel/hooks/useRafThrottledScroll.ts +25 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +240 -0
- package/src/panel/lib/panel-ruler-canvas.ts +139 -0
- package/src/panel/library/workspace-project-cache.ts +23 -0
- package/src/panel/library/workspace-project-db.ts +111 -0
- package/src/panel/library/workspace-project-sync.ts +41 -0
- package/src/panel/library/workspace-snapshot.ts +30 -0
- package/src/panel/parseOnlinePreviewSearchParams.ts +13 -0
- package/src/panel/scope/view-scope-store.ts +82 -0
- package/src/panel/types.ts +127 -0
- package/src/panel/utils/chartOptionBuilder.ts +327 -0
- package/src/panel/utils/gridPlacement.ts +189 -0
- package/src/panel/utils/mappingLayerOps.ts +142 -0
- package/src/panel/utils/panelElementDefaults.ts +161 -0
- package/src/panel/utils/panelElementNodes.ts +35 -0
- package/src/panel/utils/panelStateIO.ts +124 -0
- package/src/panel/utils/scope-autocomplete.ts +114 -0
- package/src/panel/utils/scope-field-labels.ts +46 -0
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +124 -0
- package/src/panel/utils/scope-template-spread.ts +229 -0
- package/src/panel/utils/scope-template-warnings.ts +243 -0
- package/src/panel/utils/scope-template.ts +97 -0
- package/src/panel/utils/updateElementDraft.ts +221 -0
- package/src/panel/viewportZoom.ts +26 -0
- 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>
|