@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,871 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
|
|
3
|
+
import type { State } from "@arronqzy/rx-store";
|
|
4
|
+
import {
|
|
5
|
+
BlueprintGraph,
|
|
6
|
+
abortClockNode,
|
|
7
|
+
blueprintDocumentsEqual,
|
|
8
|
+
buildBlueprintExportPayload,
|
|
9
|
+
buildLibraryRecord,
|
|
10
|
+
createLibraryBlueprintId,
|
|
11
|
+
deleteBlueprintLibraryRecord,
|
|
12
|
+
documentToRunnableGraph,
|
|
13
|
+
downloadBlueprintExport,
|
|
14
|
+
getBlueprintLibraryRecord,
|
|
15
|
+
libraryRecordFromImport,
|
|
16
|
+
listBlueprintLibrary,
|
|
17
|
+
parseBlueprintImportFile,
|
|
18
|
+
putBlueprintLibraryRecord,
|
|
19
|
+
stopAllClockSchedules,
|
|
20
|
+
useBlueprintDebugSession,
|
|
21
|
+
useBlueprintNodeSelectionGuard,
|
|
22
|
+
useBlueprintPageLifecycle,
|
|
23
|
+
type BlueprintDocument,
|
|
24
|
+
type BlueprintGraphNode,
|
|
25
|
+
type BlueprintLibraryListItem,
|
|
26
|
+
type BlueprintMetaDraft,
|
|
27
|
+
} from "@arronqzy/vue-blueprint";
|
|
28
|
+
import type { LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
|
|
29
|
+
import {
|
|
30
|
+
Button,
|
|
31
|
+
Checkbox,
|
|
32
|
+
Dropdown,
|
|
33
|
+
Input,
|
|
34
|
+
Layout,
|
|
35
|
+
Menu,
|
|
36
|
+
Modal,
|
|
37
|
+
Slider,
|
|
38
|
+
Space,
|
|
39
|
+
Switch,
|
|
40
|
+
Tabs,
|
|
41
|
+
Tag,
|
|
42
|
+
message,
|
|
43
|
+
} from "ant-design-vue";
|
|
44
|
+
import { usePanelElements } from "./hooks/usePanelElements";
|
|
45
|
+
import {
|
|
46
|
+
buildOnlinePreviewUrl,
|
|
47
|
+
useWorkspaceProjects,
|
|
48
|
+
} from "./hooks/useWorkspaceProjects";
|
|
49
|
+
import type { WorkspaceProjectRecord } from "./library/workspace-project-db";
|
|
50
|
+
import { type ViewportZoom } from "./viewportZoom";
|
|
51
|
+
import PanelCanvas from "./components/PanelCanvas.vue";
|
|
52
|
+
import ElementsLayer from "./components/ElementsLayer.vue";
|
|
53
|
+
import MoveableLayer from "./components/MoveableLayer.vue";
|
|
54
|
+
import SelectLayer from "./components/SelectLayer.vue";
|
|
55
|
+
import MaterialSidebar from "./components/MaterialSidebar.vue";
|
|
56
|
+
import WorkspaceStageSplit from "./components/WorkspaceStageSplit.vue";
|
|
57
|
+
import WorkspaceProjectNav from "./components/WorkspaceProjectNav.vue";
|
|
58
|
+
import WorkspaceConfigSidebar from "./components/WorkspaceConfigSidebar.vue";
|
|
59
|
+
import type { WorkspaceConfigFocus } from "./components/WorkspaceConfigSidebar.vue";
|
|
60
|
+
import {
|
|
61
|
+
clearViewElementScopes,
|
|
62
|
+
setViewElementScopes,
|
|
63
|
+
useViewElementScope,
|
|
64
|
+
} from "./scope/view-scope-store";
|
|
65
|
+
import { useRafThrottledScroll } from "./hooks/useRafThrottledScroll";
|
|
66
|
+
import "../tailwind.css";
|
|
67
|
+
|
|
68
|
+
const props = withDefaults(
|
|
69
|
+
defineProps<{
|
|
70
|
+
class?: string;
|
|
71
|
+
initialZoom?: number;
|
|
72
|
+
}>(),
|
|
73
|
+
{ initialZoom: 1 }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const {
|
|
77
|
+
elements,
|
|
78
|
+
allElements,
|
|
79
|
+
byId,
|
|
80
|
+
layers,
|
|
81
|
+
activeLayerId,
|
|
82
|
+
updateElement,
|
|
83
|
+
deleteElements,
|
|
84
|
+
bringElementsToFront,
|
|
85
|
+
sendElementsToBack,
|
|
86
|
+
bringElementsForward,
|
|
87
|
+
sendElementsBackward,
|
|
88
|
+
addElementFromMaterial,
|
|
89
|
+
setActiveLayer,
|
|
90
|
+
addLayer,
|
|
91
|
+
renameLayer,
|
|
92
|
+
toggleLayerLock,
|
|
93
|
+
deleteLayer,
|
|
94
|
+
toggleLayerMergeSelected,
|
|
95
|
+
mergeSelectedLayers,
|
|
96
|
+
setPrimaryLayer,
|
|
97
|
+
undo,
|
|
98
|
+
redo,
|
|
99
|
+
canUndo,
|
|
100
|
+
canRedo,
|
|
101
|
+
historyCursor,
|
|
102
|
+
exportPanelData,
|
|
103
|
+
importPanelData,
|
|
104
|
+
setReferenceCopyMode,
|
|
105
|
+
} = usePanelElements();
|
|
106
|
+
|
|
107
|
+
const selectedIds = ref<string[]>([]);
|
|
108
|
+
const zoom = ref<ViewportZoom>({ x: props.initialZoom, y: props.initialZoom });
|
|
109
|
+
const blueprintGraph = ref(BlueprintGraph.empty());
|
|
110
|
+
const blueprintMeta = ref<BlueprintMetaDraft>({ name: "未命名蓝图", remark: "" });
|
|
111
|
+
const blueprintOpen = ref(false);
|
|
112
|
+
const selectedBlueprintNodeId = ref<string | null>(null);
|
|
113
|
+
const blueprintLibraryItems = ref<BlueprintLibraryListItem[]>([]);
|
|
114
|
+
const activeBlueprintLibraryId = ref<string | null>(null);
|
|
115
|
+
const blueprintSyncedDocument = ref<BlueprintDocument | null>(null);
|
|
116
|
+
const workspaceBlueprintRef = shallowRef<{
|
|
117
|
+
document: BlueprintDocument;
|
|
118
|
+
meta: BlueprintMetaDraft;
|
|
119
|
+
} | null>(null);
|
|
120
|
+
const configFocus = ref<WorkspaceConfigFocus>("view");
|
|
121
|
+
const productName = ref("未命名产物");
|
|
122
|
+
const titleIconDataUrl = ref("");
|
|
123
|
+
const editingLayerId = ref<string | null>(null);
|
|
124
|
+
const editingLayerName = ref("");
|
|
125
|
+
const isMergingLayers = ref(false);
|
|
126
|
+
const mergeLayerName = ref("");
|
|
127
|
+
const leftWidth = ref(240);
|
|
128
|
+
const rightWidth = ref(300);
|
|
129
|
+
const importInputRef = ref<HTMLInputElement | null>(null);
|
|
130
|
+
const blueprintImportInputRef = ref<HTMLInputElement | null>(null);
|
|
131
|
+
|
|
132
|
+
const viewportEl = ref<HTMLDivElement | null>(null);
|
|
133
|
+
const canvasEl = ref<HTMLDivElement | null>(null);
|
|
134
|
+
const viewportSyncRef = ref<{ current: (() => void) | null }>({ current: null });
|
|
135
|
+
const selectedTargets = ref<HTMLElement[]>([]);
|
|
136
|
+
|
|
137
|
+
const { onScrollChange: onViewportScrollChange } = useRafThrottledScroll();
|
|
138
|
+
|
|
139
|
+
const activeLayer = computed(() => layers.value.find((l) => l.id === activeLayerId.value));
|
|
140
|
+
const mergeSelectedCount = computed(
|
|
141
|
+
() => layers.value.filter((l) => l.mergeSelected && !l.isMapping).length
|
|
142
|
+
);
|
|
143
|
+
const canMergeLayers = computed(() => mergeSelectedCount.value >= 2);
|
|
144
|
+
const uniformZoom = computed(() => zoom.value.x);
|
|
145
|
+
const selectedElement = computed(() => {
|
|
146
|
+
const id = selectedIds.value[0];
|
|
147
|
+
return id ? byId.value.get(id) ?? null : null;
|
|
148
|
+
});
|
|
149
|
+
const selectedElements = computed(() =>
|
|
150
|
+
selectedIds.value.map((id) => byId.value.get(id)).filter(Boolean) as NonNullable<
|
|
151
|
+
ReturnType<typeof byId.value.get>
|
|
152
|
+
>[]
|
|
153
|
+
);
|
|
154
|
+
const selectedNodeZOrderLabel = computed(() => {
|
|
155
|
+
if (!selectedElement.value) return "-";
|
|
156
|
+
return String(selectedElement.value.zIndex ?? 1);
|
|
157
|
+
});
|
|
158
|
+
const selectedBlueprintNode = computed(() => {
|
|
159
|
+
if (!selectedBlueprintNodeId.value) return null;
|
|
160
|
+
return blueprintGraph.value.getNode(selectedBlueprintNodeId.value) ?? null;
|
|
161
|
+
});
|
|
162
|
+
const blueprintLibraryNameById = computed(
|
|
163
|
+
() => new Map(blueprintLibraryItems.value.map((item) => [item.id, item.name]))
|
|
164
|
+
);
|
|
165
|
+
const blueprintLibraryOptions = computed(() =>
|
|
166
|
+
blueprintLibraryItems.value.map((item) => ({ id: item.id, label: item.name }))
|
|
167
|
+
);
|
|
168
|
+
const blueprintLibraryDirty = computed(() => {
|
|
169
|
+
if (!activeBlueprintLibraryId.value || !blueprintSyncedDocument.value) return false;
|
|
170
|
+
return !blueprintDocumentsEqual(blueprintGraph.value.document, blueprintSyncedDocument.value);
|
|
171
|
+
});
|
|
172
|
+
const hasUnlockedSelection = computed(() =>
|
|
173
|
+
selectedIds.value.some((id) => {
|
|
174
|
+
const node = byId.value.get(id);
|
|
175
|
+
if (!node || node.locked) return false;
|
|
176
|
+
return !layers.value.find((l) => l.id === node.layerId)?.locked;
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const viewElementScope = useViewElementScope(
|
|
181
|
+
computed(() => selectedElement.value?.id ?? null)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
function excludeSelectedNode(nodeId: string) {
|
|
185
|
+
const nextIds = selectedIds.value.filter((id) => id !== nodeId);
|
|
186
|
+
selectedIds.value = nextIds;
|
|
187
|
+
selectedTargets.value = getSelectedTargetsFromIds(canvasEl.value, nextIds);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function adjustNodeZOrder(
|
|
191
|
+
nodeId: string,
|
|
192
|
+
action: "bringForward" | "sendBackward" | "bringToFront" | "sendToBack"
|
|
193
|
+
) {
|
|
194
|
+
if (action === "bringForward") {
|
|
195
|
+
bringElementsForward([nodeId]);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (action === "sendBackward") {
|
|
199
|
+
sendElementsBackward([nodeId]);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (action === "bringToFront") {
|
|
203
|
+
bringElementsToFront([nodeId]);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
sendElementsToBack([nodeId]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function syncScrollRef(el: HTMLDivElement | null) {
|
|
210
|
+
viewportEl.value = el;
|
|
211
|
+
}
|
|
212
|
+
function syncCanvasRef(el: HTMLDivElement | null) {
|
|
213
|
+
canvasEl.value = el;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getSelectedTargetsFromIds(container: HTMLElement | null, ids: string[]) {
|
|
217
|
+
if (!container) return [];
|
|
218
|
+
const targets: HTMLElement[] = [];
|
|
219
|
+
for (const id of ids) {
|
|
220
|
+
const el = container.querySelector<HTMLElement>(`[data-element-id="${id}"]`);
|
|
221
|
+
if (el) targets.push(el);
|
|
222
|
+
}
|
|
223
|
+
return targets;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
watch([selectedIds, elements, historyCursor], () => {
|
|
227
|
+
selectedTargets.value = getSelectedTargetsFromIds(canvasEl.value, selectedIds.value);
|
|
228
|
+
}, { deep: true });
|
|
229
|
+
|
|
230
|
+
watch(elements, (next) => {
|
|
231
|
+
const existing = new Set(next.map((el) => el.id));
|
|
232
|
+
selectedIds.value = selectedIds.value.filter((id) => existing.has(id));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
watch(activeLayerId, () => {
|
|
236
|
+
selectedIds.value = [];
|
|
237
|
+
selectedTargets.value = [];
|
|
238
|
+
configFocus.value = "view";
|
|
239
|
+
selectedBlueprintNodeId.value = null;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
watch(blueprintOpen, (open) => {
|
|
243
|
+
if (!open) stopAllClockSchedules();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
function selectIds(ids: string[]) {
|
|
247
|
+
selectedIds.value = ids;
|
|
248
|
+
if (ids.length > 0) {
|
|
249
|
+
configFocus.value = "view";
|
|
250
|
+
requestSelectBlueprintNode(null);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function setUniformZoom(value: number) {
|
|
255
|
+
const rounded = Number(Math.min(4, Math.max(0.25, value)).toFixed(4));
|
|
256
|
+
zoom.value = { x: rounded, y: rounded };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function onDropMaterial(payload: { materialId: string; x: number; y: number }) {
|
|
260
|
+
if (activeLayer.value?.locked) return;
|
|
261
|
+
addElementFromMaterial(payload.materialId, payload.x, payload.y);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function onCanvasMouseDownCapture(e: MouseEvent) {
|
|
265
|
+
if (e.button !== 0) return;
|
|
266
|
+
const target = e.target as HTMLElement | null;
|
|
267
|
+
if (target?.closest(".rv-selectable")) return;
|
|
268
|
+
if (target?.closest("[data-workspace-region='blueprint'], [data-blueprint-toggle]")) return;
|
|
269
|
+
selectedIds.value = [];
|
|
270
|
+
configFocus.value = "view";
|
|
271
|
+
requestSelectBlueprintNode(null);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function applyBlueprintNodeSelection(nodeId: string | null) {
|
|
275
|
+
selectedBlueprintNodeId.value = nodeId;
|
|
276
|
+
if (nodeId) {
|
|
277
|
+
configFocus.value = "blueprint";
|
|
278
|
+
selectedIds.value = [];
|
|
279
|
+
selectedTargets.value = [];
|
|
280
|
+
} else {
|
|
281
|
+
configFocus.value = "view";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const {
|
|
286
|
+
requestSelectNode: requestSelectBlueprintNode,
|
|
287
|
+
} = useBlueprintNodeSelectionGuard(selectedBlueprintNodeId, applyBlueprintNodeSelection);
|
|
288
|
+
|
|
289
|
+
const resolveLibraryBlueprint: LibraryBlueprintResolver = async (libraryBlueprintId) => {
|
|
290
|
+
const record = await getBlueprintLibraryRecord(libraryBlueprintId);
|
|
291
|
+
if (!record) return null;
|
|
292
|
+
const items = await listBlueprintLibrary();
|
|
293
|
+
const nameById = new Map(items.map((item) => [item.id, item.name]));
|
|
294
|
+
return documentToRunnableGraph(record.document, { libraryNameById: nameById });
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
function handleBlueprintExecutionBlocked(msg: string) {
|
|
298
|
+
message.warning(msg);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function handleViewScopeUpdate(viewElementIds: string[], scope: unknown) {
|
|
302
|
+
setViewElementScopes(viewElementIds, scope);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const blueprintDebugSession = useBlueprintDebugSession({
|
|
306
|
+
graph: blueprintGraph,
|
|
307
|
+
blueprintId: activeBlueprintLibraryId,
|
|
308
|
+
blueprintName: computed(() => blueprintMeta.value.name || "未命名蓝图"),
|
|
309
|
+
resolveLibraryBlueprint,
|
|
310
|
+
libraryNameById: blueprintLibraryNameById,
|
|
311
|
+
onExecutionBlocked: handleBlueprintExecutionBlocked,
|
|
312
|
+
onViewScopeUpdate: handleViewScopeUpdate,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const blueprintDebugToolbar = computed(() => ({
|
|
316
|
+
lifecyclePhase: blueprintDebugSession.selectedLifecycleNodeId.value ?? undefined,
|
|
317
|
+
lifecycleOptions: blueprintDebugSession.lifecycleNodes.value.map((node) => ({
|
|
318
|
+
value: node.id,
|
|
319
|
+
label: node.label,
|
|
320
|
+
})),
|
|
321
|
+
onLifecyclePhaseChange: (id: string) => blueprintDebugSession.selectLifecycleNode(id),
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
function handleAbortClock(nodeId: string) {
|
|
325
|
+
blueprintDebugSession.abortClock(nodeId);
|
|
326
|
+
abortClockNode(activeBlueprintLibraryId.value ?? "local", nodeId);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const blueprintCanvasProps = computed(() => ({
|
|
330
|
+
graph: blueprintGraph.value,
|
|
331
|
+
selectedNodeId: selectedBlueprintNodeId.value,
|
|
332
|
+
executionOverlay: blueprintDebugSession.executionOverlay.value,
|
|
333
|
+
libraryNameById: blueprintLibraryNameById.value,
|
|
334
|
+
onSelectNode: requestSelectBlueprintNode,
|
|
335
|
+
onAbortClock: handleAbortClock,
|
|
336
|
+
}));
|
|
337
|
+
|
|
338
|
+
function handleUpdateBlueprintNode(
|
|
339
|
+
nodeId: string,
|
|
340
|
+
patch: Partial<BlueprintGraphNode>
|
|
341
|
+
) {
|
|
342
|
+
blueprintGraph.value = blueprintGraph.value.updateNode(nodeId, patch);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function handleUpdateAllowFalseSignalPropagation(value: boolean) {
|
|
346
|
+
blueprintGraph.value = blueprintGraph.value.withDocument({
|
|
347
|
+
...blueprintGraph.value.document,
|
|
348
|
+
allowFalseSignalPropagation: value,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function refreshBlueprintLibrary() {
|
|
353
|
+
blueprintLibraryItems.value = await listBlueprintLibrary();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
onMounted(() => {
|
|
357
|
+
void refreshBlueprintLibrary();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
function handleWorkspaceProjectApplied(record: WorkspaceProjectRecord) {
|
|
361
|
+
workspaceBlueprintRef.value = {
|
|
362
|
+
document: record.blueprintDocument,
|
|
363
|
+
meta: {
|
|
364
|
+
name: record.blueprintMeta?.name ?? "未命名蓝图",
|
|
365
|
+
remark: record.blueprintMeta?.remark ?? "",
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
activeBlueprintLibraryId.value = null;
|
|
369
|
+
blueprintSyncedDocument.value = null;
|
|
370
|
+
selectedBlueprintNodeId.value = null;
|
|
371
|
+
blueprintOpen.value = true;
|
|
372
|
+
configFocus.value = "blueprint";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const workspaceProjects = useWorkspaceProjects({
|
|
376
|
+
exportPanelData,
|
|
377
|
+
importPanelData,
|
|
378
|
+
blueprintDocument: computed(() => blueprintGraph.value.document),
|
|
379
|
+
blueprintMeta,
|
|
380
|
+
setBlueprintGraph: (graph) => {
|
|
381
|
+
blueprintGraph.value = graph;
|
|
382
|
+
},
|
|
383
|
+
setBlueprintMeta: (meta) => {
|
|
384
|
+
blueprintMeta.value = meta;
|
|
385
|
+
},
|
|
386
|
+
productName,
|
|
387
|
+
setProductName: (name) => {
|
|
388
|
+
productName.value = name;
|
|
389
|
+
},
|
|
390
|
+
titleIconDataUrl,
|
|
391
|
+
setTitleIconDataUrl: (url) => {
|
|
392
|
+
titleIconDataUrl.value = url;
|
|
393
|
+
},
|
|
394
|
+
panelRevision: computed(() => `${historyCursor.value}|${allElements.value.length}`),
|
|
395
|
+
onProjectApplied: handleWorkspaceProjectApplied,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
useBlueprintPageLifecycle({
|
|
399
|
+
graph: blueprintGraph,
|
|
400
|
+
active: blueprintOpen,
|
|
401
|
+
bootKey: computed(() => workspaceProjects.activeProjectId.value ?? undefined),
|
|
402
|
+
onUpdated: computed(() => `${activeLayerId.value}|${historyCursor.value}`),
|
|
403
|
+
resolveLibraryBlueprint,
|
|
404
|
+
libraryNameById: blueprintLibraryNameById,
|
|
405
|
+
rootLibraryBlueprintId: activeBlueprintLibraryId,
|
|
406
|
+
onExecutionBlocked: handleBlueprintExecutionBlocked,
|
|
407
|
+
onViewScopeUpdate: handleViewScopeUpdate,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
async function loadBlueprintFromLibrary(id: string) {
|
|
411
|
+
const record = await getBlueprintLibraryRecord(id);
|
|
412
|
+
if (!record) {
|
|
413
|
+
message.error("蓝图不存在或已被删除");
|
|
414
|
+
void refreshBlueprintLibrary();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
blueprintGraph.value = BlueprintGraph.fromDocument(record.document);
|
|
418
|
+
blueprintMeta.value = { name: record.name, remark: record.remark ?? "" };
|
|
419
|
+
activeBlueprintLibraryId.value = record.id;
|
|
420
|
+
blueprintSyncedDocument.value = record.document;
|
|
421
|
+
selectedBlueprintNodeId.value = null;
|
|
422
|
+
configFocus.value = "blueprint";
|
|
423
|
+
blueprintOpen.value = true;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function snapshotWorkspaceBlueprint() {
|
|
427
|
+
workspaceBlueprintRef.value = {
|
|
428
|
+
document: blueprintGraph.value.document,
|
|
429
|
+
meta: { ...blueprintMeta.value },
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function returnToWorkspaceBlueprint() {
|
|
434
|
+
const snapshot = workspaceBlueprintRef.value;
|
|
435
|
+
if (snapshot) {
|
|
436
|
+
blueprintGraph.value = BlueprintGraph.fromDocument(snapshot.document);
|
|
437
|
+
blueprintMeta.value = snapshot.meta;
|
|
438
|
+
}
|
|
439
|
+
activeBlueprintLibraryId.value = null;
|
|
440
|
+
blueprintSyncedDocument.value = null;
|
|
441
|
+
selectedBlueprintNodeId.value = null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function handleSelectBlueprintLibraryItem(id: string) {
|
|
445
|
+
if (activeBlueprintLibraryId.value === id) {
|
|
446
|
+
returnToWorkspaceBlueprint();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (!activeBlueprintLibraryId.value) snapshotWorkspaceBlueprint();
|
|
450
|
+
await loadBlueprintFromLibrary(id);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function syncBlueprintToLibrary() {
|
|
454
|
+
if (!activeBlueprintLibraryId.value) return;
|
|
455
|
+
const record = await getBlueprintLibraryRecord(activeBlueprintLibraryId.value);
|
|
456
|
+
if (!record) return;
|
|
457
|
+
await putBlueprintLibraryRecord({
|
|
458
|
+
...record,
|
|
459
|
+
document: blueprintGraph.value.document,
|
|
460
|
+
name: blueprintMeta.value.name,
|
|
461
|
+
remark: blueprintMeta.value.remark,
|
|
462
|
+
});
|
|
463
|
+
blueprintSyncedDocument.value = blueprintGraph.value.document;
|
|
464
|
+
message.success("蓝图已同步到库");
|
|
465
|
+
await refreshBlueprintLibrary();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function saveBlueprintToLibrary(meta: BlueprintMetaDraft) {
|
|
469
|
+
const id = activeBlueprintLibraryId.value ?? createLibraryBlueprintId();
|
|
470
|
+
const record = buildLibraryRecord({
|
|
471
|
+
id,
|
|
472
|
+
document: blueprintGraph.value.document,
|
|
473
|
+
meta,
|
|
474
|
+
source: "saved",
|
|
475
|
+
});
|
|
476
|
+
await putBlueprintLibraryRecord(record);
|
|
477
|
+
activeBlueprintLibraryId.value = id;
|
|
478
|
+
blueprintSyncedDocument.value = blueprintGraph.value.document;
|
|
479
|
+
blueprintMeta.value = meta;
|
|
480
|
+
await refreshBlueprintLibrary();
|
|
481
|
+
message.success("蓝图已保存到库");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function handleExport() {
|
|
485
|
+
const data = exportPanelData();
|
|
486
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
487
|
+
const url = URL.createObjectURL(blob);
|
|
488
|
+
const a = document.createElement("a");
|
|
489
|
+
a.href = url;
|
|
490
|
+
const safeName = (productName.value.trim() || "panel")
|
|
491
|
+
.replace(/[\\/:*?"<>|]/g, "-")
|
|
492
|
+
.replace(/\s+/g, "-");
|
|
493
|
+
a.download = `${safeName}-${Date.now()}.json`;
|
|
494
|
+
a.click();
|
|
495
|
+
URL.revokeObjectURL(url);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function handleImportFile(file: File) {
|
|
499
|
+
const text = await file.text();
|
|
500
|
+
const parsed = JSON.parse(text) as State;
|
|
501
|
+
const ok = importPanelData(parsed);
|
|
502
|
+
if (!ok) window.alert("导入失败:文件格式不正确");
|
|
503
|
+
else selectedIds.value = [];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function openBlueprintExport() {
|
|
507
|
+
downloadBlueprintExport(
|
|
508
|
+
buildBlueprintExportPayload(blueprintGraph.value.document, blueprintMeta.value)
|
|
509
|
+
);
|
|
510
|
+
message.success("蓝图已导出");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function handleBlueprintImportFile(file: File) {
|
|
514
|
+
const text = await file.text();
|
|
515
|
+
const payload = parseBlueprintImportFile(JSON.parse(text) as unknown);
|
|
516
|
+
const record = libraryRecordFromImport(payload);
|
|
517
|
+
await putBlueprintLibraryRecord(record);
|
|
518
|
+
await refreshBlueprintLibrary();
|
|
519
|
+
if (!activeBlueprintLibraryId.value) snapshotWorkspaceBlueprint();
|
|
520
|
+
await loadBlueprintFromLibrary(record.id);
|
|
521
|
+
message.success("蓝图已导入并加载");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function handleWorkspaceCreateProject() {
|
|
525
|
+
const result = await workspaceProjects.handleCreateProject();
|
|
526
|
+
if (result?.name) message.success(`已创建工作区「${result.name}」`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function handleWorkspaceOpenProject(id: string) {
|
|
530
|
+
try {
|
|
531
|
+
clearViewElementScopes();
|
|
532
|
+
await workspaceProjects.handleOpenProject(id);
|
|
533
|
+
selectedIds.value = [];
|
|
534
|
+
message.success("工作区已加载");
|
|
535
|
+
} catch (error) {
|
|
536
|
+
message.error(error instanceof Error ? error.message : "打开工作区失败");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function handleWorkspaceSyncProject() {
|
|
541
|
+
try {
|
|
542
|
+
const name = await workspaceProjects.handleSyncProject();
|
|
543
|
+
if (name) message.success(`已同步「${name}」`);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
message.error(error instanceof Error ? error.message : "同步失败");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function handleWorkspaceDeleteProject(id: string) {
|
|
550
|
+
await workspaceProjects.handleDeleteProject(id);
|
|
551
|
+
message.success("工作区已删除");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function openOnlinePreviewForProject(
|
|
555
|
+
projectId: string,
|
|
556
|
+
options?: { syncFirst?: boolean }
|
|
557
|
+
) {
|
|
558
|
+
await workspaceProjects.handlePreviewProject(projectId, options);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function startRenameLayer(layerId: string, name: string) {
|
|
562
|
+
editingLayerId.value = layerId;
|
|
563
|
+
editingLayerName.value = name;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function commitRenameLayer() {
|
|
567
|
+
if (!editingLayerId.value) return;
|
|
568
|
+
renameLayer(editingLayerId.value, editingLayerName.value);
|
|
569
|
+
editingLayerId.value = null;
|
|
570
|
+
editingLayerName.value = "";
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function handleMergeLayers() {
|
|
574
|
+
if (!canMergeLayers.value) {
|
|
575
|
+
message.warning("至少勾选 2 个图层后可合并");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
mergeSelectedLayers(mergeLayerName.value.trim() || undefined);
|
|
579
|
+
isMergingLayers.value = false;
|
|
580
|
+
mergeLayerName.value = "";
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function handleDeleteSelected() {
|
|
584
|
+
if (!hasUnlockedSelection.value) return;
|
|
585
|
+
deleteElements(selectedIds.value);
|
|
586
|
+
selectedIds.value = [];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
590
|
+
const cmdOrCtrl = e.metaKey || e.ctrlKey;
|
|
591
|
+
if (cmdOrCtrl && e.key.toLowerCase() === "z") {
|
|
592
|
+
e.preventDefault();
|
|
593
|
+
if (e.shiftKey) {
|
|
594
|
+
if (canRedo.value) redo();
|
|
595
|
+
} else if (canUndo.value) {
|
|
596
|
+
undo();
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if ((e.key === "Delete" || e.key === "Backspace") && hasUnlockedSelection.value) {
|
|
601
|
+
const target = e.target as HTMLElement | null;
|
|
602
|
+
if (target?.closest("input, textarea, [contenteditable='true']")) return;
|
|
603
|
+
e.preventDefault();
|
|
604
|
+
handleDeleteSelected();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
onMounted(() => window.addEventListener("keydown", onKeyDown));
|
|
609
|
+
onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
|
|
610
|
+
</script>
|
|
611
|
+
|
|
612
|
+
<template>
|
|
613
|
+
<Layout class="vue-view-panel h-screen overflow-hidden" :class="props.class">
|
|
614
|
+
<Layout.Header class="flex h-auto flex-wrap items-center gap-2 bg-[#001529] px-3 py-2">
|
|
615
|
+
<Dropdown :trigger="['click']">
|
|
616
|
+
<Button type="text" class="!text-white">文件</Button>
|
|
617
|
+
<template #overlay>
|
|
618
|
+
<Menu>
|
|
619
|
+
<Menu.Item @click="handleExport">导出面板 JSON</Menu.Item>
|
|
620
|
+
<Menu.Item @click="importInputRef?.click()">导入面板 JSON</Menu.Item>
|
|
621
|
+
</Menu>
|
|
622
|
+
</template>
|
|
623
|
+
</Dropdown>
|
|
624
|
+
<Dropdown :trigger="['click']">
|
|
625
|
+
<Button type="text" class="!text-white">编辑</Button>
|
|
626
|
+
<template #overlay>
|
|
627
|
+
<Menu>
|
|
628
|
+
<Menu.Item :disabled="!canUndo" @click="undo">撤销</Menu.Item>
|
|
629
|
+
<Menu.Item :disabled="!canRedo" @click="redo">重做</Menu.Item>
|
|
630
|
+
<Menu.Divider />
|
|
631
|
+
<Menu.Item :disabled="!hasUnlockedSelection" @click="bringElementsForward(selectedIds)">上移一层</Menu.Item>
|
|
632
|
+
<Menu.Item :disabled="!hasUnlockedSelection" @click="sendElementsBackward(selectedIds)">下移一层</Menu.Item>
|
|
633
|
+
<Menu.Item :disabled="!hasUnlockedSelection" @click="bringElementsToFront(selectedIds)">置顶</Menu.Item>
|
|
634
|
+
<Menu.Item :disabled="!hasUnlockedSelection" @click="sendElementsToBack(selectedIds)">置底</Menu.Item>
|
|
635
|
+
<Menu.Divider />
|
|
636
|
+
<Menu.Item :disabled="!hasUnlockedSelection" @click="handleDeleteSelected">删除选中</Menu.Item>
|
|
637
|
+
</Menu>
|
|
638
|
+
</template>
|
|
639
|
+
</Dropdown>
|
|
640
|
+
<Dropdown :trigger="['click']">
|
|
641
|
+
<Button type="text" class="!text-white">蓝图</Button>
|
|
642
|
+
<template #overlay>
|
|
643
|
+
<Menu>
|
|
644
|
+
<Menu.Item @click="openBlueprintExport">导出蓝图</Menu.Item>
|
|
645
|
+
<Menu.Item @click="blueprintImportInputRef?.click()">导入蓝图</Menu.Item>
|
|
646
|
+
</Menu>
|
|
647
|
+
</template>
|
|
648
|
+
</Dropdown>
|
|
649
|
+
<WorkspaceProjectNav
|
|
650
|
+
:projects="workspaceProjects.projects.value"
|
|
651
|
+
:active-project-id="workspaceProjects.activeProjectId.value"
|
|
652
|
+
:active-project-name="workspaceProjects.activeProjectName.value"
|
|
653
|
+
:dirty="workspaceProjects.dirty.value"
|
|
654
|
+
@create-project="handleWorkspaceCreateProject"
|
|
655
|
+
@open-project="handleWorkspaceOpenProject"
|
|
656
|
+
@sync-project="handleWorkspaceSyncProject"
|
|
657
|
+
@delete-project="handleWorkspaceDeleteProject"
|
|
658
|
+
@preview-project="openOnlinePreviewForProject"
|
|
659
|
+
/>
|
|
660
|
+
<div class="flex-1" />
|
|
661
|
+
<Space>
|
|
662
|
+
<Button size="small" :disabled="!canUndo" @click="undo">撤销</Button>
|
|
663
|
+
<Button size="small" :disabled="!canRedo" @click="redo">重做</Button>
|
|
664
|
+
<Button
|
|
665
|
+
size="small"
|
|
666
|
+
type="primary"
|
|
667
|
+
:disabled="!workspaceProjects.activeProjectId.value"
|
|
668
|
+
@click="workspaceProjects.activeProjectId.value && openOnlinePreviewForProject(workspaceProjects.activeProjectId.value, { syncFirst: workspaceProjects.dirty.value })"
|
|
669
|
+
>
|
|
670
|
+
预览
|
|
671
|
+
</Button>
|
|
672
|
+
</Space>
|
|
673
|
+
</Layout.Header>
|
|
674
|
+
|
|
675
|
+
<Layout class="min-h-0 flex-1">
|
|
676
|
+
<Layout.Sider :width="leftWidth" theme="light" class="min-h-0 overflow-hidden">
|
|
677
|
+
<MaterialSidebar
|
|
678
|
+
:layers="layers"
|
|
679
|
+
:all-elements="allElements"
|
|
680
|
+
:selected-ids="selectedIds"
|
|
681
|
+
:on-select-node="(nodeId, layerId) => { if (activeLayerId !== layerId) setActiveLayer(layerId); selectIds([nodeId]); }"
|
|
682
|
+
/>
|
|
683
|
+
</Layout.Sider>
|
|
684
|
+
|
|
685
|
+
<Layout.Content class="relative min-h-0 min-w-0">
|
|
686
|
+
<div class="flex h-full min-h-0 flex-col">
|
|
687
|
+
<div class="flex flex-wrap items-center gap-2 border-b bg-white px-3 py-2">
|
|
688
|
+
<Input
|
|
689
|
+
v-model:value="productName"
|
|
690
|
+
size="small"
|
|
691
|
+
class="w-[200px]"
|
|
692
|
+
placeholder="产物名称"
|
|
693
|
+
/>
|
|
694
|
+
<span class="text-xs text-gray-500">缩放</span>
|
|
695
|
+
<Slider
|
|
696
|
+
:min="0.25"
|
|
697
|
+
:max="4"
|
|
698
|
+
:step="0.05"
|
|
699
|
+
:value="uniformZoom"
|
|
700
|
+
style="width: 140px"
|
|
701
|
+
@change="(v: number | [number, number]) => setUniformZoom(Array.isArray(v) ? v[0] : v)"
|
|
702
|
+
/>
|
|
703
|
+
<Tag v-if="activeLayer">{{ activeLayer.name }}</Tag>
|
|
704
|
+
<Tag color="blue">已选 {{ selectedIds.length }}</Tag>
|
|
705
|
+
<div data-blueprint-toggle class="ml-auto flex items-center gap-2">
|
|
706
|
+
<span class="text-xs text-gray-500">蓝图</span>
|
|
707
|
+
<Switch v-model:checked="blueprintOpen" size="small" />
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<WorkspaceStageSplit
|
|
712
|
+
:blueprint-open="blueprintOpen"
|
|
713
|
+
:blueprint-props="blueprintCanvasProps"
|
|
714
|
+
:blueprint-library-items="blueprintLibraryItems"
|
|
715
|
+
:active-blueprint-library-id="activeBlueprintLibraryId"
|
|
716
|
+
:current-blueprint-label="blueprintMeta.name"
|
|
717
|
+
:can-sync-blueprint="blueprintLibraryDirty"
|
|
718
|
+
:blueprint-debug="blueprintDebugToolbar"
|
|
719
|
+
:on-select-blueprint-library-item="(id) => void handleSelectBlueprintLibraryItem(id)"
|
|
720
|
+
:on-save-blueprint="() => void saveBlueprintToLibrary(blueprintMeta)"
|
|
721
|
+
:on-sync-blueprint="() => void syncBlueprintToLibrary()"
|
|
722
|
+
@graph-change="(g) => (blueprintGraph = g)"
|
|
723
|
+
>
|
|
724
|
+
<PanelCanvas
|
|
725
|
+
class="h-full w-full"
|
|
726
|
+
:zoom="zoom"
|
|
727
|
+
:scroll-ref="syncScrollRef"
|
|
728
|
+
:canvas-ref="syncCanvasRef"
|
|
729
|
+
:viewport-sync-ref="viewportSyncRef"
|
|
730
|
+
:on-drop-material="onDropMaterial"
|
|
731
|
+
:on-canvas-mouse-down-capture="onCanvasMouseDownCapture"
|
|
732
|
+
@zoom-change="(z) => (zoom = z)"
|
|
733
|
+
@scroll-change="onViewportScrollChange"
|
|
734
|
+
>
|
|
735
|
+
<ElementsLayer
|
|
736
|
+
:elements="elements"
|
|
737
|
+
:all-elements="allElements"
|
|
738
|
+
:selected-ids="selectedIds"
|
|
739
|
+
:update-element="updateElement"
|
|
740
|
+
:layer-locked="Boolean(activeLayer?.locked)"
|
|
741
|
+
@select-ids="selectIds"
|
|
742
|
+
/>
|
|
743
|
+
<SelectLayer
|
|
744
|
+
:container="canvasEl"
|
|
745
|
+
:drag-container="viewportEl"
|
|
746
|
+
:root-container="viewportEl"
|
|
747
|
+
:selected-ids="selectedIds"
|
|
748
|
+
@selected-ids-change="selectIds"
|
|
749
|
+
/>
|
|
750
|
+
<template #viewport-overlay>
|
|
751
|
+
<MoveableLayer
|
|
752
|
+
:zoom-x="zoom.x"
|
|
753
|
+
:zoom-y="zoom.y"
|
|
754
|
+
:canvas-container="canvasEl"
|
|
755
|
+
:drag-container="viewportEl"
|
|
756
|
+
:selected-targets="selectedTargets"
|
|
757
|
+
:elements-by-id="byId"
|
|
758
|
+
:update-element="updateElement"
|
|
759
|
+
:refresh-token="historyCursor"
|
|
760
|
+
:viewport-sync-ref="viewportSyncRef"
|
|
761
|
+
/>
|
|
762
|
+
</template>
|
|
763
|
+
</PanelCanvas>
|
|
764
|
+
</WorkspaceStageSplit>
|
|
765
|
+
|
|
766
|
+
<div class="border-t bg-white px-2 py-2">
|
|
767
|
+
<div class="mb-2 flex flex-wrap items-center gap-2">
|
|
768
|
+
<span class="text-xs font-semibold text-gray-500">图层</span>
|
|
769
|
+
<Button size="small" @click="addLayer()">新建</Button>
|
|
770
|
+
<Button size="small" :disabled="!canMergeLayers" @click="isMergingLayers = true">合并</Button>
|
|
771
|
+
</div>
|
|
772
|
+
<Tabs
|
|
773
|
+
size="small"
|
|
774
|
+
:active-key="activeLayerId"
|
|
775
|
+
type="card"
|
|
776
|
+
@update:active-key="(k: string | number) => setActiveLayer(String(k))"
|
|
777
|
+
>
|
|
778
|
+
<Tabs.TabPane v-for="layer in layers" :key="layer.id">
|
|
779
|
+
<template #tab>
|
|
780
|
+
<span class="inline-flex items-center gap-1">
|
|
781
|
+
{{ layer.name }}
|
|
782
|
+
<Tag v-if="layer.isMapping" color="blue" class="!m-0">映射</Tag>
|
|
783
|
+
<Tag v-if="layer.isPrimary" color="green" class="!m-0">主</Tag>
|
|
784
|
+
</span>
|
|
785
|
+
</template>
|
|
786
|
+
</Tabs.TabPane>
|
|
787
|
+
</Tabs>
|
|
788
|
+
|
|
789
|
+
<div v-if="activeLayer" class="mt-2 flex flex-wrap items-center gap-2 rounded border px-2 py-1.5 text-xs">
|
|
790
|
+
<template v-if="editingLayerId === activeLayer.id">
|
|
791
|
+
<Input
|
|
792
|
+
v-model:value="editingLayerName"
|
|
793
|
+
size="small"
|
|
794
|
+
class="w-40"
|
|
795
|
+
@press-enter="commitRenameLayer"
|
|
796
|
+
/>
|
|
797
|
+
<Button size="small" type="primary" @click="commitRenameLayer">确定</Button>
|
|
798
|
+
</template>
|
|
799
|
+
<template v-else>
|
|
800
|
+
<span class="font-medium">{{ activeLayer.name }}</span>
|
|
801
|
+
<Button size="small" type="link" @click="startRenameLayer(activeLayer.id, activeLayer.name)">重命名</Button>
|
|
802
|
+
</template>
|
|
803
|
+
<Button size="small" @click="toggleLayerLock(activeLayer.id)">
|
|
804
|
+
{{ activeLayer.locked ? "解锁" : "锁定" }}
|
|
805
|
+
</Button>
|
|
806
|
+
<Checkbox
|
|
807
|
+
:checked="Boolean(activeLayer.mergeSelected)"
|
|
808
|
+
@change="() => toggleLayerMergeSelected(activeLayer!.id)"
|
|
809
|
+
>
|
|
810
|
+
参与合并
|
|
811
|
+
</Checkbox>
|
|
812
|
+
<Button size="small" :disabled="activeLayer.isPrimary" @click="setPrimaryLayer(activeLayer.id)">
|
|
813
|
+
设为主图层
|
|
814
|
+
</Button>
|
|
815
|
+
<Button
|
|
816
|
+
size="small"
|
|
817
|
+
danger
|
|
818
|
+
:disabled="!activeLayer.editable"
|
|
819
|
+
@click="deleteLayer(activeLayer.id)"
|
|
820
|
+
>
|
|
821
|
+
删除图层
|
|
822
|
+
</Button>
|
|
823
|
+
</div>
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
</Layout.Content>
|
|
827
|
+
|
|
828
|
+
<Layout.Sider :width="rightWidth" theme="light" class="min-h-0 overflow-hidden border-l">
|
|
829
|
+
<WorkspaceConfigSidebar
|
|
830
|
+
:config-focus="configFocus"
|
|
831
|
+
:selected-element="selectedElement"
|
|
832
|
+
:selected-elements="selectedElements"
|
|
833
|
+
:layers="layers"
|
|
834
|
+
:update-element="updateElement"
|
|
835
|
+
:view-element-scope="viewElementScope"
|
|
836
|
+
:set-reference-copy-mode="setReferenceCopyMode"
|
|
837
|
+
:node-z-order-label="selectedNodeZOrderLabel"
|
|
838
|
+
:on-exclude-selected-node="excludeSelectedNode"
|
|
839
|
+
:on-adjust-node-z-order="adjustNodeZOrder"
|
|
840
|
+
:selected-blueprint-node="selectedBlueprintNode"
|
|
841
|
+
:allow-false-signal-propagation="blueprintGraph.document.allowFalseSignalPropagation"
|
|
842
|
+
:blueprint-library-options="blueprintLibraryOptions"
|
|
843
|
+
:all-view-elements="allElements"
|
|
844
|
+
:on-update-blueprint-node="handleUpdateBlueprintNode"
|
|
845
|
+
:on-update-allow-false-signal-propagation="handleUpdateAllowFalseSignalPropagation"
|
|
846
|
+
/>
|
|
847
|
+
</Layout.Sider>
|
|
848
|
+
</Layout>
|
|
849
|
+
|
|
850
|
+
<input ref="importInputRef" type="file" accept="application/json" class="hidden" @change="async (e) => { const f = (e.target as HTMLInputElement).files?.[0]; (e.target as HTMLInputElement).value = ''; if (f) await handleImportFile(f); }" />
|
|
851
|
+
<input ref="blueprintImportInputRef" type="file" accept="application/json" class="hidden" @change="async (e) => { const f = (e.target as HTMLInputElement).files?.[0]; (e.target as HTMLInputElement).value = ''; if (f) await handleBlueprintImportFile(f); }" />
|
|
852
|
+
|
|
853
|
+
<Modal
|
|
854
|
+
v-model:open="isMergingLayers"
|
|
855
|
+
title="合并图层"
|
|
856
|
+
ok-text="合并"
|
|
857
|
+
@ok="handleMergeLayers"
|
|
858
|
+
>
|
|
859
|
+
<Input v-model:value="mergeLayerName" placeholder="合并后图层名称(可选)" />
|
|
860
|
+
<div class="mt-2 text-xs text-gray-500">已勾选 {{ mergeSelectedCount }} 个图层</div>
|
|
861
|
+
</Modal>
|
|
862
|
+
</Layout>
|
|
863
|
+
</template>
|
|
864
|
+
|
|
865
|
+
<style scoped>
|
|
866
|
+
.vue-view-panel :deep(.ant-layout-sider-children) {
|
|
867
|
+
display: flex;
|
|
868
|
+
flex-direction: column;
|
|
869
|
+
min-height: 0;
|
|
870
|
+
}
|
|
871
|
+
</style>
|