@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,526 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import { Card, Empty, Input, Switch, Tabs } from "ant-design-vue";
|
|
4
|
+
import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../types";
|
|
5
|
+
import { PANEL_MESSAGES } from "../constants/messages";
|
|
6
|
+
import {
|
|
7
|
+
concreteGridParentIdForLayer,
|
|
8
|
+
logicalGridParentIdFromConcrete,
|
|
9
|
+
} from "../utils/mappingLayerOps";
|
|
10
|
+
import MaterialPreview from "./MaterialPreview.vue";
|
|
11
|
+
import MaterialSidebarTreeNode from "./MaterialSidebarTreeNode.vue";
|
|
12
|
+
import {
|
|
13
|
+
defaultCategories,
|
|
14
|
+
getNodeDisplayName,
|
|
15
|
+
themedScrollbarClass,
|
|
16
|
+
type MaterialCategoryId,
|
|
17
|
+
type MaterialItem,
|
|
18
|
+
} from "./materialSidebarData";
|
|
19
|
+
|
|
20
|
+
export type MaterialSidebarProps = {
|
|
21
|
+
class?: string;
|
|
22
|
+
onDragMaterialStart?: (material: MaterialItem) => void;
|
|
23
|
+
layers?: PanelLayer[];
|
|
24
|
+
allElements?: PanelElement[];
|
|
25
|
+
selectedIds?: string[];
|
|
26
|
+
onSelectNode?: (nodeId: string, layerId: string) => void;
|
|
27
|
+
onNodeContextMenu?: (payload: {
|
|
28
|
+
nodeId: string;
|
|
29
|
+
layerId: string;
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
}) => void;
|
|
33
|
+
onDeleteNode?: (nodeId: string) => void;
|
|
34
|
+
onCopyNode?: (nodeId: string, mode?: ReferenceCopyMode) => void;
|
|
35
|
+
onMoveNodeToLayer?: (nodeId: string, targetLayerId: string) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(defineProps<MaterialSidebarProps>(), {
|
|
39
|
+
layers: () => [],
|
|
40
|
+
allElements: () => [],
|
|
41
|
+
selectedIds: () => [],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const leftTab = ref<"materials" | "tree">("materials");
|
|
45
|
+
const activeCategoryId = ref<MaterialCategoryId>("charts");
|
|
46
|
+
const keyword = ref("");
|
|
47
|
+
const treeKeyword = ref("");
|
|
48
|
+
const referenceOnlyTree = ref(false);
|
|
49
|
+
const draggingTreeNodeId = ref<string | null>(null);
|
|
50
|
+
const dragOverLayerId = ref<string | null>(null);
|
|
51
|
+
const expandedKeys = ref<Record<string, boolean>>({ root: true });
|
|
52
|
+
|
|
53
|
+
const categories = defaultCategories;
|
|
54
|
+
const normalizedKeyword = computed(() => keyword.value.trim().toLowerCase());
|
|
55
|
+
const isSearching = computed(() => normalizedKeyword.value.length > 0);
|
|
56
|
+
const normalizedTreeKeyword = computed(() => treeKeyword.value.trim().toLowerCase());
|
|
57
|
+
const isTreeSearching = computed(() => normalizedTreeKeyword.value.length > 0);
|
|
58
|
+
|
|
59
|
+
const activeCategory = computed(
|
|
60
|
+
() => categories.find((c) => c.id === activeCategoryId.value) ?? categories[0]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const matchedItems = computed(() => {
|
|
64
|
+
if (!isSearching.value) return [];
|
|
65
|
+
const result: Array<MaterialItem & { categoryTitle: string }> = [];
|
|
66
|
+
categories.forEach((category) => {
|
|
67
|
+
category.items.forEach((item) => {
|
|
68
|
+
const haystack = `${item.title} ${item.id} ${category.title}`.toLowerCase();
|
|
69
|
+
if (haystack.includes(normalizedKeyword.value)) {
|
|
70
|
+
result.push({ ...item, categoryTitle: category.title });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return result;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const elementsByLayer = computed(() => {
|
|
78
|
+
const map = new Map<string, PanelElement[]>();
|
|
79
|
+
for (const layer of props.layers) map.set(layer.id, []);
|
|
80
|
+
for (const el of props.allElements) {
|
|
81
|
+
const list = map.get(el.layerId) ?? [];
|
|
82
|
+
list.push(el);
|
|
83
|
+
map.set(el.layerId, list);
|
|
84
|
+
}
|
|
85
|
+
return map;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const elementsById = computed(() => {
|
|
89
|
+
const map = new Map<string, PanelElement>();
|
|
90
|
+
for (const el of props.allElements) map.set(el.id, el);
|
|
91
|
+
return map;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const layerById = computed(() => {
|
|
95
|
+
const map = new Map<string, PanelLayer>();
|
|
96
|
+
for (const layer of props.layers) map.set(layer.id, layer);
|
|
97
|
+
return map;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const effectiveGridParentByElementId = computed(() => {
|
|
101
|
+
const byId = new Map<string, PanelElement>();
|
|
102
|
+
for (const el of props.allElements) byId.set(el.id, el);
|
|
103
|
+
const map = new Map<string, string | undefined>();
|
|
104
|
+
for (const el of props.allElements) {
|
|
105
|
+
const pg = el.parentGridId;
|
|
106
|
+
if (!pg) {
|
|
107
|
+
map.set(el.id, undefined);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const parent = byId.get(pg);
|
|
111
|
+
if (parent?.layerId === el.layerId && parent.materialType === "grid") {
|
|
112
|
+
map.set(el.id, pg);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const logical = logicalGridParentIdFromConcrete(pg, byId);
|
|
116
|
+
if (logical !== undefined) {
|
|
117
|
+
const concrete = concreteGridParentIdForLayer(
|
|
118
|
+
logical,
|
|
119
|
+
el.layerId,
|
|
120
|
+
props.allElements
|
|
121
|
+
);
|
|
122
|
+
map.set(el.id, concrete ?? undefined);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
map.set(el.id, undefined);
|
|
126
|
+
}
|
|
127
|
+
return map;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const childrenByGridByLayer = computed(() => {
|
|
131
|
+
const outer = new Map<string, Map<string, PanelElement[]>>();
|
|
132
|
+
for (const el of props.allElements) {
|
|
133
|
+
const gridParentId = effectiveGridParentByElementId.value.get(el.id);
|
|
134
|
+
if (!gridParentId) continue;
|
|
135
|
+
let inner = outer.get(el.layerId);
|
|
136
|
+
if (!inner) {
|
|
137
|
+
inner = new Map();
|
|
138
|
+
outer.set(el.layerId, inner);
|
|
139
|
+
}
|
|
140
|
+
const list = inner.get(gridParentId) ?? [];
|
|
141
|
+
list.push(el);
|
|
142
|
+
inner.set(gridParentId, list);
|
|
143
|
+
}
|
|
144
|
+
return outer;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function isExpanded(key: string, defaultValue = false) {
|
|
148
|
+
return expandedKeys.value[key] ?? defaultValue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function setExpanded(key: string, next: boolean) {
|
|
152
|
+
expandedKeys.value = { ...expandedKeys.value, [key]: next };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function hasRefInSubtree(node: PanelElement, visited: Set<string>): boolean {
|
|
156
|
+
if (node.materialType === "reference") return true;
|
|
157
|
+
if (visited.has(node.id)) return false;
|
|
158
|
+
const nextVisited = new Set(visited);
|
|
159
|
+
nextVisited.add(node.id);
|
|
160
|
+
const children =
|
|
161
|
+
node.refCopyMode === "deep"
|
|
162
|
+
? node.refSnapshot ?? []
|
|
163
|
+
: node.refLayerId
|
|
164
|
+
? elementsByLayer.value.get(node.refLayerId) ?? []
|
|
165
|
+
: [];
|
|
166
|
+
return children.some((child) => hasRefInSubtree(child, nextVisited));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function nodeMatchesTreeSearch(node: PanelElement, visited: Set<string>): boolean {
|
|
170
|
+
if (!isTreeSearching.value) return true;
|
|
171
|
+
const selfText = `${getNodeDisplayName(node)} ${node.materialType ?? ""} ${node.id}`.toLowerCase();
|
|
172
|
+
if (selfText.includes(normalizedTreeKeyword.value)) return true;
|
|
173
|
+
if (visited.has(node.id)) return false;
|
|
174
|
+
const nextVisited = new Set(visited);
|
|
175
|
+
nextVisited.add(node.id);
|
|
176
|
+
const isRef = node.materialType === "reference";
|
|
177
|
+
const isGrid = node.materialType === "grid";
|
|
178
|
+
const gridChildren = isGrid
|
|
179
|
+
? [...(childrenByGridByLayer.value.get(node.layerId)?.get(node.id) ?? [])]
|
|
180
|
+
: [];
|
|
181
|
+
const children = isRef
|
|
182
|
+
? node.refCopyMode === "deep"
|
|
183
|
+
? node.refSnapshot ?? []
|
|
184
|
+
: node.refLayerId
|
|
185
|
+
? elementsByLayer.value.get(node.refLayerId) ?? []
|
|
186
|
+
: []
|
|
187
|
+
: gridChildren;
|
|
188
|
+
return children.some((child) => nodeMatchesTreeSearch(child, nextVisited));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getRootNodes(layerId: string) {
|
|
192
|
+
const layerNodes = elementsByLayer.value.get(layerId) ?? [];
|
|
193
|
+
return layerNodes
|
|
194
|
+
.filter((node) => !effectiveGridParentByElementId.value.get(node.id))
|
|
195
|
+
.filter((node) => !referenceOnlyTree.value || hasRefInSubtree(node, new Set()))
|
|
196
|
+
.filter((node) => nodeMatchesTreeSearch(node, new Set()));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getLayerDropState(layer: PanelLayer) {
|
|
200
|
+
const draggingNode = draggingTreeNodeId.value
|
|
201
|
+
? elementsById.value.get(draggingTreeNodeId.value) ?? null
|
|
202
|
+
: null;
|
|
203
|
+
const draggingSourceLayer = draggingNode
|
|
204
|
+
? layerById.value.get(draggingNode.layerId) ?? null
|
|
205
|
+
: null;
|
|
206
|
+
const dropBlockReason = !draggingNode
|
|
207
|
+
? ""
|
|
208
|
+
: draggingNode.locked
|
|
209
|
+
? PANEL_MESSAGES.nodeMoveLocked
|
|
210
|
+
: draggingSourceLayer?.locked
|
|
211
|
+
? PANEL_MESSAGES.nodeMoveSourceLayerLocked
|
|
212
|
+
: layer.locked
|
|
213
|
+
? PANEL_MESSAGES.nodeMoveTargetLayerLocked
|
|
214
|
+
: draggingNode.layerId === layer.id
|
|
215
|
+
? PANEL_MESSAGES.nodeMoveSameLayer
|
|
216
|
+
: "";
|
|
217
|
+
const canDropIntoLayer = Boolean(draggingNode) && !dropBlockReason;
|
|
218
|
+
return { draggingNode, dropBlockReason, canDropIntoLayer };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function onMaterialDragStart(e: DragEvent, item: MaterialItem) {
|
|
222
|
+
e.dataTransfer?.setData(
|
|
223
|
+
"application/x-arronqzy-material",
|
|
224
|
+
JSON.stringify({ id: item.id, title: item.title })
|
|
225
|
+
);
|
|
226
|
+
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy";
|
|
227
|
+
props.onDragMaterialStart?.(item);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function onLayerDragOver(e: DragEvent, layer: PanelLayer) {
|
|
231
|
+
const hasNodeData = e.dataTransfer?.types.includes("application/x-arronqzy-tree-node");
|
|
232
|
+
if (!hasNodeData) return;
|
|
233
|
+
const { canDropIntoLayer } = getLayerDropState(layer);
|
|
234
|
+
if (canDropIntoLayer) {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
237
|
+
} else if (e.dataTransfer) {
|
|
238
|
+
e.dataTransfer.dropEffect = "none";
|
|
239
|
+
}
|
|
240
|
+
if (dragOverLayerId.value !== layer.id) dragOverLayerId.value = layer.id;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function onLayerDrop(e: DragEvent, layer: PanelLayer) {
|
|
244
|
+
const payload = e.dataTransfer?.getData("application/x-arronqzy-tree-node");
|
|
245
|
+
if (!payload) return;
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
try {
|
|
248
|
+
const data = JSON.parse(payload) as { nodeId?: string; sourceLayerId?: string };
|
|
249
|
+
const { canDropIntoLayer } = getLayerDropState(layer);
|
|
250
|
+
if (!data.nodeId || !layer.id) return;
|
|
251
|
+
if (!canDropIntoLayer) return;
|
|
252
|
+
if (data.sourceLayerId === layer.id) return;
|
|
253
|
+
props.onMoveNodeToLayer?.(data.nodeId, layer.id);
|
|
254
|
+
} catch {
|
|
255
|
+
// ignore invalid payload
|
|
256
|
+
} finally {
|
|
257
|
+
draggingTreeNodeId.value = null;
|
|
258
|
+
dragOverLayerId.value = null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const treeSearchEmpty = computed(
|
|
263
|
+
() =>
|
|
264
|
+
isTreeSearching.value &&
|
|
265
|
+
props.layers.every((layer) => getRootNodes(layer.id).length === 0)
|
|
266
|
+
);
|
|
267
|
+
</script>
|
|
268
|
+
|
|
269
|
+
<template>
|
|
270
|
+
<aside
|
|
271
|
+
:class="[
|
|
272
|
+
'grid h-full w-full border-r border-border bg-muted/30 text-foreground',
|
|
273
|
+
props.class ?? '',
|
|
274
|
+
]"
|
|
275
|
+
style="grid-template-rows: auto 1fr"
|
|
276
|
+
>
|
|
277
|
+
<div class="border-b border-border bg-background/80 px-3 py-2 backdrop-blur-sm">
|
|
278
|
+
<Input
|
|
279
|
+
v-model:value="keyword"
|
|
280
|
+
size="small"
|
|
281
|
+
placeholder="搜索物料(名称 / 分类)"
|
|
282
|
+
class="text-xs"
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<Tabs v-model:active-key="leftTab" class="flex min-h-0 h-full flex-col">
|
|
287
|
+
<Tabs.TabPane key="materials" tab="物料" class="min-h-0 flex-1">
|
|
288
|
+
<div class="grid h-full min-h-0 grid-cols-[110px_1fr]">
|
|
289
|
+
<div :class="`overflow-auto border-r border-border ${themedScrollbarClass}`">
|
|
290
|
+
<button
|
|
291
|
+
v-for="c in categories"
|
|
292
|
+
:key="c.id"
|
|
293
|
+
type="button"
|
|
294
|
+
:class="[
|
|
295
|
+
'w-full cursor-pointer border-b border-border/40 px-2.5 py-2.5 text-left text-xs',
|
|
296
|
+
c.id === activeCategoryId
|
|
297
|
+
? 'bg-accent text-accent-foreground'
|
|
298
|
+
: 'text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground',
|
|
299
|
+
]"
|
|
300
|
+
@click="activeCategoryId = c.id"
|
|
301
|
+
>
|
|
302
|
+
{{ c.title }}
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div :class="`overflow-auto ${themedScrollbarClass}`">
|
|
307
|
+
<div class="px-2.5 py-2.5 text-xs font-semibold">
|
|
308
|
+
{{ isSearching ? `搜索结果(${matchedItems.length})` : activeCategory.title }}
|
|
309
|
+
</div>
|
|
310
|
+
<div class="grid gap-2 px-2.5 pb-3">
|
|
311
|
+
<button
|
|
312
|
+
v-for="it in isSearching ? matchedItems : activeCategory.items"
|
|
313
|
+
:key="isSearching ? `${it.id}-${(it as any).categoryTitle}` : it.id"
|
|
314
|
+
type="button"
|
|
315
|
+
draggable="true"
|
|
316
|
+
class="cursor-pointer rounded-xl border border-border bg-card px-2 py-2 text-left text-xs text-card-foreground hover:bg-accent/60"
|
|
317
|
+
@dragstart="onMaterialDragStart($event, it)"
|
|
318
|
+
>
|
|
319
|
+
<div class="flex flex-col items-stretch gap-2">
|
|
320
|
+
<MaterialPreview :id="it.id" />
|
|
321
|
+
<div class="min-w-0">
|
|
322
|
+
<div class="truncate">{{ it.title }}</div>
|
|
323
|
+
<div
|
|
324
|
+
v-if="isSearching"
|
|
325
|
+
class="mt-1 text-[11px] text-muted-foreground"
|
|
326
|
+
>
|
|
327
|
+
{{ (it as any).categoryTitle }}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</button>
|
|
332
|
+
<Empty
|
|
333
|
+
v-if="isSearching && matchedItems.length === 0"
|
|
334
|
+
class="py-5"
|
|
335
|
+
description="未匹配到物料"
|
|
336
|
+
>
|
|
337
|
+
<template #description>
|
|
338
|
+
<span class="text-xs">未匹配到物料</span>
|
|
339
|
+
<div class="text-[11px] text-muted-foreground">
|
|
340
|
+
尝试更换关键词,或切换分类后再搜索。
|
|
341
|
+
</div>
|
|
342
|
+
</template>
|
|
343
|
+
</Empty>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</Tabs.TabPane>
|
|
348
|
+
|
|
349
|
+
<Tabs.TabPane key="tree" tab="节点树" class="min-h-0 flex-1">
|
|
350
|
+
<div :class="`h-full overflow-auto px-2 py-2 text-xs ${themedScrollbarClass}`">
|
|
351
|
+
<div class="mb-2">
|
|
352
|
+
<Input
|
|
353
|
+
v-model:value="treeKeyword"
|
|
354
|
+
size="small"
|
|
355
|
+
placeholder="搜索节点(名称 / 类型 / ID)"
|
|
356
|
+
class="text-xs"
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
<div class="mb-2 flex items-center justify-between rounded border border-border bg-card px-2 py-1.5">
|
|
360
|
+
<span class="text-[11px] text-muted-foreground">仅看引用子树</span>
|
|
361
|
+
<Switch v-model:checked="referenceOnlyTree" size="small" aria-label="仅看引用子树" />
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<div class="rounded border border-border bg-card">
|
|
365
|
+
<div class="flex items-center gap-1 px-2 py-1.5 font-medium">
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
class="flex h-7 w-7 items-center justify-center rounded text-sm hover:bg-accent"
|
|
369
|
+
@click="setExpanded('root', !isExpanded('root', true))"
|
|
370
|
+
>
|
|
371
|
+
{{ isExpanded("root", true) ? "▾" : "▸" }}
|
|
372
|
+
</button>
|
|
373
|
+
<span>root</span>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div v-show="isExpanded('root', true)" class="space-y-1 border-t border-border/60 py-1">
|
|
377
|
+
<template v-for="layer in layers" :key="layer.id">
|
|
378
|
+
<div v-if="!(isTreeSearching && getRootNodes(layer.id).length === 0)">
|
|
379
|
+
<Card
|
|
380
|
+
:class="[
|
|
381
|
+
'mb-2 overflow-hidden transition-shadow',
|
|
382
|
+
layer.isMapping
|
|
383
|
+
? 'border-2 border-violet-500/70 bg-gradient-to-br from-violet-500/14 via-violet-600/10 to-fuchsia-500/12'
|
|
384
|
+
: '',
|
|
385
|
+
dragOverLayerId === layer.id
|
|
386
|
+
? getLayerDropState(layer).canDropIntoLayer
|
|
387
|
+
? 'ring-2 ring-primary/45 ring-offset-2 ring-offset-background'
|
|
388
|
+
: 'ring-2 ring-destructive/45 ring-offset-2 ring-offset-background'
|
|
389
|
+
: '',
|
|
390
|
+
]"
|
|
391
|
+
size="small"
|
|
392
|
+
:body-style="{ padding: '10px' }"
|
|
393
|
+
>
|
|
394
|
+
<div class="mb-1.5 flex flex-wrap items-center gap-1">
|
|
395
|
+
<span
|
|
396
|
+
v-if="dragOverLayerId === layer.id"
|
|
397
|
+
:class="[
|
|
398
|
+
'inline-flex h-5 w-5 shrink-0 items-center justify-center rounded text-[11px]',
|
|
399
|
+
getLayerDropState(layer).canDropIntoLayer
|
|
400
|
+
? 'bg-primary/15 text-primary'
|
|
401
|
+
: 'bg-destructive/15 text-destructive',
|
|
402
|
+
]"
|
|
403
|
+
:title="
|
|
404
|
+
getLayerDropState(layer).canDropIntoLayer
|
|
405
|
+
? '目标图层'
|
|
406
|
+
: getLayerDropState(layer).dropBlockReason
|
|
407
|
+
"
|
|
408
|
+
>
|
|
409
|
+
➜
|
|
410
|
+
</span>
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
class="flex h-7 w-7 shrink-0 items-center justify-center rounded text-sm text-muted-foreground hover:bg-accent"
|
|
414
|
+
@click="
|
|
415
|
+
setExpanded(`layer:${layer.id}`, !isExpanded(`layer:${layer.id}`, true))
|
|
416
|
+
"
|
|
417
|
+
>
|
|
418
|
+
{{
|
|
419
|
+
isExpanded(`layer:${layer.id}`, true) ? "▾" : "▸"
|
|
420
|
+
}}
|
|
421
|
+
</button>
|
|
422
|
+
<span class="min-w-0 flex-1 truncate text-xs font-medium text-foreground">
|
|
423
|
+
{{ layer.name }}
|
|
424
|
+
<span class="font-normal text-muted-foreground">
|
|
425
|
+
({{ getRootNodes(layer.id).length }})
|
|
426
|
+
</span>
|
|
427
|
+
</span>
|
|
428
|
+
<span
|
|
429
|
+
v-if="layer.isMapping"
|
|
430
|
+
class="shrink-0 rounded-md border-2 border-violet-600 bg-violet-500/22 px-1.5 py-0.5 text-[10px] font-semibold text-violet-950 dark:border-violet-400 dark:bg-violet-500/35 dark:text-violet-50"
|
|
431
|
+
>
|
|
432
|
+
映射图层
|
|
433
|
+
</span>
|
|
434
|
+
<span
|
|
435
|
+
v-if="dragOverLayerId === layer.id"
|
|
436
|
+
:class="[
|
|
437
|
+
'ml-auto shrink-0 rounded px-1.5 py-0.5 text-[10px]',
|
|
438
|
+
getLayerDropState(layer).canDropIntoLayer
|
|
439
|
+
? 'bg-primary/15 text-primary'
|
|
440
|
+
: 'bg-destructive/15 text-destructive',
|
|
441
|
+
]"
|
|
442
|
+
>
|
|
443
|
+
{{
|
|
444
|
+
getLayerDropState(layer).canDropIntoLayer
|
|
445
|
+
? "将移动到该图层"
|
|
446
|
+
: getLayerDropState(layer).dropBlockReason
|
|
447
|
+
}}
|
|
448
|
+
</span>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<div
|
|
452
|
+
:class="[
|
|
453
|
+
'mb-2 rounded-md border border-dashed px-2 py-1.5 text-[10px] transition-colors',
|
|
454
|
+
dragOverLayerId === layer.id
|
|
455
|
+
? getLayerDropState(layer).canDropIntoLayer
|
|
456
|
+
? 'border-primary/50 bg-primary/10 text-primary'
|
|
457
|
+
: 'border-destructive/50 bg-destructive/10 text-destructive'
|
|
458
|
+
: 'border-border/50 bg-muted/20 text-muted-foreground hover:border-border hover:bg-muted/35',
|
|
459
|
+
]"
|
|
460
|
+
title="拖拽节点到此图层"
|
|
461
|
+
@dragover="onLayerDragOver($event, layer)"
|
|
462
|
+
@dragleave="dragOverLayerId === layer.id && (dragOverLayerId = null)"
|
|
463
|
+
@drop="onLayerDrop($event, layer)"
|
|
464
|
+
>
|
|
465
|
+
{{
|
|
466
|
+
draggingTreeNodeId
|
|
467
|
+
? getLayerDropState(layer).canDropIntoLayer
|
|
468
|
+
? "释放以移动到该图层"
|
|
469
|
+
: getLayerDropState(layer).dropBlockReason || "不可移动到该图层"
|
|
470
|
+
: "拖拽节点到该图层"
|
|
471
|
+
}}
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<div v-show="isExpanded(`layer:${layer.id}`, true)">
|
|
475
|
+
<div
|
|
476
|
+
v-if="getRootNodes(layer.id).length === 0"
|
|
477
|
+
class="rounded border border-border/40 bg-muted/15 py-2 pl-3 text-[11px] text-muted-foreground"
|
|
478
|
+
>
|
|
479
|
+
空图层
|
|
480
|
+
</div>
|
|
481
|
+
<MaterialSidebarTreeNode
|
|
482
|
+
v-for="node in getRootNodes(layer.id)"
|
|
483
|
+
:key="node.id"
|
|
484
|
+
:node="node"
|
|
485
|
+
:level="2"
|
|
486
|
+
:path="`${layer.id}/${node.id}`"
|
|
487
|
+
:visited="new Set()"
|
|
488
|
+
:selected-ids="selectedIds"
|
|
489
|
+
:layer-by-id="layerById"
|
|
490
|
+
:elements-by-layer="elementsByLayer"
|
|
491
|
+
:children-by-grid-by-layer="childrenByGridByLayer"
|
|
492
|
+
:expanded-keys="expandedKeys"
|
|
493
|
+
:normalized-tree-keyword="normalizedTreeKeyword"
|
|
494
|
+
:is-tree-searching="isTreeSearching"
|
|
495
|
+
:dragging-tree-node-id="draggingTreeNodeId"
|
|
496
|
+
@toggle-expanded="setExpanded"
|
|
497
|
+
@select-node="(id, lid) => onSelectNode?.(id, lid)"
|
|
498
|
+
@node-context-menu="(p) => onNodeContextMenu?.(p)"
|
|
499
|
+
@delete-node="(id) => onDeleteNode?.(id)"
|
|
500
|
+
@copy-node="(id, mode) => onCopyNode?.(id, mode)"
|
|
501
|
+
@drag-start="(id) => (draggingTreeNodeId = id)"
|
|
502
|
+
@drag-end="
|
|
503
|
+
draggingTreeNodeId = null;
|
|
504
|
+
dragOverLayerId = null;
|
|
505
|
+
"
|
|
506
|
+
/>
|
|
507
|
+
</div>
|
|
508
|
+
</Card>
|
|
509
|
+
</div>
|
|
510
|
+
</template>
|
|
511
|
+
|
|
512
|
+
<Empty v-if="treeSearchEmpty" class="mx-2 my-2 py-5" description="未匹配到节点">
|
|
513
|
+
<template #description>
|
|
514
|
+
<span class="text-xs">未匹配到节点</span>
|
|
515
|
+
<div class="text-[11px] text-muted-foreground">
|
|
516
|
+
试试节点名称、类型或 ID 关键词。
|
|
517
|
+
</div>
|
|
518
|
+
</template>
|
|
519
|
+
</Empty>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
</Tabs.TabPane>
|
|
524
|
+
</Tabs>
|
|
525
|
+
</aside>
|
|
526
|
+
</template>
|