@arronqzy/react-view 0.0.1
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/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/pnpm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
- package/.turbo/turbo-lint.log +4 -0
- package/dist/styles.css +2624 -0
- package/env.d.ts +11 -0
- package/operation-guide.md +275 -0
- package/package.json +43 -0
- package/postcss.config.cjs +7 -0
- package/readme.md +21 -0
- package/src/core/createAction.ts +5 -0
- package/src/core/createView.ts +28 -0
- package/src/core/instructions.tsx +57 -0
- package/src/core/nodePool.ts +127 -0
- package/src/core/signal.ts +130 -0
- package/src/core/template.tsx +25 -0
- package/src/index.ts +7 -0
- package/src/panel/ReactViewOnlinePreview.tsx +331 -0
- package/src/panel/ReactViewPanel.tsx +3221 -0
- package/src/panel/components/ElementsLayer.tsx +891 -0
- package/src/panel/components/MaterialSidebar.tsx +1142 -0
- package/src/panel/components/MoveableLayer.tsx +803 -0
- package/src/panel/components/PanelCanvas.tsx +660 -0
- package/src/panel/components/PanelConfigSidebar.tsx +3703 -0
- package/src/panel/components/PanelRulers.tsx +89 -0
- package/src/panel/components/SelectLayer.tsx +106 -0
- package/src/panel/components/ViewElementScopePanel.tsx +79 -0
- package/src/panel/components/WorkspaceConfigSidebar.tsx +158 -0
- package/src/panel/components/WorkspaceProjectNav.tsx +220 -0
- package/src/panel/components/WorkspaceStageSplit.tsx +179 -0
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +170 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +250 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +59 -0
- package/src/panel/constants/messages.ts +34 -0
- package/src/panel/constants/zIndex.ts +6 -0
- package/src/panel/hooks/usePanelElements.ts +1108 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +278 -0
- package/src/panel/index.tsx +3 -0
- package/src/panel/library/workspace-project-cache.ts +23 -0
- package/src/panel/library/workspace-project-db.ts +112 -0
- package/src/panel/library/workspace-project-sync.ts +41 -0
- package/src/panel/library/workspace-snapshot.ts +27 -0
- package/src/panel/scope/view-scope-store.ts +55 -0
- package/src/panel/types.ts +125 -0
- package/src/panel/utils/chartOptionBuilder.ts +316 -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 +45 -0
- package/src/panel/utils/scope-template-warnings.ts +135 -0
- package/src/panel/utils/scope-template.ts +75 -0
- package/src/panel/utils/updateElementDraft.ts +221 -0
- package/src/panel/viewportZoom.ts +26 -0
- package/src/tailwind.css +58 -0
- package/src/types/react-infinite-viewer.d.ts +39 -0
- package/tailwind.config.ts +7 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,1142 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardContent,
|
|
5
|
+
CardHeader,
|
|
6
|
+
Collapsible,
|
|
7
|
+
CollapsibleContent,
|
|
8
|
+
CollapsibleTrigger,
|
|
9
|
+
Empty,
|
|
10
|
+
EmptyDescription,
|
|
11
|
+
EmptyIcon,
|
|
12
|
+
EmptyTitle,
|
|
13
|
+
Input,
|
|
14
|
+
Switch,
|
|
15
|
+
Tabs,
|
|
16
|
+
TabsContent,
|
|
17
|
+
TabsList,
|
|
18
|
+
TabsTrigger,
|
|
19
|
+
Tooltip,
|
|
20
|
+
TooltipContent,
|
|
21
|
+
TooltipProvider,
|
|
22
|
+
TooltipTrigger,
|
|
23
|
+
} from "@arronqzy/ui";
|
|
24
|
+
import type { PanelElement, ReferenceCopyMode } from "../types";
|
|
25
|
+
import type { PanelLayer } from "../types";
|
|
26
|
+
import { PANEL_MESSAGES } from "../constants/messages";
|
|
27
|
+
import {
|
|
28
|
+
concreteGridParentIdForLayer,
|
|
29
|
+
logicalGridParentIdFromConcrete,
|
|
30
|
+
} from "../utils/mappingLayerOps";
|
|
31
|
+
|
|
32
|
+
type MaterialCategoryId = "charts" | "basic" | "media";
|
|
33
|
+
|
|
34
|
+
type MaterialItem = {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type MaterialCategory = {
|
|
40
|
+
id: MaterialCategoryId;
|
|
41
|
+
title: string;
|
|
42
|
+
items: MaterialItem[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** 节点树中网格子节点顺序:与画布槽位一致,嵌套任意深度一致 */
|
|
46
|
+
function compareGridTreeChildOrder(a: PanelElement, b: PanelElement): number {
|
|
47
|
+
const ai = typeof a.gridSlotIndex === "number" ? a.gridSlotIndex : 0;
|
|
48
|
+
const bi = typeof b.gridSlotIndex === "number" ? b.gridSlotIndex : 0;
|
|
49
|
+
if (ai !== bi) return ai - bi;
|
|
50
|
+
return a.id.localeCompare(b.id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const MATERIAL_LABEL_MAP: Record<string, string> = {
|
|
54
|
+
bar: "柱状图",
|
|
55
|
+
line: "折线图",
|
|
56
|
+
pie: "饼图",
|
|
57
|
+
area: "面积图",
|
|
58
|
+
scatter: "散点图",
|
|
59
|
+
radar: "雷达图",
|
|
60
|
+
gauge: "仪表盘",
|
|
61
|
+
funnel: "漏斗图",
|
|
62
|
+
text: "文本",
|
|
63
|
+
rect: "矩形",
|
|
64
|
+
grid: "网格布局",
|
|
65
|
+
image: "图片",
|
|
66
|
+
video: "视频",
|
|
67
|
+
audio: "音频",
|
|
68
|
+
reference: "引用组件",
|
|
69
|
+
geometry: "几何",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function LockGlyph({ className = "h-3.5 w-3.5" }: { className?: string }) {
|
|
73
|
+
return (
|
|
74
|
+
<svg
|
|
75
|
+
viewBox="0 0 24 24"
|
|
76
|
+
fill="none"
|
|
77
|
+
stroke="currentColor"
|
|
78
|
+
strokeWidth="1.9"
|
|
79
|
+
strokeLinecap="round"
|
|
80
|
+
strokeLinejoin="round"
|
|
81
|
+
className={className}
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
>
|
|
84
|
+
<rect x="4.5" y="10.5" width="15" height="10" rx="2.5" />
|
|
85
|
+
<path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" />
|
|
86
|
+
<circle cx="12" cy="15.5" r="1.2" />
|
|
87
|
+
</svg>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function IconCopy({ className = "h-3.5 w-3.5" }: { className?: string }) {
|
|
92
|
+
return (
|
|
93
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" className={className}>
|
|
94
|
+
<rect x="9" y="9" width="10" height="10" rx="2" />
|
|
95
|
+
<rect x="5" y="5" width="10" height="10" rx="2" />
|
|
96
|
+
</svg>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function IconDelete({ className = "h-3.5 w-3.5" }: { className?: string }) {
|
|
101
|
+
return (
|
|
102
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" className={className}>
|
|
103
|
+
<path d="M4 7h16" />
|
|
104
|
+
<path d="M9 7V5h6v2" />
|
|
105
|
+
<path d="M7 7l1 12h8l1-12" />
|
|
106
|
+
<path d="M10 11v5M14 11v5" />
|
|
107
|
+
</svg>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function IconShallowCopy({ className = "h-3.5 w-3.5" }: { className?: string }) {
|
|
112
|
+
return (
|
|
113
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" className={className}>
|
|
114
|
+
<rect x="8.5" y="8.5" width="10" height="10" rx="2" />
|
|
115
|
+
<rect x="5.5" y="5.5" width="10" height="10" rx="2" />
|
|
116
|
+
<path d="M7.5 16.5 16.5 7.5" strokeDasharray="2 2" />
|
|
117
|
+
</svg>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function IconDeepCopy({ className = "h-3.5 w-3.5" }: { className?: string }) {
|
|
122
|
+
return (
|
|
123
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" className={className}>
|
|
124
|
+
<rect x="4.5" y="4.5" width="15" height="4.2" rx="1.6" />
|
|
125
|
+
<rect x="4.5" y="9.9" width="15" height="4.2" rx="1.6" />
|
|
126
|
+
<rect x="4.5" y="15.3" width="15" height="4.2" rx="1.6" />
|
|
127
|
+
</svg>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function MaterialPreview({ id }: { id: string }) {
|
|
132
|
+
const common =
|
|
133
|
+
"relative aspect-[4/3] w-full shrink-0 overflow-hidden rounded-md border border-border bg-gradient-to-br from-card to-muted/40";
|
|
134
|
+
|
|
135
|
+
if (id === "bar") {
|
|
136
|
+
return (
|
|
137
|
+
<div className={common}>
|
|
138
|
+
<div className="absolute inset-0 grid grid-cols-5 items-end gap-1 px-2 pb-2 pt-1.5">
|
|
139
|
+
<div className="h-2 rounded-sm bg-primary/55" />
|
|
140
|
+
<div className="h-4 rounded-sm bg-primary/65" />
|
|
141
|
+
<div className="h-7 rounded-sm bg-primary/80" />
|
|
142
|
+
<div className="h-5 rounded-sm bg-primary/70" />
|
|
143
|
+
<div className="h-3 rounded-sm bg-primary/60" />
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (id === "line" || id === "area") {
|
|
150
|
+
return (
|
|
151
|
+
<div className={common}>
|
|
152
|
+
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
153
|
+
{id === "area" ? (
|
|
154
|
+
<polygon
|
|
155
|
+
points="6,42 18,28 34,34 50,20 66,24 74,18 74,56 6,56"
|
|
156
|
+
className="fill-primary/30"
|
|
157
|
+
/>
|
|
158
|
+
) : null}
|
|
159
|
+
<polyline
|
|
160
|
+
points="6,42 18,28 34,34 50,20 66,24 74,18"
|
|
161
|
+
fill="none"
|
|
162
|
+
stroke="hsl(var(--primary))"
|
|
163
|
+
strokeWidth="3"
|
|
164
|
+
strokeLinecap="round"
|
|
165
|
+
strokeLinejoin="round"
|
|
166
|
+
/>
|
|
167
|
+
</svg>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (id === "pie") {
|
|
173
|
+
return (
|
|
174
|
+
<div className={common}>
|
|
175
|
+
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
176
|
+
<circle cx="40" cy="30" r="16" className="fill-primary/20" />
|
|
177
|
+
<path d="M40 30 L40 14 A16 16 0 0 1 55 39 Z" className="fill-primary/75" />
|
|
178
|
+
<path d="M40 30 L55 39 A16 16 0 0 1 27 43 Z" className="fill-primary/45" />
|
|
179
|
+
</svg>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (id === "scatter") {
|
|
185
|
+
return (
|
|
186
|
+
<div className={common}>
|
|
187
|
+
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
188
|
+
<circle cx="14" cy="42" r="2.5" className="fill-primary/80" />
|
|
189
|
+
<circle cx="24" cy="34" r="2.5" className="fill-primary/70" />
|
|
190
|
+
<circle cx="36" cy="28" r="2.5" className="fill-primary/75" />
|
|
191
|
+
<circle cx="48" cy="20" r="2.5" className="fill-primary/65" />
|
|
192
|
+
<circle cx="62" cy="14" r="2.5" className="fill-primary/85" />
|
|
193
|
+
</svg>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (id === "radar") {
|
|
199
|
+
return (
|
|
200
|
+
<div className={common}>
|
|
201
|
+
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
202
|
+
<polygon points="40,10 58,20 54,40 26,40 22,20" className="fill-primary/20 stroke-primary/60" />
|
|
203
|
+
<polygon points="40,16 52,23 49,36 31,36 28,24" className="fill-primary/45 stroke-primary/80" />
|
|
204
|
+
</svg>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (id === "gauge") {
|
|
210
|
+
return (
|
|
211
|
+
<div className={common}>
|
|
212
|
+
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
213
|
+
<path d="M12 44a28 28 0 0 1 56 0" className="fill-none stroke-primary/35" strokeWidth="6" />
|
|
214
|
+
<path d="M12 44a28 28 0 0 1 40-24" className="fill-none stroke-primary/80" strokeWidth="6" />
|
|
215
|
+
<line x1="40" y1="44" x2="54" y2="28" className="stroke-primary" strokeWidth="2" />
|
|
216
|
+
</svg>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (id === "funnel") {
|
|
222
|
+
return (
|
|
223
|
+
<div className={common}>
|
|
224
|
+
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
225
|
+
<polygon points="10,10 70,10 58,22 22,22" className="fill-primary/80" />
|
|
226
|
+
<polygon points="22,24 58,24 50,34 30,34" className="fill-primary/65" />
|
|
227
|
+
<polygon points="30,36 50,36 44,44 36,44" className="fill-primary/50" />
|
|
228
|
+
</svg>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (id === "text") {
|
|
234
|
+
return (
|
|
235
|
+
<div className={common}>
|
|
236
|
+
<div className="space-y-1.5 px-2 py-2">
|
|
237
|
+
<div className="h-2 w-10 rounded bg-primary/70" />
|
|
238
|
+
<div className="h-1.5 w-full rounded bg-muted-foreground/45" />
|
|
239
|
+
<div className="h-1.5 w-11/12 rounded bg-muted-foreground/45" />
|
|
240
|
+
<div className="h-1.5 w-8/12 rounded bg-muted-foreground/45" />
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (id === "rect") {
|
|
247
|
+
return (
|
|
248
|
+
<div className={common}>
|
|
249
|
+
<div className="absolute inset-2 rounded-md border-2 border-primary/80 bg-primary/15" />
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (id === "geometry") {
|
|
255
|
+
return (
|
|
256
|
+
<div className={common}>
|
|
257
|
+
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
258
|
+
<circle cx="22" cy="18" r="8" className="fill-primary/75" />
|
|
259
|
+
<rect x="36" y="10" width="16" height="16" rx="3" className="fill-primary/55" />
|
|
260
|
+
<polygon points="62,10 70,26 54,26" className="fill-primary/65" />
|
|
261
|
+
<polygon points="24,36 34,42 24,48 14,42" className="fill-primary/50" />
|
|
262
|
+
<polygon points="50,36 54,42 50,48 42,48 38,42 42,36" className="fill-primary/70" />
|
|
263
|
+
</svg>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (id === "reference") {
|
|
269
|
+
return (
|
|
270
|
+
<div className={common}>
|
|
271
|
+
<div className="absolute inset-2 rounded-md border border-dashed border-primary/70 bg-primary/10" />
|
|
272
|
+
<div className="absolute inset-0 flex items-center justify-center text-[10px] text-primary/80">
|
|
273
|
+
引用组件
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (id === "image") {
|
|
280
|
+
return (
|
|
281
|
+
<div className={common}>
|
|
282
|
+
<div className="absolute inset-0 bg-gradient-to-br from-primary/25 via-muted/60 to-card" />
|
|
283
|
+
<svg viewBox="0 0 80 60" className="absolute inset-0 h-full w-full">
|
|
284
|
+
<circle cx="16" cy="14" r="5" className="fill-primary/70" />
|
|
285
|
+
<polygon points="0,60 24,34 38,50 52,30 80,60" className="fill-primary/35" />
|
|
286
|
+
<polygon points="0,60 18,42 30,54 44,36 62,60" className="fill-primary/55" />
|
|
287
|
+
</svg>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (id === "video") {
|
|
293
|
+
return (
|
|
294
|
+
<div className={common}>
|
|
295
|
+
<div className="absolute inset-0 bg-primary/12" />
|
|
296
|
+
<div className="absolute left-2 right-2 top-2 h-1.5 rounded bg-primary/35" />
|
|
297
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
298
|
+
<div className="rounded-full bg-background/80 p-1">
|
|
299
|
+
<div className="h-0 w-0 border-b-[7px] border-l-[11px] border-t-[7px] border-b-transparent border-l-primary/85 border-t-transparent" />
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (id === "audio") {
|
|
307
|
+
return (
|
|
308
|
+
<div className={common}>
|
|
309
|
+
<div className="absolute inset-0 flex items-center justify-center gap-1">
|
|
310
|
+
<div className="h-3 w-1.5 rounded bg-primary/50" />
|
|
311
|
+
<div className="h-6 w-1.5 rounded bg-primary/75" />
|
|
312
|
+
<div className="h-8 w-1.5 rounded bg-primary/90" />
|
|
313
|
+
<div className="h-5 w-1.5 rounded bg-primary/70" />
|
|
314
|
+
<div className="h-4 w-1.5 rounded bg-primary/55" />
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (id === "grid") {
|
|
320
|
+
return (
|
|
321
|
+
<div className={common}>
|
|
322
|
+
<div className="absolute inset-0 grid grid-cols-3 grid-rows-2 gap-1.5 p-2">
|
|
323
|
+
{Array.from({ length: 6 }).map((_, idx) => (
|
|
324
|
+
<div key={idx} className="rounded-sm border border-dashed border-primary/55 bg-primary/10" />
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return <div className={common} />;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const defaultCategories: MaterialCategory[] = [
|
|
335
|
+
{
|
|
336
|
+
id: "charts",
|
|
337
|
+
title: "图表",
|
|
338
|
+
items: [
|
|
339
|
+
{ id: "bar", title: "柱状图" },
|
|
340
|
+
{ id: "line", title: "折线图" },
|
|
341
|
+
{ id: "pie", title: "饼图" },
|
|
342
|
+
{ id: "area", title: "面积图" },
|
|
343
|
+
{ id: "scatter", title: "散点图" },
|
|
344
|
+
{ id: "radar", title: "雷达图" },
|
|
345
|
+
{ id: "gauge", title: "仪表盘" },
|
|
346
|
+
{ id: "funnel", title: "漏斗图" },
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: "basic",
|
|
351
|
+
title: "基础",
|
|
352
|
+
items: [
|
|
353
|
+
{ id: "text", title: "文本" },
|
|
354
|
+
{ id: "geometry", title: "几何" },
|
|
355
|
+
{ id: "grid", title: "网格布局" },
|
|
356
|
+
{ id: "image", title: "图片" },
|
|
357
|
+
{ id: "reference", title: "引用组件" },
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "media",
|
|
362
|
+
title: "媒体",
|
|
363
|
+
items: [
|
|
364
|
+
{ id: "video", title: "视频" },
|
|
365
|
+
{ id: "audio", title: "音频" },
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
export type MaterialSidebarProps = {
|
|
371
|
+
className?: string;
|
|
372
|
+
onDragMaterialStart?: (material: MaterialItem) => void;
|
|
373
|
+
layers?: PanelLayer[];
|
|
374
|
+
allElements?: PanelElement[];
|
|
375
|
+
selectedIds?: string[];
|
|
376
|
+
onSelectNode?: (nodeId: string, layerId: string) => void;
|
|
377
|
+
onNodeContextMenu?: (payload: { nodeId: string; layerId: string; x: number; y: number }) => void;
|
|
378
|
+
onDeleteNode?: (nodeId: string) => void;
|
|
379
|
+
onCopyNode?: (nodeId: string, mode?: ReferenceCopyMode) => void;
|
|
380
|
+
onMoveNodeToLayer?: (nodeId: string, targetLayerId: string) => void;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
export function MaterialSidebar({
|
|
384
|
+
className,
|
|
385
|
+
onDragMaterialStart,
|
|
386
|
+
layers = [],
|
|
387
|
+
allElements = [],
|
|
388
|
+
selectedIds = [],
|
|
389
|
+
onSelectNode,
|
|
390
|
+
onNodeContextMenu,
|
|
391
|
+
onDeleteNode,
|
|
392
|
+
onCopyNode,
|
|
393
|
+
onMoveNodeToLayer,
|
|
394
|
+
}: MaterialSidebarProps) {
|
|
395
|
+
const categories = useMemo(() => defaultCategories, []);
|
|
396
|
+
const [leftTab, setLeftTab] = useState<"materials" | "tree">("materials");
|
|
397
|
+
const [activeCategoryId, setActiveCategoryId] =
|
|
398
|
+
useState<MaterialCategoryId>("charts");
|
|
399
|
+
const [keyword, setKeyword] = useState("");
|
|
400
|
+
const [treeKeyword, setTreeKeyword] = useState("");
|
|
401
|
+
const [referenceOnlyTree, setReferenceOnlyTree] = useState(false);
|
|
402
|
+
const [draggingTreeNodeId, setDraggingTreeNodeId] = useState<string | null>(null);
|
|
403
|
+
const [dragOverLayerId, setDragOverLayerId] = useState<string | null>(null);
|
|
404
|
+
const [expandedKeys, setExpandedKeys] = useState<Record<string, boolean>>({
|
|
405
|
+
root: true,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const activeCategory = useMemo(
|
|
409
|
+
() => categories.find((c) => c.id === activeCategoryId) ?? categories[0],
|
|
410
|
+
[activeCategoryId, categories]
|
|
411
|
+
);
|
|
412
|
+
const normalizedKeyword = keyword.trim().toLowerCase();
|
|
413
|
+
const isSearching = normalizedKeyword.length > 0;
|
|
414
|
+
const normalizedTreeKeyword = treeKeyword.trim().toLowerCase();
|
|
415
|
+
const isTreeSearching = normalizedTreeKeyword.length > 0;
|
|
416
|
+
|
|
417
|
+
const matchedItems = useMemo(() => {
|
|
418
|
+
if (!isSearching) return [];
|
|
419
|
+
const result: Array<MaterialItem & { categoryTitle: string }> = [];
|
|
420
|
+
categories.forEach((category) => {
|
|
421
|
+
category.items.forEach((item) => {
|
|
422
|
+
const haystack = `${item.title} ${item.id} ${category.title}`.toLowerCase();
|
|
423
|
+
if (haystack.includes(normalizedKeyword)) {
|
|
424
|
+
result.push({ ...item, categoryTitle: category.title });
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
return result;
|
|
429
|
+
}, [categories, isSearching, normalizedKeyword]);
|
|
430
|
+
|
|
431
|
+
const themedScrollbarClass =
|
|
432
|
+
"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";
|
|
433
|
+
|
|
434
|
+
const elementsByLayer = useMemo(() => {
|
|
435
|
+
const map = new Map<string, PanelElement[]>();
|
|
436
|
+
for (const layer of layers) map.set(layer.id, []);
|
|
437
|
+
for (const el of allElements) {
|
|
438
|
+
const list = map.get(el.layerId) ?? [];
|
|
439
|
+
list.push(el);
|
|
440
|
+
map.set(el.layerId, list);
|
|
441
|
+
}
|
|
442
|
+
return map;
|
|
443
|
+
}, [allElements, layers]);
|
|
444
|
+
const elementsById = useMemo(() => {
|
|
445
|
+
const map = new Map<string, PanelElement>();
|
|
446
|
+
for (const el of allElements) map.set(el.id, el);
|
|
447
|
+
return map;
|
|
448
|
+
}, [allElements]);
|
|
449
|
+
const layerById = useMemo(() => {
|
|
450
|
+
const map = new Map<string, PanelLayer>();
|
|
451
|
+
for (const layer of layers) map.set(layer.id, layer);
|
|
452
|
+
return map;
|
|
453
|
+
}, [layers]);
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 节点树展示用的「本图层父网格 id」:
|
|
457
|
+
* parentGridId 若指向其它图层的同源克隆网格,仍解析到当前图层上对应的网格,层级与映射同步语义一致。
|
|
458
|
+
*/
|
|
459
|
+
const effectiveGridParentByElementId = useMemo(() => {
|
|
460
|
+
const byId = new Map<string, PanelElement>();
|
|
461
|
+
for (const el of allElements) byId.set(el.id, el);
|
|
462
|
+
const map = new Map<string, string | undefined>();
|
|
463
|
+
for (const el of allElements) {
|
|
464
|
+
const pg = el.parentGridId;
|
|
465
|
+
if (!pg) {
|
|
466
|
+
map.set(el.id, undefined);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const parent = byId.get(pg);
|
|
470
|
+
if (parent?.layerId === el.layerId && parent.materialType === "grid") {
|
|
471
|
+
map.set(el.id, pg);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const logical = logicalGridParentIdFromConcrete(pg, byId);
|
|
475
|
+
if (logical !== undefined) {
|
|
476
|
+
const concrete = concreteGridParentIdForLayer(logical, el.layerId, allElements);
|
|
477
|
+
map.set(el.id, concrete ?? undefined);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
map.set(el.id, undefined);
|
|
481
|
+
}
|
|
482
|
+
return map;
|
|
483
|
+
}, [allElements]);
|
|
484
|
+
|
|
485
|
+
/** 网格 → 直接子节点(槽位顺序),键为展示用有效父网格 id */
|
|
486
|
+
const childrenByGridByLayer = useMemo(() => {
|
|
487
|
+
const outer = new Map<string, Map<string, PanelElement[]>>();
|
|
488
|
+
for (const el of allElements) {
|
|
489
|
+
const gridParentId = effectiveGridParentByElementId.get(el.id);
|
|
490
|
+
if (!gridParentId) continue;
|
|
491
|
+
let inner = outer.get(el.layerId);
|
|
492
|
+
if (!inner) {
|
|
493
|
+
inner = new Map();
|
|
494
|
+
outer.set(el.layerId, inner);
|
|
495
|
+
}
|
|
496
|
+
const list = inner.get(gridParentId) ?? [];
|
|
497
|
+
list.push(el);
|
|
498
|
+
inner.set(gridParentId, list);
|
|
499
|
+
}
|
|
500
|
+
return outer;
|
|
501
|
+
}, [allElements, effectiveGridParentByElementId]);
|
|
502
|
+
|
|
503
|
+
const getNodeDisplayName = (node: PanelElement) => {
|
|
504
|
+
const customName = node.name?.trim();
|
|
505
|
+
return customName || node.chart?.title || MATERIAL_LABEL_MAP[node.materialType ?? ""] || node.id;
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const isExpanded = (key: string, defaultValue = false) =>
|
|
509
|
+
expandedKeys[key] ?? defaultValue;
|
|
510
|
+
|
|
511
|
+
const setExpanded = (key: string, next: boolean) => {
|
|
512
|
+
setExpandedKeys((prev) => ({ ...prev, [key]: next }));
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const getNodeChildren = (node: PanelElement, sourceOverride?: PanelElement[]) => {
|
|
516
|
+
const isRef = node.materialType === "reference";
|
|
517
|
+
const isGrid = node.materialType === "grid";
|
|
518
|
+
// 必须用节点自身所在图层查找子节点;外层传入的 layerId 在深嵌套/引用子树中会错位,导致子节点不显示在对应网格下
|
|
519
|
+
const gridChildren = isGrid
|
|
520
|
+
? [...(childrenByGridByLayer.get(node.layerId)?.get(node.id) ?? [])].sort(
|
|
521
|
+
compareGridTreeChildOrder
|
|
522
|
+
)
|
|
523
|
+
: [];
|
|
524
|
+
return isRef
|
|
525
|
+
? node.refCopyMode === "deep"
|
|
526
|
+
? node.refSnapshot ?? sourceOverride ?? []
|
|
527
|
+
: node.refLayerId
|
|
528
|
+
? elementsByLayer.get(node.refLayerId) ?? []
|
|
529
|
+
: []
|
|
530
|
+
: gridChildren;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const nodeMatchesTreeSearch = (
|
|
534
|
+
node: PanelElement,
|
|
535
|
+
visited: Set<string>,
|
|
536
|
+
sourceOverride?: PanelElement[]
|
|
537
|
+
): boolean => {
|
|
538
|
+
if (!isTreeSearching) return true;
|
|
539
|
+
const selfText = `${getNodeDisplayName(node)} ${node.materialType ?? ""} ${node.id}`.toLowerCase();
|
|
540
|
+
if (selfText.includes(normalizedTreeKeyword)) return true;
|
|
541
|
+
if (visited.has(node.id)) return false;
|
|
542
|
+
const nextVisited = new Set(visited);
|
|
543
|
+
nextVisited.add(node.id);
|
|
544
|
+
const children = getNodeChildren(node, sourceOverride);
|
|
545
|
+
return children.some((child) =>
|
|
546
|
+
nodeMatchesTreeSearch(child, nextVisited, node.refSnapshot)
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const renderTreeNode = (
|
|
551
|
+
node: PanelElement,
|
|
552
|
+
level: number,
|
|
553
|
+
path: string,
|
|
554
|
+
visited: Set<string>,
|
|
555
|
+
sourceOverride?: PanelElement[]
|
|
556
|
+
) => {
|
|
557
|
+
const selected = selectedIds.includes(node.id);
|
|
558
|
+
if (!nodeMatchesTreeSearch(node, visited, sourceOverride)) return null;
|
|
559
|
+
const isRef = node.materialType === "reference";
|
|
560
|
+
const refMode = node.refCopyMode ?? "shallow";
|
|
561
|
+
const isDeepRef = isRef && refMode === "deep";
|
|
562
|
+
const children = getNodeChildren(node, sourceOverride);
|
|
563
|
+
const nodeHomeLayer = layerById.get(node.layerId);
|
|
564
|
+
const hasChildren = children.length > 0;
|
|
565
|
+
const nextVisited = new Set(visited);
|
|
566
|
+
nextVisited.add(node.id);
|
|
567
|
+
const nodeKey = `node:${path}`;
|
|
568
|
+
return (
|
|
569
|
+
<Collapsible
|
|
570
|
+
key={nodeKey}
|
|
571
|
+
open={isExpanded(nodeKey, true)}
|
|
572
|
+
onOpenChange={(open) => setExpanded(nodeKey, open)}
|
|
573
|
+
>
|
|
574
|
+
<div
|
|
575
|
+
className={[
|
|
576
|
+
"mb-1 flex items-center gap-1 rounded py-1",
|
|
577
|
+
selected ? "bg-primary/15 text-foreground" : "text-muted-foreground hover:bg-accent/60",
|
|
578
|
+
].join(" ")}
|
|
579
|
+
style={{ paddingLeft: 6 + level * 14, paddingRight: 6 }}
|
|
580
|
+
>
|
|
581
|
+
{hasChildren ? (
|
|
582
|
+
<CollapsibleTrigger asChild>
|
|
583
|
+
<button
|
|
584
|
+
type="button"
|
|
585
|
+
className="flex h-7 w-7 items-center justify-center rounded text-sm hover:bg-accent"
|
|
586
|
+
>
|
|
587
|
+
{isExpanded(nodeKey, true) ? "▾" : "▸"}
|
|
588
|
+
</button>
|
|
589
|
+
</CollapsibleTrigger>
|
|
590
|
+
) : (
|
|
591
|
+
<span className="inline-flex h-7 w-7 items-center justify-center text-sm opacity-40">•</span>
|
|
592
|
+
)}
|
|
593
|
+
<button
|
|
594
|
+
type="button"
|
|
595
|
+
className="min-w-0 flex-1 truncate text-left"
|
|
596
|
+
onClick={() => onSelectNode?.(node.id, node.layerId)}
|
|
597
|
+
onContextMenu={(e) => {
|
|
598
|
+
e.preventDefault();
|
|
599
|
+
e.stopPropagation();
|
|
600
|
+
onNodeContextMenu?.({
|
|
601
|
+
nodeId: node.id,
|
|
602
|
+
layerId: node.layerId,
|
|
603
|
+
x: e.clientX,
|
|
604
|
+
y: e.clientY,
|
|
605
|
+
});
|
|
606
|
+
}}
|
|
607
|
+
title={getNodeDisplayName(node)}
|
|
608
|
+
draggable={!node.locked}
|
|
609
|
+
onDragStart={(e) => {
|
|
610
|
+
e.stopPropagation();
|
|
611
|
+
if (node.locked) {
|
|
612
|
+
e.preventDefault();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
setDraggingTreeNodeId(node.id);
|
|
616
|
+
e.dataTransfer.setData(
|
|
617
|
+
"application/x-arronqzy-tree-node",
|
|
618
|
+
JSON.stringify({ nodeId: node.id, sourceLayerId: node.layerId })
|
|
619
|
+
);
|
|
620
|
+
e.dataTransfer.effectAllowed = "move";
|
|
621
|
+
}}
|
|
622
|
+
onDragEnd={() => {
|
|
623
|
+
setDraggingTreeNodeId(null);
|
|
624
|
+
setDragOverLayerId(null);
|
|
625
|
+
}}
|
|
626
|
+
>
|
|
627
|
+
{getNodeDisplayName(node)}
|
|
628
|
+
</button>
|
|
629
|
+
{nodeHomeLayer?.isMapping ? (
|
|
630
|
+
<span
|
|
631
|
+
className="inline-flex shrink-0 items-center rounded-md border-2 border-violet-600 bg-violet-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-violet-950 shadow-sm dark:border-violet-400 dark:bg-violet-500/35 dark:text-violet-50 dark:shadow-[0_0_14px_-3px_rgba(167,139,250,0.65)]"
|
|
632
|
+
title={
|
|
633
|
+
nodeHomeLayer.mappingBaseLayerId
|
|
634
|
+
? `映射图层「${nodeHomeLayer.name}」· 基准图层:${
|
|
635
|
+
layerById.get(nodeHomeLayer.mappingBaseLayerId)?.name ??
|
|
636
|
+
nodeHomeLayer.mappingBaseLayerId
|
|
637
|
+
}`
|
|
638
|
+
: `映射图层「${nodeHomeLayer.name}」`
|
|
639
|
+
}
|
|
640
|
+
>
|
|
641
|
+
映射图层节点
|
|
642
|
+
</span>
|
|
643
|
+
) : null}
|
|
644
|
+
{node.mappingSourceNodeId ? (
|
|
645
|
+
<span
|
|
646
|
+
className="inline-flex shrink-0 items-center rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary"
|
|
647
|
+
title={`同源节点 ${node.mappingSourceNodeId}${
|
|
648
|
+
node.mappingSourceLayerId
|
|
649
|
+
? ` · ${layerById.get(node.mappingSourceLayerId)?.name ?? node.mappingSourceLayerId}`
|
|
650
|
+
: ""
|
|
651
|
+
}`}
|
|
652
|
+
>
|
|
653
|
+
同源
|
|
654
|
+
</span>
|
|
655
|
+
) : null}
|
|
656
|
+
{node.locked ? (
|
|
657
|
+
<span
|
|
658
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded border border-border/80 bg-background/80 text-muted-foreground"
|
|
659
|
+
title="节点已锁定"
|
|
660
|
+
>
|
|
661
|
+
<LockGlyph />
|
|
662
|
+
</span>
|
|
663
|
+
) : null}
|
|
664
|
+
{isRef ? (
|
|
665
|
+
<>
|
|
666
|
+
<span
|
|
667
|
+
className={[
|
|
668
|
+
"rounded border px-1 text-[10px]",
|
|
669
|
+
isDeepRef
|
|
670
|
+
? "border-violet-500/50 bg-violet-500/15 text-violet-300"
|
|
671
|
+
: "border-sky-500/40 bg-sky-500/10 text-sky-300",
|
|
672
|
+
].join(" ")}
|
|
673
|
+
title={isDeepRef ? "深拷贝引用(冻结快照)" : "浅拷贝引用(跟随源变化)"}
|
|
674
|
+
>
|
|
675
|
+
{isDeepRef ? "深拷贝" : "浅拷贝"}
|
|
676
|
+
</span>
|
|
677
|
+
<Tooltip>
|
|
678
|
+
<TooltipTrigger asChild>
|
|
679
|
+
<button
|
|
680
|
+
type="button"
|
|
681
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded border border-border hover:bg-accent"
|
|
682
|
+
onClick={(e) => {
|
|
683
|
+
e.stopPropagation();
|
|
684
|
+
onCopyNode?.(node.id, "shallow");
|
|
685
|
+
}}
|
|
686
|
+
aria-label="浅拷贝"
|
|
687
|
+
>
|
|
688
|
+
<IconShallowCopy />
|
|
689
|
+
</button>
|
|
690
|
+
</TooltipTrigger>
|
|
691
|
+
<TooltipContent>浅拷贝</TooltipContent>
|
|
692
|
+
</Tooltip>
|
|
693
|
+
<Tooltip>
|
|
694
|
+
<TooltipTrigger asChild>
|
|
695
|
+
<button
|
|
696
|
+
type="button"
|
|
697
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded border border-border hover:bg-accent"
|
|
698
|
+
onClick={(e) => {
|
|
699
|
+
e.stopPropagation();
|
|
700
|
+
onCopyNode?.(node.id, "deep");
|
|
701
|
+
}}
|
|
702
|
+
aria-label="深拷贝"
|
|
703
|
+
>
|
|
704
|
+
<IconDeepCopy />
|
|
705
|
+
</button>
|
|
706
|
+
</TooltipTrigger>
|
|
707
|
+
<TooltipContent>深拷贝</TooltipContent>
|
|
708
|
+
</Tooltip>
|
|
709
|
+
</>
|
|
710
|
+
) : (
|
|
711
|
+
<Tooltip>
|
|
712
|
+
<TooltipTrigger asChild>
|
|
713
|
+
<button
|
|
714
|
+
type="button"
|
|
715
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded border border-border hover:bg-accent"
|
|
716
|
+
onClick={(e) => {
|
|
717
|
+
e.stopPropagation();
|
|
718
|
+
onCopyNode?.(node.id);
|
|
719
|
+
}}
|
|
720
|
+
aria-label="复制节点"
|
|
721
|
+
>
|
|
722
|
+
<IconCopy />
|
|
723
|
+
</button>
|
|
724
|
+
</TooltipTrigger>
|
|
725
|
+
<TooltipContent>复制节点</TooltipContent>
|
|
726
|
+
</Tooltip>
|
|
727
|
+
)}
|
|
728
|
+
<Tooltip>
|
|
729
|
+
<TooltipTrigger asChild>
|
|
730
|
+
<button
|
|
731
|
+
type="button"
|
|
732
|
+
disabled={node.locked}
|
|
733
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded border border-border hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
|
|
734
|
+
onClick={(e) => {
|
|
735
|
+
e.stopPropagation();
|
|
736
|
+
if (node.locked) return;
|
|
737
|
+
onDeleteNode?.(node.id);
|
|
738
|
+
}}
|
|
739
|
+
aria-label={node.locked ? "锁定节点不可删除" : "删除节点"}
|
|
740
|
+
>
|
|
741
|
+
<IconDelete />
|
|
742
|
+
</button>
|
|
743
|
+
</TooltipTrigger>
|
|
744
|
+
<TooltipContent>{node.locked ? "锁定节点不可删除" : "删除节点"}</TooltipContent>
|
|
745
|
+
</Tooltip>
|
|
746
|
+
</div>
|
|
747
|
+
{hasChildren ? (
|
|
748
|
+
<CollapsibleContent>
|
|
749
|
+
{children.map((child) => {
|
|
750
|
+
const childPath = `${path}->${child.id}`;
|
|
751
|
+
if (nextVisited.has(child.id)) {
|
|
752
|
+
return (
|
|
753
|
+
<div
|
|
754
|
+
key={`cycle:${childPath}`}
|
|
755
|
+
className="py-1 text-[10px] text-muted-foreground/80"
|
|
756
|
+
style={{ paddingLeft: 6 + (level + 1) * 14 }}
|
|
757
|
+
title="检测到循环引用,已停止向下展开"
|
|
758
|
+
>
|
|
759
|
+
{getNodeDisplayName(child)}(循环引用)
|
|
760
|
+
</div>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return renderTreeNode(child, level + 1, childPath, nextVisited, node.refSnapshot);
|
|
764
|
+
})}
|
|
765
|
+
</CollapsibleContent>
|
|
766
|
+
) : null}
|
|
767
|
+
</Collapsible>
|
|
768
|
+
);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const hasRefInSubtree = (node: PanelElement, visited: Set<string>): boolean => {
|
|
772
|
+
if (node.materialType === "reference") return true;
|
|
773
|
+
if (node.materialType !== "reference") return false;
|
|
774
|
+
if (visited.has(node.id)) return false;
|
|
775
|
+
const nextVisited = new Set(visited);
|
|
776
|
+
nextVisited.add(node.id);
|
|
777
|
+
const children =
|
|
778
|
+
node.refCopyMode === "deep"
|
|
779
|
+
? node.refSnapshot ?? []
|
|
780
|
+
: node.refLayerId
|
|
781
|
+
? elementsByLayer.get(node.refLayerId) ?? []
|
|
782
|
+
: [];
|
|
783
|
+
return children.some((child) => hasRefInSubtree(child, nextVisited));
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
return (
|
|
787
|
+
<TooltipProvider delayDuration={120}>
|
|
788
|
+
<aside
|
|
789
|
+
className={[
|
|
790
|
+
"grid h-full w-full border-r border-border bg-muted/30 text-foreground",
|
|
791
|
+
className ?? "",
|
|
792
|
+
].join(" ")}
|
|
793
|
+
style={{ gridTemplateRows: "auto 1fr" }}
|
|
794
|
+
>
|
|
795
|
+
<div className="border-b border-border bg-background/80 px-3 py-2 backdrop-blur-sm">
|
|
796
|
+
<Input
|
|
797
|
+
value={keyword}
|
|
798
|
+
onChange={(e) => setKeyword(e.target.value)}
|
|
799
|
+
placeholder="搜索物料(名称 / 分类)"
|
|
800
|
+
className="h-8 text-xs"
|
|
801
|
+
/>
|
|
802
|
+
</div>
|
|
803
|
+
|
|
804
|
+
<Tabs
|
|
805
|
+
value={leftTab}
|
|
806
|
+
onValueChange={(v) => setLeftTab(v as "materials" | "tree")}
|
|
807
|
+
className="flex min-h-0 h-full flex-col"
|
|
808
|
+
>
|
|
809
|
+
<div className="border-b border-border px-2 pb-2 pt-2">
|
|
810
|
+
<TabsList className="grid w-full grid-cols-2">
|
|
811
|
+
<TabsTrigger value="materials">物料</TabsTrigger>
|
|
812
|
+
<TabsTrigger value="tree">节点树</TabsTrigger>
|
|
813
|
+
</TabsList>
|
|
814
|
+
</div>
|
|
815
|
+
<TabsContent value="materials" className="mt-0 min-h-0 flex-1 overflow-hidden">
|
|
816
|
+
<div className="grid h-full min-h-0 grid-cols-[110px_1fr]">
|
|
817
|
+
<div className={`overflow-auto border-r border-border ${themedScrollbarClass}`}>
|
|
818
|
+
{categories.map((c) => {
|
|
819
|
+
const active = c.id === activeCategoryId;
|
|
820
|
+
return (
|
|
821
|
+
<button
|
|
822
|
+
key={c.id}
|
|
823
|
+
type="button"
|
|
824
|
+
onClick={() => setActiveCategoryId(c.id)}
|
|
825
|
+
className={[
|
|
826
|
+
"w-full cursor-pointer border-b border-border/40 px-2.5 py-2.5 text-left text-xs",
|
|
827
|
+
active
|
|
828
|
+
? "bg-accent text-accent-foreground"
|
|
829
|
+
: "text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground",
|
|
830
|
+
].join(" ")}
|
|
831
|
+
>
|
|
832
|
+
{c.title}
|
|
833
|
+
</button>
|
|
834
|
+
);
|
|
835
|
+
})}
|
|
836
|
+
</div>
|
|
837
|
+
|
|
838
|
+
<div className={`overflow-auto ${themedScrollbarClass}`}>
|
|
839
|
+
<div className="px-2.5 py-2.5 text-xs font-semibold">
|
|
840
|
+
{isSearching ? `搜索结果(${matchedItems.length})` : activeCategory.title}
|
|
841
|
+
</div>
|
|
842
|
+
<div className="grid gap-2 px-2.5 pb-3">
|
|
843
|
+
{(isSearching ? matchedItems : activeCategory.items).map((it) => (
|
|
844
|
+
<button
|
|
845
|
+
key={isSearching ? `${it.id}-${(it as any).categoryTitle}` : it.id}
|
|
846
|
+
type="button"
|
|
847
|
+
draggable
|
|
848
|
+
className="cursor-pointer rounded-xl border border-border bg-card px-2 py-2 text-left text-xs text-card-foreground hover:bg-accent/60"
|
|
849
|
+
onDragStart={(e) => {
|
|
850
|
+
e.dataTransfer.setData(
|
|
851
|
+
"application/x-arronqzy-material",
|
|
852
|
+
JSON.stringify({ id: it.id, title: it.title })
|
|
853
|
+
);
|
|
854
|
+
e.dataTransfer.effectAllowed = "copy";
|
|
855
|
+
onDragMaterialStart?.(it);
|
|
856
|
+
}}
|
|
857
|
+
>
|
|
858
|
+
<div className="flex flex-col items-stretch gap-2">
|
|
859
|
+
<MaterialPreview id={it.id} />
|
|
860
|
+
<div className="min-w-0">
|
|
861
|
+
<div className="truncate">{it.title}</div>
|
|
862
|
+
{isSearching ? (
|
|
863
|
+
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
864
|
+
{(it as any).categoryTitle}
|
|
865
|
+
</div>
|
|
866
|
+
) : null}
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
</button>
|
|
870
|
+
))}
|
|
871
|
+
{isSearching && matchedItems.length === 0 ? (
|
|
872
|
+
<Empty className="py-5">
|
|
873
|
+
<EmptyIcon className="h-8 w-8">
|
|
874
|
+
<svg
|
|
875
|
+
viewBox="0 0 24 24"
|
|
876
|
+
className="h-4 w-4"
|
|
877
|
+
fill="none"
|
|
878
|
+
stroke="currentColor"
|
|
879
|
+
strokeWidth="1.8"
|
|
880
|
+
aria-hidden="true"
|
|
881
|
+
>
|
|
882
|
+
<circle cx="11" cy="11" r="7" />
|
|
883
|
+
<path d="m20 20-3.5-3.5" />
|
|
884
|
+
</svg>
|
|
885
|
+
</EmptyIcon>
|
|
886
|
+
<EmptyTitle className="text-xs">未匹配到物料</EmptyTitle>
|
|
887
|
+
<EmptyDescription className="text-[11px]">
|
|
888
|
+
尝试更换关键词,或切换分类后再搜索。
|
|
889
|
+
</EmptyDescription>
|
|
890
|
+
</Empty>
|
|
891
|
+
) : null}
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
</TabsContent>
|
|
896
|
+
<TabsContent value="tree" className="mt-0 min-h-0 flex-1 overflow-hidden">
|
|
897
|
+
<div className={`h-full overflow-auto px-2 py-2 text-xs ${themedScrollbarClass}`}>
|
|
898
|
+
<div className="mb-2">
|
|
899
|
+
<Input
|
|
900
|
+
value={treeKeyword}
|
|
901
|
+
onChange={(e) => setTreeKeyword(e.target.value)}
|
|
902
|
+
placeholder="搜索节点(名称 / 类型 / ID)"
|
|
903
|
+
className="h-8 text-xs"
|
|
904
|
+
/>
|
|
905
|
+
</div>
|
|
906
|
+
<div className="mb-2 flex items-center justify-between rounded border border-border bg-card px-2 py-1.5">
|
|
907
|
+
<span className="text-[11px] text-muted-foreground">仅看引用子树</span>
|
|
908
|
+
<Switch
|
|
909
|
+
checked={referenceOnlyTree}
|
|
910
|
+
onCheckedChange={setReferenceOnlyTree}
|
|
911
|
+
aria-label="仅看引用子树"
|
|
912
|
+
/>
|
|
913
|
+
</div>
|
|
914
|
+
<Collapsible
|
|
915
|
+
open={isExpanded("root", true)}
|
|
916
|
+
onOpenChange={(open) => setExpanded("root", open)}
|
|
917
|
+
className="rounded border border-border bg-card"
|
|
918
|
+
>
|
|
919
|
+
<div className="flex items-center gap-1 px-2 py-1.5 font-medium">
|
|
920
|
+
<CollapsibleTrigger asChild>
|
|
921
|
+
<button
|
|
922
|
+
type="button"
|
|
923
|
+
className="flex h-7 w-7 items-center justify-center rounded text-sm hover:bg-accent"
|
|
924
|
+
>
|
|
925
|
+
{isExpanded("root", true) ? "▾" : "▸"}
|
|
926
|
+
</button>
|
|
927
|
+
</CollapsibleTrigger>
|
|
928
|
+
<span>root</span>
|
|
929
|
+
</div>
|
|
930
|
+
<CollapsibleContent className="space-y-1 border-t border-border/60 py-1">
|
|
931
|
+
{layers.map((layer) => {
|
|
932
|
+
const layerNodes = elementsByLayer.get(layer.id) ?? [];
|
|
933
|
+
const draggingNode = draggingTreeNodeId ? elementsById.get(draggingTreeNodeId) ?? null : null;
|
|
934
|
+
const draggingSourceLayer = draggingNode
|
|
935
|
+
? (layerById.get(draggingNode.layerId) ?? null)
|
|
936
|
+
: null;
|
|
937
|
+
const dropBlockReason = !draggingNode
|
|
938
|
+
? ""
|
|
939
|
+
: draggingNode.locked
|
|
940
|
+
? PANEL_MESSAGES.nodeMoveLocked
|
|
941
|
+
: draggingSourceLayer?.locked
|
|
942
|
+
? PANEL_MESSAGES.nodeMoveSourceLayerLocked
|
|
943
|
+
: layer.locked
|
|
944
|
+
? PANEL_MESSAGES.nodeMoveTargetLayerLocked
|
|
945
|
+
: draggingNode.layerId === layer.id
|
|
946
|
+
? PANEL_MESSAGES.nodeMoveSameLayer
|
|
947
|
+
: "";
|
|
948
|
+
const canDropIntoLayer = Boolean(draggingNode) && !dropBlockReason;
|
|
949
|
+
const isCurrentDropLayer = dragOverLayerId === layer.id;
|
|
950
|
+
const rootNodes = layerNodes
|
|
951
|
+
.filter((node) => !effectiveGridParentByElementId.get(node.id))
|
|
952
|
+
.filter((node) => !referenceOnlyTree || hasRefInSubtree(node, new Set<string>()))
|
|
953
|
+
.filter((node) => nodeMatchesTreeSearch(node, new Set<string>()));
|
|
954
|
+
if (isTreeSearching && rootNodes.length === 0) return null;
|
|
955
|
+
const layerKey = `layer:${layer.id}`;
|
|
956
|
+
return (
|
|
957
|
+
<Collapsible
|
|
958
|
+
key={layer.id}
|
|
959
|
+
open={isExpanded(layerKey, true)}
|
|
960
|
+
onOpenChange={(open) => setExpanded(layerKey, open)}
|
|
961
|
+
>
|
|
962
|
+
<Card
|
|
963
|
+
className={[
|
|
964
|
+
"mb-2 overflow-hidden transition-shadow",
|
|
965
|
+
layer.isMapping
|
|
966
|
+
? "border-2 border-violet-500/70 bg-gradient-to-br from-violet-500/14 via-violet-600/10 to-fuchsia-500/12 shadow-[inset_0_1px_0_0_rgba(139,92,246,0.22)] dark:border-violet-400/65 dark:from-violet-500/20 dark:via-violet-950/35 dark:to-fuchsia-950/25 dark:shadow-[inset_0_0_0_1px_rgba(167,139,250,0.12),0_0_20px_-8px_rgba(139,92,246,0.35)]"
|
|
967
|
+
: "",
|
|
968
|
+
isCurrentDropLayer
|
|
969
|
+
? canDropIntoLayer
|
|
970
|
+
? "ring-2 ring-primary/45 ring-offset-2 ring-offset-background"
|
|
971
|
+
: "ring-2 ring-destructive/45 ring-offset-2 ring-offset-background"
|
|
972
|
+
: "",
|
|
973
|
+
]
|
|
974
|
+
.filter(Boolean)
|
|
975
|
+
.join(" ")}
|
|
976
|
+
>
|
|
977
|
+
<CardHeader className="flex flex-row flex-wrap items-center gap-1 space-y-0 p-2.5 pb-1.5">
|
|
978
|
+
{isCurrentDropLayer ? (
|
|
979
|
+
<span
|
|
980
|
+
className={[
|
|
981
|
+
"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded text-[11px]",
|
|
982
|
+
canDropIntoLayer
|
|
983
|
+
? "bg-primary/15 text-primary"
|
|
984
|
+
: "bg-destructive/15 text-destructive",
|
|
985
|
+
].join(" ")}
|
|
986
|
+
title={canDropIntoLayer ? "目标图层" : dropBlockReason}
|
|
987
|
+
>
|
|
988
|
+
➜
|
|
989
|
+
</span>
|
|
990
|
+
) : null}
|
|
991
|
+
<CollapsibleTrigger asChild>
|
|
992
|
+
<button
|
|
993
|
+
type="button"
|
|
994
|
+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded text-sm text-muted-foreground hover:bg-accent"
|
|
995
|
+
>
|
|
996
|
+
{isExpanded(layerKey, true) ? "▾" : "▸"}
|
|
997
|
+
</button>
|
|
998
|
+
</CollapsibleTrigger>
|
|
999
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium text-foreground">
|
|
1000
|
+
{layer.name}
|
|
1001
|
+
<span className="font-normal text-muted-foreground">
|
|
1002
|
+
({rootNodes.length})
|
|
1003
|
+
</span>
|
|
1004
|
+
</span>
|
|
1005
|
+
{layer.isMapping ? (
|
|
1006
|
+
<span
|
|
1007
|
+
className="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 shadow-sm dark:border-violet-400 dark:bg-violet-500/35 dark:text-violet-50 dark:shadow-[0_0_12px_-2px_rgba(167,139,250,0.55)]"
|
|
1008
|
+
title={
|
|
1009
|
+
layer.mappingBaseLayerId
|
|
1010
|
+
? `映射图层 · 基准图层:${
|
|
1011
|
+
layerById.get(layer.mappingBaseLayerId)?.name ??
|
|
1012
|
+
layer.mappingBaseLayerId
|
|
1013
|
+
}`
|
|
1014
|
+
: "映射图层"
|
|
1015
|
+
}
|
|
1016
|
+
>
|
|
1017
|
+
映射图层
|
|
1018
|
+
</span>
|
|
1019
|
+
) : null}
|
|
1020
|
+
{isCurrentDropLayer ? (
|
|
1021
|
+
<span
|
|
1022
|
+
className={[
|
|
1023
|
+
"ml-auto shrink-0 rounded px-1.5 py-0.5 text-[10px]",
|
|
1024
|
+
canDropIntoLayer
|
|
1025
|
+
? "bg-primary/15 text-primary"
|
|
1026
|
+
: "bg-destructive/15 text-destructive",
|
|
1027
|
+
].join(" ")}
|
|
1028
|
+
>
|
|
1029
|
+
{canDropIntoLayer ? "将移动到该图层" : dropBlockReason}
|
|
1030
|
+
</span>
|
|
1031
|
+
) : null}
|
|
1032
|
+
</CardHeader>
|
|
1033
|
+
<CardContent className="space-y-1.5 p-2.5 pt-0">
|
|
1034
|
+
<div
|
|
1035
|
+
className={[
|
|
1036
|
+
"rounded-md border border-dashed px-2 py-1.5 text-[10px] transition-colors",
|
|
1037
|
+
isCurrentDropLayer
|
|
1038
|
+
? canDropIntoLayer
|
|
1039
|
+
? "border-primary/50 bg-primary/10 text-primary"
|
|
1040
|
+
: "border-destructive/50 bg-destructive/10 text-destructive"
|
|
1041
|
+
: "border-border/50 bg-muted/20 text-muted-foreground hover:border-border hover:bg-muted/35",
|
|
1042
|
+
].join(" ")}
|
|
1043
|
+
onDragOver={(e) => {
|
|
1044
|
+
const hasNodeData =
|
|
1045
|
+
e.dataTransfer.types.includes("application/x-arronqzy-tree-node");
|
|
1046
|
+
if (!hasNodeData) return;
|
|
1047
|
+
if (canDropIntoLayer) {
|
|
1048
|
+
e.preventDefault();
|
|
1049
|
+
e.dataTransfer.dropEffect = "move";
|
|
1050
|
+
} else {
|
|
1051
|
+
e.dataTransfer.dropEffect = "none";
|
|
1052
|
+
}
|
|
1053
|
+
if (dragOverLayerId !== layer.id) setDragOverLayerId(layer.id);
|
|
1054
|
+
}}
|
|
1055
|
+
onDragLeave={() => {
|
|
1056
|
+
if (dragOverLayerId === layer.id) setDragOverLayerId(null);
|
|
1057
|
+
}}
|
|
1058
|
+
onDrop={(e) => {
|
|
1059
|
+
const payload = e.dataTransfer.getData("application/x-arronqzy-tree-node");
|
|
1060
|
+
if (!payload) return;
|
|
1061
|
+
e.preventDefault();
|
|
1062
|
+
try {
|
|
1063
|
+
const data = JSON.parse(payload) as {
|
|
1064
|
+
nodeId?: string;
|
|
1065
|
+
sourceLayerId?: string;
|
|
1066
|
+
};
|
|
1067
|
+
if (!data.nodeId || !layer.id) return;
|
|
1068
|
+
if (!canDropIntoLayer) return;
|
|
1069
|
+
if (data.sourceLayerId === layer.id) return;
|
|
1070
|
+
onMoveNodeToLayer?.(data.nodeId, layer.id);
|
|
1071
|
+
} catch {
|
|
1072
|
+
// ignore invalid payload
|
|
1073
|
+
} finally {
|
|
1074
|
+
setDraggingTreeNodeId(null);
|
|
1075
|
+
setDragOverLayerId(null);
|
|
1076
|
+
}
|
|
1077
|
+
}}
|
|
1078
|
+
title="拖拽节点到此图层"
|
|
1079
|
+
>
|
|
1080
|
+
{draggingTreeNodeId
|
|
1081
|
+
? canDropIntoLayer
|
|
1082
|
+
? "释放以移动到该图层"
|
|
1083
|
+
: dropBlockReason || "不可移动到该图层"
|
|
1084
|
+
: "拖拽节点到该图层"}
|
|
1085
|
+
</div>
|
|
1086
|
+
<CollapsibleContent>
|
|
1087
|
+
{rootNodes.length === 0 ? (
|
|
1088
|
+
<div className="rounded border border-border/40 bg-muted/15 py-2 pl-3 text-[11px] text-muted-foreground">
|
|
1089
|
+
空图层
|
|
1090
|
+
</div>
|
|
1091
|
+
) : (
|
|
1092
|
+
rootNodes.map((node) =>
|
|
1093
|
+
renderTreeNode(node, 2, `${layer.id}/${node.id}`, new Set<string>())
|
|
1094
|
+
)
|
|
1095
|
+
)}
|
|
1096
|
+
</CollapsibleContent>
|
|
1097
|
+
</CardContent>
|
|
1098
|
+
</Card>
|
|
1099
|
+
</Collapsible>
|
|
1100
|
+
);
|
|
1101
|
+
})}
|
|
1102
|
+
{isTreeSearching &&
|
|
1103
|
+
layers.every((layer) => {
|
|
1104
|
+
const layerNodes = elementsByLayer.get(layer.id) ?? [];
|
|
1105
|
+
const rootNodes = layerNodes
|
|
1106
|
+
.filter((node) => !effectiveGridParentByElementId.get(node.id))
|
|
1107
|
+
.filter((node) => !referenceOnlyTree || hasRefInSubtree(node, new Set<string>()))
|
|
1108
|
+
.filter((node) => nodeMatchesTreeSearch(node, new Set<string>()));
|
|
1109
|
+
return rootNodes.length === 0;
|
|
1110
|
+
}) ? (
|
|
1111
|
+
<div className="mx-2 my-2">
|
|
1112
|
+
<Empty className="py-5">
|
|
1113
|
+
<EmptyIcon className="h-8 w-8">
|
|
1114
|
+
<svg
|
|
1115
|
+
viewBox="0 0 24 24"
|
|
1116
|
+
className="h-4 w-4"
|
|
1117
|
+
fill="none"
|
|
1118
|
+
stroke="currentColor"
|
|
1119
|
+
strokeWidth="1.8"
|
|
1120
|
+
aria-hidden="true"
|
|
1121
|
+
>
|
|
1122
|
+
<rect x="4" y="4" width="16" height="16" rx="2.5" />
|
|
1123
|
+
<path d="M8 10h8M8 14h5" />
|
|
1124
|
+
</svg>
|
|
1125
|
+
</EmptyIcon>
|
|
1126
|
+
<EmptyTitle className="text-xs">未匹配到节点</EmptyTitle>
|
|
1127
|
+
<EmptyDescription className="text-[11px]">
|
|
1128
|
+
试试节点名称、类型或 ID 关键词。
|
|
1129
|
+
</EmptyDescription>
|
|
1130
|
+
</Empty>
|
|
1131
|
+
</div>
|
|
1132
|
+
) : null}
|
|
1133
|
+
</CollapsibleContent>
|
|
1134
|
+
</Collapsible>
|
|
1135
|
+
</div>
|
|
1136
|
+
</TabsContent>
|
|
1137
|
+
</Tabs>
|
|
1138
|
+
</aside>
|
|
1139
|
+
</TooltipProvider>
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|