@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,3703 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardContent,
|
|
5
|
+
CardHeader,
|
|
6
|
+
CardTitle,
|
|
7
|
+
Checkbox,
|
|
8
|
+
Collapsible,
|
|
9
|
+
CollapsibleContent,
|
|
10
|
+
CollapsibleTrigger,
|
|
11
|
+
Empty,
|
|
12
|
+
EmptyDescription,
|
|
13
|
+
EmptyIcon,
|
|
14
|
+
EmptyTitle,
|
|
15
|
+
Input,
|
|
16
|
+
Select,
|
|
17
|
+
SelectContent,
|
|
18
|
+
SelectItem,
|
|
19
|
+
SelectTrigger,
|
|
20
|
+
SelectValue,
|
|
21
|
+
Textarea,
|
|
22
|
+
Tooltip,
|
|
23
|
+
TooltipContent,
|
|
24
|
+
TooltipProvider,
|
|
25
|
+
TooltipTrigger,
|
|
26
|
+
} from "@arronqzy/ui";
|
|
27
|
+
import type {
|
|
28
|
+
PanelChartConfig,
|
|
29
|
+
PanelElement,
|
|
30
|
+
PanelElementStyle,
|
|
31
|
+
ReferenceCopyMode,
|
|
32
|
+
} from "../types";
|
|
33
|
+
import { buildChartOption, CHART_TYPES, getChartValuesDisplayText } from "../utils/chartOptionBuilder";
|
|
34
|
+
import type { PanelLayer } from "../types";
|
|
35
|
+
import { PANEL_MESSAGES } from "../constants/messages";
|
|
36
|
+
import { ViewElementScopePanel } from "./ViewElementScopePanel";
|
|
37
|
+
import { hasViewElementScope } from "../scope/view-scope-store";
|
|
38
|
+
import { collectElementScopeWarnings } from "../utils/scope-template-warnings";
|
|
39
|
+
import { ScopeConfigProvider } from "./scope-config/ScopeConfigContext";
|
|
40
|
+
import { ScopeTemplateWarningsPanel } from "./scope-config/ScopeTemplateWarningsPanel";
|
|
41
|
+
|
|
42
|
+
type UpdateElement = (
|
|
43
|
+
id: string,
|
|
44
|
+
patch: Partial<PanelElement>,
|
|
45
|
+
options?: { batchId?: string; meta?: Record<string, unknown> }
|
|
46
|
+
) => void;
|
|
47
|
+
|
|
48
|
+
export type PanelConfigSidebarProps = {
|
|
49
|
+
selectedElement: PanelElement | null;
|
|
50
|
+
selectedElements?: PanelElement[];
|
|
51
|
+
layers: PanelLayer[];
|
|
52
|
+
updateElement: UpdateElement;
|
|
53
|
+
setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
|
|
54
|
+
nodeZOrderLabel?: string;
|
|
55
|
+
onExcludeSelectedNode?: (nodeId: string) => void;
|
|
56
|
+
onAdjustNodeZOrder?: (
|
|
57
|
+
nodeId: string,
|
|
58
|
+
action: "bringForward" | "sendBackward" | "bringToFront" | "sendToBack"
|
|
59
|
+
) => void;
|
|
60
|
+
/** 蓝图视图绑定节点写入的信号 scope,用于展示与模版解析 */
|
|
61
|
+
viewElementScope?: unknown;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function PanelConfigSidebar({
|
|
65
|
+
selectedElement,
|
|
66
|
+
selectedElements = [],
|
|
67
|
+
layers,
|
|
68
|
+
updateElement,
|
|
69
|
+
setReferenceCopyMode,
|
|
70
|
+
nodeZOrderLabel,
|
|
71
|
+
onExcludeSelectedNode,
|
|
72
|
+
onAdjustNodeZOrder,
|
|
73
|
+
viewElementScope,
|
|
74
|
+
}: PanelConfigSidebarProps) {
|
|
75
|
+
const [isAdvancedOptionMode, setIsAdvancedOptionMode] = useState(false);
|
|
76
|
+
const [optionJsonText, setOptionJsonText] = useState("{}");
|
|
77
|
+
const [optionJsonError, setOptionJsonError] = useState<string | null>(null);
|
|
78
|
+
const [uploadStatus, setUploadStatus] = useState<string>("");
|
|
79
|
+
const [audioStatus, setAudioStatus] = useState<string>("");
|
|
80
|
+
const [videoStatus, setVideoStatus] = useState<string>("");
|
|
81
|
+
const [geometryDrawPenColor, setGeometryDrawPenColor] = useState("#111827");
|
|
82
|
+
const [geometryDrawPenWidth, setGeometryDrawPenWidth] = useState(3);
|
|
83
|
+
const [isGeometryDrawing, setIsGeometryDrawing] = useState(false);
|
|
84
|
+
const [configSearch, setConfigSearch] = useState("");
|
|
85
|
+
const [isSearchCollapsed, setIsSearchCollapsed] = useState(false);
|
|
86
|
+
const SEARCH_COLLAPSE_STORAGE_KEY = "panel:config-search-collapsed";
|
|
87
|
+
const textEditorRef = useRef<HTMLDivElement | null>(null);
|
|
88
|
+
const geometryDrawCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
89
|
+
const geometryLastPointRef = useRef<{ x: number; y: number } | null>(null);
|
|
90
|
+
const recorderRef = useRef<MediaRecorder | null>(null);
|
|
91
|
+
const recordStreamRef = useRef<MediaStream | null>(null);
|
|
92
|
+
const audioChunksRef = useRef<BlobPart[]>([]);
|
|
93
|
+
const [isRecordingAudio, setIsRecordingAudio] = useState(false);
|
|
94
|
+
const sidebarScrollRef = useRef<HTMLElement | null>(null);
|
|
95
|
+
const themedScrollbarClass =
|
|
96
|
+
"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";
|
|
97
|
+
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
|
98
|
+
nodeInfo: true,
|
|
99
|
+
styleBackground: true,
|
|
100
|
+
styleBorder: true,
|
|
101
|
+
chartBasic: true,
|
|
102
|
+
chartAdvanced: false,
|
|
103
|
+
chartAdvancedHighFreq: true,
|
|
104
|
+
chartAdvancedLayout: false,
|
|
105
|
+
chartAdvancedAxisPointer: false,
|
|
106
|
+
reference: true,
|
|
107
|
+
});
|
|
108
|
+
const [expandedNodeCards, setExpandedNodeCards] = useState<Record<string, boolean>>({});
|
|
109
|
+
|
|
110
|
+
const isChartElement = !!selectedElement && CHART_TYPES.has(selectedElement.materialType ?? "");
|
|
111
|
+
const selectedLayer = selectedElement
|
|
112
|
+
? layers.find((layer) => layer.id === selectedElement.layerId) ?? null
|
|
113
|
+
: null;
|
|
114
|
+
const canToggleNodeLock = !!selectedElement && !selectedLayer?.locked;
|
|
115
|
+
const isNodeEditable = !!selectedElement && !selectedElement.locked && !selectedLayer?.locked;
|
|
116
|
+
const readonlyReason = !selectedElement
|
|
117
|
+
? ""
|
|
118
|
+
: selectedElement.locked
|
|
119
|
+
? PANEL_MESSAGES.nodeConfigLocked
|
|
120
|
+
: selectedLayer?.locked
|
|
121
|
+
? PANEL_MESSAGES.nodeConfigLayerLocked
|
|
122
|
+
: "";
|
|
123
|
+
const selectedChartType = (selectedElement?.materialType ?? "") as
|
|
124
|
+
| "bar"
|
|
125
|
+
| "line"
|
|
126
|
+
| "pie"
|
|
127
|
+
| "area"
|
|
128
|
+
| "scatter"
|
|
129
|
+
| "radar"
|
|
130
|
+
| "gauge"
|
|
131
|
+
| "funnel"
|
|
132
|
+
| "";
|
|
133
|
+
const normalizedSearch = useMemo(() => configSearch.trim().toLowerCase(), [configSearch]);
|
|
134
|
+
const hasSearch = normalizedSearch.length > 0;
|
|
135
|
+
const showScopePanel =
|
|
136
|
+
!!selectedElement &&
|
|
137
|
+
viewElementScope !== undefined &&
|
|
138
|
+
hasViewElementScope(selectedElement.id);
|
|
139
|
+
const scopeWarnings = useMemo(() => {
|
|
140
|
+
if (!selectedElement || viewElementScope === undefined) return [];
|
|
141
|
+
return collectElementScopeWarnings(selectedElement, viewElementScope);
|
|
142
|
+
}, [selectedElement, viewElementScope]);
|
|
143
|
+
const effectiveSelectedElements = useMemo(() => {
|
|
144
|
+
if (selectedElements.length > 0) return selectedElements;
|
|
145
|
+
return selectedElement ? [selectedElement] : [];
|
|
146
|
+
}, [selectedElement, selectedElements]);
|
|
147
|
+
const isMultiSelectMode = effectiveSelectedElements.length > 1;
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!selectedElement) {
|
|
151
|
+
setOptionJsonText("{}");
|
|
152
|
+
setOptionJsonError(null);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (CHART_TYPES.has(selectedElement.materialType ?? "")) {
|
|
156
|
+
setOptionJsonText(JSON.stringify(buildChartOption(selectedElement), null, 2));
|
|
157
|
+
} else {
|
|
158
|
+
setOptionJsonText("{}");
|
|
159
|
+
}
|
|
160
|
+
setOptionJsonError(null);
|
|
161
|
+
}, [selectedElement]);
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!selectedElement) return;
|
|
165
|
+
if (!CHART_TYPES.has(selectedElement.materialType ?? "")) return;
|
|
166
|
+
setExpandedSections((prev) => ({
|
|
167
|
+
...prev,
|
|
168
|
+
chartBasic: true,
|
|
169
|
+
}));
|
|
170
|
+
}, [selectedElement?.id, selectedElement?.materialType]);
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!selectedElement || selectedElement.materialType !== "text") return;
|
|
174
|
+
const nextHtml = selectedElement.textHtml ?? "<p>双击输入文本</p>";
|
|
175
|
+
if (textEditorRef.current && textEditorRef.current.innerHTML !== nextHtml) {
|
|
176
|
+
textEditorRef.current.innerHTML = nextHtml;
|
|
177
|
+
}
|
|
178
|
+
}, [selectedElement?.id, selectedElement?.materialType, selectedElement?.textHtml]);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (typeof window === "undefined") return;
|
|
181
|
+
const saved = window.localStorage.getItem(SEARCH_COLLAPSE_STORAGE_KEY);
|
|
182
|
+
if (saved === "1") setIsSearchCollapsed(true);
|
|
183
|
+
}, []);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (typeof window === "undefined") return;
|
|
186
|
+
window.localStorage.setItem(SEARCH_COLLAPSE_STORAGE_KEY, isSearchCollapsed ? "1" : "0");
|
|
187
|
+
}, [isSearchCollapsed]);
|
|
188
|
+
useEffect(
|
|
189
|
+
() => () => {
|
|
190
|
+
recordStreamRef.current?.getTracks().forEach((track) => track.stop());
|
|
191
|
+
},
|
|
192
|
+
[]
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const updateSelectedChart = useCallback(
|
|
196
|
+
(patch: Partial<PanelChartConfig>) => {
|
|
197
|
+
if (!selectedElement) return;
|
|
198
|
+
updateElement(selectedElement.id, {
|
|
199
|
+
chart: { ...(selectedElement.chart ?? {}), ...patch },
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
[selectedElement, updateElement]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const updateSelectedStyle = useCallback(
|
|
206
|
+
(patch: Partial<PanelElementStyle>) => {
|
|
207
|
+
if (!selectedElement) return;
|
|
208
|
+
updateElement(selectedElement.id, {
|
|
209
|
+
style: { ...(selectedElement.style ?? {}), ...patch },
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
[selectedElement, updateElement]
|
|
213
|
+
);
|
|
214
|
+
const updateSelectedText = useCallback(
|
|
215
|
+
(patch: Partial<PanelElement>) => {
|
|
216
|
+
if (!selectedElement || selectedElement.materialType !== "text") return;
|
|
217
|
+
updateElement(selectedElement.id, patch);
|
|
218
|
+
},
|
|
219
|
+
[selectedElement, updateElement]
|
|
220
|
+
);
|
|
221
|
+
const updateSelectedGeometry = useCallback(
|
|
222
|
+
(patch: Partial<PanelElement>) => {
|
|
223
|
+
if (!selectedElement || selectedElement.materialType !== "geometry") return;
|
|
224
|
+
updateElement(selectedElement.id, patch);
|
|
225
|
+
},
|
|
226
|
+
[selectedElement, updateElement]
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const redrawGeometryPadFromElement = useCallback(() => {
|
|
230
|
+
const canvas = geometryDrawCanvasRef.current;
|
|
231
|
+
if (!canvas) return;
|
|
232
|
+
const ctx = canvas.getContext("2d");
|
|
233
|
+
if (!ctx) return;
|
|
234
|
+
const width = canvas.width;
|
|
235
|
+
const height = canvas.height;
|
|
236
|
+
ctx.clearRect(0, 0, width, height);
|
|
237
|
+
ctx.fillStyle = "#ffffff";
|
|
238
|
+
ctx.fillRect(0, 0, width, height);
|
|
239
|
+
const sketch = selectedElement?.geometrySketchDataUrl;
|
|
240
|
+
if (!sketch) return;
|
|
241
|
+
const img = new Image();
|
|
242
|
+
img.onload = () => {
|
|
243
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
244
|
+
};
|
|
245
|
+
img.src = sketch;
|
|
246
|
+
}, [selectedElement?.geometrySketchDataUrl]);
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (!selectedElement || selectedElement.materialType !== "geometry") return;
|
|
250
|
+
redrawGeometryPadFromElement();
|
|
251
|
+
}, [redrawGeometryPadFromElement, selectedElement?.id, selectedElement?.materialType]);
|
|
252
|
+
|
|
253
|
+
const handleUploadBackgroundImage = useCallback(
|
|
254
|
+
async (file: File) => {
|
|
255
|
+
if (!selectedElement) return;
|
|
256
|
+
const base64 = await new Promise<string>((resolve, reject) => {
|
|
257
|
+
const reader = new FileReader();
|
|
258
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
259
|
+
reader.onerror = () => reject(new Error(PANEL_MESSAGES.readImageFailed));
|
|
260
|
+
reader.readAsDataURL(file);
|
|
261
|
+
});
|
|
262
|
+
updateSelectedStyle({
|
|
263
|
+
backgroundImage: `url("${base64}")`,
|
|
264
|
+
});
|
|
265
|
+
setUploadStatus("已写入 base64");
|
|
266
|
+
try {
|
|
267
|
+
const form = new FormData();
|
|
268
|
+
form.append("file", file);
|
|
269
|
+
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
270
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
271
|
+
const data = (await resp.json()) as { url?: string };
|
|
272
|
+
if (data.url) {
|
|
273
|
+
updateSelectedStyle({ backgroundImageRemoteUrl: data.url });
|
|
274
|
+
setUploadStatus("已上传服务器并写入 base64");
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
setUploadStatus("服务器上传失败,仅保留 base64");
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
[selectedElement, updateSelectedStyle]
|
|
281
|
+
);
|
|
282
|
+
const updateSelectedAudio = useCallback(
|
|
283
|
+
(patch: Partial<PanelElement>) => {
|
|
284
|
+
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
285
|
+
updateElement(selectedElement.id, patch);
|
|
286
|
+
},
|
|
287
|
+
[selectedElement, updateElement]
|
|
288
|
+
);
|
|
289
|
+
const handleUploadAudioFile = useCallback(
|
|
290
|
+
async (file: File) => {
|
|
291
|
+
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
292
|
+
const base64 = await new Promise<string>((resolve, reject) => {
|
|
293
|
+
const reader = new FileReader();
|
|
294
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
295
|
+
reader.onerror = () => reject(new Error(PANEL_MESSAGES.readAudioFailed));
|
|
296
|
+
reader.readAsDataURL(file);
|
|
297
|
+
});
|
|
298
|
+
updateSelectedAudio({ audioSrc: base64 });
|
|
299
|
+
setAudioStatus(PANEL_MESSAGES.audioLocalSaved);
|
|
300
|
+
try {
|
|
301
|
+
const form = new FormData();
|
|
302
|
+
form.append("file", file);
|
|
303
|
+
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
304
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
305
|
+
const data = (await resp.json()) as { url?: string };
|
|
306
|
+
if (data.url) {
|
|
307
|
+
updateSelectedAudio({ audioRemoteUrl: data.url });
|
|
308
|
+
setAudioStatus(PANEL_MESSAGES.audioRemoteUploaded);
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
setAudioStatus(PANEL_MESSAGES.audioServerUploadFailed);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
[selectedElement, updateSelectedAudio]
|
|
315
|
+
);
|
|
316
|
+
const handleUploadAudioPoster = useCallback(
|
|
317
|
+
async (file: File) => {
|
|
318
|
+
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
319
|
+
const base64 = await new Promise<string>((resolve, reject) => {
|
|
320
|
+
const reader = new FileReader();
|
|
321
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
322
|
+
reader.onerror = () => reject(new Error(PANEL_MESSAGES.readImageFailed));
|
|
323
|
+
reader.readAsDataURL(file);
|
|
324
|
+
});
|
|
325
|
+
updateSelectedAudio({ audioPosterImage: base64 });
|
|
326
|
+
setAudioStatus(PANEL_MESSAGES.audioPosterSet);
|
|
327
|
+
},
|
|
328
|
+
[selectedElement, updateSelectedAudio]
|
|
329
|
+
);
|
|
330
|
+
const stopRecordingAudio = useCallback(() => {
|
|
331
|
+
recorderRef.current?.stop();
|
|
332
|
+
}, []);
|
|
333
|
+
const startRecordingAudio = useCallback(async () => {
|
|
334
|
+
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
335
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
336
|
+
setAudioStatus(PANEL_MESSAGES.audioRecordUnsupported);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
341
|
+
recordStreamRef.current = stream;
|
|
342
|
+
const recorder = new MediaRecorder(stream);
|
|
343
|
+
recorderRef.current = recorder;
|
|
344
|
+
audioChunksRef.current = [];
|
|
345
|
+
recorder.ondataavailable = (event) => {
|
|
346
|
+
if (event.data.size > 0) audioChunksRef.current.push(event.data);
|
|
347
|
+
};
|
|
348
|
+
recorder.onstop = async () => {
|
|
349
|
+
const blob = new Blob(audioChunksRef.current, { type: recorder.mimeType || "audio/webm" });
|
|
350
|
+
const dataUrl = await new Promise<string>((resolve, reject) => {
|
|
351
|
+
const reader = new FileReader();
|
|
352
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
353
|
+
reader.onerror = () => reject(new Error(PANEL_MESSAGES.readRecordAudioFailed));
|
|
354
|
+
reader.readAsDataURL(blob);
|
|
355
|
+
});
|
|
356
|
+
updateSelectedAudio({ audioSrc: dataUrl });
|
|
357
|
+
setAudioStatus(PANEL_MESSAGES.audioRecordSaved);
|
|
358
|
+
recordStreamRef.current?.getTracks().forEach((track) => track.stop());
|
|
359
|
+
recordStreamRef.current = null;
|
|
360
|
+
recorderRef.current = null;
|
|
361
|
+
setIsRecordingAudio(false);
|
|
362
|
+
};
|
|
363
|
+
recorder.start();
|
|
364
|
+
setIsRecordingAudio(true);
|
|
365
|
+
setAudioStatus(PANEL_MESSAGES.audioRecording);
|
|
366
|
+
} catch {
|
|
367
|
+
setAudioStatus(PANEL_MESSAGES.audioRecordStartFailed);
|
|
368
|
+
setIsRecordingAudio(false);
|
|
369
|
+
}
|
|
370
|
+
}, [selectedElement, updateSelectedAudio]);
|
|
371
|
+
const updateSelectedVideo = useCallback(
|
|
372
|
+
(patch: Partial<PanelElement>) => {
|
|
373
|
+
if (!selectedElement || selectedElement.materialType !== "video") return;
|
|
374
|
+
updateElement(selectedElement.id, patch);
|
|
375
|
+
},
|
|
376
|
+
[selectedElement, updateElement]
|
|
377
|
+
);
|
|
378
|
+
const handleUploadVideoFile = useCallback(
|
|
379
|
+
async (file: File) => {
|
|
380
|
+
if (!selectedElement || selectedElement.materialType !== "video") return;
|
|
381
|
+
const base64 = await new Promise<string>((resolve, reject) => {
|
|
382
|
+
const reader = new FileReader();
|
|
383
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
384
|
+
reader.onerror = () => reject(new Error(PANEL_MESSAGES.readVideoFailed));
|
|
385
|
+
reader.readAsDataURL(file);
|
|
386
|
+
});
|
|
387
|
+
updateSelectedVideo({ videoSrc: base64 });
|
|
388
|
+
setVideoStatus(PANEL_MESSAGES.videoLocalSaved);
|
|
389
|
+
try {
|
|
390
|
+
const form = new FormData();
|
|
391
|
+
form.append("file", file);
|
|
392
|
+
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
393
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
394
|
+
const data = (await resp.json()) as { url?: string };
|
|
395
|
+
if (data.url) {
|
|
396
|
+
updateSelectedVideo({ videoRemoteUrl: data.url });
|
|
397
|
+
setVideoStatus(PANEL_MESSAGES.videoRemoteUploaded);
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
setVideoStatus(PANEL_MESSAGES.videoServerUploadFailed);
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
[selectedElement, updateSelectedVideo]
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const isSectionExpanded = (key: string, defaultValue = true) =>
|
|
407
|
+
expandedSections[key] ?? defaultValue;
|
|
408
|
+
|
|
409
|
+
const setSectionExpanded = (key: string, next: boolean) => {
|
|
410
|
+
setExpandedSections((prev) => ({ ...prev, [key]: next }));
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const renderSection = (
|
|
414
|
+
key: string,
|
|
415
|
+
title: string,
|
|
416
|
+
children: React.ReactNode,
|
|
417
|
+
defaultOpen = true,
|
|
418
|
+
searchTerms: string[] = []
|
|
419
|
+
) => {
|
|
420
|
+
const hit =
|
|
421
|
+
!hasSearch ||
|
|
422
|
+
[title, ...searchTerms].some((term) => term.toLowerCase().includes(normalizedSearch));
|
|
423
|
+
if (!hit) return null;
|
|
424
|
+
renderedSectionCount += 1;
|
|
425
|
+
return (
|
|
426
|
+
<Collapsible
|
|
427
|
+
open={hasSearch ? true : isSectionExpanded(key, defaultOpen)}
|
|
428
|
+
onOpenChange={(open) => setSectionExpanded(key, open)}
|
|
429
|
+
className="rounded-xl border border-border/70 bg-card/95 shadow-sm"
|
|
430
|
+
>
|
|
431
|
+
<div className="flex items-center gap-1.5 px-3 py-2">
|
|
432
|
+
<CollapsibleTrigger asChild>
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
435
|
+
className="flex h-6 w-6 items-center justify-center rounded-md text-xs hover:bg-accent"
|
|
436
|
+
>
|
|
437
|
+
{isSectionExpanded(key, defaultOpen) ? "▾" : "▸"}
|
|
438
|
+
</button>
|
|
439
|
+
</CollapsibleTrigger>
|
|
440
|
+
<div className="text-[11px] font-semibold tracking-wide text-muted-foreground">{title}</div>
|
|
441
|
+
</div>
|
|
442
|
+
<CollapsibleContent className="space-y-3 border-t border-border/60 bg-muted/[0.1] px-3 pb-3 pt-2.5">
|
|
443
|
+
{children}
|
|
444
|
+
</CollapsibleContent>
|
|
445
|
+
</Collapsible>
|
|
446
|
+
);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const renderFieldGroup = (title: string, children: React.ReactNode) => (
|
|
450
|
+
<div className="space-y-2.5 rounded-lg border border-border/55 bg-background/80 p-2.5">
|
|
451
|
+
<div className="text-[11px] font-semibold text-muted-foreground">{title}</div>
|
|
452
|
+
{children}
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
const isNodeCardExpanded = useCallback(
|
|
456
|
+
(id: string) => expandedNodeCards[id] ?? true,
|
|
457
|
+
[expandedNodeCards]
|
|
458
|
+
);
|
|
459
|
+
const setNodeCardExpanded = useCallback((id: string, open: boolean) => {
|
|
460
|
+
setExpandedNodeCards((prev) => ({ ...prev, [id]: open }));
|
|
461
|
+
}, []);
|
|
462
|
+
|
|
463
|
+
const renderColorField = (
|
|
464
|
+
label: string,
|
|
465
|
+
value: string,
|
|
466
|
+
onTextChange: (next: string) => void
|
|
467
|
+
) => (
|
|
468
|
+
<label className="block space-y-1.5">
|
|
469
|
+
<div className="text-[11px] text-muted-foreground">{label}</div>
|
|
470
|
+
<div className="flex items-center gap-2">
|
|
471
|
+
<Input
|
|
472
|
+
value={value}
|
|
473
|
+
onChange={(e) => onTextChange(e.target.value)}
|
|
474
|
+
placeholder="#000000"
|
|
475
|
+
className="h-7"
|
|
476
|
+
/>
|
|
477
|
+
<Input
|
|
478
|
+
type="color"
|
|
479
|
+
value={/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value || "") ? value : "#000000"}
|
|
480
|
+
onChange={(e) => onTextChange(e.target.value)}
|
|
481
|
+
className="h-7 w-10 cursor-pointer p-1"
|
|
482
|
+
aria-label={`${label}调色盘`}
|
|
483
|
+
/>
|
|
484
|
+
</div>
|
|
485
|
+
</label>
|
|
486
|
+
);
|
|
487
|
+
const optionCheckboxClass =
|
|
488
|
+
"h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40";
|
|
489
|
+
const optionInputClass =
|
|
490
|
+
"h-7 border border-border/60 bg-background/90 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-border/60";
|
|
491
|
+
const optionSelectTriggerClass =
|
|
492
|
+
"h-7 border border-border/60 bg-muted/40 shadow-none ring-0 focus:ring-0 focus:ring-offset-0 focus:border-border/60 data-[state=open]:border-border/60";
|
|
493
|
+
const optionLabelTextClass = "truncate whitespace-nowrap text-[11px] leading-none";
|
|
494
|
+
const renderOptionLabel = (label: string, keyPath: string, desc: string) => (
|
|
495
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
496
|
+
<span className={optionLabelTextClass} title={`${label}(${keyPath})`}>{label}</span>
|
|
497
|
+
<TooltipProvider delayDuration={120}>
|
|
498
|
+
<Tooltip>
|
|
499
|
+
<TooltipTrigger asChild>
|
|
500
|
+
<button
|
|
501
|
+
type="button"
|
|
502
|
+
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] text-muted-foreground"
|
|
503
|
+
aria-label={`${label}说明`}
|
|
504
|
+
>
|
|
505
|
+
?
|
|
506
|
+
</button>
|
|
507
|
+
</TooltipTrigger>
|
|
508
|
+
<TooltipContent className="max-w-[260px] text-[11px]">
|
|
509
|
+
<div>Key: {keyPath}</div>
|
|
510
|
+
<div>{desc}</div>
|
|
511
|
+
</TooltipContent>
|
|
512
|
+
</Tooltip>
|
|
513
|
+
</TooltipProvider>
|
|
514
|
+
</div>
|
|
515
|
+
);
|
|
516
|
+
const renderFormatterLabel = (label = "Tooltip Formatter(可选)") => (
|
|
517
|
+
<div className="flex min-w-0 items-center gap-1">
|
|
518
|
+
<span className={optionLabelTextClass} title={label}>{label}</span>
|
|
519
|
+
<TooltipProvider delayDuration={120}>
|
|
520
|
+
<Tooltip>
|
|
521
|
+
<TooltipTrigger asChild>
|
|
522
|
+
<button
|
|
523
|
+
type="button"
|
|
524
|
+
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] text-muted-foreground"
|
|
525
|
+
aria-label="Tooltip Formatter 说明"
|
|
526
|
+
>
|
|
527
|
+
?
|
|
528
|
+
</button>
|
|
529
|
+
</TooltipTrigger>
|
|
530
|
+
<TooltipContent className="max-w-[360px] text-[11px] leading-5">
|
|
531
|
+
<div className="font-medium">可用占位符</div>
|
|
532
|
+
<div>{"{a}=系列名, {b}=类目名, {c}=数值, {d}=百分比(饼图)"}</div>
|
|
533
|
+
<div className="mt-1 font-medium">常用模板 + 输出示例</div>
|
|
534
|
+
<div>{"1) {b}: {c} -> 周一: 120"}</div>
|
|
535
|
+
<div>{"2) {a}<br/>{b}: {c} -> 销量<br/>周一: 120"}</div>
|
|
536
|
+
<div>{"3) {b}: {c} ({d}%) -> 访问来源: 335 (42%)"}</div>
|
|
537
|
+
<div>{"4) ¥{c} / {b} -> ¥120 / 周一"}</div>
|
|
538
|
+
</TooltipContent>
|
|
539
|
+
</Tooltip>
|
|
540
|
+
</TooltipProvider>
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
const mergeOptionPatch = useCallback(
|
|
544
|
+
(
|
|
545
|
+
base: Record<string, unknown> | undefined,
|
|
546
|
+
patch: Record<string, unknown>
|
|
547
|
+
): Record<string, unknown> => {
|
|
548
|
+
const output: Record<string, unknown> = { ...(base ?? {}) };
|
|
549
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
550
|
+
const prev = output[key];
|
|
551
|
+
if (
|
|
552
|
+
value &&
|
|
553
|
+
typeof value === "object" &&
|
|
554
|
+
!Array.isArray(value) &&
|
|
555
|
+
prev &&
|
|
556
|
+
typeof prev === "object" &&
|
|
557
|
+
!Array.isArray(prev)
|
|
558
|
+
) {
|
|
559
|
+
output[key] = mergeOptionPatch(
|
|
560
|
+
prev as Record<string, unknown>,
|
|
561
|
+
value as Record<string, unknown>
|
|
562
|
+
);
|
|
563
|
+
} else {
|
|
564
|
+
output[key] = value;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return output;
|
|
568
|
+
},
|
|
569
|
+
[]
|
|
570
|
+
);
|
|
571
|
+
const updateSelectedOptionForm = useCallback(
|
|
572
|
+
(patch: Record<string, unknown>) => {
|
|
573
|
+
if (!selectedElement) return;
|
|
574
|
+
updateSelectedChart({
|
|
575
|
+
option: mergeOptionPatch(
|
|
576
|
+
(selectedElement.chart?.option as Record<string, unknown> | undefined) ?? {},
|
|
577
|
+
patch
|
|
578
|
+
),
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
[mergeOptionPatch, selectedElement, updateSelectedChart]
|
|
582
|
+
);
|
|
583
|
+
const execTextCommand = (cmd: "bold" | "italic" | "underline") => {
|
|
584
|
+
textEditorRef.current?.focus();
|
|
585
|
+
document.execCommand(cmd);
|
|
586
|
+
const next = textEditorRef.current?.innerHTML ?? "";
|
|
587
|
+
if (selectedElement?.materialType === "text") {
|
|
588
|
+
updateElement(selectedElement.id, { textHtml: next || "<p><br/></p>" });
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
let renderedSectionCount = 0;
|
|
593
|
+
return (
|
|
594
|
+
<ScopeConfigProvider
|
|
595
|
+
scope={showScopePanel ? viewElementScope : undefined}
|
|
596
|
+
warnings={scopeWarnings}
|
|
597
|
+
scrollContainerRef={sidebarScrollRef}
|
|
598
|
+
>
|
|
599
|
+
<aside
|
|
600
|
+
ref={sidebarScrollRef}
|
|
601
|
+
className={`scope-config-sidebar h-full overflow-auto border-l border-border bg-muted/[0.14] px-3 py-3 text-foreground [&_.scope-field--highlight]:rounded-md [&_.scope-field--highlight]:ring-2 [&_.scope-field--highlight]:ring-amber-400/80 [&_button[role=checkbox]]:border-2 [&_button[role=checkbox]]:border-foreground/80 [&_button[role=checkbox]]:bg-background [&_button[role=checkbox]]:ring-1 [&_button[role=checkbox]]:ring-foreground/40 [&_button[role=checkbox][data-state=checked]]:border-primary [&_button[role=checkbox][data-state=checked]]:ring-primary/40 ${themedScrollbarClass}`}
|
|
602
|
+
>
|
|
603
|
+
<div className="sticky top-0 z-20 mb-3 rounded-lg border border-border/70 bg-card/95 px-2.5 py-2 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-card/80">
|
|
604
|
+
<div className="flex items-center justify-between gap-2">
|
|
605
|
+
<div className="text-xs font-semibold tracking-wide">配置面板</div>
|
|
606
|
+
</div>
|
|
607
|
+
{showScopePanel ? (
|
|
608
|
+
<ViewElementScopePanel scope={viewElementScope} />
|
|
609
|
+
) : null}
|
|
610
|
+
<ScopeTemplateWarningsPanel />
|
|
611
|
+
<div
|
|
612
|
+
className={
|
|
613
|
+
showScopePanel || scopeWarnings.length > 0
|
|
614
|
+
? "mt-2 border-t border-border/50 pt-2"
|
|
615
|
+
: "mt-0"
|
|
616
|
+
}
|
|
617
|
+
>
|
|
618
|
+
<div className="flex items-center justify-end gap-2">
|
|
619
|
+
<button
|
|
620
|
+
type="button"
|
|
621
|
+
className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
|
|
622
|
+
onClick={() => setIsSearchCollapsed((prev) => !prev)}
|
|
623
|
+
>
|
|
624
|
+
{isSearchCollapsed ? "展开搜索" : "收起搜索"}
|
|
625
|
+
</button>
|
|
626
|
+
</div>
|
|
627
|
+
{!isSearchCollapsed ? (
|
|
628
|
+
<div className="mt-2">
|
|
629
|
+
<Input
|
|
630
|
+
value={configSearch}
|
|
631
|
+
onChange={(e) => setConfigSearch(e.target.value)}
|
|
632
|
+
placeholder="搜索配置,如:边框、tooltip、音频、网格..."
|
|
633
|
+
className="h-7"
|
|
634
|
+
data-scope-autocomplete="off"
|
|
635
|
+
/>
|
|
636
|
+
{hasSearch ? (
|
|
637
|
+
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
638
|
+
搜索中:{configSearch}
|
|
639
|
+
</div>
|
|
640
|
+
) : null}
|
|
641
|
+
</div>
|
|
642
|
+
) : null}
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
{isMultiSelectMode ? (
|
|
646
|
+
<div className="space-y-3">
|
|
647
|
+
<Card>
|
|
648
|
+
<CardHeader className="pb-2">
|
|
649
|
+
<CardTitle className="text-xs">批量设置({effectiveSelectedElements.length} 个)</CardTitle>
|
|
650
|
+
</CardHeader>
|
|
651
|
+
<CardContent className="space-y-2 text-xs">
|
|
652
|
+
<div className="grid grid-cols-2 gap-2">
|
|
653
|
+
<button
|
|
654
|
+
type="button"
|
|
655
|
+
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
656
|
+
onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: true }))}
|
|
657
|
+
>
|
|
658
|
+
全部锁定
|
|
659
|
+
</button>
|
|
660
|
+
<button
|
|
661
|
+
type="button"
|
|
662
|
+
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
663
|
+
onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: false }))}
|
|
664
|
+
>
|
|
665
|
+
全部解锁
|
|
666
|
+
</button>
|
|
667
|
+
<button
|
|
668
|
+
type="button"
|
|
669
|
+
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
670
|
+
onClick={() =>
|
|
671
|
+
effectiveSelectedElements.forEach((el) =>
|
|
672
|
+
onAdjustNodeZOrder?.(el.id, "bringForward")
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
>
|
|
676
|
+
全部上移一层
|
|
677
|
+
</button>
|
|
678
|
+
<button
|
|
679
|
+
type="button"
|
|
680
|
+
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
681
|
+
onClick={() =>
|
|
682
|
+
effectiveSelectedElements.forEach((el) =>
|
|
683
|
+
onAdjustNodeZOrder?.(el.id, "sendBackward")
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
>
|
|
687
|
+
全部下移一层
|
|
688
|
+
</button>
|
|
689
|
+
<button
|
|
690
|
+
type="button"
|
|
691
|
+
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
692
|
+
onClick={() =>
|
|
693
|
+
effectiveSelectedElements.forEach((el) =>
|
|
694
|
+
updateElement(el.id, { zIndex: 1 })
|
|
695
|
+
)
|
|
696
|
+
}
|
|
697
|
+
>
|
|
698
|
+
全部 zIndex 设为 1
|
|
699
|
+
</button>
|
|
700
|
+
<button
|
|
701
|
+
type="button"
|
|
702
|
+
className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
|
|
703
|
+
onClick={() =>
|
|
704
|
+
effectiveSelectedElements.forEach((el) =>
|
|
705
|
+
updateElement(el.id, {
|
|
706
|
+
style: {
|
|
707
|
+
...(el.style ?? {}),
|
|
708
|
+
backgroundColor: "#3b82f6",
|
|
709
|
+
},
|
|
710
|
+
})
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
>
|
|
714
|
+
全部背景色设为蓝色
|
|
715
|
+
</button>
|
|
716
|
+
</div>
|
|
717
|
+
</CardContent>
|
|
718
|
+
</Card>
|
|
719
|
+
{effectiveSelectedElements
|
|
720
|
+
.filter((el) => {
|
|
721
|
+
if (!hasSearch) return true;
|
|
722
|
+
const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`
|
|
723
|
+
.toLowerCase();
|
|
724
|
+
return text.includes(normalizedSearch);
|
|
725
|
+
})
|
|
726
|
+
.map((el) => (
|
|
727
|
+
<Card
|
|
728
|
+
key={el.id}
|
|
729
|
+
className={el.locked ? "border-amber-500/40 bg-amber-500/5" : ""}
|
|
730
|
+
>
|
|
731
|
+
<CardHeader className="pb-2">
|
|
732
|
+
<div className="flex items-center gap-2">
|
|
733
|
+
<button
|
|
734
|
+
type="button"
|
|
735
|
+
className="inline-flex h-6 w-6 items-center justify-center rounded border border-border text-[11px] hover:bg-accent"
|
|
736
|
+
onClick={() => setNodeCardExpanded(el.id, !isNodeCardExpanded(el.id))}
|
|
737
|
+
aria-label={isNodeCardExpanded(el.id) ? "收起节点配置" : "展开节点配置"}
|
|
738
|
+
>
|
|
739
|
+
{isNodeCardExpanded(el.id) ? "▾" : "▸"}
|
|
740
|
+
</button>
|
|
741
|
+
<CardTitle className="min-w-0 flex-1 text-xs truncate">
|
|
742
|
+
{el.name?.trim() || el.materialType || "节点"} · {el.id}
|
|
743
|
+
</CardTitle>
|
|
744
|
+
<button
|
|
745
|
+
type="button"
|
|
746
|
+
className="inline-flex h-6 items-center justify-center rounded border border-border px-2 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
747
|
+
onClick={() => onExcludeSelectedNode?.(el.id)}
|
|
748
|
+
title="将该节点从当前多选中剔除"
|
|
749
|
+
>
|
|
750
|
+
剔除
|
|
751
|
+
</button>
|
|
752
|
+
</div>
|
|
753
|
+
</CardHeader>
|
|
754
|
+
{isNodeCardExpanded(el.id) ? (
|
|
755
|
+
<CardContent className="space-y-2 text-xs">
|
|
756
|
+
<label className="flex items-center gap-2">
|
|
757
|
+
<Checkbox
|
|
758
|
+
checked={el.locked === true}
|
|
759
|
+
onCheckedChange={(checked) => updateElement(el.id, { locked: checked === true })}
|
|
760
|
+
/>
|
|
761
|
+
<span>锁定节点</span>
|
|
762
|
+
</label>
|
|
763
|
+
{el.locked ? (
|
|
764
|
+
<div className="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] text-amber-700 dark:text-amber-300">
|
|
765
|
+
当前节点已锁定,仅可操作锁定开关。
|
|
766
|
+
</div>
|
|
767
|
+
) : null}
|
|
768
|
+
<fieldset disabled={el.locked} className={el.locked ? "opacity-60" : ""}>
|
|
769
|
+
<div className="space-y-2">
|
|
770
|
+
<label className="block space-y-1">
|
|
771
|
+
<div>名称</div>
|
|
772
|
+
<Input
|
|
773
|
+
className="h-7"
|
|
774
|
+
value={el.name ?? ""}
|
|
775
|
+
onChange={(e) => updateElement(el.id, { name: e.target.value || undefined })}
|
|
776
|
+
/>
|
|
777
|
+
</label>
|
|
778
|
+
<div className="grid grid-cols-2 gap-2">
|
|
779
|
+
<label className="block space-y-1">
|
|
780
|
+
<div>zIndex</div>
|
|
781
|
+
<Input
|
|
782
|
+
className="h-7"
|
|
783
|
+
type="number"
|
|
784
|
+
value={el.zIndex ?? 1}
|
|
785
|
+
onChange={(e) =>
|
|
786
|
+
updateElement(el.id, { zIndex: Number(e.target.value) || 1 })
|
|
787
|
+
}
|
|
788
|
+
/>
|
|
789
|
+
</label>
|
|
790
|
+
<label className="block space-y-1">
|
|
791
|
+
<div>图层</div>
|
|
792
|
+
<Select
|
|
793
|
+
value={el.layerId}
|
|
794
|
+
onValueChange={(value) => updateElement(el.id, { layerId: value })}
|
|
795
|
+
>
|
|
796
|
+
<SelectTrigger className="h-7">
|
|
797
|
+
<SelectValue placeholder="选择图层" />
|
|
798
|
+
</SelectTrigger>
|
|
799
|
+
<SelectContent>
|
|
800
|
+
{layers.map((layer) => (
|
|
801
|
+
<SelectItem key={layer.id} value={layer.id}>
|
|
802
|
+
{layer.name}
|
|
803
|
+
</SelectItem>
|
|
804
|
+
))}
|
|
805
|
+
</SelectContent>
|
|
806
|
+
</Select>
|
|
807
|
+
</label>
|
|
808
|
+
</div>
|
|
809
|
+
<div className="grid grid-cols-2 gap-2">
|
|
810
|
+
<label className="block space-y-1">
|
|
811
|
+
<div>X</div>
|
|
812
|
+
<Input
|
|
813
|
+
className="h-7"
|
|
814
|
+
type="number"
|
|
815
|
+
value={el.x}
|
|
816
|
+
onChange={(e) => updateElement(el.id, { x: Number(e.target.value) || 0 })}
|
|
817
|
+
/>
|
|
818
|
+
</label>
|
|
819
|
+
<label className="block space-y-1">
|
|
820
|
+
<div>Y</div>
|
|
821
|
+
<Input
|
|
822
|
+
className="h-7"
|
|
823
|
+
type="number"
|
|
824
|
+
value={el.y}
|
|
825
|
+
onChange={(e) => updateElement(el.id, { y: Number(e.target.value) || 0 })}
|
|
826
|
+
/>
|
|
827
|
+
</label>
|
|
828
|
+
<label className="block space-y-1">
|
|
829
|
+
<div>旋转角度</div>
|
|
830
|
+
<Input
|
|
831
|
+
className="h-7"
|
|
832
|
+
type="number"
|
|
833
|
+
value={el.rotate ?? 0}
|
|
834
|
+
onChange={(e) =>
|
|
835
|
+
updateElement(el.id, { rotate: Number(e.target.value) || 0 })
|
|
836
|
+
}
|
|
837
|
+
/>
|
|
838
|
+
</label>
|
|
839
|
+
<label className="block space-y-1">
|
|
840
|
+
<div>宽</div>
|
|
841
|
+
<Input
|
|
842
|
+
className="h-7"
|
|
843
|
+
type="number"
|
|
844
|
+
min={1}
|
|
845
|
+
value={el.width}
|
|
846
|
+
onChange={(e) =>
|
|
847
|
+
updateElement(el.id, { width: Math.max(1, Number(e.target.value) || 1) })
|
|
848
|
+
}
|
|
849
|
+
/>
|
|
850
|
+
</label>
|
|
851
|
+
<label className="block space-y-1">
|
|
852
|
+
<div>高</div>
|
|
853
|
+
<Input
|
|
854
|
+
className="h-7"
|
|
855
|
+
type="number"
|
|
856
|
+
min={1}
|
|
857
|
+
value={el.height}
|
|
858
|
+
onChange={(e) =>
|
|
859
|
+
updateElement(el.id, { height: Math.max(1, Number(e.target.value) || 1) })
|
|
860
|
+
}
|
|
861
|
+
/>
|
|
862
|
+
</label>
|
|
863
|
+
</div>
|
|
864
|
+
<div className="grid grid-cols-2 gap-2">
|
|
865
|
+
<label className="block space-y-1">
|
|
866
|
+
<div>背景色</div>
|
|
867
|
+
<Input
|
|
868
|
+
className="h-7"
|
|
869
|
+
value={el.style?.backgroundColor ?? ""}
|
|
870
|
+
placeholder="#000000"
|
|
871
|
+
onChange={(e) =>
|
|
872
|
+
updateElement(el.id, {
|
|
873
|
+
style: {
|
|
874
|
+
...(el.style ?? {}),
|
|
875
|
+
backgroundColor: e.target.value || undefined,
|
|
876
|
+
},
|
|
877
|
+
})
|
|
878
|
+
}
|
|
879
|
+
/>
|
|
880
|
+
</label>
|
|
881
|
+
<label className="block space-y-1">
|
|
882
|
+
<div>边框色</div>
|
|
883
|
+
<Input
|
|
884
|
+
className="h-7"
|
|
885
|
+
value={el.style?.borderColor ?? ""}
|
|
886
|
+
placeholder="#000000"
|
|
887
|
+
onChange={(e) =>
|
|
888
|
+
updateElement(el.id, {
|
|
889
|
+
style: {
|
|
890
|
+
...(el.style ?? {}),
|
|
891
|
+
borderColor: e.target.value || undefined,
|
|
892
|
+
},
|
|
893
|
+
})
|
|
894
|
+
}
|
|
895
|
+
/>
|
|
896
|
+
</label>
|
|
897
|
+
</div>
|
|
898
|
+
{CHART_TYPES.has(el.materialType ?? "") ? (
|
|
899
|
+
<div className="grid grid-cols-2 gap-2">
|
|
900
|
+
<label className="block space-y-1 col-span-2">
|
|
901
|
+
<div>图表标题</div>
|
|
902
|
+
<Input
|
|
903
|
+
className="h-7"
|
|
904
|
+
value={el.chart?.title ?? ""}
|
|
905
|
+
onChange={(e) =>
|
|
906
|
+
updateElement(el.id, {
|
|
907
|
+
chart: { ...(el.chart ?? {}), title: e.target.value },
|
|
908
|
+
})
|
|
909
|
+
}
|
|
910
|
+
/>
|
|
911
|
+
</label>
|
|
912
|
+
<label className="block space-y-1">
|
|
913
|
+
<div>主色</div>
|
|
914
|
+
<Input
|
|
915
|
+
className="h-7"
|
|
916
|
+
value={el.chart?.color ?? ""}
|
|
917
|
+
onChange={(e) =>
|
|
918
|
+
updateElement(el.id, {
|
|
919
|
+
chart: { ...(el.chart ?? {}), color: e.target.value || undefined },
|
|
920
|
+
})
|
|
921
|
+
}
|
|
922
|
+
/>
|
|
923
|
+
</label>
|
|
924
|
+
<label className="flex items-center gap-2">
|
|
925
|
+
<Checkbox
|
|
926
|
+
checked={el.chart?.colorMode === "gradient"}
|
|
927
|
+
onCheckedChange={(checked) =>
|
|
928
|
+
updateElement(el.id, {
|
|
929
|
+
chart: { ...(el.chart ?? {}), colorMode: checked ? "gradient" : "solid" },
|
|
930
|
+
})
|
|
931
|
+
}
|
|
932
|
+
/>
|
|
933
|
+
<span>主色渐变</span>
|
|
934
|
+
</label>
|
|
935
|
+
{el.chart?.colorMode === "gradient" ? (
|
|
936
|
+
<>
|
|
937
|
+
<label className="block space-y-1">
|
|
938
|
+
<div>渐变起始色</div>
|
|
939
|
+
<Input
|
|
940
|
+
className="h-7"
|
|
941
|
+
value={el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}
|
|
942
|
+
onChange={(e) =>
|
|
943
|
+
updateElement(el.id, {
|
|
944
|
+
chart: { ...(el.chart ?? {}), gradientFrom: e.target.value || "#3b82f6" },
|
|
945
|
+
})
|
|
946
|
+
}
|
|
947
|
+
/>
|
|
948
|
+
</label>
|
|
949
|
+
<label className="block space-y-1">
|
|
950
|
+
<div>渐变结束色</div>
|
|
951
|
+
<Input
|
|
952
|
+
className="h-7"
|
|
953
|
+
value={el.chart?.gradientTo ?? "#22d3ee"}
|
|
954
|
+
onChange={(e) =>
|
|
955
|
+
updateElement(el.id, {
|
|
956
|
+
chart: { ...(el.chart ?? {}), gradientTo: e.target.value || "#22d3ee" },
|
|
957
|
+
})
|
|
958
|
+
}
|
|
959
|
+
/>
|
|
960
|
+
</label>
|
|
961
|
+
<label className="block space-y-1 col-span-2">
|
|
962
|
+
<div>渐变方向</div>
|
|
963
|
+
<Select
|
|
964
|
+
value={el.chart?.gradientDirection ?? "to-right"}
|
|
965
|
+
onValueChange={(value) =>
|
|
966
|
+
updateElement(el.id, {
|
|
967
|
+
chart: {
|
|
968
|
+
...(el.chart ?? {}),
|
|
969
|
+
gradientDirection: value as
|
|
970
|
+
| "to-right"
|
|
971
|
+
| "to-bottom"
|
|
972
|
+
| "to-bottom-right"
|
|
973
|
+
| "to-top-right",
|
|
974
|
+
},
|
|
975
|
+
})
|
|
976
|
+
}
|
|
977
|
+
>
|
|
978
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
979
|
+
<SelectContent>
|
|
980
|
+
<SelectItem value="to-right">左 → 右</SelectItem>
|
|
981
|
+
<SelectItem value="to-bottom">上 → 下</SelectItem>
|
|
982
|
+
<SelectItem value="to-bottom-right">左上 → 右下</SelectItem>
|
|
983
|
+
<SelectItem value="to-top-right">左下 → 右上</SelectItem>
|
|
984
|
+
</SelectContent>
|
|
985
|
+
</Select>
|
|
986
|
+
</label>
|
|
987
|
+
<div className="col-span-2 space-y-1">
|
|
988
|
+
<div className="text-[11px] text-muted-foreground">渐变预览</div>
|
|
989
|
+
<div
|
|
990
|
+
className="h-6 rounded border border-border/60"
|
|
991
|
+
style={{
|
|
992
|
+
backgroundImage: `linear-gradient(${
|
|
993
|
+
(el.chart?.gradientDirection ?? "to-right") === "to-bottom"
|
|
994
|
+
? "to bottom"
|
|
995
|
+
: (el.chart?.gradientDirection ?? "to-right") === "to-bottom-right"
|
|
996
|
+
? "to bottom right"
|
|
997
|
+
: (el.chart?.gradientDirection ?? "to-right") === "to-top-right"
|
|
998
|
+
? "to top right"
|
|
999
|
+
: "to right"
|
|
1000
|
+
}, ${el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}, ${el.chart?.gradientTo ?? "#22d3ee"})`,
|
|
1001
|
+
}}
|
|
1002
|
+
/>
|
|
1003
|
+
</div>
|
|
1004
|
+
</>
|
|
1005
|
+
) : null}
|
|
1006
|
+
<label className="block space-y-1">
|
|
1007
|
+
<div>渲染</div>
|
|
1008
|
+
<Select
|
|
1009
|
+
value={el.chart?.renderMode ?? "canvas"}
|
|
1010
|
+
onValueChange={(value) =>
|
|
1011
|
+
updateElement(el.id, {
|
|
1012
|
+
chart: { ...(el.chart ?? {}), renderMode: value as "canvas" | "svg" },
|
|
1013
|
+
})
|
|
1014
|
+
}
|
|
1015
|
+
>
|
|
1016
|
+
<SelectTrigger className="h-7">
|
|
1017
|
+
<SelectValue />
|
|
1018
|
+
</SelectTrigger>
|
|
1019
|
+
<SelectContent>
|
|
1020
|
+
<SelectItem value="canvas">canvas</SelectItem>
|
|
1021
|
+
<SelectItem value="svg">svg</SelectItem>
|
|
1022
|
+
</SelectContent>
|
|
1023
|
+
</Select>
|
|
1024
|
+
</label>
|
|
1025
|
+
<label className="block space-y-1">
|
|
1026
|
+
<div>Tooltip 背景</div>
|
|
1027
|
+
<Input
|
|
1028
|
+
className="h-7"
|
|
1029
|
+
value={el.chart?.tooltipBackgroundColor ?? ""}
|
|
1030
|
+
placeholder="#0f172a"
|
|
1031
|
+
onChange={(e) =>
|
|
1032
|
+
updateElement(el.id, {
|
|
1033
|
+
chart: {
|
|
1034
|
+
...(el.chart ?? {}),
|
|
1035
|
+
tooltipBackgroundColor: e.target.value || undefined,
|
|
1036
|
+
},
|
|
1037
|
+
})
|
|
1038
|
+
}
|
|
1039
|
+
/>
|
|
1040
|
+
</label>
|
|
1041
|
+
<label className="flex items-center gap-2">
|
|
1042
|
+
<Checkbox
|
|
1043
|
+
checked={el.chart?.tooltipShow ?? true}
|
|
1044
|
+
onCheckedChange={(checked) =>
|
|
1045
|
+
updateElement(el.id, {
|
|
1046
|
+
chart: { ...(el.chart ?? {}), tooltipShow: checked === true },
|
|
1047
|
+
})
|
|
1048
|
+
}
|
|
1049
|
+
/>
|
|
1050
|
+
<span>显示 Tooltip</span>
|
|
1051
|
+
</label>
|
|
1052
|
+
<label className="block space-y-1">
|
|
1053
|
+
<div>Tooltip 触发方式</div>
|
|
1054
|
+
<Select
|
|
1055
|
+
value={el.chart?.tooltipTrigger ?? "axis"}
|
|
1056
|
+
onValueChange={(value) =>
|
|
1057
|
+
updateElement(el.id, {
|
|
1058
|
+
chart: { ...(el.chart ?? {}), tooltipTrigger: value as "axis" | "item" },
|
|
1059
|
+
})
|
|
1060
|
+
}
|
|
1061
|
+
>
|
|
1062
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1063
|
+
<SelectContent>
|
|
1064
|
+
<SelectItem value="axis">axis</SelectItem>
|
|
1065
|
+
<SelectItem value="item">item</SelectItem>
|
|
1066
|
+
</SelectContent>
|
|
1067
|
+
</Select>
|
|
1068
|
+
</label>
|
|
1069
|
+
<label className="block space-y-1">
|
|
1070
|
+
<div>Tooltip 文字色</div>
|
|
1071
|
+
<Input
|
|
1072
|
+
className="h-7"
|
|
1073
|
+
value={el.chart?.tooltipTextColor ?? ""}
|
|
1074
|
+
placeholder="#f8fafc"
|
|
1075
|
+
onChange={(e) =>
|
|
1076
|
+
updateElement(el.id, {
|
|
1077
|
+
chart: {
|
|
1078
|
+
...(el.chart ?? {}),
|
|
1079
|
+
tooltipTextColor: e.target.value || undefined,
|
|
1080
|
+
},
|
|
1081
|
+
})
|
|
1082
|
+
}
|
|
1083
|
+
/>
|
|
1084
|
+
</label>
|
|
1085
|
+
<label className="block space-y-1 col-span-2">
|
|
1086
|
+
{renderFormatterLabel("Tooltip Formatter")}
|
|
1087
|
+
<Input
|
|
1088
|
+
className="h-7"
|
|
1089
|
+
value={el.chart?.tooltipFormatter ?? ""}
|
|
1090
|
+
placeholder="{b}: {c}"
|
|
1091
|
+
onChange={(e) =>
|
|
1092
|
+
updateElement(el.id, {
|
|
1093
|
+
chart: {
|
|
1094
|
+
...(el.chart ?? {}),
|
|
1095
|
+
tooltipFormatter: e.target.value || undefined,
|
|
1096
|
+
},
|
|
1097
|
+
})
|
|
1098
|
+
}
|
|
1099
|
+
/>
|
|
1100
|
+
</label>
|
|
1101
|
+
<label className="block space-y-1 col-span-2">
|
|
1102
|
+
<div>类目(逗号分隔)</div>
|
|
1103
|
+
<Input
|
|
1104
|
+
className="h-7"
|
|
1105
|
+
value={(el.chart?.labels ?? []).join(",")}
|
|
1106
|
+
onChange={(e) =>
|
|
1107
|
+
updateElement(el.id, {
|
|
1108
|
+
chart: {
|
|
1109
|
+
...(el.chart ?? {}),
|
|
1110
|
+
labels: e.target.value
|
|
1111
|
+
.split(",")
|
|
1112
|
+
.map((s) => s.trim())
|
|
1113
|
+
.filter(Boolean),
|
|
1114
|
+
},
|
|
1115
|
+
})
|
|
1116
|
+
}
|
|
1117
|
+
/>
|
|
1118
|
+
</label>
|
|
1119
|
+
<label className="block space-y-1 col-span-2">
|
|
1120
|
+
<div>数值(逗号分隔)</div>
|
|
1121
|
+
<Input
|
|
1122
|
+
className="h-7"
|
|
1123
|
+
value={getChartValuesDisplayText(el.chart)}
|
|
1124
|
+
onChange={(e) =>
|
|
1125
|
+
updateElement(el.id, {
|
|
1126
|
+
chart: {
|
|
1127
|
+
...(el.chart ?? {}),
|
|
1128
|
+
valuesText: e.target.value,
|
|
1129
|
+
},
|
|
1130
|
+
})
|
|
1131
|
+
}
|
|
1132
|
+
/>
|
|
1133
|
+
</label>
|
|
1134
|
+
{["bar", "line", "area", "scatter"].includes(el.materialType ?? "") ? (
|
|
1135
|
+
<>
|
|
1136
|
+
<label className="block space-y-1">
|
|
1137
|
+
<div>X 轴名称</div>
|
|
1138
|
+
<Input
|
|
1139
|
+
className="h-7"
|
|
1140
|
+
value={el.chart?.xAxisName ?? ""}
|
|
1141
|
+
onChange={(e) =>
|
|
1142
|
+
updateElement(el.id, {
|
|
1143
|
+
chart: { ...(el.chart ?? {}), xAxisName: e.target.value },
|
|
1144
|
+
})
|
|
1145
|
+
}
|
|
1146
|
+
/>
|
|
1147
|
+
</label>
|
|
1148
|
+
<label className="block space-y-1">
|
|
1149
|
+
<div>Y 轴名称</div>
|
|
1150
|
+
<Input
|
|
1151
|
+
className="h-7"
|
|
1152
|
+
value={el.chart?.yAxisName ?? ""}
|
|
1153
|
+
onChange={(e) =>
|
|
1154
|
+
updateElement(el.id, {
|
|
1155
|
+
chart: { ...(el.chart ?? {}), yAxisName: e.target.value },
|
|
1156
|
+
})
|
|
1157
|
+
}
|
|
1158
|
+
/>
|
|
1159
|
+
</label>
|
|
1160
|
+
<label className="block space-y-1">
|
|
1161
|
+
<div>X 轴标签颜色</div>
|
|
1162
|
+
<Input
|
|
1163
|
+
className="h-7"
|
|
1164
|
+
value={el.chart?.xAxisLabelColor ?? ""}
|
|
1165
|
+
placeholder="#64748b"
|
|
1166
|
+
onChange={(e) =>
|
|
1167
|
+
updateElement(el.id, {
|
|
1168
|
+
chart: { ...(el.chart ?? {}), xAxisLabelColor: e.target.value || undefined },
|
|
1169
|
+
})
|
|
1170
|
+
}
|
|
1171
|
+
/>
|
|
1172
|
+
</label>
|
|
1173
|
+
<label className="block space-y-1">
|
|
1174
|
+
<div>Y 轴标签颜色</div>
|
|
1175
|
+
<Input
|
|
1176
|
+
className="h-7"
|
|
1177
|
+
value={el.chart?.yAxisLabelColor ?? ""}
|
|
1178
|
+
placeholder="#64748b"
|
|
1179
|
+
onChange={(e) =>
|
|
1180
|
+
updateElement(el.id, {
|
|
1181
|
+
chart: { ...(el.chart ?? {}), yAxisLabelColor: e.target.value || undefined },
|
|
1182
|
+
})
|
|
1183
|
+
}
|
|
1184
|
+
/>
|
|
1185
|
+
</label>
|
|
1186
|
+
<label className="block space-y-1">
|
|
1187
|
+
<div>X 轴标签字号</div>
|
|
1188
|
+
<Input
|
|
1189
|
+
className="h-7"
|
|
1190
|
+
type="number"
|
|
1191
|
+
min={8}
|
|
1192
|
+
max={48}
|
|
1193
|
+
value={el.chart?.xAxisLabelFontSize ?? 10}
|
|
1194
|
+
onChange={(e) =>
|
|
1195
|
+
updateElement(el.id, {
|
|
1196
|
+
chart: {
|
|
1197
|
+
...(el.chart ?? {}),
|
|
1198
|
+
xAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
|
|
1199
|
+
},
|
|
1200
|
+
})
|
|
1201
|
+
}
|
|
1202
|
+
/>
|
|
1203
|
+
</label>
|
|
1204
|
+
<label className="block space-y-1">
|
|
1205
|
+
<div>Y 轴标签字号</div>
|
|
1206
|
+
<Input
|
|
1207
|
+
className="h-7"
|
|
1208
|
+
type="number"
|
|
1209
|
+
min={8}
|
|
1210
|
+
max={48}
|
|
1211
|
+
value={el.chart?.yAxisLabelFontSize ?? 10}
|
|
1212
|
+
onChange={(e) =>
|
|
1213
|
+
updateElement(el.id, {
|
|
1214
|
+
chart: {
|
|
1215
|
+
...(el.chart ?? {}),
|
|
1216
|
+
yAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
|
|
1217
|
+
},
|
|
1218
|
+
})
|
|
1219
|
+
}
|
|
1220
|
+
/>
|
|
1221
|
+
</label>
|
|
1222
|
+
<label className="flex items-center gap-2">
|
|
1223
|
+
<Checkbox
|
|
1224
|
+
checked={el.chart?.xAxisLabelAutoEllipsis ?? false}
|
|
1225
|
+
onCheckedChange={(checked) =>
|
|
1226
|
+
updateElement(el.id, {
|
|
1227
|
+
chart: { ...(el.chart ?? {}), xAxisLabelAutoEllipsis: checked === true },
|
|
1228
|
+
})
|
|
1229
|
+
}
|
|
1230
|
+
/>
|
|
1231
|
+
<span>X 轴标签自动缩略</span>
|
|
1232
|
+
</label>
|
|
1233
|
+
<label className="flex items-center gap-2">
|
|
1234
|
+
<Checkbox
|
|
1235
|
+
checked={el.chart?.yAxisLabelAutoEllipsis ?? false}
|
|
1236
|
+
onCheckedChange={(checked) =>
|
|
1237
|
+
updateElement(el.id, {
|
|
1238
|
+
chart: { ...(el.chart ?? {}), yAxisLabelAutoEllipsis: checked === true },
|
|
1239
|
+
})
|
|
1240
|
+
}
|
|
1241
|
+
/>
|
|
1242
|
+
<span>Y 轴标签自动缩略</span>
|
|
1243
|
+
</label>
|
|
1244
|
+
<label className="flex items-center gap-2">
|
|
1245
|
+
<Checkbox
|
|
1246
|
+
checked={el.chart?.xAxisTickShow ?? true}
|
|
1247
|
+
onCheckedChange={(checked) =>
|
|
1248
|
+
updateElement(el.id, {
|
|
1249
|
+
chart: { ...(el.chart ?? {}), xAxisTickShow: checked === true },
|
|
1250
|
+
})
|
|
1251
|
+
}
|
|
1252
|
+
/>
|
|
1253
|
+
<span>X 轴刻度线</span>
|
|
1254
|
+
</label>
|
|
1255
|
+
<label className="flex items-center gap-2">
|
|
1256
|
+
<Checkbox
|
|
1257
|
+
checked={el.chart?.yAxisTickShow ?? true}
|
|
1258
|
+
onCheckedChange={(checked) =>
|
|
1259
|
+
updateElement(el.id, {
|
|
1260
|
+
chart: { ...(el.chart ?? {}), yAxisTickShow: checked === true },
|
|
1261
|
+
})
|
|
1262
|
+
}
|
|
1263
|
+
/>
|
|
1264
|
+
<span>Y 轴刻度线</span>
|
|
1265
|
+
</label>
|
|
1266
|
+
<label className="block space-y-1">
|
|
1267
|
+
<div>X 轴刻度线颜色</div>
|
|
1268
|
+
<Input
|
|
1269
|
+
className="h-7"
|
|
1270
|
+
value={el.chart?.xAxisTickColor ?? ""}
|
|
1271
|
+
placeholder="#94a3b8"
|
|
1272
|
+
onChange={(e) =>
|
|
1273
|
+
updateElement(el.id, {
|
|
1274
|
+
chart: { ...(el.chart ?? {}), xAxisTickColor: e.target.value || undefined },
|
|
1275
|
+
})
|
|
1276
|
+
}
|
|
1277
|
+
/>
|
|
1278
|
+
</label>
|
|
1279
|
+
<label className="block space-y-1">
|
|
1280
|
+
<div>Y 轴刻度线颜色</div>
|
|
1281
|
+
<Input
|
|
1282
|
+
className="h-7"
|
|
1283
|
+
value={el.chart?.yAxisTickColor ?? ""}
|
|
1284
|
+
placeholder="#94a3b8"
|
|
1285
|
+
onChange={(e) =>
|
|
1286
|
+
updateElement(el.id, {
|
|
1287
|
+
chart: { ...(el.chart ?? {}), yAxisTickColor: e.target.value || undefined },
|
|
1288
|
+
})
|
|
1289
|
+
}
|
|
1290
|
+
/>
|
|
1291
|
+
</label>
|
|
1292
|
+
<label className="flex items-center gap-2">
|
|
1293
|
+
<Checkbox
|
|
1294
|
+
checked={el.chart?.xAxisSplitLineShow ?? false}
|
|
1295
|
+
onCheckedChange={(checked) =>
|
|
1296
|
+
updateElement(el.id, {
|
|
1297
|
+
chart: { ...(el.chart ?? {}), xAxisSplitLineShow: checked === true },
|
|
1298
|
+
})
|
|
1299
|
+
}
|
|
1300
|
+
/>
|
|
1301
|
+
<span>X 轴分割线</span>
|
|
1302
|
+
</label>
|
|
1303
|
+
<label className="flex items-center gap-2">
|
|
1304
|
+
<Checkbox
|
|
1305
|
+
checked={el.chart?.yAxisSplitLineShow ?? true}
|
|
1306
|
+
onCheckedChange={(checked) =>
|
|
1307
|
+
updateElement(el.id, {
|
|
1308
|
+
chart: { ...(el.chart ?? {}), yAxisSplitLineShow: checked === true },
|
|
1309
|
+
})
|
|
1310
|
+
}
|
|
1311
|
+
/>
|
|
1312
|
+
<span>Y 轴分割线</span>
|
|
1313
|
+
</label>
|
|
1314
|
+
<label className="block space-y-1">
|
|
1315
|
+
<div>X 轴分割线颜色</div>
|
|
1316
|
+
<Input
|
|
1317
|
+
className="h-7"
|
|
1318
|
+
value={el.chart?.xAxisSplitLineColor ?? ""}
|
|
1319
|
+
placeholder="#e2e8f0"
|
|
1320
|
+
onChange={(e) =>
|
|
1321
|
+
updateElement(el.id, {
|
|
1322
|
+
chart: { ...(el.chart ?? {}), xAxisSplitLineColor: e.target.value || undefined },
|
|
1323
|
+
})
|
|
1324
|
+
}
|
|
1325
|
+
/>
|
|
1326
|
+
</label>
|
|
1327
|
+
<label className="block space-y-1">
|
|
1328
|
+
<div>Y 轴分割线颜色</div>
|
|
1329
|
+
<Input
|
|
1330
|
+
className="h-7"
|
|
1331
|
+
value={el.chart?.yAxisSplitLineColor ?? ""}
|
|
1332
|
+
placeholder="#e2e8f0"
|
|
1333
|
+
onChange={(e) =>
|
|
1334
|
+
updateElement(el.id, {
|
|
1335
|
+
chart: { ...(el.chart ?? {}), yAxisSplitLineColor: e.target.value || undefined },
|
|
1336
|
+
})
|
|
1337
|
+
}
|
|
1338
|
+
/>
|
|
1339
|
+
</label>
|
|
1340
|
+
</>
|
|
1341
|
+
) : null}
|
|
1342
|
+
{el.materialType === "gauge" ? (
|
|
1343
|
+
<label className="block space-y-1">
|
|
1344
|
+
<div>仪表盘值</div>
|
|
1345
|
+
<Input
|
|
1346
|
+
className="h-7"
|
|
1347
|
+
type="number"
|
|
1348
|
+
value={el.chart?.values?.[0] ?? 0}
|
|
1349
|
+
onChange={(e) =>
|
|
1350
|
+
updateElement(el.id, {
|
|
1351
|
+
chart: {
|
|
1352
|
+
...(el.chart ?? {}),
|
|
1353
|
+
values: [Number(e.target.value) || 0],
|
|
1354
|
+
},
|
|
1355
|
+
})
|
|
1356
|
+
}
|
|
1357
|
+
/>
|
|
1358
|
+
</label>
|
|
1359
|
+
) : null}
|
|
1360
|
+
{el.materialType === "bar" ? (
|
|
1361
|
+
<label className="block space-y-1">
|
|
1362
|
+
<div>柱宽</div>
|
|
1363
|
+
<Input
|
|
1364
|
+
className="h-7"
|
|
1365
|
+
type="number"
|
|
1366
|
+
min={1}
|
|
1367
|
+
value={el.chart?.barWidth ?? 24}
|
|
1368
|
+
onChange={(e) =>
|
|
1369
|
+
updateElement(el.id, {
|
|
1370
|
+
chart: {
|
|
1371
|
+
...(el.chart ?? {}),
|
|
1372
|
+
barWidth: Math.max(1, Number(e.target.value) || 1),
|
|
1373
|
+
},
|
|
1374
|
+
})
|
|
1375
|
+
}
|
|
1376
|
+
/>
|
|
1377
|
+
</label>
|
|
1378
|
+
) : null}
|
|
1379
|
+
{(el.materialType === "line" || el.materialType === "area") ? (
|
|
1380
|
+
<label className="flex items-center gap-2">
|
|
1381
|
+
<Checkbox
|
|
1382
|
+
checked={el.chart?.smooth ?? true}
|
|
1383
|
+
onCheckedChange={(checked) =>
|
|
1384
|
+
updateElement(el.id, {
|
|
1385
|
+
chart: { ...(el.chart ?? {}), smooth: checked === true },
|
|
1386
|
+
})
|
|
1387
|
+
}
|
|
1388
|
+
/>
|
|
1389
|
+
<span>平滑曲线</span>
|
|
1390
|
+
</label>
|
|
1391
|
+
) : null}
|
|
1392
|
+
{el.materialType === "pie" ? (
|
|
1393
|
+
<>
|
|
1394
|
+
<label className="block space-y-1">
|
|
1395
|
+
<div>内半径</div>
|
|
1396
|
+
<Input
|
|
1397
|
+
className="h-7"
|
|
1398
|
+
type="number"
|
|
1399
|
+
min={0}
|
|
1400
|
+
max={99}
|
|
1401
|
+
value={el.chart?.pieInnerRadius ?? 30}
|
|
1402
|
+
onChange={(e) =>
|
|
1403
|
+
updateElement(el.id, {
|
|
1404
|
+
chart: {
|
|
1405
|
+
...(el.chart ?? {}),
|
|
1406
|
+
pieInnerRadius: Math.max(0, Math.min(99, Number(e.target.value) || 0)),
|
|
1407
|
+
},
|
|
1408
|
+
})
|
|
1409
|
+
}
|
|
1410
|
+
/>
|
|
1411
|
+
</label>
|
|
1412
|
+
<label className="block space-y-1">
|
|
1413
|
+
<div>外半径</div>
|
|
1414
|
+
<Input
|
|
1415
|
+
className="h-7"
|
|
1416
|
+
type="number"
|
|
1417
|
+
min={1}
|
|
1418
|
+
max={100}
|
|
1419
|
+
value={el.chart?.pieOuterRadius ?? 65}
|
|
1420
|
+
onChange={(e) =>
|
|
1421
|
+
updateElement(el.id, {
|
|
1422
|
+
chart: {
|
|
1423
|
+
...(el.chart ?? {}),
|
|
1424
|
+
pieOuterRadius: Math.max(1, Math.min(100, Number(e.target.value) || 1)),
|
|
1425
|
+
},
|
|
1426
|
+
})
|
|
1427
|
+
}
|
|
1428
|
+
/>
|
|
1429
|
+
</label>
|
|
1430
|
+
</>
|
|
1431
|
+
) : null}
|
|
1432
|
+
<label className="block space-y-1">
|
|
1433
|
+
{renderOptionLabel("网格左边距", "grid.left", "控制绘图区左侧留白。")}
|
|
1434
|
+
<Input
|
|
1435
|
+
className="h-7"
|
|
1436
|
+
type="number"
|
|
1437
|
+
value={Number((el.chart?.option as any)?.grid?.left ?? 28)}
|
|
1438
|
+
onChange={(e) =>
|
|
1439
|
+
updateElement(el.id, {
|
|
1440
|
+
chart: {
|
|
1441
|
+
...(el.chart ?? {}),
|
|
1442
|
+
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1443
|
+
grid: { left: Number(e.target.value) || 0 },
|
|
1444
|
+
}),
|
|
1445
|
+
},
|
|
1446
|
+
})
|
|
1447
|
+
}
|
|
1448
|
+
/>
|
|
1449
|
+
</label>
|
|
1450
|
+
<label className="block space-y-1">
|
|
1451
|
+
{renderOptionLabel("网格右边距", "grid.right", "控制绘图区右侧留白。")}
|
|
1452
|
+
<Input
|
|
1453
|
+
className="h-7"
|
|
1454
|
+
type="number"
|
|
1455
|
+
value={Number((el.chart?.option as any)?.grid?.right ?? 10)}
|
|
1456
|
+
onChange={(e) =>
|
|
1457
|
+
updateElement(el.id, {
|
|
1458
|
+
chart: {
|
|
1459
|
+
...(el.chart ?? {}),
|
|
1460
|
+
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1461
|
+
grid: { right: Number(e.target.value) || 0 },
|
|
1462
|
+
}),
|
|
1463
|
+
},
|
|
1464
|
+
})
|
|
1465
|
+
}
|
|
1466
|
+
/>
|
|
1467
|
+
</label>
|
|
1468
|
+
<label className="block space-y-1">
|
|
1469
|
+
{renderOptionLabel("网格上边距", "grid.top", "控制绘图区顶部留白。")}
|
|
1470
|
+
<Input
|
|
1471
|
+
className="h-7"
|
|
1472
|
+
type="number"
|
|
1473
|
+
value={Number((el.chart?.option as any)?.grid?.top ?? 30)}
|
|
1474
|
+
onChange={(e) =>
|
|
1475
|
+
updateElement(el.id, {
|
|
1476
|
+
chart: {
|
|
1477
|
+
...(el.chart ?? {}),
|
|
1478
|
+
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1479
|
+
grid: { top: Number(e.target.value) || 0 },
|
|
1480
|
+
}),
|
|
1481
|
+
},
|
|
1482
|
+
})
|
|
1483
|
+
}
|
|
1484
|
+
/>
|
|
1485
|
+
</label>
|
|
1486
|
+
<label className="block space-y-1">
|
|
1487
|
+
{renderOptionLabel("网格下边距", "grid.bottom", "控制绘图区底部留白。")}
|
|
1488
|
+
<Input
|
|
1489
|
+
className="h-7"
|
|
1490
|
+
type="number"
|
|
1491
|
+
value={Number((el.chart?.option as any)?.grid?.bottom ?? 20)}
|
|
1492
|
+
onChange={(e) =>
|
|
1493
|
+
updateElement(el.id, {
|
|
1494
|
+
chart: {
|
|
1495
|
+
...(el.chart ?? {}),
|
|
1496
|
+
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1497
|
+
grid: { bottom: Number(e.target.value) || 0 },
|
|
1498
|
+
}),
|
|
1499
|
+
},
|
|
1500
|
+
})
|
|
1501
|
+
}
|
|
1502
|
+
/>
|
|
1503
|
+
</label>
|
|
1504
|
+
<label className="block space-y-1">
|
|
1505
|
+
{renderOptionLabel("图例位置", "legend.top", "设置图例在容器中的停靠位置。")}
|
|
1506
|
+
<Select
|
|
1507
|
+
value={String((el.chart?.option as any)?.legend?.top ?? "top")}
|
|
1508
|
+
onValueChange={(value) =>
|
|
1509
|
+
updateElement(el.id, {
|
|
1510
|
+
chart: {
|
|
1511
|
+
...(el.chart ?? {}),
|
|
1512
|
+
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1513
|
+
legend: { top: value },
|
|
1514
|
+
}),
|
|
1515
|
+
},
|
|
1516
|
+
})
|
|
1517
|
+
}
|
|
1518
|
+
>
|
|
1519
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1520
|
+
<SelectContent>
|
|
1521
|
+
<SelectItem value="top">top</SelectItem>
|
|
1522
|
+
<SelectItem value="bottom">bottom</SelectItem>
|
|
1523
|
+
<SelectItem value="left">left</SelectItem>
|
|
1524
|
+
<SelectItem value="right">right</SelectItem>
|
|
1525
|
+
</SelectContent>
|
|
1526
|
+
</Select>
|
|
1527
|
+
</label>
|
|
1528
|
+
<label className="block space-y-1">
|
|
1529
|
+
{renderOptionLabel("图例排列", "legend.orient", "图例横向或纵向排列。")}
|
|
1530
|
+
<Select
|
|
1531
|
+
value={String((el.chart?.option as any)?.legend?.orient ?? "horizontal")}
|
|
1532
|
+
onValueChange={(value) =>
|
|
1533
|
+
updateElement(el.id, {
|
|
1534
|
+
chart: {
|
|
1535
|
+
...(el.chart ?? {}),
|
|
1536
|
+
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1537
|
+
legend: { orient: value },
|
|
1538
|
+
}),
|
|
1539
|
+
},
|
|
1540
|
+
})
|
|
1541
|
+
}
|
|
1542
|
+
>
|
|
1543
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1544
|
+
<SelectContent>
|
|
1545
|
+
<SelectItem value="horizontal">horizontal</SelectItem>
|
|
1546
|
+
<SelectItem value="vertical">vertical</SelectItem>
|
|
1547
|
+
</SelectContent>
|
|
1548
|
+
</Select>
|
|
1549
|
+
</label>
|
|
1550
|
+
<label className="flex items-center gap-2">
|
|
1551
|
+
<Checkbox
|
|
1552
|
+
checked={Boolean((el.chart?.option as any)?.legend?.show ?? true)}
|
|
1553
|
+
onCheckedChange={(checked) =>
|
|
1554
|
+
updateElement(el.id, {
|
|
1555
|
+
chart: {
|
|
1556
|
+
...(el.chart ?? {}),
|
|
1557
|
+
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1558
|
+
legend: { show: checked === true },
|
|
1559
|
+
}),
|
|
1560
|
+
},
|
|
1561
|
+
})
|
|
1562
|
+
}
|
|
1563
|
+
/>
|
|
1564
|
+
<span>显示图例(legend.show)</span>
|
|
1565
|
+
</label>
|
|
1566
|
+
<label className="flex items-center gap-2">
|
|
1567
|
+
<Checkbox
|
|
1568
|
+
checked={Boolean(
|
|
1569
|
+
Array.isArray((el.chart?.option as any)?.dataZoom) &&
|
|
1570
|
+
(el.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "inside")
|
|
1571
|
+
)}
|
|
1572
|
+
onCheckedChange={(checked) => {
|
|
1573
|
+
const prev = Array.isArray((el.chart?.option as any)?.dataZoom)
|
|
1574
|
+
? [...(el.chart?.option as any).dataZoom]
|
|
1575
|
+
: [];
|
|
1576
|
+
const next = checked
|
|
1577
|
+
? [...prev.filter((z: any) => z?.type !== "inside"), { type: "inside" }]
|
|
1578
|
+
: prev.filter((z: any) => z?.type !== "inside");
|
|
1579
|
+
updateElement(el.id, {
|
|
1580
|
+
chart: {
|
|
1581
|
+
...(el.chart ?? {}),
|
|
1582
|
+
option: mergeOptionPatch(el.chart?.option as any, { dataZoom: next }),
|
|
1583
|
+
},
|
|
1584
|
+
});
|
|
1585
|
+
}}
|
|
1586
|
+
/>
|
|
1587
|
+
<span>内置缩放(dataZoom[type=inside])</span>
|
|
1588
|
+
</label>
|
|
1589
|
+
<label className="flex items-center gap-2">
|
|
1590
|
+
<Checkbox
|
|
1591
|
+
checked={Boolean(
|
|
1592
|
+
Array.isArray((el.chart?.option as any)?.dataZoom) &&
|
|
1593
|
+
(el.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "slider")
|
|
1594
|
+
)}
|
|
1595
|
+
onCheckedChange={(checked) => {
|
|
1596
|
+
const prev = Array.isArray((el.chart?.option as any)?.dataZoom)
|
|
1597
|
+
? [...(el.chart?.option as any).dataZoom]
|
|
1598
|
+
: [];
|
|
1599
|
+
const next = checked
|
|
1600
|
+
? [...prev.filter((z: any) => z?.type !== "slider"), { type: "slider" }]
|
|
1601
|
+
: prev.filter((z: any) => z?.type !== "slider");
|
|
1602
|
+
updateElement(el.id, {
|
|
1603
|
+
chart: {
|
|
1604
|
+
...(el.chart ?? {}),
|
|
1605
|
+
option: mergeOptionPatch(el.chart?.option as any, { dataZoom: next }),
|
|
1606
|
+
},
|
|
1607
|
+
});
|
|
1608
|
+
}}
|
|
1609
|
+
/>
|
|
1610
|
+
<span>滑块缩放(dataZoom[type=slider])</span>
|
|
1611
|
+
</label>
|
|
1612
|
+
<label className="flex items-center gap-2">
|
|
1613
|
+
<Checkbox
|
|
1614
|
+
checked={Boolean((el.chart?.option as any)?.axisPointer?.show ?? false)}
|
|
1615
|
+
onCheckedChange={(checked) =>
|
|
1616
|
+
updateElement(el.id, {
|
|
1617
|
+
chart: {
|
|
1618
|
+
...(el.chart ?? {}),
|
|
1619
|
+
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1620
|
+
axisPointer: { show: checked === true },
|
|
1621
|
+
}),
|
|
1622
|
+
},
|
|
1623
|
+
})
|
|
1624
|
+
}
|
|
1625
|
+
/>
|
|
1626
|
+
<span>显示轴指示器(axisPointer.show)</span>
|
|
1627
|
+
</label>
|
|
1628
|
+
<label className="block space-y-1">
|
|
1629
|
+
<div>轴指示器类型(axisPointer.type)</div>
|
|
1630
|
+
<Select
|
|
1631
|
+
value={String((el.chart?.option as any)?.axisPointer?.type ?? "line")}
|
|
1632
|
+
onValueChange={(value) =>
|
|
1633
|
+
updateElement(el.id, {
|
|
1634
|
+
chart: {
|
|
1635
|
+
...(el.chart ?? {}),
|
|
1636
|
+
option: mergeOptionPatch(el.chart?.option as any, {
|
|
1637
|
+
axisPointer: { type: value },
|
|
1638
|
+
}),
|
|
1639
|
+
},
|
|
1640
|
+
})
|
|
1641
|
+
}
|
|
1642
|
+
>
|
|
1643
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1644
|
+
<SelectContent>
|
|
1645
|
+
<SelectItem value="line">line</SelectItem>
|
|
1646
|
+
<SelectItem value="shadow">shadow</SelectItem>
|
|
1647
|
+
<SelectItem value="cross">cross</SelectItem>
|
|
1648
|
+
</SelectContent>
|
|
1649
|
+
</Select>
|
|
1650
|
+
</label>
|
|
1651
|
+
<label className="block space-y-1 col-span-2">
|
|
1652
|
+
<div>高级 option JSON(覆盖基础配置)</div>
|
|
1653
|
+
<Textarea
|
|
1654
|
+
className="h-28 font-mono text-[11px]"
|
|
1655
|
+
defaultValue={JSON.stringify(el.chart?.option ?? {}, null, 2)}
|
|
1656
|
+
placeholder='例如:{"grid":{"left":24}}'
|
|
1657
|
+
onBlur={(e) => {
|
|
1658
|
+
const nextText = e.target.value.trim();
|
|
1659
|
+
if (!nextText) {
|
|
1660
|
+
updateElement(el.id, { chart: { ...(el.chart ?? {}), option: undefined } });
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
try {
|
|
1664
|
+
const parsed = JSON.parse(nextText) as Record<string, unknown>;
|
|
1665
|
+
updateElement(el.id, { chart: { ...(el.chart ?? {}), option: parsed } });
|
|
1666
|
+
} catch {
|
|
1667
|
+
// ignore invalid json input on blur
|
|
1668
|
+
}
|
|
1669
|
+
}}
|
|
1670
|
+
/>
|
|
1671
|
+
</label>
|
|
1672
|
+
</div>
|
|
1673
|
+
) : null}
|
|
1674
|
+
{el.materialType === "text" ? (
|
|
1675
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1676
|
+
<label className="block space-y-1 col-span-2">
|
|
1677
|
+
<div>文本内容(HTML)</div>
|
|
1678
|
+
<Textarea
|
|
1679
|
+
className="h-24"
|
|
1680
|
+
value={el.textHtml ?? ""}
|
|
1681
|
+
onChange={(e) => updateElement(el.id, { textHtml: e.target.value || "<p><br/></p>" })}
|
|
1682
|
+
/>
|
|
1683
|
+
</label>
|
|
1684
|
+
<label className="block space-y-1">
|
|
1685
|
+
<div>字体大小</div>
|
|
1686
|
+
<Input
|
|
1687
|
+
className="h-7"
|
|
1688
|
+
type="number"
|
|
1689
|
+
min={8}
|
|
1690
|
+
value={el.textFontSize ?? 14}
|
|
1691
|
+
onChange={(e) => updateElement(el.id, { textFontSize: Math.max(8, Number(e.target.value) || 14) })}
|
|
1692
|
+
/>
|
|
1693
|
+
</label>
|
|
1694
|
+
<label className="block space-y-1">
|
|
1695
|
+
<div>文字颜色</div>
|
|
1696
|
+
<Input
|
|
1697
|
+
className="h-7"
|
|
1698
|
+
value={el.textColor ?? ""}
|
|
1699
|
+
onChange={(e) => updateElement(el.id, { textColor: e.target.value || undefined })}
|
|
1700
|
+
/>
|
|
1701
|
+
</label>
|
|
1702
|
+
</div>
|
|
1703
|
+
) : null}
|
|
1704
|
+
{el.materialType === "audio" ? (
|
|
1705
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1706
|
+
<label className="block space-y-1 col-span-2">
|
|
1707
|
+
<div>音频 URL</div>
|
|
1708
|
+
<Input
|
|
1709
|
+
className="h-7"
|
|
1710
|
+
value={el.audioRemoteUrl ?? ""}
|
|
1711
|
+
onChange={(e) => updateElement(el.id, { audioRemoteUrl: e.target.value || undefined })}
|
|
1712
|
+
/>
|
|
1713
|
+
</label>
|
|
1714
|
+
<label className="block space-y-1">
|
|
1715
|
+
<div>动效</div>
|
|
1716
|
+
<Select
|
|
1717
|
+
value={el.audioVisualEffect ?? "pulse"}
|
|
1718
|
+
onValueChange={(value) =>
|
|
1719
|
+
updateElement(el.id, { audioVisualEffect: value as "none" | "pulse" | "ripple" })
|
|
1720
|
+
}
|
|
1721
|
+
>
|
|
1722
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1723
|
+
<SelectContent>
|
|
1724
|
+
<SelectItem value="none">none</SelectItem>
|
|
1725
|
+
<SelectItem value="pulse">pulse</SelectItem>
|
|
1726
|
+
<SelectItem value="ripple">ripple</SelectItem>
|
|
1727
|
+
</SelectContent>
|
|
1728
|
+
</Select>
|
|
1729
|
+
</label>
|
|
1730
|
+
<label className="block space-y-1">
|
|
1731
|
+
<div>速度</div>
|
|
1732
|
+
<Select
|
|
1733
|
+
value={el.audioVisualSpeed ?? "normal"}
|
|
1734
|
+
onValueChange={(value) =>
|
|
1735
|
+
updateElement(el.id, { audioVisualSpeed: value as "slow" | "normal" | "fast" })
|
|
1736
|
+
}
|
|
1737
|
+
>
|
|
1738
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1739
|
+
<SelectContent>
|
|
1740
|
+
<SelectItem value="slow">slow</SelectItem>
|
|
1741
|
+
<SelectItem value="normal">normal</SelectItem>
|
|
1742
|
+
<SelectItem value="fast">fast</SelectItem>
|
|
1743
|
+
</SelectContent>
|
|
1744
|
+
</Select>
|
|
1745
|
+
</label>
|
|
1746
|
+
</div>
|
|
1747
|
+
) : null}
|
|
1748
|
+
{el.materialType === "video" ? (
|
|
1749
|
+
<label className="block space-y-1">
|
|
1750
|
+
<div>视频 URL</div>
|
|
1751
|
+
<Input
|
|
1752
|
+
className="h-7"
|
|
1753
|
+
value={el.videoRemoteUrl ?? ""}
|
|
1754
|
+
onChange={(e) => updateElement(el.id, { videoRemoteUrl: e.target.value || undefined })}
|
|
1755
|
+
/>
|
|
1756
|
+
</label>
|
|
1757
|
+
) : null}
|
|
1758
|
+
{el.materialType === "grid" ? (
|
|
1759
|
+
<div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
|
|
1760
|
+
<label className="block space-y-1.5">
|
|
1761
|
+
<div>行</div>
|
|
1762
|
+
<Input className="h-7" type="number" min={1} value={el.gridRows ?? 2} onChange={(e) => updateElement(el.id, { gridRows: Math.max(1, Number(e.target.value) || 2) })} />
|
|
1763
|
+
</label>
|
|
1764
|
+
<label className="block space-y-1.5">
|
|
1765
|
+
<div>列</div>
|
|
1766
|
+
<Input className="h-7" type="number" min={1} value={el.gridCols ?? 3} onChange={(e) => updateElement(el.id, { gridCols: Math.max(1, Number(e.target.value) || 3) })} />
|
|
1767
|
+
</label>
|
|
1768
|
+
</div>
|
|
1769
|
+
) : null}
|
|
1770
|
+
{el.materialType === "geometry" ? (
|
|
1771
|
+
<div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
|
|
1772
|
+
<label className="block space-y-1.5">
|
|
1773
|
+
<div>形状</div>
|
|
1774
|
+
<Select
|
|
1775
|
+
value={el.geometryShape ?? "rect"}
|
|
1776
|
+
onValueChange={(value) =>
|
|
1777
|
+
updateElement(el.id, { geometryShape: value as PanelElement["geometryShape"] })
|
|
1778
|
+
}
|
|
1779
|
+
>
|
|
1780
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1781
|
+
<SelectContent>
|
|
1782
|
+
<SelectItem value="rect">矩形</SelectItem>
|
|
1783
|
+
<SelectItem value="circle">圆形</SelectItem>
|
|
1784
|
+
<SelectItem value="triangle">三角形</SelectItem>
|
|
1785
|
+
<SelectItem value="diamond">菱形</SelectItem>
|
|
1786
|
+
<SelectItem value="hexagon">六边形</SelectItem>
|
|
1787
|
+
<SelectItem value="star">星形</SelectItem>
|
|
1788
|
+
<SelectItem value="heart">爱心</SelectItem>
|
|
1789
|
+
</SelectContent>
|
|
1790
|
+
</Select>
|
|
1791
|
+
</label>
|
|
1792
|
+
<label className="block space-y-1.5">
|
|
1793
|
+
<div>颜色</div>
|
|
1794
|
+
<Input
|
|
1795
|
+
className="h-7"
|
|
1796
|
+
value={el.geometryColor ?? "#3b82f6"}
|
|
1797
|
+
onChange={(e) => updateElement(el.id, { geometryColor: e.target.value || "#3b82f6" })}
|
|
1798
|
+
/>
|
|
1799
|
+
</label>
|
|
1800
|
+
</div>
|
|
1801
|
+
) : null}
|
|
1802
|
+
{el.materialType === "reference" ? (
|
|
1803
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1804
|
+
<label className="block space-y-1.5">
|
|
1805
|
+
<div>引用图层</div>
|
|
1806
|
+
<Select
|
|
1807
|
+
value={el.refLayerId ?? "__none__"}
|
|
1808
|
+
onValueChange={(value) => updateElement(el.id, { refLayerId: value === "__none__" ? undefined : value })}
|
|
1809
|
+
>
|
|
1810
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1811
|
+
<SelectContent>
|
|
1812
|
+
<SelectItem value="__none__">无</SelectItem>
|
|
1813
|
+
{layers.filter((l) => l.id !== el.layerId).map((l) => (
|
|
1814
|
+
<SelectItem key={l.id} value={l.id}>{l.name}</SelectItem>
|
|
1815
|
+
))}
|
|
1816
|
+
</SelectContent>
|
|
1817
|
+
</Select>
|
|
1818
|
+
</label>
|
|
1819
|
+
<label className="block space-y-1.5">
|
|
1820
|
+
<div>拷贝</div>
|
|
1821
|
+
<Select
|
|
1822
|
+
value={el.refCopyMode ?? "shallow"}
|
|
1823
|
+
onValueChange={(value) => setReferenceCopyMode?.(el.id, value as ReferenceCopyMode)}
|
|
1824
|
+
>
|
|
1825
|
+
<SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
|
|
1826
|
+
<SelectContent>
|
|
1827
|
+
<SelectItem value="shallow">shallow</SelectItem>
|
|
1828
|
+
<SelectItem value="deep">deep</SelectItem>
|
|
1829
|
+
</SelectContent>
|
|
1830
|
+
</Select>
|
|
1831
|
+
</label>
|
|
1832
|
+
</div>
|
|
1833
|
+
) : null}
|
|
1834
|
+
</div>
|
|
1835
|
+
</fieldset>
|
|
1836
|
+
</CardContent>
|
|
1837
|
+
) : null}
|
|
1838
|
+
</Card>
|
|
1839
|
+
))}
|
|
1840
|
+
{hasSearch &&
|
|
1841
|
+
effectiveSelectedElements.every((el) => {
|
|
1842
|
+
const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`
|
|
1843
|
+
.toLowerCase();
|
|
1844
|
+
return !text.includes(normalizedSearch);
|
|
1845
|
+
}) ? (
|
|
1846
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
1847
|
+
未匹配到可编辑节点,请更换关键词。
|
|
1848
|
+
</div>
|
|
1849
|
+
) : null}
|
|
1850
|
+
</div>
|
|
1851
|
+
) : null}
|
|
1852
|
+
{!isMultiSelectMode && !selectedElement ? (
|
|
1853
|
+
<Empty className="py-7">
|
|
1854
|
+
<EmptyIcon>
|
|
1855
|
+
<svg
|
|
1856
|
+
viewBox="0 0 24 24"
|
|
1857
|
+
className="h-4 w-4"
|
|
1858
|
+
fill="none"
|
|
1859
|
+
stroke="currentColor"
|
|
1860
|
+
strokeWidth="1.8"
|
|
1861
|
+
aria-hidden="true"
|
|
1862
|
+
>
|
|
1863
|
+
<rect x="4" y="4" width="16" height="16" rx="2.5" />
|
|
1864
|
+
<path d="M8 9h8M8 12h8M8 15h5" />
|
|
1865
|
+
</svg>
|
|
1866
|
+
</EmptyIcon>
|
|
1867
|
+
<EmptyTitle>暂无可配置节点</EmptyTitle>
|
|
1868
|
+
<EmptyDescription>请先在画布中选中一个节点,再到这里进行配置。</EmptyDescription>
|
|
1869
|
+
</Empty>
|
|
1870
|
+
) : !isMultiSelectMode && selectedElement ? (
|
|
1871
|
+
<div className="space-y-3">
|
|
1872
|
+
{!isNodeEditable ? (
|
|
1873
|
+
<div className="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] text-amber-700 dark:text-amber-300">
|
|
1874
|
+
{readonlyReason}
|
|
1875
|
+
</div>
|
|
1876
|
+
) : null}
|
|
1877
|
+
<div className="rounded-lg border border-border/60 bg-card/80 px-2.5 py-2 text-xs">
|
|
1878
|
+
<label className="flex items-center gap-2">
|
|
1879
|
+
<Checkbox
|
|
1880
|
+
checked={selectedElement.locked === true}
|
|
1881
|
+
disabled={!canToggleNodeLock}
|
|
1882
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
1883
|
+
onCheckedChange={(checked) =>
|
|
1884
|
+
updateElement(selectedElement.id, {
|
|
1885
|
+
locked: checked === true,
|
|
1886
|
+
})
|
|
1887
|
+
}
|
|
1888
|
+
/>
|
|
1889
|
+
<span>锁定节点(禁止层级/位置/大小/旋转)</span>
|
|
1890
|
+
</label>
|
|
1891
|
+
</div>
|
|
1892
|
+
<fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
|
|
1893
|
+
<div className="space-y-3.5 text-xs">
|
|
1894
|
+
{renderSection(
|
|
1895
|
+
"nodeInfo",
|
|
1896
|
+
"节点信息",
|
|
1897
|
+
<>
|
|
1898
|
+
<label className="block space-y-1">
|
|
1899
|
+
<div>节点名称</div>
|
|
1900
|
+
<Input
|
|
1901
|
+
value={selectedElement.name ?? ""}
|
|
1902
|
+
onChange={(e) =>
|
|
1903
|
+
updateElement(selectedElement.id, {
|
|
1904
|
+
name: e.target.value || undefined,
|
|
1905
|
+
})
|
|
1906
|
+
}
|
|
1907
|
+
placeholder="自定义节点名称(显示在节点树)"
|
|
1908
|
+
className="h-7"
|
|
1909
|
+
/>
|
|
1910
|
+
</label>
|
|
1911
|
+
<div className="grid grid-cols-3 gap-2">
|
|
1912
|
+
<label className="block space-y-1">
|
|
1913
|
+
<div>X</div>
|
|
1914
|
+
<Input
|
|
1915
|
+
type="number"
|
|
1916
|
+
value={selectedElement.x}
|
|
1917
|
+
onChange={(e) =>
|
|
1918
|
+
updateElement(selectedElement.id, {
|
|
1919
|
+
x: Number(e.target.value) || 0,
|
|
1920
|
+
})
|
|
1921
|
+
}
|
|
1922
|
+
className="h-7"
|
|
1923
|
+
/>
|
|
1924
|
+
</label>
|
|
1925
|
+
<label className="block space-y-1">
|
|
1926
|
+
<div>Y</div>
|
|
1927
|
+
<Input
|
|
1928
|
+
type="number"
|
|
1929
|
+
value={selectedElement.y}
|
|
1930
|
+
onChange={(e) =>
|
|
1931
|
+
updateElement(selectedElement.id, {
|
|
1932
|
+
y: Number(e.target.value) || 0,
|
|
1933
|
+
})
|
|
1934
|
+
}
|
|
1935
|
+
className="h-7"
|
|
1936
|
+
/>
|
|
1937
|
+
</label>
|
|
1938
|
+
</div>
|
|
1939
|
+
<div className="grid grid-cols-3 gap-2">
|
|
1940
|
+
<label className="block space-y-1">
|
|
1941
|
+
<div>旋转角度</div>
|
|
1942
|
+
<Input
|
|
1943
|
+
type="number"
|
|
1944
|
+
value={selectedElement.rotate ?? 0}
|
|
1945
|
+
onChange={(e) =>
|
|
1946
|
+
updateElement(selectedElement.id, {
|
|
1947
|
+
rotate: Number(e.target.value) || 0,
|
|
1948
|
+
})
|
|
1949
|
+
}
|
|
1950
|
+
className="h-7"
|
|
1951
|
+
/>
|
|
1952
|
+
</label>
|
|
1953
|
+
<label className="block space-y-1">
|
|
1954
|
+
<div>宽</div>
|
|
1955
|
+
<Input
|
|
1956
|
+
type="number"
|
|
1957
|
+
min={1}
|
|
1958
|
+
value={selectedElement?.width ?? 1}
|
|
1959
|
+
onChange={(e) => {
|
|
1960
|
+
if (!selectedElement) return;
|
|
1961
|
+
updateElement(selectedElement.id, {
|
|
1962
|
+
width: Math.max(1, Number(e.target.value) || 1),
|
|
1963
|
+
});
|
|
1964
|
+
}}
|
|
1965
|
+
className="h-7"
|
|
1966
|
+
/>
|
|
1967
|
+
</label>
|
|
1968
|
+
<label className="block space-y-1">
|
|
1969
|
+
<div>高</div>
|
|
1970
|
+
<Input
|
|
1971
|
+
type="number"
|
|
1972
|
+
min={1}
|
|
1973
|
+
value={selectedElement.height}
|
|
1974
|
+
onChange={(e) =>
|
|
1975
|
+
updateElement(selectedElement.id, {
|
|
1976
|
+
height: Math.max(1, Number(e.target.value) || 1),
|
|
1977
|
+
})
|
|
1978
|
+
}
|
|
1979
|
+
className="h-7"
|
|
1980
|
+
/>
|
|
1981
|
+
</label>
|
|
1982
|
+
</div>
|
|
1983
|
+
<div className="space-y-1.5">
|
|
1984
|
+
<div className="text-[11px] text-muted-foreground">节点层级</div>
|
|
1985
|
+
<div className="text-[11px] text-muted-foreground/90">
|
|
1986
|
+
当前 zIndex:{nodeZOrderLabel ?? "-"}
|
|
1987
|
+
</div>
|
|
1988
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1989
|
+
<button
|
|
1990
|
+
type="button"
|
|
1991
|
+
className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
|
|
1992
|
+
onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringForward")}
|
|
1993
|
+
>
|
|
1994
|
+
上移一层
|
|
1995
|
+
</button>
|
|
1996
|
+
<button
|
|
1997
|
+
type="button"
|
|
1998
|
+
className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
|
|
1999
|
+
onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendBackward")}
|
|
2000
|
+
>
|
|
2001
|
+
下移一层
|
|
2002
|
+
</button>
|
|
2003
|
+
<button
|
|
2004
|
+
type="button"
|
|
2005
|
+
className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
|
|
2006
|
+
onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringToFront")}
|
|
2007
|
+
>
|
|
2008
|
+
置顶
|
|
2009
|
+
</button>
|
|
2010
|
+
<button
|
|
2011
|
+
type="button"
|
|
2012
|
+
className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
|
|
2013
|
+
onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendToBack")}
|
|
2014
|
+
>
|
|
2015
|
+
置底
|
|
2016
|
+
</button>
|
|
2017
|
+
</div>
|
|
2018
|
+
</div>
|
|
2019
|
+
<div className="truncate text-muted-foreground">ID: {selectedElement.id}</div>
|
|
2020
|
+
<div className="text-muted-foreground">类型: {selectedElement.materialType ?? selectedElement.id}</div>
|
|
2021
|
+
</>,
|
|
2022
|
+
true,
|
|
2023
|
+
["名称", "id", "类型", "锁定", "locked", "name"]
|
|
2024
|
+
)}
|
|
2025
|
+
|
|
2026
|
+
{renderSection(
|
|
2027
|
+
"styleBackground",
|
|
2028
|
+
"通用样式 / 背景",
|
|
2029
|
+
<>
|
|
2030
|
+
{renderFieldGroup(
|
|
2031
|
+
"背景填充",
|
|
2032
|
+
<>
|
|
2033
|
+
{renderColorField(
|
|
2034
|
+
"背景色",
|
|
2035
|
+
selectedElement.style?.backgroundColor ?? "",
|
|
2036
|
+
(next) => updateSelectedStyle({ backgroundColor: next || undefined })
|
|
2037
|
+
)}
|
|
2038
|
+
<label className="block space-y-1">
|
|
2039
|
+
<div>背景图</div>
|
|
2040
|
+
<Input
|
|
2041
|
+
value={selectedElement.style?.backgroundImage ?? ""}
|
|
2042
|
+
onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
|
|
2043
|
+
placeholder='url("https://...") / linear-gradient(...)'
|
|
2044
|
+
className="h-7"
|
|
2045
|
+
/>
|
|
2046
|
+
</label>
|
|
2047
|
+
<div className="flex items-center gap-2">
|
|
2048
|
+
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
2049
|
+
上传图片
|
|
2050
|
+
<Input
|
|
2051
|
+
type="file"
|
|
2052
|
+
accept="image/*"
|
|
2053
|
+
className="hidden"
|
|
2054
|
+
onChange={async (e) => {
|
|
2055
|
+
const file = e.target.files?.[0];
|
|
2056
|
+
e.currentTarget.value = "";
|
|
2057
|
+
if (!file) return;
|
|
2058
|
+
await handleUploadBackgroundImage(file);
|
|
2059
|
+
}}
|
|
2060
|
+
/>
|
|
2061
|
+
</label>
|
|
2062
|
+
{uploadStatus ? (
|
|
2063
|
+
<span className="text-[11px] text-muted-foreground">{uploadStatus}</span>
|
|
2064
|
+
) : null}
|
|
2065
|
+
</div>
|
|
2066
|
+
</>
|
|
2067
|
+
)}
|
|
2068
|
+
{renderFieldGroup(
|
|
2069
|
+
"背景布局",
|
|
2070
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2071
|
+
<label className="block space-y-1">
|
|
2072
|
+
<div>背景尺寸</div>
|
|
2073
|
+
<Select
|
|
2074
|
+
value={selectedElement.style?.backgroundSize ?? "__none__"}
|
|
2075
|
+
onValueChange={(value) =>
|
|
2076
|
+
updateSelectedStyle({
|
|
2077
|
+
backgroundSize: value === "__none__" ? undefined : value,
|
|
2078
|
+
})
|
|
2079
|
+
}
|
|
2080
|
+
>
|
|
2081
|
+
<SelectTrigger className="h-7">
|
|
2082
|
+
<SelectValue placeholder="选择背景尺寸" />
|
|
2083
|
+
</SelectTrigger>
|
|
2084
|
+
<SelectContent>
|
|
2085
|
+
<SelectItem value="__none__">默认</SelectItem>
|
|
2086
|
+
<SelectItem value="cover">cover</SelectItem>
|
|
2087
|
+
<SelectItem value="contain">contain</SelectItem>
|
|
2088
|
+
<SelectItem value="100% 100%">100% 100%</SelectItem>
|
|
2089
|
+
<SelectItem value="auto">auto</SelectItem>
|
|
2090
|
+
</SelectContent>
|
|
2091
|
+
</Select>
|
|
2092
|
+
</label>
|
|
2093
|
+
<label className="block space-y-1">
|
|
2094
|
+
<div>背景位置</div>
|
|
2095
|
+
<Select
|
|
2096
|
+
value={selectedElement.style?.backgroundPosition ?? "__none__"}
|
|
2097
|
+
onValueChange={(value) =>
|
|
2098
|
+
updateSelectedStyle({
|
|
2099
|
+
backgroundPosition: value === "__none__" ? undefined : value,
|
|
2100
|
+
})
|
|
2101
|
+
}
|
|
2102
|
+
>
|
|
2103
|
+
<SelectTrigger className="h-7">
|
|
2104
|
+
<SelectValue placeholder="选择背景位置" />
|
|
2105
|
+
</SelectTrigger>
|
|
2106
|
+
<SelectContent>
|
|
2107
|
+
<SelectItem value="__none__">默认</SelectItem>
|
|
2108
|
+
<SelectItem value="center">center</SelectItem>
|
|
2109
|
+
<SelectItem value="top">top</SelectItem>
|
|
2110
|
+
<SelectItem value="bottom">bottom</SelectItem>
|
|
2111
|
+
<SelectItem value="left">left</SelectItem>
|
|
2112
|
+
<SelectItem value="right">right</SelectItem>
|
|
2113
|
+
<SelectItem value="top left">top left</SelectItem>
|
|
2114
|
+
<SelectItem value="top right">top right</SelectItem>
|
|
2115
|
+
<SelectItem value="bottom left">bottom left</SelectItem>
|
|
2116
|
+
<SelectItem value="bottom right">bottom right</SelectItem>
|
|
2117
|
+
</SelectContent>
|
|
2118
|
+
</Select>
|
|
2119
|
+
</label>
|
|
2120
|
+
</div>
|
|
2121
|
+
)}
|
|
2122
|
+
</>,
|
|
2123
|
+
true,
|
|
2124
|
+
["背景色", "背景图", "background", "backgroundSize", "backgroundPosition", "布局"]
|
|
2125
|
+
)}
|
|
2126
|
+
|
|
2127
|
+
{renderSection(
|
|
2128
|
+
"styleBorder",
|
|
2129
|
+
"通用样式 / 边框",
|
|
2130
|
+
<>
|
|
2131
|
+
{renderFieldGroup(
|
|
2132
|
+
"边框几何",
|
|
2133
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2134
|
+
<label className="block space-y-1">
|
|
2135
|
+
<div>边框宽度(px)</div>
|
|
2136
|
+
<Input
|
|
2137
|
+
type="number"
|
|
2138
|
+
min={0}
|
|
2139
|
+
value={selectedElement.style?.borderWidth ?? 0}
|
|
2140
|
+
onChange={(e) =>
|
|
2141
|
+
updateSelectedStyle({ borderWidth: Math.max(0, Number(e.target.value) || 0) })
|
|
2142
|
+
}
|
|
2143
|
+
className="h-7"
|
|
2144
|
+
/>
|
|
2145
|
+
</label>
|
|
2146
|
+
<label className="block space-y-1">
|
|
2147
|
+
<div>边框圆角(px)</div>
|
|
2148
|
+
<Input
|
|
2149
|
+
type="number"
|
|
2150
|
+
min={0}
|
|
2151
|
+
value={selectedElement.style?.borderRadius ?? 0}
|
|
2152
|
+
onChange={(e) =>
|
|
2153
|
+
updateSelectedStyle({ borderRadius: Math.max(0, Number(e.target.value) || 0) })
|
|
2154
|
+
}
|
|
2155
|
+
className="h-7"
|
|
2156
|
+
/>
|
|
2157
|
+
</label>
|
|
2158
|
+
</div>
|
|
2159
|
+
)}
|
|
2160
|
+
{renderFieldGroup(
|
|
2161
|
+
"边框视觉",
|
|
2162
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2163
|
+
<label className="block space-y-1">
|
|
2164
|
+
<div>边框样式</div>
|
|
2165
|
+
<Select
|
|
2166
|
+
value={selectedElement.style?.borderStyle ?? "solid"}
|
|
2167
|
+
onValueChange={(value) =>
|
|
2168
|
+
updateSelectedStyle({
|
|
2169
|
+
borderStyle: value as NonNullable<PanelElementStyle["borderStyle"]>,
|
|
2170
|
+
})
|
|
2171
|
+
}
|
|
2172
|
+
>
|
|
2173
|
+
<SelectTrigger className="h-7">
|
|
2174
|
+
<SelectValue placeholder="请选择边框样式" />
|
|
2175
|
+
</SelectTrigger>
|
|
2176
|
+
<SelectContent>
|
|
2177
|
+
<SelectItem value="none">none</SelectItem>
|
|
2178
|
+
<SelectItem value="solid">solid</SelectItem>
|
|
2179
|
+
<SelectItem value="dashed">dashed</SelectItem>
|
|
2180
|
+
<SelectItem value="dotted">dotted</SelectItem>
|
|
2181
|
+
<SelectItem value="double">double</SelectItem>
|
|
2182
|
+
</SelectContent>
|
|
2183
|
+
</Select>
|
|
2184
|
+
</label>
|
|
2185
|
+
{renderColorField(
|
|
2186
|
+
"边框颜色",
|
|
2187
|
+
selectedElement.style?.borderColor ?? "",
|
|
2188
|
+
(next) => updateSelectedStyle({ borderColor: next || undefined })
|
|
2189
|
+
)}
|
|
2190
|
+
</div>
|
|
2191
|
+
)}
|
|
2192
|
+
</>,
|
|
2193
|
+
true,
|
|
2194
|
+
["边框", "border", "宽度", "圆角", "颜色", "样式"]
|
|
2195
|
+
)}
|
|
2196
|
+
|
|
2197
|
+
{isChartElement ? (
|
|
2198
|
+
<>
|
|
2199
|
+
{renderSection(
|
|
2200
|
+
"chartBasic",
|
|
2201
|
+
"图表配置 / 基础",
|
|
2202
|
+
<>
|
|
2203
|
+
{renderFieldGroup(
|
|
2204
|
+
"基础显示",
|
|
2205
|
+
<>
|
|
2206
|
+
<label className="block space-y-1.5">
|
|
2207
|
+
<div>标题</div>
|
|
2208
|
+
<Input
|
|
2209
|
+
value={selectedElement.chart?.title ?? ""}
|
|
2210
|
+
onChange={(e) => updateSelectedChart({ title: e.target.value })}
|
|
2211
|
+
className="h-7"
|
|
2212
|
+
/>
|
|
2213
|
+
</label>
|
|
2214
|
+
|
|
2215
|
+
{renderColorField(
|
|
2216
|
+
"主色",
|
|
2217
|
+
selectedElement.chart?.color ?? "#3b82f6",
|
|
2218
|
+
(next) => updateSelectedChart({ color: next || "#3b82f6" })
|
|
2219
|
+
)}
|
|
2220
|
+
<label className="flex items-center gap-2">
|
|
2221
|
+
<Checkbox
|
|
2222
|
+
checked={selectedElement.chart?.colorMode === "gradient"}
|
|
2223
|
+
onCheckedChange={(checked) =>
|
|
2224
|
+
updateSelectedChart({ colorMode: checked ? "gradient" : "solid" })
|
|
2225
|
+
}
|
|
2226
|
+
/>
|
|
2227
|
+
<span>主色使用渐变</span>
|
|
2228
|
+
</label>
|
|
2229
|
+
{selectedElement.chart?.colorMode === "gradient" ? (
|
|
2230
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2231
|
+
{renderColorField(
|
|
2232
|
+
"渐变起始色",
|
|
2233
|
+
selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6",
|
|
2234
|
+
(next) => updateSelectedChart({ gradientFrom: next || "#3b82f6" })
|
|
2235
|
+
)}
|
|
2236
|
+
{renderColorField(
|
|
2237
|
+
"渐变结束色",
|
|
2238
|
+
selectedElement.chart?.gradientTo ?? "#22d3ee",
|
|
2239
|
+
(next) => updateSelectedChart({ gradientTo: next || "#22d3ee" })
|
|
2240
|
+
)}
|
|
2241
|
+
<label className="block space-y-1 col-span-2">
|
|
2242
|
+
<div>渐变方向</div>
|
|
2243
|
+
<Select
|
|
2244
|
+
value={selectedElement.chart?.gradientDirection ?? "to-right"}
|
|
2245
|
+
onValueChange={(value) =>
|
|
2246
|
+
updateSelectedChart({
|
|
2247
|
+
gradientDirection: value as
|
|
2248
|
+
| "to-right"
|
|
2249
|
+
| "to-bottom"
|
|
2250
|
+
| "to-bottom-right"
|
|
2251
|
+
| "to-top-right",
|
|
2252
|
+
})
|
|
2253
|
+
}
|
|
2254
|
+
>
|
|
2255
|
+
<SelectTrigger className="h-7">
|
|
2256
|
+
<SelectValue placeholder="请选择渐变方向" />
|
|
2257
|
+
</SelectTrigger>
|
|
2258
|
+
<SelectContent>
|
|
2259
|
+
<SelectItem value="to-right">左 → 右</SelectItem>
|
|
2260
|
+
<SelectItem value="to-bottom">上 → 下</SelectItem>
|
|
2261
|
+
<SelectItem value="to-bottom-right">左上 → 右下</SelectItem>
|
|
2262
|
+
<SelectItem value="to-top-right">左下 → 右上</SelectItem>
|
|
2263
|
+
</SelectContent>
|
|
2264
|
+
</Select>
|
|
2265
|
+
</label>
|
|
2266
|
+
<div className="col-span-2 space-y-1">
|
|
2267
|
+
<div className="text-[11px] text-muted-foreground">渐变预览</div>
|
|
2268
|
+
<div
|
|
2269
|
+
className="h-6 rounded border border-border/60"
|
|
2270
|
+
style={{
|
|
2271
|
+
backgroundImage: `linear-gradient(${
|
|
2272
|
+
(selectedElement.chart?.gradientDirection ?? "to-right") === "to-bottom"
|
|
2273
|
+
? "to bottom"
|
|
2274
|
+
: (selectedElement.chart?.gradientDirection ?? "to-right") === "to-bottom-right"
|
|
2275
|
+
? "to bottom right"
|
|
2276
|
+
: (selectedElement.chart?.gradientDirection ?? "to-right") === "to-top-right"
|
|
2277
|
+
? "to top right"
|
|
2278
|
+
: "to right"
|
|
2279
|
+
}, ${selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6"}, ${selectedElement.chart?.gradientTo ?? "#22d3ee"})`,
|
|
2280
|
+
}}
|
|
2281
|
+
/>
|
|
2282
|
+
</div>
|
|
2283
|
+
</div>
|
|
2284
|
+
) : null}
|
|
2285
|
+
<label className="block space-y-1.5">
|
|
2286
|
+
<div>显示模式</div>
|
|
2287
|
+
<Select
|
|
2288
|
+
value={selectedElement.chart?.renderMode ?? "canvas"}
|
|
2289
|
+
onValueChange={(value) =>
|
|
2290
|
+
updateSelectedChart({ renderMode: value as "canvas" | "svg" })
|
|
2291
|
+
}
|
|
2292
|
+
>
|
|
2293
|
+
<SelectTrigger className="h-7">
|
|
2294
|
+
<SelectValue placeholder="请选择显示模式" />
|
|
2295
|
+
</SelectTrigger>
|
|
2296
|
+
<SelectContent>
|
|
2297
|
+
<SelectItem value="canvas">Canvas</SelectItem>
|
|
2298
|
+
<SelectItem value="svg">SVG</SelectItem>
|
|
2299
|
+
</SelectContent>
|
|
2300
|
+
</Select>
|
|
2301
|
+
</label>
|
|
2302
|
+
</>
|
|
2303
|
+
)}
|
|
2304
|
+
{renderFieldGroup(
|
|
2305
|
+
"提示框 Tooltip",
|
|
2306
|
+
<>
|
|
2307
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2308
|
+
<label className="flex items-center gap-2">
|
|
2309
|
+
<Checkbox
|
|
2310
|
+
checked={selectedElement.chart?.tooltipShow ?? true}
|
|
2311
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2312
|
+
onCheckedChange={(checked) =>
|
|
2313
|
+
updateSelectedChart({ tooltipShow: checked === true })
|
|
2314
|
+
}
|
|
2315
|
+
/>
|
|
2316
|
+
<span>显示 Tooltip</span>
|
|
2317
|
+
</label>
|
|
2318
|
+
<label className="block space-y-1">
|
|
2319
|
+
<div>Tooltip 触发方式</div>
|
|
2320
|
+
<Select
|
|
2321
|
+
value={selectedElement.chart?.tooltipTrigger ?? "axis"}
|
|
2322
|
+
onValueChange={(value) =>
|
|
2323
|
+
updateSelectedChart({ tooltipTrigger: value as "axis" | "item" })
|
|
2324
|
+
}
|
|
2325
|
+
>
|
|
2326
|
+
<SelectTrigger className="h-7">
|
|
2327
|
+
<SelectValue placeholder="请选择触发方式" />
|
|
2328
|
+
</SelectTrigger>
|
|
2329
|
+
<SelectContent>
|
|
2330
|
+
<SelectItem value="axis">axis</SelectItem>
|
|
2331
|
+
<SelectItem value="item">item</SelectItem>
|
|
2332
|
+
</SelectContent>
|
|
2333
|
+
</Select>
|
|
2334
|
+
</label>
|
|
2335
|
+
</div>
|
|
2336
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2337
|
+
{renderColorField(
|
|
2338
|
+
"Tooltip 背景色",
|
|
2339
|
+
selectedElement.chart?.tooltipBackgroundColor ?? "#0f172a",
|
|
2340
|
+
(next) => updateSelectedChart({ tooltipBackgroundColor: next || "#0f172a" })
|
|
2341
|
+
)}
|
|
2342
|
+
{renderColorField(
|
|
2343
|
+
"Tooltip 文字色",
|
|
2344
|
+
selectedElement.chart?.tooltipTextColor ?? "#f8fafc",
|
|
2345
|
+
(next) => updateSelectedChart({ tooltipTextColor: next || "#f8fafc" })
|
|
2346
|
+
)}
|
|
2347
|
+
</div>
|
|
2348
|
+
<label className="block space-y-1">
|
|
2349
|
+
{renderFormatterLabel("Tooltip Formatter(可选)")}
|
|
2350
|
+
<Input
|
|
2351
|
+
value={selectedElement.chart?.tooltipFormatter ?? ""}
|
|
2352
|
+
onChange={(e) =>
|
|
2353
|
+
updateSelectedChart({ tooltipFormatter: e.target.value || undefined })
|
|
2354
|
+
}
|
|
2355
|
+
placeholder="例如:{b}: {c}"
|
|
2356
|
+
className="h-7"
|
|
2357
|
+
/>
|
|
2358
|
+
</label>
|
|
2359
|
+
</>
|
|
2360
|
+
)}
|
|
2361
|
+
{renderFieldGroup(
|
|
2362
|
+
"数据",
|
|
2363
|
+
<>
|
|
2364
|
+
<label className="block space-y-1">
|
|
2365
|
+
<div>类目(逗号分隔)</div>
|
|
2366
|
+
<Input
|
|
2367
|
+
value={(selectedElement.chart?.labels ?? []).join(",")}
|
|
2368
|
+
onChange={(e) =>
|
|
2369
|
+
updateSelectedChart({
|
|
2370
|
+
labels: e.target.value
|
|
2371
|
+
.split(",")
|
|
2372
|
+
.map((s) => s.trim())
|
|
2373
|
+
.filter(Boolean),
|
|
2374
|
+
})
|
|
2375
|
+
}
|
|
2376
|
+
className="h-7"
|
|
2377
|
+
/>
|
|
2378
|
+
</label>
|
|
2379
|
+
|
|
2380
|
+
<label className="block space-y-1">
|
|
2381
|
+
<div>数值(逗号分隔)</div>
|
|
2382
|
+
<Input
|
|
2383
|
+
value={getChartValuesDisplayText(selectedElement.chart)}
|
|
2384
|
+
onChange={(e) =>
|
|
2385
|
+
updateSelectedChart({
|
|
2386
|
+
valuesText: e.target.value,
|
|
2387
|
+
})
|
|
2388
|
+
}
|
|
2389
|
+
className="h-7"
|
|
2390
|
+
/>
|
|
2391
|
+
</label>
|
|
2392
|
+
</>
|
|
2393
|
+
)}
|
|
2394
|
+
|
|
2395
|
+
{selectedChartType === "bar" ||
|
|
2396
|
+
selectedChartType === "line" ||
|
|
2397
|
+
selectedChartType === "area" ||
|
|
2398
|
+
selectedChartType === "scatter" ? (
|
|
2399
|
+
renderFieldGroup(
|
|
2400
|
+
"坐标轴",
|
|
2401
|
+
<>
|
|
2402
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2403
|
+
<label className="block">
|
|
2404
|
+
<div className="mb-1">X 轴名称</div>
|
|
2405
|
+
<Input
|
|
2406
|
+
value={selectedElement.chart?.xAxisName ?? ""}
|
|
2407
|
+
onChange={(e) => updateSelectedChart({ xAxisName: e.target.value })}
|
|
2408
|
+
placeholder="例如:日期 / 类目"
|
|
2409
|
+
className="h-7"
|
|
2410
|
+
/>
|
|
2411
|
+
</label>
|
|
2412
|
+
<label className="block">
|
|
2413
|
+
<div className="mb-1">Y 轴名称</div>
|
|
2414
|
+
<Input
|
|
2415
|
+
value={selectedElement.chart?.yAxisName ?? ""}
|
|
2416
|
+
onChange={(e) => updateSelectedChart({ yAxisName: e.target.value })}
|
|
2417
|
+
placeholder="例如:销量 / 数值"
|
|
2418
|
+
className="h-7"
|
|
2419
|
+
/>
|
|
2420
|
+
</label>
|
|
2421
|
+
</div>
|
|
2422
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2423
|
+
<label className="flex items-center gap-2">
|
|
2424
|
+
<Checkbox
|
|
2425
|
+
checked={selectedElement.chart?.xAxisTickShow ?? true}
|
|
2426
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2427
|
+
onCheckedChange={(checked) =>
|
|
2428
|
+
updateSelectedChart({ xAxisTickShow: checked === true })
|
|
2429
|
+
}
|
|
2430
|
+
/>
|
|
2431
|
+
<span>X 轴刻度线</span>
|
|
2432
|
+
</label>
|
|
2433
|
+
<label className="flex items-center gap-2">
|
|
2434
|
+
<Checkbox
|
|
2435
|
+
checked={selectedElement.chart?.yAxisTickShow ?? true}
|
|
2436
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2437
|
+
onCheckedChange={(checked) =>
|
|
2438
|
+
updateSelectedChart({ yAxisTickShow: checked === true })
|
|
2439
|
+
}
|
|
2440
|
+
/>
|
|
2441
|
+
<span>Y 轴刻度线</span>
|
|
2442
|
+
</label>
|
|
2443
|
+
</div>
|
|
2444
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2445
|
+
{renderColorField(
|
|
2446
|
+
"X 轴刻度线颜色",
|
|
2447
|
+
selectedElement.chart?.xAxisTickColor ?? "#94a3b8",
|
|
2448
|
+
(next) => updateSelectedChart({ xAxisTickColor: next || "#94a3b8" })
|
|
2449
|
+
)}
|
|
2450
|
+
{renderColorField(
|
|
2451
|
+
"Y 轴刻度线颜色",
|
|
2452
|
+
selectedElement.chart?.yAxisTickColor ?? "#94a3b8",
|
|
2453
|
+
(next) => updateSelectedChart({ yAxisTickColor: next || "#94a3b8" })
|
|
2454
|
+
)}
|
|
2455
|
+
</div>
|
|
2456
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2457
|
+
<label className="flex items-center gap-2">
|
|
2458
|
+
<Checkbox
|
|
2459
|
+
checked={selectedElement.chart?.xAxisSplitLineShow ?? false}
|
|
2460
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2461
|
+
onCheckedChange={(checked) =>
|
|
2462
|
+
updateSelectedChart({ xAxisSplitLineShow: checked === true })
|
|
2463
|
+
}
|
|
2464
|
+
/>
|
|
2465
|
+
<span>X 轴分割线</span>
|
|
2466
|
+
</label>
|
|
2467
|
+
<label className="flex items-center gap-2">
|
|
2468
|
+
<Checkbox
|
|
2469
|
+
checked={selectedElement.chart?.yAxisSplitLineShow ?? true}
|
|
2470
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2471
|
+
onCheckedChange={(checked) =>
|
|
2472
|
+
updateSelectedChart({ yAxisSplitLineShow: checked === true })
|
|
2473
|
+
}
|
|
2474
|
+
/>
|
|
2475
|
+
<span>Y 轴分割线</span>
|
|
2476
|
+
</label>
|
|
2477
|
+
</div>
|
|
2478
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2479
|
+
{renderColorField(
|
|
2480
|
+
"X 轴分割线颜色",
|
|
2481
|
+
selectedElement.chart?.xAxisSplitLineColor ?? "#e2e8f0",
|
|
2482
|
+
(next) => updateSelectedChart({ xAxisSplitLineColor: next || "#e2e8f0" })
|
|
2483
|
+
)}
|
|
2484
|
+
{renderColorField(
|
|
2485
|
+
"Y 轴分割线颜色",
|
|
2486
|
+
selectedElement.chart?.yAxisSplitLineColor ?? "#e2e8f0",
|
|
2487
|
+
(next) => updateSelectedChart({ yAxisSplitLineColor: next || "#e2e8f0" })
|
|
2488
|
+
)}
|
|
2489
|
+
</div>
|
|
2490
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2491
|
+
{renderColorField(
|
|
2492
|
+
"X 轴标签颜色",
|
|
2493
|
+
selectedElement.chart?.xAxisLabelColor ?? "#64748b",
|
|
2494
|
+
(next) => updateSelectedChart({ xAxisLabelColor: next || "#64748b" })
|
|
2495
|
+
)}
|
|
2496
|
+
{renderColorField(
|
|
2497
|
+
"Y 轴标签颜色",
|
|
2498
|
+
selectedElement.chart?.yAxisLabelColor ?? "#64748b",
|
|
2499
|
+
(next) => updateSelectedChart({ yAxisLabelColor: next || "#64748b" })
|
|
2500
|
+
)}
|
|
2501
|
+
</div>
|
|
2502
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2503
|
+
<label className="block">
|
|
2504
|
+
<div className="mb-1">X 轴标签字号</div>
|
|
2505
|
+
<Input
|
|
2506
|
+
type="number"
|
|
2507
|
+
min={8}
|
|
2508
|
+
max={48}
|
|
2509
|
+
value={selectedElement.chart?.xAxisLabelFontSize ?? 10}
|
|
2510
|
+
onChange={(e) =>
|
|
2511
|
+
updateSelectedChart({
|
|
2512
|
+
xAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
|
|
2513
|
+
})
|
|
2514
|
+
}
|
|
2515
|
+
className="h-7"
|
|
2516
|
+
/>
|
|
2517
|
+
</label>
|
|
2518
|
+
<label className="block">
|
|
2519
|
+
<div className="mb-1">Y 轴标签字号</div>
|
|
2520
|
+
<Input
|
|
2521
|
+
type="number"
|
|
2522
|
+
min={8}
|
|
2523
|
+
max={48}
|
|
2524
|
+
value={selectedElement.chart?.yAxisLabelFontSize ?? 10}
|
|
2525
|
+
onChange={(e) =>
|
|
2526
|
+
updateSelectedChart({
|
|
2527
|
+
yAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
|
|
2528
|
+
})
|
|
2529
|
+
}
|
|
2530
|
+
className="h-7"
|
|
2531
|
+
/>
|
|
2532
|
+
</label>
|
|
2533
|
+
</div>
|
|
2534
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2535
|
+
<label className="flex items-center gap-2">
|
|
2536
|
+
<Checkbox
|
|
2537
|
+
checked={selectedElement.chart?.xAxisLabelAutoEllipsis ?? false}
|
|
2538
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2539
|
+
onCheckedChange={(checked) =>
|
|
2540
|
+
updateSelectedChart({ xAxisLabelAutoEllipsis: checked === true })
|
|
2541
|
+
}
|
|
2542
|
+
/>
|
|
2543
|
+
<span>X 轴标签自动缩略</span>
|
|
2544
|
+
</label>
|
|
2545
|
+
<label className="flex items-center gap-2">
|
|
2546
|
+
<Checkbox
|
|
2547
|
+
checked={selectedElement.chart?.yAxisLabelAutoEllipsis ?? false}
|
|
2548
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2549
|
+
onCheckedChange={(checked) =>
|
|
2550
|
+
updateSelectedChart({ yAxisLabelAutoEllipsis: checked === true })
|
|
2551
|
+
}
|
|
2552
|
+
/>
|
|
2553
|
+
<span>Y 轴标签自动缩略</span>
|
|
2554
|
+
</label>
|
|
2555
|
+
</div>
|
|
2556
|
+
</>
|
|
2557
|
+
)
|
|
2558
|
+
) : null}
|
|
2559
|
+
|
|
2560
|
+
{selectedChartType === "bar" ||
|
|
2561
|
+
selectedChartType === "line" ||
|
|
2562
|
+
selectedChartType === "area" ||
|
|
2563
|
+
selectedChartType === "pie" ? (
|
|
2564
|
+
renderFieldGroup(
|
|
2565
|
+
"系列",
|
|
2566
|
+
<>
|
|
2567
|
+
{selectedChartType === "bar" ? (
|
|
2568
|
+
<label className="block space-y-1">
|
|
2569
|
+
<div>柱宽(px)</div>
|
|
2570
|
+
<Input
|
|
2571
|
+
type="number"
|
|
2572
|
+
min={1}
|
|
2573
|
+
value={selectedElement.chart?.barWidth ?? 24}
|
|
2574
|
+
onChange={(e) =>
|
|
2575
|
+
updateSelectedChart({ barWidth: Math.max(1, Number(e.target.value) || 1) })
|
|
2576
|
+
}
|
|
2577
|
+
className="h-7"
|
|
2578
|
+
/>
|
|
2579
|
+
</label>
|
|
2580
|
+
) : null}
|
|
2581
|
+
|
|
2582
|
+
{selectedChartType === "line" || selectedChartType === "area" ? (
|
|
2583
|
+
<label className="flex items-center gap-2">
|
|
2584
|
+
<Checkbox
|
|
2585
|
+
checked={selectedElement.chart?.smooth ?? true}
|
|
2586
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2587
|
+
onCheckedChange={(checked) =>
|
|
2588
|
+
updateSelectedChart({ smooth: checked === true })
|
|
2589
|
+
}
|
|
2590
|
+
/>
|
|
2591
|
+
<span>平滑曲线</span>
|
|
2592
|
+
</label>
|
|
2593
|
+
) : null}
|
|
2594
|
+
|
|
2595
|
+
{selectedChartType === "pie" ? (
|
|
2596
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2597
|
+
<label className="block space-y-1">
|
|
2598
|
+
<div>内半径(%)</div>
|
|
2599
|
+
<Input
|
|
2600
|
+
type="number"
|
|
2601
|
+
min={0}
|
|
2602
|
+
max={99}
|
|
2603
|
+
value={selectedElement.chart?.pieInnerRadius ?? 30}
|
|
2604
|
+
onChange={(e) =>
|
|
2605
|
+
updateSelectedChart({
|
|
2606
|
+
pieInnerRadius: Math.max(
|
|
2607
|
+
0,
|
|
2608
|
+
Math.min(99, Number(e.target.value) || 0)
|
|
2609
|
+
),
|
|
2610
|
+
})
|
|
2611
|
+
}
|
|
2612
|
+
className="h-7"
|
|
2613
|
+
/>
|
|
2614
|
+
</label>
|
|
2615
|
+
<label className="block space-y-1">
|
|
2616
|
+
<div>外半径(%)</div>
|
|
2617
|
+
<Input
|
|
2618
|
+
type="number"
|
|
2619
|
+
min={1}
|
|
2620
|
+
max={100}
|
|
2621
|
+
value={selectedElement.chart?.pieOuterRadius ?? 65}
|
|
2622
|
+
onChange={(e) =>
|
|
2623
|
+
updateSelectedChart({
|
|
2624
|
+
pieOuterRadius: Math.max(
|
|
2625
|
+
1,
|
|
2626
|
+
Math.min(100, Number(e.target.value) || 1)
|
|
2627
|
+
),
|
|
2628
|
+
})
|
|
2629
|
+
}
|
|
2630
|
+
className="h-7"
|
|
2631
|
+
/>
|
|
2632
|
+
</label>
|
|
2633
|
+
</div>
|
|
2634
|
+
) : null}
|
|
2635
|
+
</>
|
|
2636
|
+
)
|
|
2637
|
+
) : null}
|
|
2638
|
+
</>,
|
|
2639
|
+
true,
|
|
2640
|
+
[
|
|
2641
|
+
"图表",
|
|
2642
|
+
"title",
|
|
2643
|
+
"tooltip",
|
|
2644
|
+
"x轴",
|
|
2645
|
+
"y轴",
|
|
2646
|
+
"axis",
|
|
2647
|
+
"label",
|
|
2648
|
+
"刻度线",
|
|
2649
|
+
"分割线",
|
|
2650
|
+
"render",
|
|
2651
|
+
"svg",
|
|
2652
|
+
"canvas",
|
|
2653
|
+
]
|
|
2654
|
+
)}
|
|
2655
|
+
|
|
2656
|
+
{renderSection(
|
|
2657
|
+
"chartAdvanced",
|
|
2658
|
+
"图表配置 / 高级",
|
|
2659
|
+
<>
|
|
2660
|
+
{renderFieldGroup(
|
|
2661
|
+
"常用项(表单)",
|
|
2662
|
+
<div className="grid grid-cols-1 gap-2">
|
|
2663
|
+
<div className="rounded-lg border border-border/60 bg-background/70 p-3">
|
|
2664
|
+
<Collapsible
|
|
2665
|
+
open={isSectionExpanded("chartAdvancedLayout", false)}
|
|
2666
|
+
onOpenChange={(open) => setSectionExpanded("chartAdvancedLayout", open)}
|
|
2667
|
+
>
|
|
2668
|
+
<div className="mb-2 flex items-center gap-1.5">
|
|
2669
|
+
<CollapsibleTrigger asChild>
|
|
2670
|
+
<button
|
|
2671
|
+
type="button"
|
|
2672
|
+
className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
|
|
2673
|
+
aria-label={isSectionExpanded("chartAdvancedLayout", false) ? "收起布局与坐标" : "展开布局与坐标"}
|
|
2674
|
+
>
|
|
2675
|
+
{isSectionExpanded("chartAdvancedLayout", false) ? "▾" : "▸"}
|
|
2676
|
+
</button>
|
|
2677
|
+
</CollapsibleTrigger>
|
|
2678
|
+
<div className="text-[11px] font-medium text-muted-foreground">布局与坐标</div>
|
|
2679
|
+
</div>
|
|
2680
|
+
<CollapsibleContent>
|
|
2681
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2682
|
+
<label className="block space-y-1.5">
|
|
2683
|
+
<div className={optionLabelTextClass}>网格左距(grid.left)</div>
|
|
2684
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.left ?? 28)} onChange={(e) => updateSelectedOptionForm({ grid: { left: Number(e.target.value) || 0 } })} />
|
|
2685
|
+
</label>
|
|
2686
|
+
<label className="block space-y-1.5">
|
|
2687
|
+
<div className={optionLabelTextClass}>网格右距(grid.right)</div>
|
|
2688
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.right ?? 10)} onChange={(e) => updateSelectedOptionForm({ grid: { right: Number(e.target.value) || 0 } })} />
|
|
2689
|
+
</label>
|
|
2690
|
+
<label className="block space-y-1.5">
|
|
2691
|
+
<div className={optionLabelTextClass}>网格上距(grid.top)</div>
|
|
2692
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.top ?? 30)} onChange={(e) => updateSelectedOptionForm({ grid: { top: Number(e.target.value) || 0 } })} />
|
|
2693
|
+
</label>
|
|
2694
|
+
<label className="block space-y-1.5">
|
|
2695
|
+
<div className={optionLabelTextClass}>网格下距(grid.bottom)</div>
|
|
2696
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.bottom ?? 20)} onChange={(e) => updateSelectedOptionForm({ grid: { bottom: Number(e.target.value) || 0 } })} />
|
|
2697
|
+
</label>
|
|
2698
|
+
<label className="block space-y-1.5">
|
|
2699
|
+
<div className={optionLabelTextClass}>图例位置(legend.top)</div>
|
|
2700
|
+
<Select value={String((selectedElement.chart?.option as any)?.legend?.top ?? "top")} onValueChange={(value) => updateSelectedOptionForm({ legend: { top: value } })}>
|
|
2701
|
+
<SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder="选择图例位置" /></SelectTrigger>
|
|
2702
|
+
<SelectContent>
|
|
2703
|
+
<SelectItem value="top">顶部</SelectItem>
|
|
2704
|
+
<SelectItem value="bottom">底部</SelectItem>
|
|
2705
|
+
<SelectItem value="left">左侧</SelectItem>
|
|
2706
|
+
<SelectItem value="right">右侧</SelectItem>
|
|
2707
|
+
</SelectContent>
|
|
2708
|
+
</Select>
|
|
2709
|
+
</label>
|
|
2710
|
+
<label className="block space-y-1.5">
|
|
2711
|
+
<div className={optionLabelTextClass}>图例排列(legend.orient)</div>
|
|
2712
|
+
<Select value={String((selectedElement.chart?.option as any)?.legend?.orient ?? "horizontal")} onValueChange={(value) => updateSelectedOptionForm({ legend: { orient: value } })}>
|
|
2713
|
+
<SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder="选择图例排列" /></SelectTrigger>
|
|
2714
|
+
<SelectContent>
|
|
2715
|
+
<SelectItem value="horizontal">横向</SelectItem>
|
|
2716
|
+
<SelectItem value="vertical">纵向</SelectItem>
|
|
2717
|
+
</SelectContent>
|
|
2718
|
+
</Select>
|
|
2719
|
+
</label>
|
|
2720
|
+
<label className="block space-y-1.5">
|
|
2721
|
+
{renderOptionLabel("X 轴最小值", "xAxis.min", "设置横轴最小值,留空可在 JSON 里删除。")}
|
|
2722
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.min ?? 0)} onChange={(e) => updateSelectedOptionForm({ xAxis: { min: Number(e.target.value) || 0 } })} />
|
|
2723
|
+
</label>
|
|
2724
|
+
<label className="block space-y-1.5">
|
|
2725
|
+
{renderOptionLabel("X 轴最大值", "xAxis.max", "设置横轴最大值,留空可在 JSON 里删除。")}
|
|
2726
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.max ?? 100)} onChange={(e) => updateSelectedOptionForm({ xAxis: { max: Number(e.target.value) || 0 } })} />
|
|
2727
|
+
</label>
|
|
2728
|
+
<label className="block space-y-1.5">
|
|
2729
|
+
{renderOptionLabel("Y 轴最小值", "yAxis.min", "设置纵轴最小值,留空可在 JSON 里删除。")}
|
|
2730
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.min ?? 0)} onChange={(e) => updateSelectedOptionForm({ yAxis: { min: Number(e.target.value) || 0 } })} />
|
|
2731
|
+
</label>
|
|
2732
|
+
<label className="block space-y-1.5">
|
|
2733
|
+
{renderOptionLabel("Y 轴最大值", "yAxis.max", "设置纵轴最大值,留空可在 JSON 里删除。")}
|
|
2734
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.max ?? 100)} onChange={(e) => updateSelectedOptionForm({ yAxis: { max: Number(e.target.value) || 0 } })} />
|
|
2735
|
+
</label>
|
|
2736
|
+
<label className="block space-y-1.5">
|
|
2737
|
+
{renderOptionLabel("X 轴标签旋转", "xAxis.axisLabel.rotate", "单位为度。")}
|
|
2738
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.axisLabel?.rotate ?? 0)} onChange={(e) => updateSelectedOptionForm({ xAxis: { axisLabel: { rotate: Number(e.target.value) || 0 } } })} />
|
|
2739
|
+
</label>
|
|
2740
|
+
<label className="block space-y-1.5">
|
|
2741
|
+
{renderOptionLabel("Y 轴标签旋转", "yAxis.axisLabel.rotate", "单位为度。")}
|
|
2742
|
+
<Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.axisLabel?.rotate ?? 0)} onChange={(e) => updateSelectedOptionForm({ yAxis: { axisLabel: { rotate: Number(e.target.value) || 0 } } })} />
|
|
2743
|
+
</label>
|
|
2744
|
+
</div>
|
|
2745
|
+
</CollapsibleContent>
|
|
2746
|
+
</Collapsible>
|
|
2747
|
+
</div>
|
|
2748
|
+
<div className="rounded-lg border border-border/60 bg-background/70 p-3">
|
|
2749
|
+
<div className="space-y-2">
|
|
2750
|
+
<Collapsible
|
|
2751
|
+
open={isSectionExpanded("chartAdvancedHighFreq", true)}
|
|
2752
|
+
onOpenChange={(open) => setSectionExpanded("chartAdvancedHighFreq", open)}
|
|
2753
|
+
>
|
|
2754
|
+
<div className="mb-2 flex items-center gap-1.5">
|
|
2755
|
+
<CollapsibleTrigger asChild>
|
|
2756
|
+
<button
|
|
2757
|
+
type="button"
|
|
2758
|
+
className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
|
|
2759
|
+
aria-label={isSectionExpanded("chartAdvancedHighFreq", true) ? "收起高频项" : "展开高频项"}
|
|
2760
|
+
>
|
|
2761
|
+
{isSectionExpanded("chartAdvancedHighFreq", true) ? "▾" : "▸"}
|
|
2762
|
+
</button>
|
|
2763
|
+
</CollapsibleTrigger>
|
|
2764
|
+
<div className="text-[11px] font-medium text-muted-foreground">高频项</div>
|
|
2765
|
+
</div>
|
|
2766
|
+
<CollapsibleContent>
|
|
2767
|
+
<div className="grid grid-cols-1 gap-2">
|
|
2768
|
+
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2769
|
+
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.legend?.show ?? true)} onCheckedChange={(checked) => updateSelectedOptionForm({ legend: { show: checked === true } })} />
|
|
2770
|
+
{renderOptionLabel("显示图例", "legend.show", "控制图例显隐。")}
|
|
2771
|
+
</label>
|
|
2772
|
+
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2773
|
+
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.grid?.containLabel ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ grid: { containLabel: checked === true } })} />
|
|
2774
|
+
{renderOptionLabel("网格包含标签", "grid.containLabel", "自动为坐标轴标签预留空间。")}
|
|
2775
|
+
</label>
|
|
2776
|
+
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2777
|
+
<Checkbox className={optionCheckboxClass} checked={Boolean(Array.isArray((selectedElement.chart?.option as any)?.dataZoom) && (selectedElement.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "inside"))} onCheckedChange={(checked) => { const prev = Array.isArray((selectedElement.chart?.option as any)?.dataZoom) ? [...(selectedElement.chart?.option as any).dataZoom] : []; const next = checked ? [...prev.filter((z: any) => z?.type !== "inside"), { type: "inside" }] : prev.filter((z: any) => z?.type !== "inside"); updateSelectedOptionForm({ dataZoom: next }); }} />
|
|
2778
|
+
{renderOptionLabel("内置缩放", "dataZoom[type=inside]", "启用鼠标滚轮/手势缩放。")}
|
|
2779
|
+
</label>
|
|
2780
|
+
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2781
|
+
<Checkbox className={optionCheckboxClass} checked={Boolean(Array.isArray((selectedElement.chart?.option as any)?.dataZoom) && (selectedElement.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "slider"))} onCheckedChange={(checked) => { const prev = Array.isArray((selectedElement.chart?.option as any)?.dataZoom) ? [...(selectedElement.chart?.option as any).dataZoom] : []; const next = checked ? [...prev.filter((z: any) => z?.type !== "slider"), { type: "slider" }] : prev.filter((z: any) => z?.type !== "slider"); updateSelectedOptionForm({ dataZoom: next }); }} />
|
|
2782
|
+
{renderOptionLabel("滑块缩放", "dataZoom[type=slider]", "显示底部拖拽缩放条。")}
|
|
2783
|
+
</label>
|
|
2784
|
+
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2785
|
+
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.animation ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ animation: checked === true })} />
|
|
2786
|
+
{renderOptionLabel("开启动画", "animation", "开启后切换数据时有过渡动效。")}
|
|
2787
|
+
</label>
|
|
2788
|
+
<label className="block space-y-1.5">
|
|
2789
|
+
{renderOptionLabel("动画时长", "animationDuration", "动画持续时间,单位毫秒。")}
|
|
2790
|
+
<Input type="number" min={0} className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.animationDuration ?? 300)} onChange={(e) => updateSelectedOptionForm({ animationDuration: Math.max(0, Number(e.target.value) || 0) })} />
|
|
2791
|
+
</label>
|
|
2792
|
+
</div>
|
|
2793
|
+
</CollapsibleContent>
|
|
2794
|
+
</Collapsible>
|
|
2795
|
+
<Collapsible
|
|
2796
|
+
open={isSectionExpanded("chartAdvancedAxisPointer", false)}
|
|
2797
|
+
onOpenChange={(open) => setSectionExpanded("chartAdvancedAxisPointer", open)}
|
|
2798
|
+
>
|
|
2799
|
+
<div className="mb-2 flex items-center gap-1.5">
|
|
2800
|
+
<CollapsibleTrigger asChild>
|
|
2801
|
+
<button
|
|
2802
|
+
type="button"
|
|
2803
|
+
className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
|
|
2804
|
+
aria-label={isSectionExpanded("chartAdvancedAxisPointer", false) ? "收起轴指示器项" : "展开轴指示器项"}
|
|
2805
|
+
>
|
|
2806
|
+
{isSectionExpanded("chartAdvancedAxisPointer", false) ? "▾" : "▸"}
|
|
2807
|
+
</button>
|
|
2808
|
+
</CollapsibleTrigger>
|
|
2809
|
+
<div className="text-[11px] font-medium text-muted-foreground">轴指示器与对齐</div>
|
|
2810
|
+
</div>
|
|
2811
|
+
<CollapsibleContent>
|
|
2812
|
+
<div className="grid grid-cols-1 gap-2">
|
|
2813
|
+
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2814
|
+
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.show ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { show: checked === true } })} />
|
|
2815
|
+
{renderOptionLabel("显示轴指示器", "axisPointer.show", "悬停时显示轴对齐提示。")}
|
|
2816
|
+
</label>
|
|
2817
|
+
<label className="block space-y-1.5 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2818
|
+
{renderOptionLabel("轴指示器类型", "axisPointer.type", "设置指示器样式:线/阴影/十字。")}
|
|
2819
|
+
<Select value={String((selectedElement.chart?.option as any)?.axisPointer?.type ?? "line")} onValueChange={(value) => updateSelectedOptionForm({ axisPointer: { type: value } })}>
|
|
2820
|
+
<SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder="选择轴指示器类型" /></SelectTrigger>
|
|
2821
|
+
<SelectContent>
|
|
2822
|
+
<SelectItem value="line">线</SelectItem>
|
|
2823
|
+
<SelectItem value="shadow">阴影</SelectItem>
|
|
2824
|
+
<SelectItem value="cross">十字</SelectItem>
|
|
2825
|
+
</SelectContent>
|
|
2826
|
+
</Select>
|
|
2827
|
+
</label>
|
|
2828
|
+
<label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
|
|
2829
|
+
<Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.snap ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { snap: checked === true } })} />
|
|
2830
|
+
{renderOptionLabel("轴指示器吸附", "axisPointer.snap", "指示器吸附到最近数据点。")}
|
|
2831
|
+
</label>
|
|
2832
|
+
</div>
|
|
2833
|
+
</CollapsibleContent>
|
|
2834
|
+
</Collapsible>
|
|
2835
|
+
</div>
|
|
2836
|
+
</div>
|
|
2837
|
+
</div>
|
|
2838
|
+
)}
|
|
2839
|
+
{renderFieldGroup(
|
|
2840
|
+
"JSON 高级模式",
|
|
2841
|
+
<>
|
|
2842
|
+
<label className="flex items-center gap-2 rounded-md bg-background/70 px-2 py-1.5">
|
|
2843
|
+
<Checkbox
|
|
2844
|
+
checked={isAdvancedOptionMode}
|
|
2845
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2846
|
+
onCheckedChange={(checked) => setIsAdvancedOptionMode(checked === true)}
|
|
2847
|
+
/>
|
|
2848
|
+
<span>开启高级模式(直接编辑图表 JSON 配置)</span>
|
|
2849
|
+
</label>
|
|
2850
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
2851
|
+
基础配置会先生成图表配置,高级模式会在此基础上覆盖(深度合并)。
|
|
2852
|
+
</div>
|
|
2853
|
+
{isAdvancedOptionMode ? (
|
|
2854
|
+
<>
|
|
2855
|
+
<Textarea
|
|
2856
|
+
value={optionJsonText}
|
|
2857
|
+
onChange={(e) => {
|
|
2858
|
+
const next = e.target.value;
|
|
2859
|
+
setOptionJsonText(next);
|
|
2860
|
+
try {
|
|
2861
|
+
const parsed = JSON.parse(next) as Record<string, unknown>;
|
|
2862
|
+
updateSelectedChart({ option: parsed });
|
|
2863
|
+
setOptionJsonError(null);
|
|
2864
|
+
} catch {
|
|
2865
|
+
setOptionJsonError("JSON 格式错误,修正后会自动应用");
|
|
2866
|
+
}
|
|
2867
|
+
}}
|
|
2868
|
+
spellCheck={false}
|
|
2869
|
+
className="h-44 font-mono text-[11px]"
|
|
2870
|
+
/>
|
|
2871
|
+
{optionJsonError ? (
|
|
2872
|
+
<div className="rounded border border-destructive/40 bg-destructive/10 px-2 py-1.5 text-[11px] text-destructive">
|
|
2873
|
+
{optionJsonError}
|
|
2874
|
+
</div>
|
|
2875
|
+
) : (
|
|
2876
|
+
<div className="rounded border border-emerald-500/30 bg-emerald-500/10 px-2 py-1.5 text-[11px] text-emerald-700 dark:text-emerald-300">
|
|
2877
|
+
JSON 有效,已实时应用到当前图表。
|
|
2878
|
+
</div>
|
|
2879
|
+
)}
|
|
2880
|
+
</>
|
|
2881
|
+
) : null}
|
|
2882
|
+
</>
|
|
2883
|
+
)}
|
|
2884
|
+
</>,
|
|
2885
|
+
false,
|
|
2886
|
+
["json", "option", "高级", "echarts"]
|
|
2887
|
+
)}
|
|
2888
|
+
</>
|
|
2889
|
+
) : selectedElement.materialType === "text" ? (
|
|
2890
|
+
renderSection(
|
|
2891
|
+
"textConfig",
|
|
2892
|
+
"文本配置",
|
|
2893
|
+
<>
|
|
2894
|
+
{renderFieldGroup(
|
|
2895
|
+
"文本内容",
|
|
2896
|
+
<>
|
|
2897
|
+
<div className="flex items-center gap-1">
|
|
2898
|
+
<button
|
|
2899
|
+
type="button"
|
|
2900
|
+
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
|
|
2901
|
+
onClick={() => execTextCommand("bold")}
|
|
2902
|
+
>
|
|
2903
|
+
B
|
|
2904
|
+
</button>
|
|
2905
|
+
<button
|
|
2906
|
+
type="button"
|
|
2907
|
+
className="rounded border border-border px-2 py-1 text-[11px] italic hover:bg-accent"
|
|
2908
|
+
onClick={() => execTextCommand("italic")}
|
|
2909
|
+
>
|
|
2910
|
+
I
|
|
2911
|
+
</button>
|
|
2912
|
+
<button
|
|
2913
|
+
type="button"
|
|
2914
|
+
className="rounded border border-border px-2 py-1 text-[11px] underline hover:bg-accent"
|
|
2915
|
+
onClick={() => execTextCommand("underline")}
|
|
2916
|
+
>
|
|
2917
|
+
U
|
|
2918
|
+
</button>
|
|
2919
|
+
</div>
|
|
2920
|
+
<div
|
|
2921
|
+
ref={textEditorRef}
|
|
2922
|
+
className="min-h-[120px] rounded border border-border bg-background px-2 py-1.5 text-xs leading-6 outline-none"
|
|
2923
|
+
style={{
|
|
2924
|
+
fontFamily: selectedElement.textFontFamily || undefined,
|
|
2925
|
+
fontSize: selectedElement.textFontSize
|
|
2926
|
+
? `${selectedElement.textFontSize}px`
|
|
2927
|
+
: undefined,
|
|
2928
|
+
fontWeight: selectedElement.textFontWeight || undefined,
|
|
2929
|
+
color: selectedElement.textColor || undefined,
|
|
2930
|
+
lineHeight: selectedElement.textLineHeight
|
|
2931
|
+
? String(selectedElement.textLineHeight)
|
|
2932
|
+
: undefined,
|
|
2933
|
+
textAlign: selectedElement.textAlign ?? "left",
|
|
2934
|
+
}}
|
|
2935
|
+
contentEditable
|
|
2936
|
+
suppressContentEditableWarning
|
|
2937
|
+
onInput={(e) => {
|
|
2938
|
+
const nextHtml = (e.currentTarget as HTMLDivElement).innerHTML;
|
|
2939
|
+
updateSelectedText({
|
|
2940
|
+
textHtml: nextHtml || "<p><br/></p>",
|
|
2941
|
+
});
|
|
2942
|
+
}}
|
|
2943
|
+
/>
|
|
2944
|
+
</>
|
|
2945
|
+
)}
|
|
2946
|
+
{renderFieldGroup(
|
|
2947
|
+
"文字样式",
|
|
2948
|
+
<>
|
|
2949
|
+
<label className="block space-y-1">
|
|
2950
|
+
<div>字体</div>
|
|
2951
|
+
<Input
|
|
2952
|
+
value={selectedElement.textFontFamily ?? ""}
|
|
2953
|
+
onChange={(e) =>
|
|
2954
|
+
updateSelectedText({
|
|
2955
|
+
textFontFamily: e.target.value || undefined,
|
|
2956
|
+
})
|
|
2957
|
+
}
|
|
2958
|
+
placeholder="如:Inter, PingFang SC, Microsoft YaHei"
|
|
2959
|
+
className="h-7"
|
|
2960
|
+
/>
|
|
2961
|
+
</label>
|
|
2962
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2963
|
+
<label className="block space-y-1">
|
|
2964
|
+
<div>字号(px)</div>
|
|
2965
|
+
<Input
|
|
2966
|
+
type="number"
|
|
2967
|
+
min={8}
|
|
2968
|
+
max={200}
|
|
2969
|
+
step={1}
|
|
2970
|
+
value={selectedElement.textFontSize ?? 14}
|
|
2971
|
+
onChange={(e) =>
|
|
2972
|
+
updateSelectedText({
|
|
2973
|
+
textFontSize: Math.max(8, Number(e.target.value) || 14),
|
|
2974
|
+
})
|
|
2975
|
+
}
|
|
2976
|
+
className="h-7"
|
|
2977
|
+
/>
|
|
2978
|
+
</label>
|
|
2979
|
+
<label className="block space-y-1">
|
|
2980
|
+
<div>字重</div>
|
|
2981
|
+
<Select
|
|
2982
|
+
value={selectedElement.textFontWeight ?? "400"}
|
|
2983
|
+
onValueChange={(value) =>
|
|
2984
|
+
updateSelectedText({
|
|
2985
|
+
textFontWeight: value,
|
|
2986
|
+
})
|
|
2987
|
+
}
|
|
2988
|
+
>
|
|
2989
|
+
<SelectTrigger className="h-7">
|
|
2990
|
+
<SelectValue placeholder="选择字重" />
|
|
2991
|
+
</SelectTrigger>
|
|
2992
|
+
<SelectContent>
|
|
2993
|
+
<SelectItem value="300">300</SelectItem>
|
|
2994
|
+
<SelectItem value="400">400</SelectItem>
|
|
2995
|
+
<SelectItem value="500">500</SelectItem>
|
|
2996
|
+
<SelectItem value="600">600</SelectItem>
|
|
2997
|
+
<SelectItem value="700">700</SelectItem>
|
|
2998
|
+
</SelectContent>
|
|
2999
|
+
</Select>
|
|
3000
|
+
</label>
|
|
3001
|
+
</div>
|
|
3002
|
+
<div className="grid grid-cols-2 gap-2">
|
|
3003
|
+
<label className="block space-y-1">
|
|
3004
|
+
<div>对齐</div>
|
|
3005
|
+
<Select
|
|
3006
|
+
value={selectedElement.textAlign ?? "left"}
|
|
3007
|
+
onValueChange={(value) =>
|
|
3008
|
+
updateSelectedText({
|
|
3009
|
+
textAlign: value as "left" | "center" | "right" | "justify",
|
|
3010
|
+
})
|
|
3011
|
+
}
|
|
3012
|
+
>
|
|
3013
|
+
<SelectTrigger className="h-7">
|
|
3014
|
+
<SelectValue placeholder="选择对齐方式" />
|
|
3015
|
+
</SelectTrigger>
|
|
3016
|
+
<SelectContent>
|
|
3017
|
+
<SelectItem value="left">左对齐</SelectItem>
|
|
3018
|
+
<SelectItem value="center">居中</SelectItem>
|
|
3019
|
+
<SelectItem value="right">右对齐</SelectItem>
|
|
3020
|
+
<SelectItem value="justify">两端对齐</SelectItem>
|
|
3021
|
+
</SelectContent>
|
|
3022
|
+
</Select>
|
|
3023
|
+
</label>
|
|
3024
|
+
<label className="block space-y-1">
|
|
3025
|
+
<div>行高</div>
|
|
3026
|
+
<Input
|
|
3027
|
+
type="number"
|
|
3028
|
+
min={1}
|
|
3029
|
+
max={3}
|
|
3030
|
+
step={0.1}
|
|
3031
|
+
value={selectedElement.textLineHeight ?? 1.6}
|
|
3032
|
+
onChange={(e) =>
|
|
3033
|
+
updateSelectedText({
|
|
3034
|
+
textLineHeight: Math.min(
|
|
3035
|
+
3,
|
|
3036
|
+
Math.max(1, Number(e.target.value) || 1.6)
|
|
3037
|
+
),
|
|
3038
|
+
})
|
|
3039
|
+
}
|
|
3040
|
+
className="h-7"
|
|
3041
|
+
/>
|
|
3042
|
+
</label>
|
|
3043
|
+
</div>
|
|
3044
|
+
{renderColorField(
|
|
3045
|
+
"文字颜色",
|
|
3046
|
+
selectedElement.textColor ?? "",
|
|
3047
|
+
(next) =>
|
|
3048
|
+
updateSelectedText({
|
|
3049
|
+
textColor: next || undefined,
|
|
3050
|
+
})
|
|
3051
|
+
)}
|
|
3052
|
+
</>
|
|
3053
|
+
)}
|
|
3054
|
+
{renderFieldGroup(
|
|
3055
|
+
"输入能力",
|
|
3056
|
+
<label className="flex items-center gap-2">
|
|
3057
|
+
<Checkbox
|
|
3058
|
+
checked={selectedElement.textAllowInput ?? true}
|
|
3059
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
3060
|
+
onCheckedChange={(checked) =>
|
|
3061
|
+
updateSelectedText({
|
|
3062
|
+
textAllowInput: checked !== false,
|
|
3063
|
+
})
|
|
3064
|
+
}
|
|
3065
|
+
/>
|
|
3066
|
+
<span>允许在画布内直接输入(默认开启)</span>
|
|
3067
|
+
</label>
|
|
3068
|
+
)}
|
|
3069
|
+
</>,
|
|
3070
|
+
true,
|
|
3071
|
+
["文本", "富文本", "字体", "颜色", "对齐", "行高", "输入"]
|
|
3072
|
+
)
|
|
3073
|
+
) : selectedElement.materialType === "audio" ? (
|
|
3074
|
+
renderSection(
|
|
3075
|
+
"audioConfig",
|
|
3076
|
+
"音频配置",
|
|
3077
|
+
<>
|
|
3078
|
+
{renderFieldGroup(
|
|
3079
|
+
"音频来源",
|
|
3080
|
+
<>
|
|
3081
|
+
<label className="block space-y-1">
|
|
3082
|
+
<div>音频 URL</div>
|
|
3083
|
+
<Input
|
|
3084
|
+
value={selectedElement.audioRemoteUrl ?? ""}
|
|
3085
|
+
onChange={(e) =>
|
|
3086
|
+
updateSelectedAudio({
|
|
3087
|
+
audioRemoteUrl: e.target.value || undefined,
|
|
3088
|
+
audioSrc: e.target.value || selectedElement.audioSrc,
|
|
3089
|
+
})
|
|
3090
|
+
}
|
|
3091
|
+
placeholder="https://example.com/audio.mp3"
|
|
3092
|
+
className="h-7"
|
|
3093
|
+
/>
|
|
3094
|
+
</label>
|
|
3095
|
+
<div className="flex items-center gap-2">
|
|
3096
|
+
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
3097
|
+
上传音频
|
|
3098
|
+
<Input
|
|
3099
|
+
type="file"
|
|
3100
|
+
accept="audio/*"
|
|
3101
|
+
className="hidden"
|
|
3102
|
+
onChange={async (e) => {
|
|
3103
|
+
const file = e.target.files?.[0];
|
|
3104
|
+
e.currentTarget.value = "";
|
|
3105
|
+
if (!file) return;
|
|
3106
|
+
await handleUploadAudioFile(file);
|
|
3107
|
+
}}
|
|
3108
|
+
/>
|
|
3109
|
+
</label>
|
|
3110
|
+
<button
|
|
3111
|
+
type="button"
|
|
3112
|
+
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent disabled:opacity-50"
|
|
3113
|
+
onClick={isRecordingAudio ? stopRecordingAudio : startRecordingAudio}
|
|
3114
|
+
>
|
|
3115
|
+
{isRecordingAudio ? "停止录音" : "开始录音"}
|
|
3116
|
+
</button>
|
|
3117
|
+
</div>
|
|
3118
|
+
{audioStatus ? (
|
|
3119
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3120
|
+
{audioStatus}
|
|
3121
|
+
</div>
|
|
3122
|
+
) : null}
|
|
3123
|
+
<audio
|
|
3124
|
+
controls
|
|
3125
|
+
className="h-8 w-full"
|
|
3126
|
+
src={selectedElement.audioSrc || selectedElement.audioRemoteUrl || ""}
|
|
3127
|
+
/>
|
|
3128
|
+
</>
|
|
3129
|
+
)}
|
|
3130
|
+
{renderFieldGroup(
|
|
3131
|
+
"展示样式",
|
|
3132
|
+
<>
|
|
3133
|
+
<label className="flex items-center gap-2">
|
|
3134
|
+
<Checkbox
|
|
3135
|
+
checked={selectedElement.mediaAutoPauseOnEdit !== false}
|
|
3136
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
3137
|
+
onCheckedChange={(checked) =>
|
|
3138
|
+
updateSelectedAudio({
|
|
3139
|
+
mediaAutoPauseOnEdit: checked !== false,
|
|
3140
|
+
})
|
|
3141
|
+
}
|
|
3142
|
+
/>
|
|
3143
|
+
<span>编辑时自动暂停媒体</span>
|
|
3144
|
+
</label>
|
|
3145
|
+
<label className="block space-y-1">
|
|
3146
|
+
<div>预设喇叭图标</div>
|
|
3147
|
+
<Select
|
|
3148
|
+
value={selectedElement.audioIconPreset ?? "__none__"}
|
|
3149
|
+
onValueChange={(value) =>
|
|
3150
|
+
updateSelectedAudio({
|
|
3151
|
+
audioIconPreset:
|
|
3152
|
+
value === "__none__"
|
|
3153
|
+
? undefined
|
|
3154
|
+
: (value as "speaker" | "music" | "headphone" | "wave"),
|
|
3155
|
+
})
|
|
3156
|
+
}
|
|
3157
|
+
>
|
|
3158
|
+
<SelectTrigger className="h-7">
|
|
3159
|
+
<SelectValue placeholder="选择图标" />
|
|
3160
|
+
</SelectTrigger>
|
|
3161
|
+
<SelectContent>
|
|
3162
|
+
<SelectItem value="__none__">默认(显示进度条)</SelectItem>
|
|
3163
|
+
<SelectItem value="speaker">喇叭</SelectItem>
|
|
3164
|
+
<SelectItem value="music">音符</SelectItem>
|
|
3165
|
+
<SelectItem value="headphone">耳机</SelectItem>
|
|
3166
|
+
<SelectItem value="wave">声波</SelectItem>
|
|
3167
|
+
</SelectContent>
|
|
3168
|
+
</Select>
|
|
3169
|
+
</label>
|
|
3170
|
+
<label className="block space-y-1">
|
|
3171
|
+
<div>播放动效</div>
|
|
3172
|
+
<Select
|
|
3173
|
+
value={selectedElement.audioVisualEffect ?? "pulse"}
|
|
3174
|
+
onValueChange={(value) =>
|
|
3175
|
+
updateSelectedAudio({
|
|
3176
|
+
audioVisualEffect: value as "none" | "pulse" | "ripple",
|
|
3177
|
+
})
|
|
3178
|
+
}
|
|
3179
|
+
>
|
|
3180
|
+
<SelectTrigger className="h-7">
|
|
3181
|
+
<SelectValue placeholder="选择动效" />
|
|
3182
|
+
</SelectTrigger>
|
|
3183
|
+
<SelectContent>
|
|
3184
|
+
<SelectItem value="none">无动效</SelectItem>
|
|
3185
|
+
<SelectItem value="pulse">呼吸高亮</SelectItem>
|
|
3186
|
+
<SelectItem value="ripple">波纹扩散</SelectItem>
|
|
3187
|
+
</SelectContent>
|
|
3188
|
+
</Select>
|
|
3189
|
+
</label>
|
|
3190
|
+
<label className="block space-y-1">
|
|
3191
|
+
<div>动效速度</div>
|
|
3192
|
+
<Select
|
|
3193
|
+
value={selectedElement.audioVisualSpeed ?? "normal"}
|
|
3194
|
+
onValueChange={(value) =>
|
|
3195
|
+
updateSelectedAudio({
|
|
3196
|
+
audioVisualSpeed: value as "slow" | "normal" | "fast",
|
|
3197
|
+
})
|
|
3198
|
+
}
|
|
3199
|
+
>
|
|
3200
|
+
<SelectTrigger className="h-7">
|
|
3201
|
+
<SelectValue placeholder="选择速度" />
|
|
3202
|
+
</SelectTrigger>
|
|
3203
|
+
<SelectContent>
|
|
3204
|
+
<SelectItem value="slow">慢</SelectItem>
|
|
3205
|
+
<SelectItem value="normal">中</SelectItem>
|
|
3206
|
+
<SelectItem value="fast">快</SelectItem>
|
|
3207
|
+
</SelectContent>
|
|
3208
|
+
</Select>
|
|
3209
|
+
</label>
|
|
3210
|
+
<div className="flex items-center gap-2">
|
|
3211
|
+
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
3212
|
+
上传占位图
|
|
3213
|
+
<Input
|
|
3214
|
+
type="file"
|
|
3215
|
+
accept="image/*"
|
|
3216
|
+
className="hidden"
|
|
3217
|
+
onChange={async (e) => {
|
|
3218
|
+
const file = e.target.files?.[0];
|
|
3219
|
+
e.currentTarget.value = "";
|
|
3220
|
+
if (!file) return;
|
|
3221
|
+
await handleUploadAudioPoster(file);
|
|
3222
|
+
}}
|
|
3223
|
+
/>
|
|
3224
|
+
</label>
|
|
3225
|
+
<button
|
|
3226
|
+
type="button"
|
|
3227
|
+
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
|
|
3228
|
+
onClick={() =>
|
|
3229
|
+
updateSelectedAudio({
|
|
3230
|
+
audioPosterImage: undefined,
|
|
3231
|
+
})
|
|
3232
|
+
}
|
|
3233
|
+
>
|
|
3234
|
+
清空占位图
|
|
3235
|
+
</button>
|
|
3236
|
+
</div>
|
|
3237
|
+
{selectedElement.audioPosterImage ? (
|
|
3238
|
+
<img
|
|
3239
|
+
src={selectedElement.audioPosterImage}
|
|
3240
|
+
alt="音频占位图预览"
|
|
3241
|
+
className="h-20 w-full rounded border border-border/60 object-cover"
|
|
3242
|
+
/>
|
|
3243
|
+
) : null}
|
|
3244
|
+
<div className="text-[11px] text-muted-foreground">
|
|
3245
|
+
设置占位图或图标后,节点上将隐藏进度条,改为点击图标播放/暂停。
|
|
3246
|
+
</div>
|
|
3247
|
+
</>
|
|
3248
|
+
)}
|
|
3249
|
+
</>,
|
|
3250
|
+
true,
|
|
3251
|
+
["音频", "url", "上传", "录音", "icon", "占位", "动效", "自动暂停", "media"]
|
|
3252
|
+
)
|
|
3253
|
+
) : selectedElement.materialType === "video" ? (
|
|
3254
|
+
renderSection(
|
|
3255
|
+
"videoConfig",
|
|
3256
|
+
"视频配置",
|
|
3257
|
+
<>
|
|
3258
|
+
{renderFieldGroup(
|
|
3259
|
+
"视频来源",
|
|
3260
|
+
<>
|
|
3261
|
+
<label className="block space-y-1">
|
|
3262
|
+
<div>视频 URL</div>
|
|
3263
|
+
<Input
|
|
3264
|
+
value={selectedElement.videoRemoteUrl ?? ""}
|
|
3265
|
+
onChange={(e) =>
|
|
3266
|
+
updateSelectedVideo({
|
|
3267
|
+
videoRemoteUrl: e.target.value || undefined,
|
|
3268
|
+
videoSrc: e.target.value || selectedElement.videoSrc,
|
|
3269
|
+
})
|
|
3270
|
+
}
|
|
3271
|
+
placeholder="https://example.com/video.mp4"
|
|
3272
|
+
className="h-7"
|
|
3273
|
+
/>
|
|
3274
|
+
</label>
|
|
3275
|
+
<div className="flex items-center gap-2">
|
|
3276
|
+
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
3277
|
+
上传视频
|
|
3278
|
+
<Input
|
|
3279
|
+
type="file"
|
|
3280
|
+
accept="video/*"
|
|
3281
|
+
className="hidden"
|
|
3282
|
+
onChange={async (e) => {
|
|
3283
|
+
const file = e.target.files?.[0];
|
|
3284
|
+
e.currentTarget.value = "";
|
|
3285
|
+
if (!file) return;
|
|
3286
|
+
await handleUploadVideoFile(file);
|
|
3287
|
+
}}
|
|
3288
|
+
/>
|
|
3289
|
+
</label>
|
|
3290
|
+
</div>
|
|
3291
|
+
{videoStatus ? (
|
|
3292
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3293
|
+
{videoStatus}
|
|
3294
|
+
</div>
|
|
3295
|
+
) : null}
|
|
3296
|
+
<video
|
|
3297
|
+
controls
|
|
3298
|
+
className="h-36 w-full rounded border border-border/60 bg-black/80 object-contain"
|
|
3299
|
+
src={selectedElement.videoSrc || selectedElement.videoRemoteUrl || ""}
|
|
3300
|
+
/>
|
|
3301
|
+
<label className="flex items-center gap-2">
|
|
3302
|
+
<Checkbox
|
|
3303
|
+
checked={selectedElement.mediaAutoPauseOnEdit !== false}
|
|
3304
|
+
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
3305
|
+
onCheckedChange={(checked) =>
|
|
3306
|
+
updateSelectedVideo({
|
|
3307
|
+
mediaAutoPauseOnEdit: checked !== false,
|
|
3308
|
+
})
|
|
3309
|
+
}
|
|
3310
|
+
/>
|
|
3311
|
+
<span>编辑时自动暂停媒体</span>
|
|
3312
|
+
</label>
|
|
3313
|
+
</>
|
|
3314
|
+
)}
|
|
3315
|
+
</>,
|
|
3316
|
+
true,
|
|
3317
|
+
["视频", "url", "上传", "预览", "自动暂停", "media"]
|
|
3318
|
+
)
|
|
3319
|
+
) : null}
|
|
3320
|
+
{selectedElement
|
|
3321
|
+
? selectedElement.parentGridId
|
|
3322
|
+
? renderSection(
|
|
3323
|
+
"gridChildSpan",
|
|
3324
|
+
"网格子节点占位",
|
|
3325
|
+
<>
|
|
3326
|
+
{renderFieldGroup(
|
|
3327
|
+
"跨槽位",
|
|
3328
|
+
<div className="grid grid-cols-2 gap-2">
|
|
3329
|
+
<label className="block space-y-1">
|
|
3330
|
+
<div>跨列(colSpan)</div>
|
|
3331
|
+
<Input
|
|
3332
|
+
type="number"
|
|
3333
|
+
min={1}
|
|
3334
|
+
max={12}
|
|
3335
|
+
value={selectedElement.gridColSpan ?? 1}
|
|
3336
|
+
onChange={(e) =>
|
|
3337
|
+
updateElement(selectedElement.id, {
|
|
3338
|
+
gridColSpan: Math.max(1, Math.min(12, Number(e.target.value) || 1)),
|
|
3339
|
+
})
|
|
3340
|
+
}
|
|
3341
|
+
className="h-7"
|
|
3342
|
+
/>
|
|
3343
|
+
</label>
|
|
3344
|
+
<label className="block space-y-1">
|
|
3345
|
+
<div>跨行(rowSpan)</div>
|
|
3346
|
+
<Input
|
|
3347
|
+
type="number"
|
|
3348
|
+
min={1}
|
|
3349
|
+
max={12}
|
|
3350
|
+
value={selectedElement.gridRowSpan ?? 1}
|
|
3351
|
+
onChange={(e) =>
|
|
3352
|
+
updateElement(selectedElement.id, {
|
|
3353
|
+
gridRowSpan: Math.max(1, Math.min(12, Number(e.target.value) || 1)),
|
|
3354
|
+
})
|
|
3355
|
+
}
|
|
3356
|
+
className="h-7"
|
|
3357
|
+
/>
|
|
3358
|
+
</label>
|
|
3359
|
+
</div>
|
|
3360
|
+
)}
|
|
3361
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3362
|
+
网格子节点可跨越多格,占据更大区域,便于复杂布局。
|
|
3363
|
+
</div>
|
|
3364
|
+
</>,
|
|
3365
|
+
true,
|
|
3366
|
+
["网格", "跨列", "跨行", "span", "slot"]
|
|
3367
|
+
)
|
|
3368
|
+
: null
|
|
3369
|
+
: null}
|
|
3370
|
+
{selectedElement.materialType === "geometry" ? (
|
|
3371
|
+
renderSection(
|
|
3372
|
+
"geometryConfig",
|
|
3373
|
+
"几何配置",
|
|
3374
|
+
<>
|
|
3375
|
+
{renderFieldGroup(
|
|
3376
|
+
"基础形状",
|
|
3377
|
+
<>
|
|
3378
|
+
<label className="block space-y-1">
|
|
3379
|
+
<div>形状</div>
|
|
3380
|
+
<Select
|
|
3381
|
+
value={selectedElement.geometryShape ?? "rect"}
|
|
3382
|
+
onValueChange={(value) =>
|
|
3383
|
+
updateSelectedGeometry({
|
|
3384
|
+
geometryShape: value as PanelElement["geometryShape"],
|
|
3385
|
+
})
|
|
3386
|
+
}
|
|
3387
|
+
>
|
|
3388
|
+
<SelectTrigger className="h-7">
|
|
3389
|
+
<SelectValue placeholder="选择形状" />
|
|
3390
|
+
</SelectTrigger>
|
|
3391
|
+
<SelectContent>
|
|
3392
|
+
<SelectItem value="rect">矩形</SelectItem>
|
|
3393
|
+
<SelectItem value="circle">圆形</SelectItem>
|
|
3394
|
+
<SelectItem value="triangle">三角形</SelectItem>
|
|
3395
|
+
<SelectItem value="diamond">菱形</SelectItem>
|
|
3396
|
+
<SelectItem value="hexagon">六边形</SelectItem>
|
|
3397
|
+
<SelectItem value="star">星形</SelectItem>
|
|
3398
|
+
<SelectItem value="heart">爱心</SelectItem>
|
|
3399
|
+
</SelectContent>
|
|
3400
|
+
</Select>
|
|
3401
|
+
</label>
|
|
3402
|
+
{renderColorField(
|
|
3403
|
+
"几何颜色",
|
|
3404
|
+
selectedElement.geometryColor ?? "#3b82f6",
|
|
3405
|
+
(next) => updateSelectedGeometry({ geometryColor: next || "#3b82f6" })
|
|
3406
|
+
)}
|
|
3407
|
+
</>
|
|
3408
|
+
)}
|
|
3409
|
+
{renderFieldGroup(
|
|
3410
|
+
"高级(Canvas 脚本)",
|
|
3411
|
+
<>
|
|
3412
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3413
|
+
可输入 Canvas 绘制逻辑,变量:ctx、width、height、element。脚本异常会被安全忽略。
|
|
3414
|
+
</div>
|
|
3415
|
+
<Textarea
|
|
3416
|
+
value={selectedElement.geometryScript ?? ""}
|
|
3417
|
+
onChange={(e) => updateSelectedGeometry({ geometryScript: e.target.value || undefined })}
|
|
3418
|
+
spellCheck={false}
|
|
3419
|
+
className="h-36 font-mono text-[11px]"
|
|
3420
|
+
placeholder="// 例: ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.fillRect(8,8,width-16,height-16);"
|
|
3421
|
+
/>
|
|
3422
|
+
</>
|
|
3423
|
+
)}
|
|
3424
|
+
{renderFieldGroup(
|
|
3425
|
+
"手绘叠加",
|
|
3426
|
+
<>
|
|
3427
|
+
<div className="flex items-center gap-2">
|
|
3428
|
+
<label className="block space-y-1">
|
|
3429
|
+
<div className="text-[11px]">画笔颜色</div>
|
|
3430
|
+
<Input
|
|
3431
|
+
type="color"
|
|
3432
|
+
value={geometryDrawPenColor}
|
|
3433
|
+
onChange={(e) => setGeometryDrawPenColor(e.target.value)}
|
|
3434
|
+
className="h-7 w-10 p-1"
|
|
3435
|
+
/>
|
|
3436
|
+
</label>
|
|
3437
|
+
<label className="block space-y-1">
|
|
3438
|
+
<div className="text-[11px]">画笔粗细</div>
|
|
3439
|
+
<Input
|
|
3440
|
+
type="number"
|
|
3441
|
+
min={1}
|
|
3442
|
+
max={24}
|
|
3443
|
+
value={geometryDrawPenWidth}
|
|
3444
|
+
onChange={(e) => setGeometryDrawPenWidth(Math.max(1, Math.min(24, Number(e.target.value) || 1)))}
|
|
3445
|
+
className="h-7 w-20"
|
|
3446
|
+
/>
|
|
3447
|
+
</label>
|
|
3448
|
+
</div>
|
|
3449
|
+
<div className="rounded border border-border/60 bg-white p-2">
|
|
3450
|
+
<canvas
|
|
3451
|
+
ref={geometryDrawCanvasRef}
|
|
3452
|
+
width={320}
|
|
3453
|
+
height={180}
|
|
3454
|
+
className="h-[180px] w-full cursor-crosshair rounded border border-border/60"
|
|
3455
|
+
onPointerDown={(e) => {
|
|
3456
|
+
const canvas = geometryDrawCanvasRef.current;
|
|
3457
|
+
if (!canvas) return;
|
|
3458
|
+
const ctx = canvas.getContext("2d");
|
|
3459
|
+
if (!ctx) return;
|
|
3460
|
+
const rect = canvas.getBoundingClientRect();
|
|
3461
|
+
const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
|
|
3462
|
+
const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
|
|
3463
|
+
geometryLastPointRef.current = { x, y };
|
|
3464
|
+
setIsGeometryDrawing(true);
|
|
3465
|
+
ctx.lineCap = "round";
|
|
3466
|
+
ctx.lineJoin = "round";
|
|
3467
|
+
ctx.strokeStyle = geometryDrawPenColor;
|
|
3468
|
+
ctx.lineWidth = geometryDrawPenWidth;
|
|
3469
|
+
ctx.beginPath();
|
|
3470
|
+
ctx.moveTo(x, y);
|
|
3471
|
+
}}
|
|
3472
|
+
onPointerMove={(e) => {
|
|
3473
|
+
if (!isGeometryDrawing) return;
|
|
3474
|
+
const canvas = geometryDrawCanvasRef.current;
|
|
3475
|
+
const last = geometryLastPointRef.current;
|
|
3476
|
+
if (!canvas || !last) return;
|
|
3477
|
+
const ctx = canvas.getContext("2d");
|
|
3478
|
+
if (!ctx) return;
|
|
3479
|
+
const rect = canvas.getBoundingClientRect();
|
|
3480
|
+
const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
|
|
3481
|
+
const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
|
|
3482
|
+
ctx.lineTo(x, y);
|
|
3483
|
+
ctx.stroke();
|
|
3484
|
+
geometryLastPointRef.current = { x, y };
|
|
3485
|
+
}}
|
|
3486
|
+
onPointerUp={() => {
|
|
3487
|
+
setIsGeometryDrawing(false);
|
|
3488
|
+
geometryLastPointRef.current = null;
|
|
3489
|
+
}}
|
|
3490
|
+
onPointerLeave={() => {
|
|
3491
|
+
setIsGeometryDrawing(false);
|
|
3492
|
+
geometryLastPointRef.current = null;
|
|
3493
|
+
}}
|
|
3494
|
+
/>
|
|
3495
|
+
</div>
|
|
3496
|
+
<div className="flex items-center gap-2">
|
|
3497
|
+
<button
|
|
3498
|
+
type="button"
|
|
3499
|
+
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
|
|
3500
|
+
onClick={() => {
|
|
3501
|
+
const canvas = geometryDrawCanvasRef.current;
|
|
3502
|
+
if (!canvas) return;
|
|
3503
|
+
updateSelectedGeometry({ geometrySketchDataUrl: canvas.toDataURL("image/png") });
|
|
3504
|
+
}}
|
|
3505
|
+
>
|
|
3506
|
+
应用手绘到节点
|
|
3507
|
+
</button>
|
|
3508
|
+
<button
|
|
3509
|
+
type="button"
|
|
3510
|
+
className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
|
|
3511
|
+
onClick={() => {
|
|
3512
|
+
updateSelectedGeometry({ geometrySketchDataUrl: undefined });
|
|
3513
|
+
redrawGeometryPadFromElement();
|
|
3514
|
+
}}
|
|
3515
|
+
>
|
|
3516
|
+
清空手绘
|
|
3517
|
+
</button>
|
|
3518
|
+
</div>
|
|
3519
|
+
</>
|
|
3520
|
+
)}
|
|
3521
|
+
</>,
|
|
3522
|
+
true,
|
|
3523
|
+
["几何", "geometry", "形状", "canvas", "脚本", "手绘"]
|
|
3524
|
+
)
|
|
3525
|
+
) : selectedElement.materialType === "grid" ? (
|
|
3526
|
+
renderSection(
|
|
3527
|
+
"gridConfig",
|
|
3528
|
+
"网格布局配置",
|
|
3529
|
+
<>
|
|
3530
|
+
{renderFieldGroup(
|
|
3531
|
+
"网格参数",
|
|
3532
|
+
<div className="grid grid-cols-2 gap-2">
|
|
3533
|
+
<label className="block space-y-1">
|
|
3534
|
+
<div>行数</div>
|
|
3535
|
+
<Input
|
|
3536
|
+
type="number"
|
|
3537
|
+
min={1}
|
|
3538
|
+
max={12}
|
|
3539
|
+
value={selectedElement.gridRows ?? 2}
|
|
3540
|
+
onChange={(e) =>
|
|
3541
|
+
updateElement(selectedElement.id, {
|
|
3542
|
+
gridRows: Math.max(1, Math.min(12, Number(e.target.value) || 2)),
|
|
3543
|
+
})
|
|
3544
|
+
}
|
|
3545
|
+
className="h-7"
|
|
3546
|
+
/>
|
|
3547
|
+
</label>
|
|
3548
|
+
<label className="block space-y-1">
|
|
3549
|
+
<div>列数</div>
|
|
3550
|
+
<Input
|
|
3551
|
+
type="number"
|
|
3552
|
+
min={1}
|
|
3553
|
+
max={12}
|
|
3554
|
+
value={selectedElement.gridCols ?? 3}
|
|
3555
|
+
onChange={(e) =>
|
|
3556
|
+
updateElement(selectedElement.id, {
|
|
3557
|
+
gridCols: Math.max(1, Math.min(12, Number(e.target.value) || 3)),
|
|
3558
|
+
})
|
|
3559
|
+
}
|
|
3560
|
+
className="h-7"
|
|
3561
|
+
/>
|
|
3562
|
+
</label>
|
|
3563
|
+
<label className="block space-y-1">
|
|
3564
|
+
<div>间距(px)</div>
|
|
3565
|
+
<Input
|
|
3566
|
+
type="number"
|
|
3567
|
+
min={0}
|
|
3568
|
+
max={80}
|
|
3569
|
+
value={selectedElement.gridGap ?? 8}
|
|
3570
|
+
onChange={(e) =>
|
|
3571
|
+
updateElement(selectedElement.id, {
|
|
3572
|
+
gridGap: Math.max(0, Math.min(80, Number(e.target.value) || 0)),
|
|
3573
|
+
})
|
|
3574
|
+
}
|
|
3575
|
+
className="h-7"
|
|
3576
|
+
/>
|
|
3577
|
+
</label>
|
|
3578
|
+
<label className="block space-y-1">
|
|
3579
|
+
<div>内边距(px)</div>
|
|
3580
|
+
<Input
|
|
3581
|
+
type="number"
|
|
3582
|
+
min={0}
|
|
3583
|
+
max={100}
|
|
3584
|
+
value={selectedElement.gridPadding ?? 10}
|
|
3585
|
+
onChange={(e) =>
|
|
3586
|
+
updateElement(selectedElement.id, {
|
|
3587
|
+
gridPadding: Math.max(0, Math.min(100, Number(e.target.value) || 0)),
|
|
3588
|
+
})
|
|
3589
|
+
}
|
|
3590
|
+
className="h-7"
|
|
3591
|
+
/>
|
|
3592
|
+
</label>
|
|
3593
|
+
<label className="block space-y-1">
|
|
3594
|
+
<div>吸附阈值(px)</div>
|
|
3595
|
+
<Input
|
|
3596
|
+
type="number"
|
|
3597
|
+
min={8}
|
|
3598
|
+
max={120}
|
|
3599
|
+
value={selectedElement.gridSnapThreshold ?? 36}
|
|
3600
|
+
onChange={(e) =>
|
|
3601
|
+
updateElement(selectedElement.id, {
|
|
3602
|
+
gridSnapThreshold: Math.max(
|
|
3603
|
+
8,
|
|
3604
|
+
Math.min(120, Number(e.target.value) || 36)
|
|
3605
|
+
),
|
|
3606
|
+
})
|
|
3607
|
+
}
|
|
3608
|
+
className="h-7"
|
|
3609
|
+
/>
|
|
3610
|
+
</label>
|
|
3611
|
+
</div>
|
|
3612
|
+
)}
|
|
3613
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3614
|
+
其他节点拖拽靠近该网格槽位中心时会自动吸附,并在节点树显示为该网格子节点。
|
|
3615
|
+
</div>
|
|
3616
|
+
</>,
|
|
3617
|
+
true,
|
|
3618
|
+
["网格", "grid", "行", "列", "间距", "内边距", "吸附", "阈值"]
|
|
3619
|
+
)
|
|
3620
|
+
) : selectedElement.materialType === "reference" ? (
|
|
3621
|
+
renderSection(
|
|
3622
|
+
"reference",
|
|
3623
|
+
"引用组件配置",
|
|
3624
|
+
<>
|
|
3625
|
+
{renderFieldGroup(
|
|
3626
|
+
"引用源",
|
|
3627
|
+
<label className="block space-y-1">
|
|
3628
|
+
<div>引用图层</div>
|
|
3629
|
+
<Select
|
|
3630
|
+
value={selectedElement.refLayerId ?? "__none__"}
|
|
3631
|
+
onValueChange={(value) =>
|
|
3632
|
+
updateElement(selectedElement.id, {
|
|
3633
|
+
refLayerId: value === "__none__" ? undefined : value,
|
|
3634
|
+
})
|
|
3635
|
+
}
|
|
3636
|
+
>
|
|
3637
|
+
<SelectTrigger className="h-7">
|
|
3638
|
+
<SelectValue placeholder="请选择图层" />
|
|
3639
|
+
</SelectTrigger>
|
|
3640
|
+
<SelectContent>
|
|
3641
|
+
<SelectItem value="__none__">无(不引用)</SelectItem>
|
|
3642
|
+
{layers
|
|
3643
|
+
.filter((l) => l.id !== selectedElement.layerId)
|
|
3644
|
+
.map((l) => (
|
|
3645
|
+
<SelectItem key={l.id} value={l.id}>
|
|
3646
|
+
{l.name}
|
|
3647
|
+
</SelectItem>
|
|
3648
|
+
))}
|
|
3649
|
+
</SelectContent>
|
|
3650
|
+
</Select>
|
|
3651
|
+
</label>
|
|
3652
|
+
)}
|
|
3653
|
+
{renderFieldGroup(
|
|
3654
|
+
"拷贝策略",
|
|
3655
|
+
<>
|
|
3656
|
+
<label className="block space-y-1">
|
|
3657
|
+
<div>拷贝模式</div>
|
|
3658
|
+
<Select
|
|
3659
|
+
value={selectedElement.refCopyMode ?? "shallow"}
|
|
3660
|
+
onValueChange={(value) =>
|
|
3661
|
+
setReferenceCopyMode?.(
|
|
3662
|
+
selectedElement.id,
|
|
3663
|
+
value as ReferenceCopyMode
|
|
3664
|
+
)
|
|
3665
|
+
}
|
|
3666
|
+
>
|
|
3667
|
+
<SelectTrigger className="h-7">
|
|
3668
|
+
<SelectValue placeholder="请选择拷贝模式" />
|
|
3669
|
+
</SelectTrigger>
|
|
3670
|
+
<SelectContent>
|
|
3671
|
+
<SelectItem value="shallow">浅拷贝(跟随源图层变化)</SelectItem>
|
|
3672
|
+
<SelectItem value="deep">深拷贝(冻结当前引用快照)</SelectItem>
|
|
3673
|
+
</SelectContent>
|
|
3674
|
+
</Select>
|
|
3675
|
+
</label>
|
|
3676
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3677
|
+
浅拷贝会实时同步被引用图层;深拷贝会固定当前快照,不再随源变化。
|
|
3678
|
+
</div>
|
|
3679
|
+
</>
|
|
3680
|
+
)}
|
|
3681
|
+
</>,
|
|
3682
|
+
true,
|
|
3683
|
+
["引用", "ref", "图层", "浅拷贝", "深拷贝", "snapshot"]
|
|
3684
|
+
)
|
|
3685
|
+
) : (
|
|
3686
|
+
<div className="text-xs leading-6 text-muted-foreground">
|
|
3687
|
+
当前节点不是图表类型,暂无图表配置项。
|
|
3688
|
+
</div>
|
|
3689
|
+
)}
|
|
3690
|
+
{hasSearch && renderedSectionCount === 0 ? (
|
|
3691
|
+
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3692
|
+
未找到匹配项,请尝试更换关键词。
|
|
3693
|
+
</div>
|
|
3694
|
+
) : null}
|
|
3695
|
+
</div>
|
|
3696
|
+
</fieldset>
|
|
3697
|
+
</div>
|
|
3698
|
+
) : null}
|
|
3699
|
+
</aside>
|
|
3700
|
+
</ScopeConfigProvider>
|
|
3701
|
+
);
|
|
3702
|
+
}
|
|
3703
|
+
|